Skip to main content

Callback Route Strategies

Understanding how Signia handles OAuth2/OIDC callback routing across different frameworks and platforms.

Overview

The Signia SDK uses a layered architecture with callback route strategies to maintain separation of concerns between protocol clients and framework-specific routing.

The Challenge

Different frameworks handle routing differently:

  • React (Web): /oidc-callback/login, /oidc-callback/logout
  • React Native: myapp://callback?flow=login
  • Flutter: Deep link handling
  • Express: /auth/callback/login

The core OAuth2/OIDC protocol shouldn't know about these framework-specific patterns.

Architecture Layers

1. Core Protocol Layer (Framework-Agnostic)

Located in @getsignia/signia-auth-oauth2 and @getsignia/signia-auth-oidc:

Responsibility:

  • Token exchange
  • PKCE implementation
  • State management
  • Protocol compliance

Key Principle: NO framework-specific routing assumptions

// Core OAuth2 client - no routing logic
class OAuth2Client {
async authorize() {
// Uses callback strategy to format redirect URI
const redirectUri = this.callbackStrategy.formatAuthorizationRedirectUri(
this.config.redirectUri,
'login'
);

const authUrl = `${this.config.authorizationEndpoint}?` +
`redirect_uri=${encodeURIComponent(redirectUri)}` +
`&client_id=${this.config.clientId}` +
// ... other params
}
}

2. Framework Adapter Layer

Implements CallbackRouteStrategy interface for each platform:

Responsibility:

  • Transform base redirect URIs to framework-specific callback patterns
  • Parse incoming callback URLs
  • Handle framework-specific routing
export interface CallbackRouteStrategy {
// Transform base redirectUri for authorization requests
formatAuthorizationRedirectUri(
baseRedirectUri: string,
flow: 'login' | 'logout'
): string;

// Parse incoming callback URLs to determine flow type
parseCallbackRequest(
url: string,
baseRedirectUri: string
): {
flow: 'login' | 'logout' | null;
code?: string;
state?: string;
};
}

3. Framework UI Layer

Provides provider components and hooks:

Responsibility:

  • UI components
  • State management
  • Framework integration

Built-in Strategies

ReactCallbackStrategy

For React web applications:

Pattern:

Base:    http://localhost:3000/oidc-callback
Login: http://localhost:3000/oidc-callback/login
Logout: http://localhost:3000/oidc-callback/logout

Implementation:

class ReactCallbackStrategy implements CallbackRouteStrategy {
formatAuthorizationRedirectUri(
baseRedirectUri: string,
flow: 'login' | 'logout'
): string {
// Append /login or /logout to base URI
const url = new URL(baseRedirectUri);
url.pathname = `${url.pathname}/${flow}`;
return url.toString();
}

parseCallbackRequest(url: string, baseRedirectUri: string) {
const urlObj = new URL(url);
const basePath = new URL(baseRedirectUri).pathname;

// Check if path is /oidc-callback/login or /oidc-callback/logout
if (urlObj.pathname === `${basePath}/login`) {
return {
flow: 'login',
code: urlObj.searchParams.get('code') || undefined,
state: urlObj.searchParams.get('state') || undefined
};
}

if (urlObj.pathname === `${basePath}/logout`) {
return { flow: 'logout' };
}

return { flow: null };
}
}

Usage:

// Automatically applied by SigniaAuthProvider
<SigniaAuthProvider config={{
clientId: 'YOUR_CLIENT_ID',
redirectUri: 'http://localhost:3000/oidc-callback',
issuer: 'https://tenant.signiaauth.com',
scopes: ['openid', 'profile', 'email']
}}>
<App />
</SigniaAuthProvider>

ReactNativeCallbackStrategy

For React Native applications:

Pattern:

Base:    myapp://callback
Login: myapp://callback?flow=login
Logout: myapp://callback?flow=logout

Why query parameters?

  • Deep links don't support path segments reliably across platforms
  • Query parameters work consistently on iOS and Android

Implementation:

