2525 *
2626 */
2727
28- import { Controller , Req , Res , Get , UseBefore } from 'routing-controllers'
28+ import { Controller , Req , Res , Get , QueryParam } from 'routing-controllers'
2929import type { Request , Response } from 'express'
30- import { Service } from 'typedi'
31- import OAuth2CallbackMiddleware from '../middlewares/OAuth2CallbackMiddleware.js'
30+ import { Service , Container } from 'typedi'
31+ import { OAuth2Service } from '../services/OAuth2Service.js'
32+ import { OAuth2ProviderManager } from '../services/OAuth2ProviderManager.js'
33+ import type { UserInfo } from '../types/oauth2.js'
3234
3335/**
34- * OAuth2 Callback Controller
36+ * OAuth2 Callback Controller (Multi-Provider)
3537 *
36- * Handles the OAuth2/OIDC callback from the identity provider.
38+ * Handles the OAuth2/OIDC callback from any configured identity provider.
3739 * This controller receives the authorization code and state parameter
3840 * after the user authenticates with the OIDC provider.
3941 *
40- * The OAuth2CallbackMiddleware handles:
42+ * This controller handles:
4143 * - State validation (CSRF protection)
4244 * - Authorization code exchange for tokens
4345 * - User info retrieval
4446 * - Session storage
4547 * - Redirect to original page
4648 *
49+ * Supports both multi-provider mode (retrieves provider from session) and
50+ * legacy single-provider mode (uses existing OAuth2Service).
51+ *
4752 * Endpoint: GET /oauth2/callback
4853 *
4954 * Query Parameters (from OIDC provider):
@@ -52,21 +57,23 @@ import OAuth2CallbackMiddleware from '../middlewares/OAuth2CallbackMiddleware.js
5257 * - error (optional): Error code if authentication failed
5358 * - error_description (optional): Human-readable error description
5459 *
55- * Flow:
60+ * Multi-Provider Flow:
5661 * OIDC Provider → /oauth2/callback?code=XXX&state=YYY
57- * → OAuth2CallbackMiddleware → Original Page (with authenticated session)
62+ * → Retrieve provider from session → Use correct OAuth2 client
63+ * → Exchange code for tokens → Original Page (with authenticated session)
5864 *
5965 * Success Flow:
6066 * 1. Validate state parameter
61- * 2. Exchange authorization code for tokens (access, refresh, ID)
62- * 3. Fetch user information from UserInfo endpoint
63- * 4. Store tokens and user data in session
64- * 5. Redirect to original page or home
67+ * 2. Retrieve provider from session (or use legacy service)
68+ * 3. Exchange authorization code for tokens (access, refresh, ID)
69+ * 4. Fetch user information from UserInfo endpoint
70+ * 5. Store tokens, provider name, and user data in session
71+ * 6. Redirect to original page or home
6572 *
6673 * Error Flow:
6774 * 1. Parse error from query parameters
68- * 2. Display user-friendly error page
69- * 3. Allow user to retry authentication
75+ * 2. Log error details
76+ * 3. Redirect to home with error parameter
7077 *
7178 * @example
7279 * // Successful callback URL from OIDC provider
@@ -77,22 +84,216 @@ import OAuth2CallbackMiddleware from '../middlewares/OAuth2CallbackMiddleware.js
7784 */
7885@Service ( )
7986@Controller ( )
80- @UseBefore ( OAuth2CallbackMiddleware )
8187export class OAuth2CallbackController {
88+ private providerManager : OAuth2ProviderManager
89+ private legacyOAuth2Service : OAuth2Service
90+
91+ constructor ( ) {
92+ this . providerManager = Container . get ( OAuth2ProviderManager )
93+ this . legacyOAuth2Service = Container . get ( OAuth2Service )
94+ }
95+
8296 /**
8397 * Handle OAuth2/OIDC callback
8498 *
85- * The actual logic is handled by OAuth2CallbackMiddleware .
86- * This method exists only as the routing endpoint definition .
99+ * Processes the callback from any configured OIDC provider .
100+ * Supports both multi-provider mode and legacy single-provider mode .
87101 *
88- * @param {Request } request - Express request object with query params (code, state)
89- * @param {Response } response - Express response object (redirected by middleware)
90- * @returns {Response } Response object (handled by middleware)
102+ * @param code - Authorization code from OIDC provider
103+ * @param state - State parameter for CSRF validation
104+ * @param error - Error code if authentication failed
105+ * @param errorDescription - Human-readable error description
106+ * @param request - Express request object
107+ * @param response - Express response object
108+ * @returns Response with redirect to original page or error page
91109 */
92110 @Get ( '/oauth2/callback' )
93- callback ( @Req ( ) request : Request , @Res ( ) response : Response ) : Response {
94- // The middleware handles all the logic and redirects the user
95- // This method should never actually execute
96- return response
111+ async callback (
112+ @QueryParam ( 'code' ) code : string ,
113+ @QueryParam ( 'state' ) state : string ,
114+ @QueryParam ( 'error' ) error : string ,
115+ @QueryParam ( 'error_description' ) errorDescription : string ,
116+ @Req ( ) request : Request ,
117+ @Res ( ) response : Response
118+ ) : Promise < Response > {
119+ console . log ( 'OAuth2CallbackController: Processing OAuth2 callback' )
120+
121+ const session = request . session as any
122+
123+ // Handle error from provider
124+ if ( error ) {
125+ console . error ( `OAuth2CallbackController: Error from provider: ${ error } ` )
126+ console . error ( `OAuth2CallbackController: Description: ${ errorDescription || 'N/A' } ` )
127+ return response . redirect ( `/?oauth2_error=${ encodeURIComponent ( error ) } ` )
128+ }
129+
130+ // Validate required parameters
131+ if ( ! code ) {
132+ console . error ( 'OAuth2CallbackController: Missing authorization code' )
133+ return response . redirect ( '/?oauth2_error=missing_code' )
134+ }
135+
136+ if ( ! state ) {
137+ console . error ( 'OAuth2CallbackController: Missing state parameter' )
138+ return response . redirect ( '/?oauth2_error=missing_state' )
139+ }
140+
141+ // Validate state (CSRF protection)
142+ const storedState = session . oauth2_state
143+ if ( ! storedState || storedState !== state ) {
144+ console . error ( 'OAuth2CallbackController: State mismatch (CSRF protection)' )
145+ console . error ( ` Expected: ${ storedState } ` )
146+ console . error ( ` Received: ${ state } ` )
147+ return response . redirect ( '/?oauth2_error=invalid_state' )
148+ }
149+
150+ // Get code verifier from session (PKCE)
151+ const codeVerifier = session . oauth2_code_verifier
152+ if ( ! codeVerifier ) {
153+ console . error ( 'OAuth2CallbackController: Code verifier not found in session' )
154+ return response . redirect ( '/?oauth2_error=missing_verifier' )
155+ }
156+
157+ // Check if multi-provider mode (provider stored in session)
158+ const provider = session . oauth2_provider
159+
160+ try {
161+ if ( provider ) {
162+ // Multi-provider mode
163+ await this . handleMultiProviderCallback ( session , code , codeVerifier , provider )
164+ } else {
165+ // Legacy single-provider mode
166+ await this . handleLegacyCallback ( session , code , codeVerifier )
167+ }
168+
169+ // Clean up temporary session data
170+ delete session . oauth2_code_verifier
171+ delete session . oauth2_state
172+
173+ // Redirect to original page
174+ const redirectUrl = session . oauth2_redirect_page || '/'
175+ delete session . oauth2_redirect_page
176+
177+ console . log (
178+ `OAuth2CallbackController: Authentication successful, redirecting to: ${ redirectUrl } `
179+ )
180+ return response . redirect ( redirectUrl )
181+ } catch ( error ) {
182+ console . error ( 'OAuth2CallbackController: Token exchange failed:' , error )
183+ const errorMessage = error instanceof Error ? error . message : 'Unknown error'
184+ return response . redirect (
185+ `/?oauth2_error=token_exchange_failed&details=${ encodeURIComponent ( errorMessage ) } `
186+ )
187+ }
188+ }
189+
190+ /**
191+ * Handle multi-provider callback
192+ */
193+ private async handleMultiProviderCallback (
194+ session : any ,
195+ code : string ,
196+ codeVerifier : string ,
197+ provider : string
198+ ) : Promise < void > {
199+ console . log ( `OAuth2CallbackController: Multi-provider mode - ${ provider } ` )
200+
201+ const client = this . providerManager . getProvider ( provider )
202+ if ( ! client ) {
203+ throw new Error ( `Provider not found: ${ provider } ` )
204+ }
205+
206+ // Exchange code for tokens
207+ console . log ( `OAuth2CallbackController: Exchanging authorization code for tokens` )
208+ const tokens = await client . validateAuthorizationCode ( code , codeVerifier )
209+
210+ // Store tokens in session
211+ session . oauth2_access_token = tokens . accessToken
212+ session . oauth2_refresh_token = tokens . refreshToken
213+ session . oauth2_id_token = tokens . idToken
214+ session . oauth2_provider = provider
215+
216+ console . log ( `OAuth2CallbackController: Tokens received and stored` )
217+
218+ // Fetch user info
219+ console . log ( `OAuth2CallbackController: Fetching user info` )
220+ const userInfo = await this . fetchUserInfo ( client , tokens . accessToken )
221+
222+ // Store user in session
223+ session . user = {
224+ username : userInfo . preferred_username || userInfo . email || userInfo . sub ,
225+ email : userInfo . email ,
226+ name : userInfo . name ,
227+ provider : provider ,
228+ sub : userInfo . sub
229+ }
230+
231+ console . log (
232+ `OAuth2CallbackController: User authenticated via ${ provider } : ${ session . user . username } `
233+ )
234+ }
235+
236+ /**
237+ * Handle legacy single-provider callback
238+ */
239+ private async handleLegacyCallback (
240+ session : any ,
241+ code : string ,
242+ codeVerifier : string
243+ ) : Promise < void > {
244+ console . log ( 'OAuth2CallbackController: Legacy single-provider mode' )
245+
246+ // Exchange code for tokens using legacy service
247+ console . log ( `OAuth2CallbackController: Exchanging authorization code for tokens` )
248+ const tokens = await this . legacyOAuth2Service . exchangeCodeForTokens ( code , codeVerifier )
249+
250+ // Store tokens in session
251+ session . oauth2_access_token = tokens . accessToken
252+ session . oauth2_refresh_token = tokens . refreshToken
253+ session . oauth2_id_token = tokens . idToken
254+
255+ console . log ( `OAuth2CallbackController: Tokens received and stored` )
256+
257+ // Fetch user info
258+ console . log ( `OAuth2CallbackController: Fetching user info` )
259+ const userInfo = await this . legacyOAuth2Service . getUserInfo ( tokens . accessToken )
260+
261+ // Store user in session
262+ session . user = {
263+ username : userInfo . preferred_username || userInfo . email || userInfo . sub ,
264+ email : userInfo . email ,
265+ name : userInfo . name ,
266+ sub : userInfo . sub
267+ }
268+
269+ console . log ( `OAuth2CallbackController: User authenticated (legacy): ${ session . user . username } ` )
270+ }
271+
272+ /**
273+ * Fetch user info from UserInfo endpoint
274+ */
275+ private async fetchUserInfo ( client : any , accessToken : string ) : Promise < UserInfo > {
276+ const userInfoEndpoint = client . getUserInfoEndpoint ( )
277+
278+ console . log ( `OAuth2CallbackController: Calling UserInfo endpoint: ${ userInfoEndpoint } ` )
279+
280+ const response = await fetch ( userInfoEndpoint , {
281+ headers : {
282+ Authorization : `Bearer ${ accessToken } ` ,
283+ Accept : 'application/json'
284+ }
285+ } )
286+
287+ if ( ! response . ok ) {
288+ const errorText = await response . text ( )
289+ throw new Error (
290+ `UserInfo request failed: ${ response . status } ${ response . statusText } - ${ errorText } `
291+ )
292+ }
293+
294+ const userInfo = await response . json ( )
295+ console . log ( `OAuth2CallbackController: UserInfo retrieved: ${ userInfo . sub } ` )
296+
297+ return userInfo as UserInfo
97298 }
98299}
0 commit comments