@@ -27,7 +27,8 @@ import {
2727 AgentTokenResponse ,
2828 RefreshTokenRequest ,
2929 AgentTokenRequest ,
30- GetUserInfo200Response
30+ GetUserInfo200Response ,
31+ PrivilegedTokenResponse
3132} from "@approvio/api"
3233import {
3334 mapAgentChallengeRequestToService ,
@@ -44,17 +45,21 @@ import {generateErrorPayload} from "@controllers/error"
4445import {
4546 validateGenerateTokenRequest ,
4647 validateRefreshAgentTokenRequest ,
47- validateRefreshTokenRequest
48+ validateRefreshTokenRequest ,
49+ validateExchangePrivilegeTokenRequest
4850} from "./auth.validators"
4951import {
5052 generateErrorResponseForGenerateToken ,
5153 generateErrorResponseForRefreshAgentToken ,
5254 generateErrorResponseForRefreshUserToken ,
5355 generateErrorResponseForEntityInfo ,
5456 mapToTokenResponse ,
55- mapToEntityInfoResponse
57+ mapToEntityInfoResponse ,
58+ generateErrorResponseForExchangePrivilegeToken ,
59+ mapToPrivilegeTokenExchange
5660} from "./auth.mappers"
5761import { logSuccess } from "@utils"
62+ import { HttpStatusCode } from "axios"
5863
5964/**
6065 * ┌─────────────────────────────────────────────────────────────────────────────────────────┐
@@ -69,20 +74,19 @@ import {logSuccess} from "@utils"
6974 * │ │ │ │ challenge │ │ │
7075 * │ │ │ │ + Auth URL │ │ │
7176 * │ │ │ │ │ │ │
72- * │ │ │ │ Store PKCE ─────────────────────►│ Session │
77+ * │ │ │ │ Store PKCE ──────┼ ───────────────►│ Session │
7378 * │ │ │ │ {state, codeV} │ │ Storage │
7479 * │ │ │ │ │ │ │
7580 * │ │ 302 Redirect │ ◄───────────────│ Redirect to │ │ │
7681 * │ │ to OIDC │ │ OIDC Provider │ │ │
7782 * │ │ │ │ │ │ │
7883 * │ │ │ │ │ │ │
79- * │ │ │ │ │ │ │
8084 * │ 2. User Authentication │
81- * │ │ ─────────────────────────────────────────────────►│ User login │ │
85+ * │ │ ────────────────────────────────────────────────── ►│ User login │ │
8286 * │ │ │ │ │ & consent │ │
8387 * │ │ │ │ │ │ │
8488 * │ 3. GET /auth/callback?code=abc&state=xyz │
85- * │ │ ◄─────────────────────────────────────────────────│ Authorization │ │
89+ * │ │ ◄──────────────────────────────────────────────────┤ Authorization │ │
8690 * │ │ │ │ │ code callback │ │
8791 * │ │ │ │ │ │ │
8892 * │ │ ──────────────┼────────────────►│ Any server can │ │ │
@@ -92,42 +96,34 @@ import {logSuccess} from "@utils"
9296 * │ │ to success │ │ /success?code=.. │ │ │
9397 * │ │ │ │ │ │ │
9498 * │ │ │ │ │ │ │
95- * │ │ │ │ │ │ │
96- * │ │ │ │ │ │ │
97- * │ │ │ │ │ │ │
9899 * │ 4. POST /auth/token {code, state} │
99- * │ │ ──────────────┼────────────────►│ Retrieve PKCE ◄─────────────────│ Lookup by │
100- * │ │ │ │ data from DB │ │ state │
100+ * │ │ ──────────────┼────────────────►│ Retrieve PKCE ◄──┼ ────────────────┤ Lookup │
101+ * │ │ │ │ {codeVerifier} │ │ by state │
101102 * │ │ │ │ │ │ │
102- * │ │ │ │ Exchange code ────────────────── ►│ Token │
103- * │ │ │ │ + codeVerifier │ │ Exchange │
103+ * │ │ │ │ Exchange code ──►│ Token Exchange │ │
104+ * │ │ │ │ + codeVerifier │ │ │
104105 * │ │ │ │ │ │ │
105- * │ │ │ │ Get user info ────────────────── ►│ User │
106- * │ │ │ │ (basic claims) │ │ Claims │
106+ * │ │ │ │ Get user info ──►│ User Claims │ │
107+ * │ │ │ │ (basic claims) │ │ │
107108 * │ │ │ │ │ │ │
108- * │ │ │ │ Lookup user ── ──────────────────►│ Enhanced │
109- * │ │ │ │ orgRole & roles │ │ User Data │
109+ * │ │ │ │ JIT Provision ◄──┼ ────────────────┤ Find or │
110+ * │ │ │ │ (Find/Create) │ │ Create │
110111 * │ │ │ │ │ │ │
111- * │ │ Enhanced JWT │ ◄───────────────│ Generate JWT │ │ │
112- * │ │ w/ orgRole, │ │ with enhanced │ │ │
113- * │ │ roles, etc. │ │ payload │ │ │
112+ * │ │ App JWT │ ◄───────────────│ Generate JWT │ │ │
113+ * │ │ w/ orgRole │ │ payload │ │ │
114114 * │ │
115- * │ Enhanced JWT Payload: │
115+ * │ App JWT Payload: │
116116 * │ { │
117117 * │ "sub": "user-uuid", // OIDC standard │
118118 * │ "email": "user@example .com", // OIDC standard │
119119 * │ "name": "John Doe", // OIDC standard │
120- * │ "orgRole": "admin", // Enhanced: admin/member │
121- * │ "roles": [ // Enhanced: specific permissions │
122- * │ {"name": "approver", "scope": {"type": "space", "spaceId": "space-123"}}, │
123- * │ {"name": "viewer", "scope": {"type": "group", "groupId": "group-456"}} │
124- * │ ] │
120+ * │ "orgRole": "admin" // Extended: admin/member │
125121 * │ } │
126122 * │ │
127123 * │ Load Balancer Benefits: │
128124 * │ • PKCE data stored in shared database - any server can access │
129125 * │ • Stateless authentication - no server affinity required │
130- * │ • Enhanced JWT includes organizational context not available from OIDC provider │
126+ * │ • App JWT includes organizational context derived during JIT provisioning │
131127 * └─────────────────────────────────────────────────────────────────────────────────────────┘
132128 */
133129@Controller ( "auth" )
@@ -138,6 +134,7 @@ export class AuthController {
138134 ) { }
139135
140136 @PublicRoute ( )
137+ @HttpCode ( HttpStatusCode . Found )
141138 @Get ( "login" )
142139 async login ( @Res ( ) res : Response ) : Promise < void > {
143140 const result = await pipe (
@@ -321,4 +318,46 @@ export class AuthController {
321318
322319 return result . right
323320 }
321+
322+ @PublicRoute ( )
323+ @HttpCode ( HttpStatusCode . Found )
324+ @Get ( "initiatePrivilegedTokenExchange" )
325+ async initiatePrivilegeToken ( @Res ( ) res : Response ) : Promise < void > {
326+ const runInitiation = ( ) => this . authService . initiatePrivilegeTokenGeneration ( )
327+
328+ const result = await pipe ( runInitiation ( ) , logSuccess ( "Privilege token initiation started" , "AuthController" ) ) ( )
329+
330+ if ( isLeft ( result ) ) {
331+ Logger . error ( "Failed to initiate privilege token generation" , result . left )
332+ res . redirect ( "/auth/error" )
333+ return
334+ }
335+
336+ Logger . debug ( `Redirecting to IDP for step-up: ${ result . right } ` )
337+ res . redirect ( result . right )
338+ }
339+
340+ @Post ( "exchangePrivilegedToken" )
341+ @HttpCode ( 200 )
342+ async exchangePrivilegeToken (
343+ @Body ( ) body : unknown ,
344+ @GetAuthenticatedEntity ( ) requestor : AuthenticatedEntity
345+ ) : Promise < PrivilegedTokenResponse > {
346+ const result = await pipe (
347+ body ,
348+ TE . right ,
349+ TE . chainW ( rawBody => TE . fromEither ( validateExchangePrivilegeTokenRequest ( rawBody ) ) ) ,
350+ TE . chainEitherKW ( mapToPrivilegeTokenExchange ) ,
351+ TE . chainW ( mappedRequest => this . authService . exchangePrivilegeToken ( mappedRequest , requestor ) ) ,
352+ TE . map ( accessToken => ( { accessToken} ) ) ,
353+ logSuccess ( "Privilege token exchanged" , "AuthController" )
354+ ) ( )
355+
356+ if ( isLeft ( result ) ) {
357+ Logger . error ( "Privilege token exchange failed" , result . left )
358+ throw generateErrorResponseForExchangePrivilegeToken ( result . left , "Failed to exchange privilege token" )
359+ }
360+
361+ return result . right
362+ }
324363}
0 commit comments