-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathauth.controller.ts
More file actions
117 lines (111 loc) · 3.36 KB
/
Copy pathauth.controller.ts
File metadata and controls
117 lines (111 loc) · 3.36 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
import {
Body,
Controller,
Get,
HttpStatus,
Logger,
Post,
Query,
Res,
} from "@nestjs/common";
import { Response } from "express";
import { AuthService } from "./auth.service";
import {
AuthResultQueryDto,
LogoutQueryDto,
OAuthCallbackQueryDto,
RefreshTokenDto,
} from "./dto";
import { Public } from "./public.decorator";
/**
* Thin HTTP layer that exposes the OAuth entrypoints to the frontend.
* All routes are public because authorization happens via bearer tokens on other controllers.
*/
@Controller("auth")
export class AuthController {
private readonly logger = new Logger(AuthController.name);
constructor(private readonly authService: AuthService) {}
/**
* Backend endpoint the SPA uses to refresh provider tokens.
* Delegates to AuthService so only the backend interacts with Keycloak using the client secret.
*/
@Public()
@Post("refresh")
async refreshToken(@Body() body: RefreshTokenDto) {
const tokens = await this.authService.refreshAccessToken(
body.refresh_token,
);
return {
access_token: tokens.access_token,
refresh_token: tokens.refresh_token,
id_token: tokens.id_token,
expires_in: tokens.expires_in,
};
}
/**
* Redirects the browser to the Keycloak authorization endpoint.
* Using a backend redirect keeps client_id/secret pairing server-side (no exposed secrets).
*/
@Public()
@Get("login")
async getLoginUrl(@Res() res: Response) {
try {
const loginUrl = this.authService.getLoginUrl();
res.redirect(loginUrl);
} catch {
res
.status(HttpStatus.INTERNAL_SERVER_ERROR)
.json({ error: "Failed to generate login URL" });
}
}
/**
* Drives the browser through the Keycloak logout endpoint (if an ID token hint is provided).
* This ensures realm sessions are terminated and the SPA gets a clean slate.
*/
@Public()
@Get("logout")
async logout(@Query() query: LogoutQueryDto, @Res() res: Response) {
try {
const logoutUrl = this.authService.getLogoutUrl(query.id_token_hint);
res.redirect(logoutUrl);
} catch {
res
.status(HttpStatus.INTERNAL_SERVER_ERROR)
.json({ error: "Failed to generate logout URL" });
}
}
/**
* Receives the redirect from Keycloak after the user authenticates.
* Converts the authorization code into tokens and then bounces the browser back to the SPA
* with an opaque `auth_result` identifier.
*/
@Public()
@Get("callback")
async oauthCallback(
@Query() query: OAuthCallbackQueryDto,
@Res() res: Response,
) {
try {
const resultId = await this.authService.handleCallback(
query.code,
query.state,
);
const redirectUrl = this.authService.buildAuthResultRedirect(resultId);
return res.redirect(redirectUrl);
} catch (error) {
this.logger.error("OAuth callback handling failed:", error);
const redirectUrl =
this.authService.buildErrorRedirect("callback_failed");
return res.redirect(redirectUrl);
}
}
/**
* One-time endpoint the SPA calls immediately after redirect to retrieve the provider tokens.
* The `resultId` is invalidated after the first successful read to keep the flow stateless.
*/
@Public()
@Get("result")
async consumeResult(@Query() query: AuthResultQueryDto) {
return this.authService.consumeAuthResult(query.result);
}
}