mirror of
https://github.com/OpenBankProject/API-Explorer-II.git
synced 2026-02-06 10:47:04 +00:00
can login again
This commit is contained in:
parent
fa7866e981
commit
e34b939a0e
@ -50,9 +50,8 @@ import { OAuth2CallbackController } from './controllers/OAuth2CallbackController
|
||||
import { OAuth2ConnectController } from './controllers/OAuth2ConnectController.js'
|
||||
import { OAuth2ProvidersController } from './controllers/OAuth2ProvidersController.js'
|
||||
|
||||
// Import middlewares
|
||||
import OAuth2AuthorizationMiddleware from './middlewares/OAuth2AuthorizationMiddleware.js'
|
||||
import OAuth2CallbackMiddleware from './middlewares/OAuth2CallbackMiddleware.js'
|
||||
// Import OAuth2 routes (plain Express, not routing-controllers)
|
||||
import oauth2Routes from './routes/oauth2.js'
|
||||
|
||||
// ES module equivalent of __dirname
|
||||
const __filename = fileURLToPath(import.meta.url)
|
||||
@ -226,18 +225,14 @@ let instance: any
|
||||
|
||||
const routePrefix = '/api'
|
||||
|
||||
// Register OAuth2 routes BEFORE routing-controllers (plain Express)
|
||||
app.use(routePrefix, oauth2Routes)
|
||||
console.log('OAuth2 routes registered (plain Express)')
|
||||
|
||||
const server = useExpressServer(app, {
|
||||
routePrefix: routePrefix,
|
||||
controllers: [
|
||||
OpeyController,
|
||||
OBPController,
|
||||
StatusController,
|
||||
UserController,
|
||||
OAuth2CallbackController,
|
||||
OAuth2ConnectController,
|
||||
OAuth2ProvidersController
|
||||
],
|
||||
middlewares: [OAuth2AuthorizationMiddleware, OAuth2CallbackMiddleware]
|
||||
controllers: [OpeyController, OBPController, StatusController, UserController],
|
||||
middlewares: []
|
||||
})
|
||||
|
||||
instance = server.listen(port)
|
||||
|
||||
@ -25,30 +25,25 @@
|
||||
*
|
||||
*/
|
||||
|
||||
import { Controller, Req, Res, Get, QueryParam } from 'routing-controllers'
|
||||
import { Controller, Req, Res, Get, UseBefore } from 'routing-controllers'
|
||||
import type { Request, Response } from 'express'
|
||||
import { Service, Container } from 'typedi'
|
||||
import { OAuth2Service } from '../services/OAuth2Service.js'
|
||||
import { OAuth2ProviderManager } from '../services/OAuth2ProviderManager.js'
|
||||
import type { UserInfo } from '../types/oauth2.js'
|
||||
import { Service } from 'typedi'
|
||||
import OAuth2CallbackMiddleware from '../middlewares/OAuth2CallbackMiddleware.js'
|
||||
|
||||
/**
|
||||
* OAuth2 Callback Controller (Multi-Provider)
|
||||
* OAuth2 Callback Controller
|
||||
*
|
||||
* Handles the OAuth2/OIDC callback from any configured identity provider.
|
||||
* Handles the OAuth2/OIDC callback from the identity provider.
|
||||
* This controller receives the authorization code and state parameter
|
||||
* after the user authenticates with the OIDC provider.
|
||||
*
|
||||
* This controller handles:
|
||||
* The OAuth2CallbackMiddleware handles:
|
||||
* - State validation (CSRF protection)
|
||||
* - Authorization code exchange for tokens
|
||||
* - User info retrieval
|
||||
* - Session storage
|
||||
* - Redirect to original page
|
||||
*
|
||||
* Supports both multi-provider mode (retrieves provider from session) and
|
||||
* legacy single-provider mode (uses existing OAuth2Service).
|
||||
*
|
||||
* Endpoint: GET /oauth2/callback
|
||||
*
|
||||
* Query Parameters (from OIDC provider):
|
||||
@ -57,23 +52,21 @@ import type { UserInfo } from '../types/oauth2.js'
|
||||
* - error (optional): Error code if authentication failed
|
||||
* - error_description (optional): Human-readable error description
|
||||
*
|
||||
* Multi-Provider Flow:
|
||||
* Flow:
|
||||
* OIDC Provider → /oauth2/callback?code=XXX&state=YYY
|
||||
* → Retrieve provider from session → Use correct OAuth2 client
|
||||
* → Exchange code for tokens → Original Page (with authenticated session)
|
||||
* → OAuth2CallbackMiddleware → Original Page (with authenticated session)
|
||||
*
|
||||
* Success Flow:
|
||||
* 1. Validate state parameter
|
||||
* 2. Retrieve provider from session (or use legacy service)
|
||||
* 3. Exchange authorization code for tokens (access, refresh, ID)
|
||||
* 4. Fetch user information from UserInfo endpoint
|
||||
* 5. Store tokens, provider name, and user data in session
|
||||
* 6. Redirect to original page or home
|
||||
* 2. Exchange authorization code for tokens (access, refresh, ID)
|
||||
* 3. Fetch user information from UserInfo endpoint
|
||||
* 4. Store tokens and user data in session
|
||||
* 5. Redirect to original page or home
|
||||
*
|
||||
* Error Flow:
|
||||
* 1. Parse error from query parameters
|
||||
* 2. Log error details
|
||||
* 3. Redirect to home with error parameter
|
||||
* 2. Display user-friendly error page
|
||||
* 3. Allow user to retry authentication
|
||||
*
|
||||
* @example
|
||||
* // Successful callback URL from OIDC provider
|
||||
@ -84,216 +77,22 @@ import type { UserInfo } from '../types/oauth2.js'
|
||||
*/
|
||||
@Service()
|
||||
@Controller()
|
||||
@UseBefore(OAuth2CallbackMiddleware)
|
||||
export class OAuth2CallbackController {
|
||||
private providerManager: OAuth2ProviderManager
|
||||
private legacyOAuth2Service: OAuth2Service
|
||||
|
||||
constructor() {
|
||||
this.providerManager = Container.get(OAuth2ProviderManager)
|
||||
this.legacyOAuth2Service = Container.get(OAuth2Service)
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle OAuth2/OIDC callback
|
||||
*
|
||||
* Processes the callback from any configured OIDC provider.
|
||||
* Supports both multi-provider mode and legacy single-provider mode.
|
||||
* The actual logic is handled by OAuth2CallbackMiddleware.
|
||||
* This method exists only as the routing endpoint definition.
|
||||
*
|
||||
* @param code - Authorization code from OIDC provider
|
||||
* @param state - State parameter for CSRF validation
|
||||
* @param error - Error code if authentication failed
|
||||
* @param errorDescription - Human-readable error description
|
||||
* @param request - Express request object
|
||||
* @param response - Express response object
|
||||
* @returns Response with redirect to original page or error page
|
||||
* @param {Request} request - Express request object with query params (code, state)
|
||||
* @param {Response} response - Express response object (redirected by middleware)
|
||||
* @returns {Response} Response object (handled by middleware)
|
||||
*/
|
||||
@Get('/oauth2/callback')
|
||||
async callback(
|
||||
@QueryParam('code') code: string,
|
||||
@QueryParam('state') state: string,
|
||||
@QueryParam('error') error: string,
|
||||
@QueryParam('error_description') errorDescription: string,
|
||||
@Req() request: Request,
|
||||
@Res() response: Response
|
||||
): Promise<Response> {
|
||||
console.log('OAuth2CallbackController: Processing OAuth2 callback')
|
||||
|
||||
const session = request.session as any
|
||||
|
||||
// Handle error from provider
|
||||
if (error) {
|
||||
console.error(`OAuth2CallbackController: Error from provider: ${error}`)
|
||||
console.error(`OAuth2CallbackController: Description: ${errorDescription || 'N/A'}`)
|
||||
return response.redirect(`/?oauth2_error=${encodeURIComponent(error)}`)
|
||||
}
|
||||
|
||||
// Validate required parameters
|
||||
if (!code) {
|
||||
console.error('OAuth2CallbackController: Missing authorization code')
|
||||
return response.redirect('/?oauth2_error=missing_code')
|
||||
}
|
||||
|
||||
if (!state) {
|
||||
console.error('OAuth2CallbackController: Missing state parameter')
|
||||
return response.redirect('/?oauth2_error=missing_state')
|
||||
}
|
||||
|
||||
// Validate state (CSRF protection)
|
||||
const storedState = session.oauth2_state
|
||||
if (!storedState || storedState !== state) {
|
||||
console.error('OAuth2CallbackController: State mismatch (CSRF protection)')
|
||||
console.error(` Expected: ${storedState}`)
|
||||
console.error(` Received: ${state}`)
|
||||
return response.redirect('/?oauth2_error=invalid_state')
|
||||
}
|
||||
|
||||
// Get code verifier from session (PKCE)
|
||||
const codeVerifier = session.oauth2_code_verifier
|
||||
if (!codeVerifier) {
|
||||
console.error('OAuth2CallbackController: Code verifier not found in session')
|
||||
return response.redirect('/?oauth2_error=missing_verifier')
|
||||
}
|
||||
|
||||
// Check if multi-provider mode (provider stored in session)
|
||||
const provider = session.oauth2_provider
|
||||
|
||||
try {
|
||||
if (provider) {
|
||||
// Multi-provider mode
|
||||
await this.handleMultiProviderCallback(session, code, codeVerifier, provider)
|
||||
} else {
|
||||
// Legacy single-provider mode
|
||||
await this.handleLegacyCallback(session, code, codeVerifier)
|
||||
}
|
||||
|
||||
// Clean up temporary session data
|
||||
delete session.oauth2_code_verifier
|
||||
delete session.oauth2_state
|
||||
|
||||
// Redirect to original page
|
||||
const redirectUrl = session.oauth2_redirect_page || '/'
|
||||
delete session.oauth2_redirect_page
|
||||
|
||||
console.log(
|
||||
`OAuth2CallbackController: Authentication successful, redirecting to: ${redirectUrl}`
|
||||
)
|
||||
return response.redirect(redirectUrl)
|
||||
} catch (error) {
|
||||
console.error('OAuth2CallbackController: Token exchange failed:', error)
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error'
|
||||
return response.redirect(
|
||||
`/?oauth2_error=token_exchange_failed&details=${encodeURIComponent(errorMessage)}`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle multi-provider callback
|
||||
*/
|
||||
private async handleMultiProviderCallback(
|
||||
session: any,
|
||||
code: string,
|
||||
codeVerifier: string,
|
||||
provider: string
|
||||
): Promise<void> {
|
||||
console.log(`OAuth2CallbackController: Multi-provider mode - ${provider}`)
|
||||
|
||||
const client = this.providerManager.getProvider(provider)
|
||||
if (!client) {
|
||||
throw new Error(`Provider not found: ${provider}`)
|
||||
}
|
||||
|
||||
// Exchange code for tokens
|
||||
console.log(`OAuth2CallbackController: Exchanging authorization code for tokens`)
|
||||
const tokens = await client.exchangeAuthorizationCode(code, codeVerifier)
|
||||
|
||||
// Store tokens in session
|
||||
session.oauth2_access_token = tokens.accessToken
|
||||
session.oauth2_refresh_token = tokens.refreshToken
|
||||
session.oauth2_id_token = tokens.idToken
|
||||
session.oauth2_provider = provider
|
||||
|
||||
console.log(`OAuth2CallbackController: Tokens received and stored`)
|
||||
|
||||
// Fetch user info
|
||||
console.log(`OAuth2CallbackController: Fetching user info`)
|
||||
const userInfo = await this.fetchUserInfo(client, tokens.accessToken)
|
||||
|
||||
// Store user in session
|
||||
session.user = {
|
||||
username: userInfo.preferred_username || userInfo.email || userInfo.sub,
|
||||
email: userInfo.email,
|
||||
name: userInfo.name,
|
||||
provider: provider,
|
||||
sub: userInfo.sub
|
||||
}
|
||||
|
||||
console.log(
|
||||
`OAuth2CallbackController: User authenticated via ${provider}: ${session.user.username}`
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle legacy single-provider callback
|
||||
*/
|
||||
private async handleLegacyCallback(
|
||||
session: any,
|
||||
code: string,
|
||||
codeVerifier: string
|
||||
): Promise<void> {
|
||||
console.log('OAuth2CallbackController: Legacy single-provider mode')
|
||||
|
||||
// Exchange code for tokens using legacy service
|
||||
console.log(`OAuth2CallbackController: Exchanging authorization code for tokens`)
|
||||
const tokens = await this.legacyOAuth2Service.exchangeCodeForTokens(code, codeVerifier)
|
||||
|
||||
// Store tokens in session
|
||||
session.oauth2_access_token = tokens.accessToken
|
||||
session.oauth2_refresh_token = tokens.refreshToken
|
||||
session.oauth2_id_token = tokens.idToken
|
||||
|
||||
console.log(`OAuth2CallbackController: Tokens received and stored`)
|
||||
|
||||
// Fetch user info
|
||||
console.log(`OAuth2CallbackController: Fetching user info`)
|
||||
const userInfo = await this.legacyOAuth2Service.getUserInfo(tokens.accessToken)
|
||||
|
||||
// Store user in session
|
||||
session.user = {
|
||||
username: userInfo.preferred_username || userInfo.email || userInfo.sub,
|
||||
email: userInfo.email,
|
||||
name: userInfo.name,
|
||||
sub: userInfo.sub
|
||||
}
|
||||
|
||||
console.log(`OAuth2CallbackController: User authenticated (legacy): ${session.user.username}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch user info from UserInfo endpoint
|
||||
*/
|
||||
private async fetchUserInfo(client: any, accessToken: string): Promise<UserInfo> {
|
||||
const userInfoEndpoint = client.getUserInfoEndpoint()
|
||||
|
||||
console.log(`OAuth2CallbackController: Calling UserInfo endpoint: ${userInfoEndpoint}`)
|
||||
|
||||
const response = await fetch(userInfoEndpoint, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
Accept: 'application/json'
|
||||
}
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
throw new Error(
|
||||
`UserInfo request failed: ${response.status} ${response.statusText} - ${errorText}`
|
||||
)
|
||||
}
|
||||
|
||||
const userInfo = await response.json()
|
||||
console.log(`OAuth2CallbackController: UserInfo retrieved: ${userInfo.sub}`)
|
||||
|
||||
return userInfo as UserInfo
|
||||
callback(@Req() request: Request, @Res() response: Response): Response {
|
||||
// The middleware handles all the logic and redirects the user
|
||||
// This method should never actually execute
|
||||
return response
|
||||
}
|
||||
}
|
||||
|
||||
@ -25,175 +25,53 @@
|
||||
*
|
||||
*/
|
||||
|
||||
import { Controller, Req, Res, Get, QueryParam } from 'routing-controllers'
|
||||
import { Controller, Req, Res, Get, UseBefore } from 'routing-controllers'
|
||||
import type { Request, Response } from 'express'
|
||||
import { Service, Container } from 'typedi'
|
||||
import { OAuth2Service } from '../services/OAuth2Service.js'
|
||||
import { OAuth2ProviderManager } from '../services/OAuth2ProviderManager.js'
|
||||
import { PKCEUtils } from '../utils/pkce.js'
|
||||
import { Service } from 'typedi'
|
||||
import OAuth2AuthorizationMiddleware from '../middlewares/OAuth2AuthorizationMiddleware.js'
|
||||
|
||||
/**
|
||||
* OAuth2 Connect Controller (Multi-Provider)
|
||||
* OAuth2 Connect Controller
|
||||
*
|
||||
* Handles the OAuth2/OIDC login initiation endpoint with support for multiple providers.
|
||||
* This controller generates PKCE parameters and redirects to the selected OIDC provider.
|
||||
* Handles the OAuth2/OIDC login initiation endpoint.
|
||||
* This controller triggers the OAuth2 authorization flow by delegating to
|
||||
* the OAuth2AuthorizationMiddleware which generates PKCE parameters and
|
||||
* redirects to the OIDC provider.
|
||||
*
|
||||
* Endpoint: GET /oauth2/connect
|
||||
*
|
||||
* Query Parameters:
|
||||
* - provider (optional): Provider name (e.g., "obp-oidc", "keycloak")
|
||||
* - redirect (optional): URL to redirect to after successful authentication
|
||||
*
|
||||
* Multi-Provider Flow:
|
||||
* User selects provider → /oauth2/connect?provider=obp-oidc&redirect=/resource-docs
|
||||
* → Generate PKCE → Store in session → Redirect to OIDC provider
|
||||
*
|
||||
* Legacy Flow (backward compatible):
|
||||
* User clicks login → /oauth2/connect → Uses existing OAuth2Service (single provider)
|
||||
* Flow:
|
||||
* User clicks login → /oauth2/connect → OAuth2AuthorizationMiddleware
|
||||
* → OIDC Provider Authorization Endpoint
|
||||
*
|
||||
* @example
|
||||
* // Multi-provider login
|
||||
* <a href="/oauth2/connect?provider=obp-oidc&redirect=/messages">Login with OBP-OIDC</a>
|
||||
*
|
||||
* // Legacy single-provider login (backward compatible)
|
||||
* // User initiates login
|
||||
* <a href="/oauth2/connect?redirect=/messages">Login</a>
|
||||
*
|
||||
* // JavaScript redirect
|
||||
* window.location.href = '/oauth2/connect?redirect=' + encodeURIComponent(window.location.pathname)
|
||||
*/
|
||||
@Service()
|
||||
@Controller()
|
||||
@UseBefore(OAuth2AuthorizationMiddleware)
|
||||
export class OAuth2ConnectController {
|
||||
private providerManager: OAuth2ProviderManager
|
||||
private legacyOAuth2Service: OAuth2Service
|
||||
|
||||
constructor() {
|
||||
this.providerManager = Container.get(OAuth2ProviderManager)
|
||||
this.legacyOAuth2Service = Container.get(OAuth2Service)
|
||||
}
|
||||
|
||||
/**
|
||||
* Initiate OAuth2/OIDC authentication flow
|
||||
*
|
||||
* Supports both multi-provider mode (with provider parameter) and legacy single-provider mode.
|
||||
* The actual logic is handled by OAuth2AuthorizationMiddleware.
|
||||
* This method exists only as the routing endpoint definition.
|
||||
*
|
||||
* @param provider - Provider name (e.g., "obp-oidc", "keycloak") - optional for backward compatibility
|
||||
* @param redirect - URL to redirect after authentication - optional
|
||||
* @param request - Express request object
|
||||
* @param response - Express response object
|
||||
* @returns Response with redirect to OIDC provider
|
||||
* @param {Request} request - Express request object
|
||||
* @param {Response} response - Express response object (redirected by middleware)
|
||||
* @returns {Response} Response object (handled by middleware)
|
||||
*/
|
||||
@Get('/oauth2/connect')
|
||||
connect(
|
||||
@QueryParam('provider') provider: string,
|
||||
@QueryParam('redirect') redirect: string,
|
||||
@Req() request: Request,
|
||||
@Res() response: Response
|
||||
): Response {
|
||||
console.log('OAuth2ConnectController: Starting authentication flow')
|
||||
console.log(` Provider: ${provider || '(legacy mode)'}`)
|
||||
console.log(` Redirect: ${redirect || '/'}`)
|
||||
|
||||
const session = request.session as any
|
||||
|
||||
// Store redirect URL in session
|
||||
session.oauth2_redirect_page = redirect || '/'
|
||||
|
||||
// Multi-provider mode: Use provider from query param
|
||||
if (provider) {
|
||||
return this.handleMultiProviderLogin(provider, session, response)
|
||||
}
|
||||
|
||||
// Legacy single-provider mode: Use existing OAuth2Service
|
||||
return this.handleLegacyLogin(session, response)
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle multi-provider login
|
||||
*/
|
||||
private handleMultiProviderLogin(provider: string, session: any, response: Response): Response {
|
||||
console.log(`OAuth2ConnectController: Multi-provider mode - ${provider}`)
|
||||
|
||||
const client = this.providerManager.getProvider(provider)
|
||||
|
||||
if (!client) {
|
||||
console.error(`OAuth2ConnectController: Provider not found: ${provider}`)
|
||||
const availableProviders = this.providerManager.getAvailableProviders()
|
||||
console.error(
|
||||
`OAuth2ConnectController: Available providers: ${availableProviders.join(', ') || 'none'}`
|
||||
)
|
||||
return response.status(400).json({
|
||||
error: 'invalid_provider',
|
||||
message: `Provider "${provider}" is not available`,
|
||||
availableProviders: availableProviders
|
||||
})
|
||||
}
|
||||
|
||||
// Store provider name in session for callback
|
||||
session.oauth2_provider = provider
|
||||
|
||||
// Generate PKCE parameters
|
||||
const codeVerifier = PKCEUtils.generateCodeVerifier()
|
||||
const codeChallenge = PKCEUtils.generateCodeChallenge(codeVerifier)
|
||||
const state = PKCEUtils.generateState()
|
||||
|
||||
// Store in session
|
||||
session.oauth2_code_verifier = codeVerifier
|
||||
session.oauth2_state = state
|
||||
|
||||
// Build authorization URL
|
||||
const authUrl = this.buildAuthorizationUrl(client, state, codeChallenge)
|
||||
|
||||
console.log(`OAuth2ConnectController: Redirecting to ${provider} authorization endpoint`)
|
||||
return response.redirect(authUrl)
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle legacy single-provider login
|
||||
*/
|
||||
private handleLegacyLogin(session: any, response: Response): Response {
|
||||
console.log('OAuth2ConnectController: Legacy single-provider mode')
|
||||
|
||||
if (!this.legacyOAuth2Service.isInitialized()) {
|
||||
console.error('OAuth2ConnectController: OAuth2 service not initialized')
|
||||
return response.status(503).json({
|
||||
error: 'oauth2_unavailable',
|
||||
message: 'OAuth2 authentication is not available'
|
||||
})
|
||||
}
|
||||
|
||||
// Generate PKCE parameters
|
||||
const codeVerifier = PKCEUtils.generateCodeVerifier()
|
||||
const codeChallenge = PKCEUtils.generateCodeChallenge(codeVerifier)
|
||||
const state = PKCEUtils.generateState()
|
||||
|
||||
// Store in session
|
||||
session.oauth2_code_verifier = codeVerifier
|
||||
session.oauth2_state = state
|
||||
|
||||
// Use legacy service to create authorization URL
|
||||
const authUrl = this.legacyOAuth2Service.createAuthorizationURL(state, [
|
||||
'openid',
|
||||
'profile',
|
||||
'email'
|
||||
])
|
||||
|
||||
console.log('OAuth2ConnectController: Redirecting to legacy OIDC provider')
|
||||
return response.redirect(authUrl.toString())
|
||||
}
|
||||
|
||||
/**
|
||||
* Build authorization URL for multi-provider
|
||||
*/
|
||||
private buildAuthorizationUrl(client: any, state: string, codeChallenge: string): string {
|
||||
const authEndpoint = client.getAuthorizationEndpoint()
|
||||
const params = new URLSearchParams({
|
||||
client_id: client.clientId,
|
||||
redirect_uri: client.getRedirectUri(),
|
||||
response_type: 'code',
|
||||
scope: 'openid profile email',
|
||||
state: state,
|
||||
code_challenge: codeChallenge,
|
||||
code_challenge_method: 'S256'
|
||||
})
|
||||
|
||||
return `${authEndpoint}?${params.toString()}`
|
||||
connect(@Req() request: Request, @Res() response: Response): Response {
|
||||
// The middleware handles all the logic and redirects the user
|
||||
// This method should never actually execute
|
||||
return response
|
||||
}
|
||||
}
|
||||
|
||||
311
server/routes/oauth2.ts
Normal file
311
server/routes/oauth2.ts
Normal file
@ -0,0 +1,311 @@
|
||||
/*
|
||||
* Open Bank Project - API Explorer II
|
||||
* Copyright (C) 2023-2025, TESOBE GmbH
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* Email: contact@tesobe.com
|
||||
* TESOBE GmbH
|
||||
* Osloerstrasse 16/17
|
||||
* Berlin 13359, Germany
|
||||
*
|
||||
* This product includes software developed at
|
||||
* TESOBE (http://www.tesobe.com/)
|
||||
*
|
||||
*/
|
||||
|
||||
import { Router } from 'express'
|
||||
import type { Request, Response } from 'express'
|
||||
import { Container } from 'typedi'
|
||||
import { OAuth2Service } from '../services/OAuth2Service.js'
|
||||
import { OAuth2ProviderManager } from '../services/OAuth2ProviderManager.js'
|
||||
import { PKCEUtils } from '../utils/pkce.js'
|
||||
import type { UserInfo } from '../types/oauth2.js'
|
||||
|
||||
const router = Router()
|
||||
|
||||
// Get services from container
|
||||
const providerManager = Container.get(OAuth2ProviderManager)
|
||||
const legacyOAuth2Service = Container.get(OAuth2Service)
|
||||
|
||||
/**
|
||||
* GET /oauth2/providers
|
||||
* Get list of available OAuth2 providers
|
||||
*/
|
||||
router.get('/oauth2/providers', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const availableProviders = providerManager.getAvailableProviders()
|
||||
const providerList = availableProviders.map((name) => {
|
||||
const status = providerManager.getProviderStatus(name)
|
||||
return {
|
||||
name,
|
||||
status: status?.status || 'unknown',
|
||||
available: status?.available || false
|
||||
}
|
||||
})
|
||||
|
||||
res.json({ providers: providerList })
|
||||
} catch (error) {
|
||||
console.error('Error fetching providers:', error)
|
||||
res.status(500).json({ error: 'Failed to fetch providers' })
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* GET /oauth2/connect
|
||||
* Initiate OAuth2 authentication flow
|
||||
* Query params:
|
||||
* - provider: Provider name (optional, uses legacy if not specified)
|
||||
* - redirect: URL to redirect after auth (optional)
|
||||
*/
|
||||
router.get('/oauth2/connect', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const provider = req.query.provider as string | undefined
|
||||
const redirect = (req.query.redirect as string) || '/'
|
||||
const session = req.session as any
|
||||
|
||||
console.log('OAuth2 Connect: Starting authentication flow')
|
||||
console.log(` Provider: ${provider || '(legacy mode)'}`)
|
||||
console.log(` Redirect: ${redirect}`)
|
||||
|
||||
// Store redirect URL in session
|
||||
session.oauth2_redirect_page = redirect
|
||||
|
||||
// Generate PKCE parameters
|
||||
const codeVerifier = PKCEUtils.generateCodeVerifier()
|
||||
const codeChallenge = PKCEUtils.generateCodeChallenge(codeVerifier)
|
||||
const state = PKCEUtils.generateState()
|
||||
|
||||
// Store in session
|
||||
session.oauth2_code_verifier = codeVerifier
|
||||
session.oauth2_state = state
|
||||
|
||||
let authUrl: string
|
||||
|
||||
if (provider) {
|
||||
// Multi-provider mode
|
||||
console.log(`OAuth2 Connect: Using multi-provider mode - ${provider}`)
|
||||
|
||||
const client = providerManager.getProvider(provider)
|
||||
if (!client) {
|
||||
const availableProviders = providerManager.getAvailableProviders()
|
||||
console.error(`OAuth2 Connect: Provider not found: ${provider}`)
|
||||
return res.status(400).json({
|
||||
error: 'invalid_provider',
|
||||
message: `Provider "${provider}" is not available`,
|
||||
availableProviders
|
||||
})
|
||||
}
|
||||
|
||||
// Store provider name for callback
|
||||
session.oauth2_provider = provider
|
||||
|
||||
// Build authorization URL
|
||||
const authEndpoint = client.getAuthorizationEndpoint()
|
||||
const params = new URLSearchParams({
|
||||
client_id: client.clientId,
|
||||
redirect_uri: client.getRedirectUri(),
|
||||
response_type: 'code',
|
||||
scope: 'openid profile email',
|
||||
state: state,
|
||||
code_challenge: codeChallenge,
|
||||
code_challenge_method: 'S256'
|
||||
})
|
||||
|
||||
authUrl = `${authEndpoint}?${params.toString()}`
|
||||
} else {
|
||||
// Legacy single-provider mode
|
||||
console.log('OAuth2 Connect: Using legacy single-provider mode')
|
||||
|
||||
if (!legacyOAuth2Service.isInitialized()) {
|
||||
console.error('OAuth2 Connect: OAuth2 service not initialized')
|
||||
return res.status(503).json({
|
||||
error: 'oauth2_unavailable',
|
||||
message: 'OAuth2 authentication is not available'
|
||||
})
|
||||
}
|
||||
|
||||
authUrl = legacyOAuth2Service
|
||||
.createAuthorizationURL(state, ['openid', 'profile', 'email'])
|
||||
.toString()
|
||||
}
|
||||
|
||||
// Save session before redirect
|
||||
session.save((err: any) => {
|
||||
if (err) {
|
||||
console.error('OAuth2 Connect: Failed to save session:', err)
|
||||
return res.status(500).json({ error: 'session_error' })
|
||||
}
|
||||
|
||||
console.log('OAuth2 Connect: Redirecting to authorization endpoint')
|
||||
res.redirect(authUrl)
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('OAuth2 Connect: Error:', error)
|
||||
res.status(500).json({
|
||||
error: 'authentication_failed',
|
||||
message: error instanceof Error ? error.message : 'Unknown error'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* GET /oauth2/callback
|
||||
* Handle OAuth2 callback after user authentication
|
||||
* Query params:
|
||||
* - code: Authorization code
|
||||
* - state: State parameter for CSRF validation
|
||||
* - error: Error code (if auth failed)
|
||||
* - error_description: Error description
|
||||
*/
|
||||
router.get('/oauth2/callback', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const code = req.query.code as string
|
||||
const state = req.query.state as string
|
||||
const error = req.query.error as string
|
||||
const errorDescription = req.query.error_description as string
|
||||
const session = req.session as any
|
||||
|
||||
console.log('OAuth2 Callback: Processing callback')
|
||||
|
||||
// Handle error from provider
|
||||
if (error) {
|
||||
console.error(`OAuth2 Callback: Error from provider: ${error}`)
|
||||
console.error(`OAuth2 Callback: Description: ${errorDescription || 'N/A'}`)
|
||||
return res.redirect(`/?oauth2_error=${encodeURIComponent(error)}`)
|
||||
}
|
||||
|
||||
// Validate required parameters
|
||||
if (!code) {
|
||||
console.error('OAuth2 Callback: Missing authorization code')
|
||||
return res.redirect('/?oauth2_error=missing_code')
|
||||
}
|
||||
|
||||
if (!state) {
|
||||
console.error('OAuth2 Callback: Missing state parameter')
|
||||
return res.redirect('/?oauth2_error=missing_state')
|
||||
}
|
||||
|
||||
// Validate state (CSRF protection)
|
||||
const storedState = session.oauth2_state
|
||||
if (!storedState || storedState !== state) {
|
||||
console.error('OAuth2 Callback: State mismatch (CSRF protection)')
|
||||
return res.redirect('/?oauth2_error=invalid_state')
|
||||
}
|
||||
|
||||
// Get code verifier from session (PKCE)
|
||||
const codeVerifier = session.oauth2_code_verifier
|
||||
if (!codeVerifier) {
|
||||
console.error('OAuth2 Callback: Code verifier not found in session')
|
||||
return res.redirect('/?oauth2_error=missing_verifier')
|
||||
}
|
||||
|
||||
// Check if multi-provider mode
|
||||
const provider = session.oauth2_provider
|
||||
|
||||
let tokens: any
|
||||
let userInfo: UserInfo
|
||||
|
||||
if (provider) {
|
||||
// Multi-provider mode
|
||||
console.log(`OAuth2 Callback: Multi-provider mode - ${provider}`)
|
||||
|
||||
const client = providerManager.getProvider(provider)
|
||||
if (!client) {
|
||||
console.error(`OAuth2 Callback: Provider not found: ${provider}`)
|
||||
return res.redirect('/?oauth2_error=provider_not_found')
|
||||
}
|
||||
|
||||
// Exchange code for tokens
|
||||
console.log('OAuth2 Callback: Exchanging authorization code for tokens')
|
||||
tokens = await client.exchangeAuthorizationCode(code, codeVerifier)
|
||||
|
||||
// Fetch user info
|
||||
console.log('OAuth2 Callback: Fetching user info')
|
||||
const userInfoEndpoint = client.getUserInfoEndpoint()
|
||||
const userInfoResponse = await fetch(userInfoEndpoint, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${tokens.accessToken}`,
|
||||
Accept: 'application/json'
|
||||
}
|
||||
})
|
||||
|
||||
if (!userInfoResponse.ok) {
|
||||
throw new Error(`UserInfo request failed: ${userInfoResponse.status}`)
|
||||
}
|
||||
|
||||
userInfo = (await userInfoResponse.json()) as UserInfo
|
||||
|
||||
// Store provider in session
|
||||
session.oauth2_provider = provider
|
||||
} else {
|
||||
// Legacy single-provider mode
|
||||
console.log('OAuth2 Callback: Legacy single-provider mode')
|
||||
|
||||
// Exchange code for tokens
|
||||
tokens = await legacyOAuth2Service.exchangeCodeForTokens(code, codeVerifier)
|
||||
|
||||
// Fetch user info
|
||||
userInfo = await legacyOAuth2Service.getUserInfo(tokens.accessToken)
|
||||
}
|
||||
|
||||
// Store tokens in session
|
||||
session.oauth2_access_token = tokens.accessToken
|
||||
session.oauth2_refresh_token = tokens.refreshToken
|
||||
session.oauth2_id_token = tokens.idToken
|
||||
|
||||
console.log('OAuth2 Callback: Tokens received and stored')
|
||||
|
||||
// Store user in session (using oauth2_user key to match UserController)
|
||||
session.oauth2_user = {
|
||||
username: userInfo.preferred_username || userInfo.email || userInfo.sub,
|
||||
email: userInfo.email,
|
||||
email_verified: userInfo.email_verified || false,
|
||||
name: userInfo.name,
|
||||
given_name: userInfo.given_name,
|
||||
family_name: userInfo.family_name,
|
||||
provider: provider || 'obp-oidc',
|
||||
sub: userInfo.sub
|
||||
}
|
||||
|
||||
// Also store clientConfig for OBP API calls
|
||||
session.clientConfig = {
|
||||
oauth2: {
|
||||
accessToken: tokens.accessToken,
|
||||
tokenType: 'Bearer'
|
||||
}
|
||||
}
|
||||
|
||||
console.log(
|
||||
`OAuth2 Callback: User authenticated: ${session.oauth2_user.username} via ${session.oauth2_user.provider}`
|
||||
)
|
||||
|
||||
// Clean up temporary session data
|
||||
delete session.oauth2_code_verifier
|
||||
delete session.oauth2_state
|
||||
|
||||
// Redirect to original page
|
||||
const redirectUrl = session.oauth2_redirect_page || '/'
|
||||
delete session.oauth2_redirect_page
|
||||
|
||||
console.log(`OAuth2 Callback: Authentication successful, redirecting to: ${redirectUrl}`)
|
||||
res.redirect(redirectUrl)
|
||||
} catch (error) {
|
||||
console.error('OAuth2 Callback: Error:', error)
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error'
|
||||
res.redirect(`/?oauth2_error=token_exchange_failed&details=${encodeURIComponent(errorMessage)}`)
|
||||
}
|
||||
})
|
||||
|
||||
export default router
|
||||
Loading…
Reference in New Issue
Block a user