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:
- Base URI matches database config
- Strategy appends correct suffix
- 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:
- URL format matches strategy expectations
- Query parameters present (code, state)
- 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:
- URL scheme registered (iOS/Android)
- Query parameters included in strategy
- 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
- Core Concepts - Understanding OIDC flows
- React SDK - React integration
- React Native SDK - Mobile integration