Skip to content

Commit 5b731df

Browse files
feat: implement step-up authentication mechanism
1 parent ccc65c4 commit 5b731df

21 files changed

Lines changed: 952 additions & 129 deletions

app/controllers/src/auth/auth.controller.ts

Lines changed: 67 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@ import {
2727
AgentTokenResponse,
2828
RefreshTokenRequest,
2929
AgentTokenRequest,
30-
GetUserInfo200Response
30+
GetUserInfo200Response,
31+
PrivilegedTokenResponse
3132
} from "@approvio/api"
3233
import {
3334
mapAgentChallengeRequestToService,
@@ -44,17 +45,21 @@ import {generateErrorPayload} from "@controllers/error"
4445
import {
4546
validateGenerateTokenRequest,
4647
validateRefreshAgentTokenRequest,
47-
validateRefreshTokenRequest
48+
validateRefreshTokenRequest,
49+
validateExchangePrivilegeTokenRequest
4850
} from "./auth.validators"
4951
import {
5052
generateErrorResponseForGenerateToken,
5153
generateErrorResponseForRefreshAgentToken,
5254
generateErrorResponseForRefreshUserToken,
5355
generateErrorResponseForEntityInfo,
5456
mapToTokenResponse,
55-
mapToEntityInfoResponse
57+
mapToEntityInfoResponse,
58+
generateErrorResponseForExchangePrivilegeToken,
59+
mapToPrivilegeTokenExchange
5660
} from "./auth.mappers"
5761
import {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

Comments
 (0)