diff --git a/gateway/src/index.ts b/gateway/src/index.ts index 5dbd5c0..320f1d9 100644 --- a/gateway/src/index.ts +++ b/gateway/src/index.ts @@ -236,11 +236,18 @@ async function main(): Promise { // OAuth client metadata (for AT Protocol discovery) app.get('/client-metadata.json', (_req, res) => { + // Build redirect URIs array - include both legacy and OIDC callback paths + const redirectUris = [config.redirectUri]; + // Add OIDC callback if OIDC is enabled + if (config.oidc.enabled && config.oidc.issuer) { + redirectUris.push(`${config.oidc.issuer}/oauth/callback`); + } + res.json({ client_id: config.clientId, client_name: 'ATAuth Gateway', client_uri: config.clientId.replace('/client-metadata.json', ''), - redirect_uris: [config.redirectUri], + redirect_uris: redirectUris, grant_types: ['authorization_code', 'refresh_token'], response_types: ['code'], scope: 'atproto transition:generic', diff --git a/gateway/src/routes/oidc/authorize.ts b/gateway/src/routes/oidc/authorize.ts index ca9ccb2..95cf60b 100644 --- a/gateway/src/routes/oidc/authorize.ts +++ b/gateway/src/routes/oidc/authorize.ts @@ -371,6 +371,118 @@ export function createAuthorizeRouter( } }); + /** + * GET /oauth/callback + * Handle the AT Protocol OAuth callback and complete the OIDC flow + */ + router.get('/callback', async (req: Request, res: Response) => { + try { + const { code, state, error, error_description, iss } = req.query as { + code?: string; + state?: string; + error?: string; + error_description?: string; + iss?: string; + }; + + console.log(`[OIDC Callback] Received callback with state: ${state}`); + + // Handle AT Protocol OAuth errors + if (error) { + console.error(`[OIDC Callback] AT Protocol error: ${error} - ${error_description}`); + // We need to redirect back to the original client with the error + // But we need the state to find the original request + if (state) { + const savedState = db.getOAuthState(state); + if (savedState) { + return redirectWithError( + res, + savedState.redirect_uri, + undefined, + 'access_denied', + error_description || 'Authentication failed' + ); + } + } + return res.status(400).json({ + error: 'access_denied', + error_description: error_description || 'Authentication failed', + }); + } + + if (!code || !state) { + return res.status(400).json({ + error: 'invalid_request', + error_description: 'Missing code or state', + }); + } + + // Get the saved state + const savedState = db.getOAuthState(state); + if (!savedState) { + return res.status(400).json({ + error: 'invalid_request', + error_description: 'Invalid or expired state', + }); + } + + // Exchange the AT Protocol code for tokens + const callbackResult = await oauthService.handleCallback(code, state, iss || ''); + if (!callbackResult) { + return res.status(500).json({ + error: 'server_error', + error_description: 'Failed to complete authentication', + }); + } + + // Get the user's DID and handle from the AT Protocol session + const { did, handle } = callbackResult; + console.log(`[OIDC Callback] Authenticated user: ${handle} (${did})`); + + // Get the OIDC authorization code we saved earlier (stored in code_verifier field) + const oidcAuthCode = savedState.code_verifier; + if (!oidcAuthCode) { + return res.status(500).json({ + error: 'server_error', + error_description: 'OIDC authorization state not found', + }); + } + + // Update the authorization code with the user's identity + const authData = db.getAuthorizationCode(oidcAuthCode); + if (!authData) { + return res.status(400).json({ + error: 'invalid_request', + error_description: 'OIDC authorization expired', + }); + } + + // Update with user info + db.updateAuthorizationCodeUser(oidcAuthCode, did, handle); + + // Build the redirect URL back to the original client + const clientRedirectUrl = new URL(authData.redirect_uri); + clientRedirectUrl.searchParams.set('code', oidcAuthCode); + if (authData.state) { + clientRedirectUrl.searchParams.set('state', authData.state); + } + + console.log(`[OIDC Callback] Redirecting to client: ${authData.redirect_uri}`); + + // Clean up the AT Protocol state + db.deleteOAuthState(state); + + // Redirect back to the OIDC client + res.redirect(clientRedirectUrl.toString()); + } catch (error) { + console.error('[OIDC Callback] Error:', error); + res.status(500).json({ + error: 'server_error', + error_description: 'Internal server error', + }); + } + }); + return router; } diff --git a/gateway/src/services/database.ts b/gateway/src/services/database.ts index 77e73f6..8939273 100644 --- a/gateway/src/services/database.ts +++ b/gateway/src/services/database.ts @@ -636,6 +636,11 @@ export class DatabaseService { stmt.run(code); } + updateAuthorizationCodeUser(code: string, did: string, handle: string): void { + const stmt = this.db.prepare('UPDATE authorization_codes SET did = ?, handle = ? WHERE code = ?'); + stmt.run(did, handle, code); + } + cleanupExpiredAuthorizationCodes(): number { const now = Math.floor(Date.now() / 1000); const stmt = this.db.prepare('DELETE FROM authorization_codes WHERE expires_at < ?');