Extension OAuth - Generic OAuth Callback Handler
Simplified OAuth callback handling for any custom OAuth flow (extensions, sessions, multi-tenant, etc.).
Overview
A flexible OAuth callback handler that:
- Detects custom OAuth flows via a state parameter marker
- Handles token exchange automatically (Google, GitHub, or custom providers)
- Sends
postMessageto the opener window with results - Provides full state access in callbacks
Key Feature: You control what data flows through OAuth via the state parameter.
Quick Start
Server (10 lines)
import { createRpcAiServer } from 'simple-rpc-ai-backend';
const server = createRpcAiServer({
oauth: {
enabled: true,
googleClientId: process.env.GOOGLE_CLIENT_ID,
googleClientSecret: process.env.GOOGLE_CLIENT_SECRET
},
extensionOAuth: {
enabled: true,
onUserAuthenticated: async (stateData, userId, userInfo) => {
console.log('State:', stateData);
console.log('User:', userInfo.email);
await yourCustomLogic(stateData, userId, userInfo);
}
}
});
Client (5 lines)
import { encodeOAuthState } from 'simple-rpc-ai-backend';
const state = encodeOAuthState({
isExtensionAuth: true, // Required marker
sessionId: 'abc-123',
returnPath: '/dashboard'
});
const popup = window.open(`/login/google?state=${state}`);
window.addEventListener('message', (event) => {
if (event.data.type === 'oauth-complete' && event.data.success) {
console.log('User:', event.data.user);
console.log('Your state:', event.data.state);
}
});
That’s it! No boilerplate, no provider-specific code.
Use Cases
Extension UUID Linking
const state = encodeOAuthState({
isExtensionAuth: true,
extensionUUID: crypto.randomUUID()
});
onUserAuthenticated: (stateData, userId) => {
await linkExtension(stateData.extensionUUID, userId);
};
Session-Based Auth
const state = encodeOAuthState({
isExtensionAuth: true,
sessionId: getSessionId(),
deviceId: getDeviceId()
});
onUserAuthenticated: (stateData, userId) => {
await linkSession(stateData.sessionId, userId);
await registerDevice(stateData.deviceId, userId);
};
Multi-Tenant
const state = encodeOAuthState({
isExtensionAuth: true,
tenantId: getTenantId(),
workspaceId: getWorkspaceId(),
inviteCode: getInviteCode()
});
onUserAuthenticated: (stateData, userId) => {
await addToTenant(userId, stateData.tenantId);
await grantWorkspaceAccess(userId, stateData.workspaceId);
await acceptInvite(stateData.inviteCode, userId);
};
Data Flow
Client → Server (via state parameter)
Send custom data to the server by encoding it in the state parameter:
const state = encodeOAuthState({
isExtensionAuth: true,
extensionUUID: 'abc-123',
sessionId: 'xyz-456',
anything: 'you want'
});
Server → Client (via postMessage)
The handler posts a message back to the opener window when OAuth completes:
window.addEventListener('message', (event) => {
if (event.data?.type === 'oauth-complete') {
if (event.data.success) {
console.log(event.data.user);
console.log(event.data.state);
} else {
console.error(event.data.error);
}
}
});
Customizing the Handler
const server = createRpcAiServer({
extensionOAuth: {
enabled: true,
isExtensionOAuth: (state) => state?.flow === 'extension',
onUserAuthenticated: async (state, userId, user) => {
await linkExtension(state.extensionId, userId);
},
successTemplate: (user, state) => `
<html><body>
<script>
window.opener?.postMessage({
type: 'oauth-complete',
success: true,
user: ${JSON.stringify(user)},
state: ${JSON.stringify(state)}
}, '*');
setTimeout(() => window.close(), 500);
</script>
</body></html>
`
}
});
API Reference
encodeOAuthState(data: any): string
Encodes custom data into a Base64 state parameter.
decodeOAuthState(state: string): any | null
Decodes the state parameter back to an object.
createExtensionOAuthHandler(config)
Creates Express middleware for OAuth callback handling.
{
enabled?: boolean;
isExtensionOAuth?: (state: any) => boolean;
onUserAuthenticated?: (state: any, userId: string, user: object) => void | Promise<void>;
tokenExchangeHandlers?: Record<string, (code: string, callbackUrl: string) => Promise<UserInfo>>;
successTemplate?: (user: object, state: any) => string;
errorTemplate?: (error: string, state?: any) => string;
}
Environment Variables
GOOGLE_CLIENT_ID=your-google-client-id
GOOGLE_CLIENT_SECRET=your-google-client-secret
GITHUB_CLIENT_ID=your-github-client-id
GITHUB_CLIENT_SECRET=your-github-client-secret
OAUTH_BASE_URL=https://your-server.com
Security Checklist
- ✅ Email redaction in logs
- ✅ State validation
- ✅ HTTPS required for production
- ✅ No token storage (exchanged & discarded)
- ✅ Customizable detection logic
Troubleshooting
| Symptom | Resolution |
|---|---|
oauth-complete not firing | Ensure state includes isExtensionAuth: true (or customize isExtensionOAuth). |
| Popup closes before message | Increase the timeout in the success template. |
| Custom provider not exchanging tokens | Add a handler under tokenExchangeHandlers. |