class ReactNativeCallbackStrategy implements CallbackRouteStrategy {
formatAuthorizationRedirectUri(
baseRedirectUri: string,
flow: 'login' | 'logout'
): string {
// Add flow as query parameter
const url = new URL(baseRedirectUri);
url.searchParams.set('flow', flow);
return url.toString();
}

parseCallbackRequest(url: string, baseRedirectUri: string) {
const urlObj = new URL(url);
const flow = urlObj.searchParams.get('flow');

if (flow === 'login') {
return {
flow: 'login',
code: urlObj.searchParams.get('code') || undefined,
state: urlObj.searchParams.get('state') || undefined
};
}

if (flow === 'logout') {
return { flow: 'logout' };
}

return { flow: null };
}
}

NodeCallbackStrategy

For Express/Node.js applications:

Pattern:

Base:    http://localhost:3001/auth/callback
Login: http://localhost:3001/auth/callback/login
Logout: http://localhost:3001/auth/callback/logout

Implementation:

class NodeCallbackStrategy implements CallbackRouteStrategy {
formatAuthorizationRedirectUri(
baseRedirectUri: string,
flow: 'login' | 'logout'
): string {
return `${baseRedirectUri}/${flow}`;
}

parseCallbackRequest(url: string, baseRedirectUri: string) {
const urlObj = new URL(url);
const basePath = new URL(baseRedirectUri).pathname;

if (urlObj.pathname === `${basePath}/login`) {
return {
flow: 'login',
code: urlObj.searchParams.get('code') || undefined,
state: urlObj.searchParams.get('state') || undefined
};
}

if (urlObj.pathname === `${basePath}/logout`) {
return { flow: 'logout' };
}

return { flow: null };
}
}

Custom Strategies

Creating a Custom Strategy

Implement the CallbackRouteStrategy interface:

import { CallbackRouteStrategy } from '@getsignia/signia-auth-sdk';

class CustomCallbackStrategy implements CallbackRouteStrategy {
formatAuthorizationRedirectUri(
baseRedirectUri: string,
flow: 'login' | 'logout'
): string {
// Your custom logic
// Example: use hash fragment
return `${baseRedirectUri}#${flow}`;
}

parseCallbackRequest(url: string, baseRedirectUri: string) {
// Your custom parsing logic
const hash = new URL(url).hash.substring(1);

if (hash === 'login') {
const params = new URLSearchParams(new URL(url).search);
return {
flow: 'login',
code: params.get('code') || undefined,
state: params.get('state') || undefined
};
}

if (hash === 'logout') {
return { flow: 'logout' };
}

return { flow: null };
}
}

Using a Custom Strategy

React:

import { SigniaAuthProvider } from '@getsignia/signia-auth-ui-react';
import { CustomCallbackStrategy } from './CustomCallbackStrategy';

const customStrategy = new CustomCallbackStrategy();

<SigniaAuthProvider
config={{
clientId: 'YOUR_CLIENT_ID',
redirectUri: 'http://localhost:3000/oidc-callback',
issuer: 'https://tenant.signiaauth.com',
scopes: ['openid', 'profile', 'email']
}}
callbackStrategy={customStrategy}
>
<App />
</SigniaAuthProvider>

Node.js:

import { OIDCClient } from '@getsignia/signia-auth-sdk';
import { CustomCallbackStrategy } from './CustomCallbackStrategy';

const oidcClient = new OIDCClient({
clientId: 'YOUR_CLIENT_ID',
redirectUri: 'http://localhost:3001/auth/callback',
issuer: 'https://tenant.signiaauth.com',
scopes: ['openid', 'profile'],
callbackStrategy: new CustomCallbackStrategy()
});

Database Configuration

Backend Validation

The auth backend's login_redirect_url field should match the base redirect URI only:

-- Stored in database
INSERT INTO applications (name, login_redirect_url)
VALUES ('My App', 'http://localhost:5173/oidc-callback');

Strategy appends flow-specific paths:

Configured: http://localhost:5173/oidc-callback
Login: http://localhost:5173/oidc-callback/login
Logout: http://localhost:5173/oidc-callback/logout

Backend validation:

function validateRedirectUri(requestedUri: string, configuredUri: string): boolean {
// Check if requested URI starts with configured base
return requestedUri.startsWith(configuredUri);
}

// Example
validateRedirectUri(
'http://localhost:5173/oidc-callback/login', // Requested
'http://localhost:5173/oidc-callback' // Configured
); // ✅ Valid

Common Patterns

