1- import {
2- Controller ,
3- Get ,
4- Post ,
5- Res ,
6- Logger ,
7- Query ,
8- Body ,
9- HttpCode ,
10- BadRequestException ,
11- Headers ,
12- Req
13- } from "@nestjs/common"
14- import { Response , Request } from "express"
1+ import { Controller , Get , Post , Body , HttpCode , Logger , Headers , Req } from "@nestjs/common"
2+ import { Request } from "express"
153import { AuthService , GenerateChallengeRequest , IdentityService } from "@services"
164import { isLeft } from "fp-ts/lib/Either"
175import * as TE from "fp-ts/TaskEither"
186import { PublicRoute } from "../../../main/src/auth/jwt.authguard"
197import { GetAuthenticatedEntity } from "../../../main/src/auth"
208import {
21- TokenRequest ,
229 TokenResponse ,
23- SuccessfulAuthResponse ,
24- FailedAuthResponse ,
2510 AgentChallengeRequest ,
2611 AgentChallengeResponse ,
2712 AgentTokenResponse ,
2813 RefreshTokenRequest ,
2914 AgentTokenRequest ,
30- GetUserInfo200Response ,
31- PrivilegedTokenResponse
15+ GetUserInfo200Response
3216} from "@approvio/api"
3317import {
3418 mapAgentChallengeRequestToService ,
@@ -41,90 +25,26 @@ import {
4125} from "./agent-auth.mappers"
4226import { pipe } from "fp-ts/lib/function"
4327import { AuthenticatedEntity } from "@domain"
44- import { generateErrorPayload } from "@controllers/error"
45- import {
46- validateGenerateTokenRequest ,
47- validateRefreshAgentTokenRequest ,
48- validateRefreshTokenRequest ,
49- validateExchangePrivilegeTokenRequest
50- } from "./auth.validators"
28+ import { validateRefreshAgentTokenRequest } from "./auth.validators"
5129import {
52- generateErrorResponseForGenerateToken ,
5330 generateErrorResponseForRefreshAgentToken ,
54- generateErrorResponseForRefreshUserToken ,
5531 generateErrorResponseForEntityInfo ,
5632 mapToTokenResponse ,
57- mapToEntityInfoResponse ,
58- generateErrorResponseForExchangePrivilegeToken ,
59- mapToPrivilegeTokenExchange
33+ mapToEntityInfoResponse
6034} from "./auth.mappers"
6135import { logSuccess } from "@utils"
62- import { HttpStatusCode } from "axios"
6336
6437/**
65- * ┌─────────────────────────────────────────────────────────────────────────────────────────┐
66- * │ OIDC Authentication HTTP Endpoints Flow │
67- * │ (Load Balancer Compatible) │
68- * ├─────────────────────────────────────────────────────────────────────────────────────────┤
69- * │ │
70- * │ Frontend Load Balancer Backend A/B OIDC Provider Database │
71- * │ │ │ │ │ │ │
72- * │ 1. GET /auth/login │
73- * │ │ ──────────────┼────────────────►│ Generate PKCE │ │ │
74- * │ │ │ │ challenge │ │ │
75- * │ │ │ │ + Auth URL │ │ │
76- * │ │ │ │ │ │ │
77- * │ │ │ │ Store PKCE ──────┼───────────────►│ Session │
78- * │ │ │ │ {state, codeV} │ │ Storage │
79- * │ │ │ │ │ │ │
80- * │ │ 302 Redirect │ ◄───────────────│ Redirect to │ │ │
81- * │ │ to OIDC │ │ OIDC Provider │ │ │
82- * │ │ │ │ │ │ │
83- * │ │ │ │ │ │ │
84- * │ 2. User Authentication │
85- * │ │ ──────────────────────────────────────────────────►│ User login │ │
86- * │ │ │ │ │ & consent │ │
87- * │ │ │ │ │ │ │
88- * │ 3. GET /auth/callback?code=abc&state=xyz │
89- * │ │ ◄──────────────────────────────────────────────────┤ Authorization │ │
90- * │ │ │ │ │ code callback │ │
91- * │ │ │ │ │ │ │
92- * │ │ ──────────────┼────────────────►│ Any server can │ │ │
93- * │ │ │ │ handle callback │ │ │
94- * │ │ │ │ │ │ │
95- * │ │ 302 Redirect │ ◄───────────────│ Redirect to │ │ │
96- * │ │ to success │ │ /success?code=.. │ │ │
97- * │ │ │ │ │ │ │
98- * │ │ │ │ │ │ │
99- * │ 4. POST /auth/token {code, state} │
100- * │ │ ──────────────┼────────────────►│ Retrieve PKCE ◄──┼────────────────┤ Lookup │
101- * │ │ │ │ {codeVerifier} │ │ by state │
102- * │ │ │ │ │ │ │
103- * │ │ │ │ Exchange code ──►│ Token Exchange │ │
104- * │ │ │ │ + codeVerifier │ │ │
105- * │ │ │ │ │ │ │
106- * │ │ │ │ Get user info ──►│ User Claims │ │
107- * │ │ │ │ (basic claims) │ │ │
108- * │ │ │ │ │ │ │
109- * │ │ │ │ JIT Provision ◄──┼────────────────┤ Find or │
110- * │ │ │ │ (Find/Create) │ │ Create │
111- * │ │ │ │ │ │ │
112- * │ │ App JWT │ ◄───────────────│ Generate JWT │ │ │
113- * │ │ w/ orgRole │ │ payload │ │ │
114- * │ │
115- * │ App JWT Payload: │
116- * │ { │
117- * │ "sub": "user-uuid", // OIDC standard │
118- * │ "email": "user@example .com", // OIDC standard │
119- * │ "name": "John Doe", // OIDC standard │
120- * │ "orgRole": "admin" // Extended: admin/member │
121- * │ } │
122- * │ │
123- * │ Load Balancer Benefits: │
124- * │ • PKCE data stored in shared database - any server can access │
125- * │ • Stateless authentication - no server affinity required │
126- * │ • App JWT includes organizational context derived during JIT provisioning │
127- * └─────────────────────────────────────────────────────────────────────────────────────────┘
38+ * OIDC Authentication HTTP Endpoints Flow
39+ *
40+ * The authentication flows (Web, CLI, and Agents) are documented in detail
41+ * with Mermaid diagrams in `docs/authentication.md`.
42+ *
43+ * This controller handles agent/machine-to-machine authentication and
44+ * the shared /auth/info endpoint.
45+ *
46+ * - Web endpoints (including login initiation) are handled in `WebAuthController` (/auth/web/*)
47+ * - CLI endpoints are handled in `CliAuthController` (/auth/cli/*)
12848 */
12949@Controller ( "auth" )
13050export class AuthController {
@@ -133,79 +53,6 @@ export class AuthController {
13353 private readonly identityService : IdentityService
13454 ) { }
13555
136- @PublicRoute ( )
137- @HttpCode ( HttpStatusCode . Found )
138- @Get ( "login" )
139- async login ( @Res ( ) res : Response ) : Promise < void > {
140- const result = await pipe (
141- this . authService . initiateOidcLogin ( ) ,
142- logSuccess ( "OIDC login initiated" , "AuthController" )
143- ) ( )
144-
145- if ( isLeft ( result ) ) {
146- Logger . error ( "Failed to initiate OIDC login" , result . left )
147- res . redirect ( "/auth/error" )
148- return
149- }
150-
151- Logger . debug ( `Redirecting to OIDC provider: ${ result . right } ` )
152- res . redirect ( result . right )
153- }
154-
155- @PublicRoute ( )
156- @Get ( "callback" )
157- async callback ( @Query ( "code" ) code : string , @Query ( "state" ) state : string , @Res ( ) res : Response ) : Promise < void > {
158- if ( ! code || ! state ) {
159- Logger . error ( "Missing code or state in OIDC callback" )
160- res . redirect ( "/auth/error" )
161- return
162- }
163-
164- // Redirect to success page with code and state for frontend to exchange for JWT
165- res . redirect ( `/auth/success?code=${ code } &state=${ state } ` )
166- }
167-
168- @PublicRoute ( )
169- @Post ( "token" )
170- async generateToken ( @Body ( ) body : TokenRequest ) : Promise < TokenResponse > {
171- const runOidcLogin = ( req : TokenRequest ) => this . authService . completeOidcLogin ( req . code , req . state )
172-
173- const result = await pipe (
174- body ,
175- TE . right ,
176- TE . chainW ( raw => TE . fromEither ( validateGenerateTokenRequest ( raw ) ) ) ,
177- TE . chainW ( validated => runOidcLogin ( validated ) ) ,
178- TE . map ( mapToTokenResponse ) ,
179- logSuccess ( "Token generated" , "AuthController" )
180- ) ( )
181-
182- if ( isLeft ( result ) ) {
183- Logger . error ( "OIDC login completion failed" , result . left )
184- throw generateErrorResponseForGenerateToken ( result . left , "Failed to generate token" )
185- }
186-
187- return result . right
188- }
189-
190- @PublicRoute ( )
191- @Get ( "success" )
192- async success ( @Query ( "code" ) code : string , @Query ( "state" ) state : string ) : Promise < SuccessfulAuthResponse > {
193- if ( ! code ) throw new BadRequestException ( generateErrorPayload ( "MISSING_CODE" , "missing code" ) )
194- if ( ! state ) throw new BadRequestException ( generateErrorPayload ( "MISSING_STATE" , "missing state" ) )
195- return {
196- message : "Authentication successful. Use the code and state to generate a JWT token." ,
197- code : code ,
198- state : state ,
199- b64encoded : Buffer . from ( `${ code } :${ state } ` , "utf-8" ) . toString ( "base64" )
200- }
201- }
202-
203- @PublicRoute ( )
204- @Get ( "error" )
205- async error ( ) : Promise < FailedAuthResponse > {
206- return { message : "Authentication failed. Please try again." }
207- }
208-
20956 @Get ( "info" )
21057 async getEntityInfo (
21158 @GetAuthenticatedEntity ( ) authenticatedEntity : AuthenticatedEntity
@@ -265,29 +112,6 @@ export class AuthController {
265112 return result . right
266113 }
267114
268- @PublicRoute ( )
269- @Post ( "refresh" )
270- @HttpCode ( 200 )
271- async refreshUserToken ( @Body ( ) body : RefreshTokenRequest ) : Promise < TokenResponse > {
272- const refreshUserToken = ( refreshToken : string ) => this . authService . refreshTokenForUser ( refreshToken )
273-
274- const result = await pipe (
275- body ,
276- TE . right ,
277- TE . chainW ( rawBody => TE . fromEither ( validateRefreshTokenRequest ( rawBody ) ) ) ,
278- TE . chainW ( validatedBody => refreshUserToken ( validatedBody . refreshToken ) ) ,
279- TE . map ( serviceResult => mapToTokenResponse ( serviceResult ) ) ,
280- logSuccess ( "User token refreshed" , "AuthController" )
281- ) ( )
282-
283- if ( isLeft ( result ) ) {
284- Logger . error ( "User token refresh failed" , result . left )
285- throw generateErrorResponseForRefreshUserToken ( result . left , "Failed to refresh token" )
286- }
287-
288- return result . right
289- }
290-
291115 @PublicRoute ( )
292116 @Post ( "agents/refresh" )
293117 @HttpCode ( 200 )
@@ -318,46 +142,4 @@ export class AuthController {
318142
319143 return result . right
320144 }
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- }
363145}
0 commit comments