@quanticjs/auth-web-bff 5.0.0 → 5.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bff.controller.d.ts +3 -1
- package/dist/bff.controller.js +27 -7
- package/dist/bff.service.d.ts +5 -0
- package/dist/bff.service.js +24 -3
- package/dist/interfaces.d.ts +3 -0
- package/package.json +2 -2
package/dist/bff.controller.d.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { Request, Response } from 'express';
|
|
2
2
|
import { BffService } from './bff.service';
|
|
3
|
+
import { type BffModuleOptions } from './interfaces';
|
|
3
4
|
export declare class BffController {
|
|
4
5
|
private readonly bffService;
|
|
5
|
-
|
|
6
|
+
private readonly callbackPath;
|
|
7
|
+
constructor(bffService: BffService, options: BffModuleOptions);
|
|
6
8
|
login(returnTo: string, res: Response): void;
|
|
7
9
|
callback(code: string, state: string, req: Request, res: Response): Promise<void>;
|
|
8
10
|
refresh(req: Request, res: Response): Promise<void>;
|
package/dist/bff.controller.js
CHANGED
|
@@ -17,18 +17,21 @@ const common_1 = require("@nestjs/common");
|
|
|
17
17
|
const swagger_1 = require("@nestjs/swagger");
|
|
18
18
|
const core_1 = require("@quanticjs/core");
|
|
19
19
|
const bff_service_1 = require("./bff.service");
|
|
20
|
+
const interfaces_1 = require("./interfaces");
|
|
20
21
|
const VERIFIER_COOKIE = 'pkce_verifier';
|
|
21
22
|
let BffController = class BffController {
|
|
22
23
|
bffService;
|
|
23
|
-
|
|
24
|
+
callbackPath;
|
|
25
|
+
constructor(bffService, options) {
|
|
24
26
|
this.bffService = bffService;
|
|
27
|
+
this.callbackPath = options.callbackPath ?? '/api/auth/callback';
|
|
25
28
|
}
|
|
26
29
|
login(returnTo, res) {
|
|
27
30
|
const { url, codeVerifier } = this.bffService.getAuthorizationUrl(returnTo);
|
|
28
31
|
res.cookie(VERIFIER_COOKIE, codeVerifier, {
|
|
29
32
|
httpOnly: true,
|
|
30
33
|
sameSite: 'lax',
|
|
31
|
-
path:
|
|
34
|
+
path: this.callbackPath,
|
|
32
35
|
maxAge: 5 * 60 * 1000,
|
|
33
36
|
});
|
|
34
37
|
res.redirect(url);
|
|
@@ -36,20 +39,27 @@ let BffController = class BffController {
|
|
|
36
39
|
async callback(code, state, req, res) {
|
|
37
40
|
const codeVerifier = req.cookies?.[VERIFIER_COOKIE];
|
|
38
41
|
if (!codeVerifier || !code) {
|
|
39
|
-
res.redirect('/auth/login');
|
|
42
|
+
res.redirect('/api/auth/login');
|
|
40
43
|
return;
|
|
41
44
|
}
|
|
42
|
-
res.clearCookie(VERIFIER_COOKIE, { path:
|
|
45
|
+
res.clearCookie(VERIFIER_COOKIE, { path: this.callbackPath });
|
|
43
46
|
try {
|
|
44
47
|
const { sessionId, returnTo } = await this.bffService.handleCallback(code, state, codeVerifier);
|
|
45
48
|
res.cookie(this.bffService.getCookieName(), sessionId, this.bffService.getCookieOptions());
|
|
49
|
+
res.cookie(this.bffService.getCsrfCookieName(), this.bffService.generateCsrfToken(), this.bffService.getCsrfCookieOptions());
|
|
46
50
|
res.redirect(returnTo);
|
|
47
51
|
}
|
|
48
52
|
catch {
|
|
49
|
-
res.redirect('/auth/login?error=callback_failed');
|
|
53
|
+
res.redirect('/api/auth/login?error=callback_failed');
|
|
50
54
|
}
|
|
51
55
|
}
|
|
52
56
|
async refresh(req, res) {
|
|
57
|
+
const csrfHeader = req.headers['x-csrf-token'];
|
|
58
|
+
const csrfCookie = req.cookies?.[this.bffService.getCsrfCookieName()];
|
|
59
|
+
if (!this.bffService.validateCsrf(csrfHeader, csrfCookie)) {
|
|
60
|
+
res.status(403).json({ message: 'Invalid CSRF token' });
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
53
63
|
const sessionId = req.cookies?.[this.bffService.getCookieName()];
|
|
54
64
|
if (!sessionId) {
|
|
55
65
|
res.status(401).json({ message: 'No session' });
|
|
@@ -63,12 +73,21 @@ let BffController = class BffController {
|
|
|
63
73
|
res.json({ success: true });
|
|
64
74
|
}
|
|
65
75
|
async logout(req, res) {
|
|
76
|
+
const csrfHeader = req.headers['x-csrf-token'];
|
|
77
|
+
const csrfCookie = req.cookies?.[this.bffService.getCsrfCookieName()];
|
|
78
|
+
if (!this.bffService.validateCsrf(csrfHeader, csrfCookie)) {
|
|
79
|
+
res.status(403).json({ message: 'Invalid CSRF token' });
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
66
82
|
const cookieName = this.bffService.getCookieName();
|
|
67
83
|
const sessionId = req.cookies?.[cookieName];
|
|
68
84
|
if (sessionId) {
|
|
69
85
|
await this.bffService.destroySession(sessionId);
|
|
70
86
|
}
|
|
71
|
-
res
|
|
87
|
+
res
|
|
88
|
+
.clearCookie(cookieName, this.bffService.getCookieOptions())
|
|
89
|
+
.clearCookie(this.bffService.getCsrfCookieName(), this.bffService.getCsrfCookieOptions())
|
|
90
|
+
.json({ success: true });
|
|
72
91
|
}
|
|
73
92
|
async me(req, res) {
|
|
74
93
|
const cookieName = this.bffService.getCookieName();
|
|
@@ -143,5 +162,6 @@ __decorate([
|
|
|
143
162
|
exports.BffController = BffController = __decorate([
|
|
144
163
|
(0, swagger_1.ApiTags)('auth'),
|
|
145
164
|
(0, common_1.Controller)('auth'),
|
|
146
|
-
|
|
165
|
+
__param(1, (0, common_1.Inject)(interfaces_1.BFF_OPTIONS)),
|
|
166
|
+
__metadata("design:paramtypes", [bff_service_1.BffService, Object])
|
|
147
167
|
], BffController);
|
package/dist/bff.service.d.ts
CHANGED
|
@@ -10,6 +10,7 @@ export declare class BffService implements OnModuleInit {
|
|
|
10
10
|
private readonly sessionTtl;
|
|
11
11
|
private readonly sessionPrefix;
|
|
12
12
|
private readonly cookieName;
|
|
13
|
+
private readonly csrfCookieName;
|
|
13
14
|
constructor(options: BffModuleOptions, redis: Redis | undefined);
|
|
14
15
|
onModuleInit(): Promise<void>;
|
|
15
16
|
getAuthorizationUrl(returnTo?: string): {
|
|
@@ -27,6 +28,10 @@ export declare class BffService implements OnModuleInit {
|
|
|
27
28
|
getUserInfo(sessionId: string): Promise<Record<string, unknown> | null>;
|
|
28
29
|
getCookieOptions(): Record<string, unknown>;
|
|
29
30
|
getCookieName(): string;
|
|
31
|
+
getCsrfCookieName(): string;
|
|
32
|
+
getCsrfCookieOptions(): Record<string, unknown>;
|
|
33
|
+
generateCsrfToken(): string;
|
|
34
|
+
validateCsrf(headerValue: string | undefined, cookieValue: string | undefined): boolean;
|
|
30
35
|
private saveSession;
|
|
31
36
|
private toPublicKeycloakUrl;
|
|
32
37
|
private getCallbackUrl;
|
package/dist/bff.service.js
CHANGED
|
@@ -16,6 +16,7 @@ exports.BffService = void 0;
|
|
|
16
16
|
const common_1 = require("@nestjs/common");
|
|
17
17
|
const openid_client_1 = require("openid-client");
|
|
18
18
|
const uuid_1 = require("uuid");
|
|
19
|
+
const crypto_1 = require("crypto");
|
|
19
20
|
const core_1 = require("@quanticjs/core");
|
|
20
21
|
const interfaces_1 = require("./interfaces");
|
|
21
22
|
function extractRealmRoles(accessToken) {
|
|
@@ -38,12 +39,14 @@ let BffService = class BffService {
|
|
|
38
39
|
sessionTtl;
|
|
39
40
|
sessionPrefix;
|
|
40
41
|
cookieName;
|
|
42
|
+
csrfCookieName;
|
|
41
43
|
constructor(options, redis) {
|
|
42
44
|
this.options = options;
|
|
43
45
|
this.redis = redis;
|
|
44
46
|
this.sessionTtl = options.session?.ttlSeconds ?? 7 * 24 * 3600;
|
|
45
47
|
this.sessionPrefix = options.session?.prefix ?? 'session:';
|
|
46
|
-
this.cookieName = options.session?.cookieName ?? 'sid';
|
|
48
|
+
this.cookieName = options.session?.cookieName ?? '__Host-sid';
|
|
49
|
+
this.csrfCookieName = options.csrf?.cookieName ?? '__Host-csrf';
|
|
47
50
|
}
|
|
48
51
|
async onModuleInit() {
|
|
49
52
|
const { keycloak } = this.options;
|
|
@@ -166,7 +169,7 @@ let BffService = class BffService {
|
|
|
166
169
|
return {
|
|
167
170
|
httpOnly: true,
|
|
168
171
|
secure: process.env.NODE_ENV === 'production',
|
|
169
|
-
sameSite: '
|
|
172
|
+
sameSite: 'strict',
|
|
170
173
|
path: '/',
|
|
171
174
|
maxAge: this.sessionTtl * 1000,
|
|
172
175
|
};
|
|
@@ -174,6 +177,24 @@ let BffService = class BffService {
|
|
|
174
177
|
getCookieName() {
|
|
175
178
|
return this.cookieName;
|
|
176
179
|
}
|
|
180
|
+
getCsrfCookieName() {
|
|
181
|
+
return this.csrfCookieName;
|
|
182
|
+
}
|
|
183
|
+
getCsrfCookieOptions() {
|
|
184
|
+
return {
|
|
185
|
+
httpOnly: false,
|
|
186
|
+
secure: process.env.NODE_ENV === 'production',
|
|
187
|
+
sameSite: 'strict',
|
|
188
|
+
path: '/',
|
|
189
|
+
maxAge: this.sessionTtl * 1000,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
generateCsrfToken() {
|
|
193
|
+
return (0, crypto_1.randomBytes)(32).toString('base64url');
|
|
194
|
+
}
|
|
195
|
+
validateCsrf(headerValue, cookieValue) {
|
|
196
|
+
return !!headerValue && !!cookieValue && headerValue === cookieValue;
|
|
197
|
+
}
|
|
177
198
|
async saveSession(sessionId, data) {
|
|
178
199
|
if (!this.redis)
|
|
179
200
|
throw new Error('Redis not available');
|
|
@@ -185,7 +206,7 @@ let BffService = class BffService {
|
|
|
185
206
|
return url.replace(this.internalKeycloakBase, this.publicKeycloakBase);
|
|
186
207
|
}
|
|
187
208
|
getCallbackUrl() {
|
|
188
|
-
const callbackPath = this.options.callbackPath ?? '/auth/callback';
|
|
209
|
+
const callbackPath = this.options.callbackPath ?? '/api/auth/callback';
|
|
189
210
|
return `${this.options.publicUrl}${callbackPath}`;
|
|
190
211
|
}
|
|
191
212
|
};
|
package/dist/interfaces.d.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@quanticjs/auth-web-bff",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.2.0",
|
|
4
4
|
"description": "BFF authentication module — Keycloak OIDC, Redis sessions, httpOnly cookies",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"clean": "rm -rf dist"
|
|
10
10
|
},
|
|
11
11
|
"dependencies": {
|
|
12
|
-
"@quanticjs/core": "^5.
|
|
12
|
+
"@quanticjs/core": "^5.2.0"
|
|
13
13
|
},
|
|
14
14
|
"peerDependencies": {
|
|
15
15
|
"@nestjs/common": "^10.0.0 || ^11.0.0",
|