Pattern 1: Path-Based (Web)

✅ Used by: React, Vue, Angular
✅ Format: /oidc-callback/login
✅ Pros: Clean URLs, framework-agnostic
❌ Cons: Requires routing configuration

Pattern 2: Query Parameter (Mobile)

✅ Used by: React Native, Flutter
✅ Format: myapp://callback?flow=login
✅ Pros: Works with deep linking
❌ Cons: Longer URLs

Pattern 3: Hash Fragment (Legacy)

⚠️ Legacy pattern
✅ Format: /oidc-callback#login
✅ Pros: No server-side routing needed
❌ Cons: Limited support, harder to parse

Best Practices

1. Use Default Strategies

Default strategies are well-tested and handle edge cases:

// ✅ Use default (automatic)
<SigniaAuthProvider config={{
clientId: 'YOUR_CLIENT_ID',
redirectUri: 'http://localhost:3000/oidc-callback',
issuer: 'https://tenant.signiaauth.com',
scopes: ['openid', 'profile', 'email']
}}>
<App />
</SigniaAuthProvider>

// ❌ Don't create custom unless necessary
<SigniaAuthProvider
config={{
clientId: 'YOUR_CLIENT_ID',
redirectUri: 'http://localhost:3000/oidc-callback',
issuer: 'https://tenant.signiaauth.com',
scopes: ['openid', 'profile', 'email']
}}
callbackStrategy={myCustomStrategy}
>
<App />
</SigniaAuthProvider>

2. Match Backend Configuration

Ensure your redirect URI matches the database:

// ✅ Correct
Database: http://localhost:5173/oidc-callback
SDK Config: http://localhost:5173/oidc-callback

// ❌ Wrong
Database: http://localhost:5173/callback
SDK Config: http://localhost:5173/oidc-callback

3. Test Both Flows

Always test login and logout:

describe('Callback routing', () => {
it('should handle login callback', async () => {
await client.login();
// Verify redirected to /oidc-callback/login
});

it('should handle logout callback', async () => {
await client.logout();
// Verify redirected to /oidc-callback/logout
});
});

4. Handle Errors Gracefully

Parse callback errors:

const callback = strategy.parseCallbackRequest(url, baseUri);

if (callback.flow === null) {
// Invalid callback URL
return redirectToError('Invalid callback URL');
}

if (callback.error) {
// OAuth error returned
return handleOAuthError(callback.error);
}

Troubleshooting

"Invalid redirect URI" error

Cause: Redirect URI mismatch

Check:

  1. Base URI matches database config
  2. Strategy appends correct suffix
  3. No typos in configuration
// Debug strategy output
const loginUri = strategy.formatAuthorizationRedirectUri(
'http://localhost:5173/oidc-callback',
'login'
);
console.log('Login URI:', loginUri);
// Should be: http://localhost:5173/oidc-callback/login

Callback not being parsed

Cause: Strategy not recognizing callback URL

Check:

  1. URL format matches strategy expectations
  2. Query parameters present (code, state)
  3. Path segments correct
// Debug callback parsing
const result = strategy.parseCallbackRequest(
'http://localhost:5173/oidc-callback/login?code=abc&state=xyz',
'http://localhost:5173/oidc-callback'
);
console.log('Parsed:', result);
// Should be: { flow: 'login', code: 'abc', state: 'xyz' }

Deep linking not working (mobile)

Cause: Deep link not configured properly

Check:

  1. URL scheme registered (iOS/Android)
  2. Query parameters included in strategy
  3. App handles incoming URLs

Migration Guide

Upgrading from Hardcoded Paths

If you previously hardcoded callback paths:

Before (hardcoded):

// ❌ Old approach
const oidcClient = new OIDCClient({
clientId: 'YOUR_CLIENT_ID',
redirectUri: 'http://localhost:5173/oidc-callback/login', // Hardcoded /login
issuer: 'https://tenant.signiaauth.com'
});

After (strategy-based):

// ✅ New approach
const oidcClient = new OIDCClient({
clientId: 'YOUR_CLIENT_ID',
redirectUri: 'http://localhost:5173/oidc-callback', // Base URI only
issuer: 'https://tenant.signiaauth.com'
});
// Strategy automatically appends /login or /logout

Next Steps