@quanticjs/auth-web-bff 4.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 +11 -0
- package/dist/bff.controller.js +147 -0
- package/dist/bff.middleware.d.ts +8 -0
- package/dist/bff.middleware.js +35 -0
- package/dist/bff.module.d.ts +6 -0
- package/dist/bff.module.js +36 -0
- package/dist/bff.service.d.ts +33 -0
- package/dist/bff.service.js +193 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +13 -0
- package/dist/interfaces.d.ts +28 -0
- package/dist/interfaces.js +4 -0
- package/package.json +29 -0
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { Request, Response } from 'express';
|
|
2
|
+
import { BffService } from './bff.service';
|
|
3
|
+
export declare class BffController {
|
|
4
|
+
private readonly bffService;
|
|
5
|
+
constructor(bffService: BffService);
|
|
6
|
+
login(returnTo: string, res: Response): void;
|
|
7
|
+
callback(code: string, state: string, req: Request, res: Response): Promise<void>;
|
|
8
|
+
refresh(req: Request, res: Response): Promise<void>;
|
|
9
|
+
logout(req: Request, res: Response): Promise<void>;
|
|
10
|
+
me(req: Request, res: Response): Promise<void>;
|
|
11
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
9
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
10
|
+
};
|
|
11
|
+
var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
12
|
+
return function (target, key) { decorator(target, key, paramIndex); }
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.BffController = void 0;
|
|
16
|
+
const common_1 = require("@nestjs/common");
|
|
17
|
+
const swagger_1 = require("@nestjs/swagger");
|
|
18
|
+
const core_1 = require("@quanticjs/core");
|
|
19
|
+
const bff_service_1 = require("./bff.service");
|
|
20
|
+
const VERIFIER_COOKIE = 'pkce_verifier';
|
|
21
|
+
let BffController = class BffController {
|
|
22
|
+
bffService;
|
|
23
|
+
constructor(bffService) {
|
|
24
|
+
this.bffService = bffService;
|
|
25
|
+
}
|
|
26
|
+
login(returnTo, res) {
|
|
27
|
+
const { url, codeVerifier } = this.bffService.getAuthorizationUrl(returnTo);
|
|
28
|
+
res.cookie(VERIFIER_COOKIE, codeVerifier, {
|
|
29
|
+
httpOnly: true,
|
|
30
|
+
sameSite: 'lax',
|
|
31
|
+
path: '/auth/callback',
|
|
32
|
+
maxAge: 5 * 60 * 1000,
|
|
33
|
+
});
|
|
34
|
+
res.redirect(url);
|
|
35
|
+
}
|
|
36
|
+
async callback(code, state, req, res) {
|
|
37
|
+
const codeVerifier = req.cookies?.[VERIFIER_COOKIE];
|
|
38
|
+
if (!codeVerifier || !code) {
|
|
39
|
+
res.redirect('/auth/login');
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
res.clearCookie(VERIFIER_COOKIE, { path: '/auth/callback' });
|
|
43
|
+
try {
|
|
44
|
+
const { sessionId, returnTo } = await this.bffService.handleCallback(code, state, codeVerifier);
|
|
45
|
+
res.cookie(this.bffService.getCookieName(), sessionId, this.bffService.getCookieOptions());
|
|
46
|
+
res.redirect(returnTo);
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
res.redirect('/auth/login?error=callback_failed');
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
async refresh(req, res) {
|
|
53
|
+
const sessionId = req.cookies?.[this.bffService.getCookieName()];
|
|
54
|
+
if (!sessionId) {
|
|
55
|
+
res.status(401).json({ message: 'No session' });
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
const accessToken = await this.bffService.refreshSession(sessionId);
|
|
59
|
+
if (!accessToken) {
|
|
60
|
+
res.clearCookie(this.bffService.getCookieName()).status(401).json({ message: 'Session expired' });
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
res.json({ success: true });
|
|
64
|
+
}
|
|
65
|
+
async logout(req, res) {
|
|
66
|
+
const cookieName = this.bffService.getCookieName();
|
|
67
|
+
const sessionId = req.cookies?.[cookieName];
|
|
68
|
+
if (sessionId) {
|
|
69
|
+
await this.bffService.destroySession(sessionId);
|
|
70
|
+
}
|
|
71
|
+
res.clearCookie(cookieName, this.bffService.getCookieOptions()).json({ success: true });
|
|
72
|
+
}
|
|
73
|
+
async me(req, res) {
|
|
74
|
+
const cookieName = this.bffService.getCookieName();
|
|
75
|
+
const sessionId = req.cookies?.[cookieName];
|
|
76
|
+
if (!sessionId) {
|
|
77
|
+
res.status(401).json({ message: 'Not authenticated' });
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
const userInfo = await this.bffService.getUserInfo(sessionId);
|
|
81
|
+
if (!userInfo) {
|
|
82
|
+
res.clearCookie(cookieName).status(401).json({ message: 'Session expired' });
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
res.json(userInfo);
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
exports.BffController = BffController;
|
|
89
|
+
__decorate([
|
|
90
|
+
(0, core_1.Public)(),
|
|
91
|
+
(0, common_1.Get)('login'),
|
|
92
|
+
(0, swagger_1.ApiOperation)({ summary: 'Redirect to Keycloak login' }),
|
|
93
|
+
__param(0, (0, common_1.Query)('returnTo')),
|
|
94
|
+
__param(1, (0, common_1.Res)()),
|
|
95
|
+
__metadata("design:type", Function),
|
|
96
|
+
__metadata("design:paramtypes", [String, Object]),
|
|
97
|
+
__metadata("design:returntype", void 0)
|
|
98
|
+
], BffController.prototype, "login", null);
|
|
99
|
+
__decorate([
|
|
100
|
+
(0, core_1.Public)(),
|
|
101
|
+
(0, common_1.Get)('callback'),
|
|
102
|
+
(0, swagger_1.ApiOperation)({ summary: 'OIDC callback — exchange code for tokens' }),
|
|
103
|
+
__param(0, (0, common_1.Query)('code')),
|
|
104
|
+
__param(1, (0, common_1.Query)('state')),
|
|
105
|
+
__param(2, (0, common_1.Req)()),
|
|
106
|
+
__param(3, (0, common_1.Res)()),
|
|
107
|
+
__metadata("design:type", Function),
|
|
108
|
+
__metadata("design:paramtypes", [String, String, Object, Object]),
|
|
109
|
+
__metadata("design:returntype", Promise)
|
|
110
|
+
], BffController.prototype, "callback", null);
|
|
111
|
+
__decorate([
|
|
112
|
+
(0, core_1.Public)(),
|
|
113
|
+
(0, common_1.Post)('refresh'),
|
|
114
|
+
(0, common_1.HttpCode)(200),
|
|
115
|
+
(0, swagger_1.ApiOperation)({ summary: 'Refresh access token' }),
|
|
116
|
+
__param(0, (0, common_1.Req)()),
|
|
117
|
+
__param(1, (0, common_1.Res)()),
|
|
118
|
+
__metadata("design:type", Function),
|
|
119
|
+
__metadata("design:paramtypes", [Object, Object]),
|
|
120
|
+
__metadata("design:returntype", Promise)
|
|
121
|
+
], BffController.prototype, "refresh", null);
|
|
122
|
+
__decorate([
|
|
123
|
+
(0, core_1.Public)(),
|
|
124
|
+
(0, common_1.Post)('logout'),
|
|
125
|
+
(0, common_1.HttpCode)(200),
|
|
126
|
+
(0, swagger_1.ApiOperation)({ summary: 'Logout — clear session' }),
|
|
127
|
+
__param(0, (0, common_1.Req)()),
|
|
128
|
+
__param(1, (0, common_1.Res)()),
|
|
129
|
+
__metadata("design:type", Function),
|
|
130
|
+
__metadata("design:paramtypes", [Object, Object]),
|
|
131
|
+
__metadata("design:returntype", Promise)
|
|
132
|
+
], BffController.prototype, "logout", null);
|
|
133
|
+
__decorate([
|
|
134
|
+
(0, core_1.Public)(),
|
|
135
|
+
(0, common_1.Get)('me'),
|
|
136
|
+
(0, swagger_1.ApiOperation)({ summary: 'Get current user info' }),
|
|
137
|
+
__param(0, (0, common_1.Req)()),
|
|
138
|
+
__param(1, (0, common_1.Res)()),
|
|
139
|
+
__metadata("design:type", Function),
|
|
140
|
+
__metadata("design:paramtypes", [Object, Object]),
|
|
141
|
+
__metadata("design:returntype", Promise)
|
|
142
|
+
], BffController.prototype, "me", null);
|
|
143
|
+
exports.BffController = BffController = __decorate([
|
|
144
|
+
(0, swagger_1.ApiTags)('auth'),
|
|
145
|
+
(0, common_1.Controller)('auth'),
|
|
146
|
+
__metadata("design:paramtypes", [bff_service_1.BffService])
|
|
147
|
+
], BffController);
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { NestMiddleware } from '@nestjs/common';
|
|
2
|
+
import { Request, Response, NextFunction } from 'express';
|
|
3
|
+
import { BffService } from './bff.service';
|
|
4
|
+
export declare class BffMiddleware implements NestMiddleware {
|
|
5
|
+
private readonly bffService;
|
|
6
|
+
constructor(bffService: BffService);
|
|
7
|
+
use(req: Request, _res: Response, next: NextFunction): Promise<void>;
|
|
8
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
9
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.BffMiddleware = void 0;
|
|
13
|
+
const common_1 = require("@nestjs/common");
|
|
14
|
+
const bff_service_1 = require("./bff.service");
|
|
15
|
+
let BffMiddleware = class BffMiddleware {
|
|
16
|
+
bffService;
|
|
17
|
+
constructor(bffService) {
|
|
18
|
+
this.bffService = bffService;
|
|
19
|
+
}
|
|
20
|
+
async use(req, _res, next) {
|
|
21
|
+
const sessionId = req.cookies?.[this.bffService.getCookieName()];
|
|
22
|
+
if (!sessionId)
|
|
23
|
+
return next();
|
|
24
|
+
const accessToken = await this.bffService.getAccessToken(sessionId);
|
|
25
|
+
if (accessToken) {
|
|
26
|
+
req.headers.authorization = `Bearer ${accessToken}`;
|
|
27
|
+
}
|
|
28
|
+
next();
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
exports.BffMiddleware = BffMiddleware;
|
|
32
|
+
exports.BffMiddleware = BffMiddleware = __decorate([
|
|
33
|
+
(0, common_1.Injectable)(),
|
|
34
|
+
__metadata("design:paramtypes", [bff_service_1.BffService])
|
|
35
|
+
], BffMiddleware);
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { DynamicModule, MiddlewareConsumer, NestModule } from '@nestjs/common';
|
|
2
|
+
import { type BffModuleOptions } from './interfaces';
|
|
3
|
+
export declare class BffModule implements NestModule {
|
|
4
|
+
static forRoot(options: BffModuleOptions): DynamicModule;
|
|
5
|
+
configure(consumer: MiddlewareConsumer): void;
|
|
6
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
var BffModule_1;
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.BffModule = void 0;
|
|
11
|
+
const common_1 = require("@nestjs/common");
|
|
12
|
+
const bff_controller_1 = require("./bff.controller");
|
|
13
|
+
const bff_service_1 = require("./bff.service");
|
|
14
|
+
const bff_middleware_1 = require("./bff.middleware");
|
|
15
|
+
const interfaces_1 = require("./interfaces");
|
|
16
|
+
let BffModule = BffModule_1 = class BffModule {
|
|
17
|
+
static forRoot(options) {
|
|
18
|
+
return {
|
|
19
|
+
module: BffModule_1,
|
|
20
|
+
controllers: [bff_controller_1.BffController],
|
|
21
|
+
providers: [
|
|
22
|
+
{ provide: interfaces_1.BFF_OPTIONS, useValue: options },
|
|
23
|
+
bff_service_1.BffService,
|
|
24
|
+
],
|
|
25
|
+
exports: [bff_service_1.BffService],
|
|
26
|
+
global: true,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
configure(consumer) {
|
|
30
|
+
consumer.apply(bff_middleware_1.BffMiddleware).forRoutes('*');
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
exports.BffModule = BffModule;
|
|
34
|
+
exports.BffModule = BffModule = BffModule_1 = __decorate([
|
|
35
|
+
(0, common_1.Module)({})
|
|
36
|
+
], BffModule);
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { OnModuleInit } from '@nestjs/common';
|
|
2
|
+
import type { Redis } from 'ioredis';
|
|
3
|
+
import { type BffModuleOptions, type SessionData } from './interfaces';
|
|
4
|
+
export declare class BffService implements OnModuleInit {
|
|
5
|
+
private readonly options;
|
|
6
|
+
private readonly redis;
|
|
7
|
+
private client;
|
|
8
|
+
private internalKeycloakBase;
|
|
9
|
+
private publicKeycloakBase;
|
|
10
|
+
private readonly sessionTtl;
|
|
11
|
+
private readonly sessionPrefix;
|
|
12
|
+
private readonly cookieName;
|
|
13
|
+
constructor(options: BffModuleOptions, redis: Redis | undefined);
|
|
14
|
+
onModuleInit(): Promise<void>;
|
|
15
|
+
getAuthorizationUrl(returnTo?: string): {
|
|
16
|
+
url: string;
|
|
17
|
+
codeVerifier: string;
|
|
18
|
+
};
|
|
19
|
+
handleCallback(code: string, state: string, codeVerifier: string): Promise<{
|
|
20
|
+
sessionId: string;
|
|
21
|
+
returnTo: string;
|
|
22
|
+
}>;
|
|
23
|
+
getSession(sessionId: string): Promise<SessionData | null>;
|
|
24
|
+
getAccessToken(sessionId: string): Promise<string | null>;
|
|
25
|
+
refreshSession(sessionId: string, session?: SessionData): Promise<string | null>;
|
|
26
|
+
destroySession(sessionId: string): Promise<void>;
|
|
27
|
+
getUserInfo(sessionId: string): Promise<Record<string, unknown> | null>;
|
|
28
|
+
getCookieOptions(): Record<string, unknown>;
|
|
29
|
+
getCookieName(): string;
|
|
30
|
+
private saveSession;
|
|
31
|
+
private toPublicKeycloakUrl;
|
|
32
|
+
private getCallbackUrl;
|
|
33
|
+
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
9
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
10
|
+
};
|
|
11
|
+
var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
12
|
+
return function (target, key) { decorator(target, key, paramIndex); }
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.BffService = void 0;
|
|
16
|
+
const common_1 = require("@nestjs/common");
|
|
17
|
+
const openid_client_1 = require("openid-client");
|
|
18
|
+
const uuid_1 = require("uuid");
|
|
19
|
+
const core_1 = require("@quanticjs/core");
|
|
20
|
+
const interfaces_1 = require("./interfaces");
|
|
21
|
+
let BffService = class BffService {
|
|
22
|
+
options;
|
|
23
|
+
redis;
|
|
24
|
+
client;
|
|
25
|
+
internalKeycloakBase;
|
|
26
|
+
publicKeycloakBase;
|
|
27
|
+
sessionTtl;
|
|
28
|
+
sessionPrefix;
|
|
29
|
+
cookieName;
|
|
30
|
+
constructor(options, redis) {
|
|
31
|
+
this.options = options;
|
|
32
|
+
this.redis = redis;
|
|
33
|
+
this.sessionTtl = options.session?.ttlSeconds ?? 7 * 24 * 3600;
|
|
34
|
+
this.sessionPrefix = options.session?.prefix ?? 'session:';
|
|
35
|
+
this.cookieName = options.session?.cookieName ?? 'sid';
|
|
36
|
+
}
|
|
37
|
+
async onModuleInit() {
|
|
38
|
+
const { keycloak } = this.options;
|
|
39
|
+
this.publicKeycloakBase = keycloak.url;
|
|
40
|
+
this.internalKeycloakBase = keycloak.internalUrl ?? keycloak.url;
|
|
41
|
+
const issuerUrl = `${this.internalKeycloakBase}/realms/${keycloak.realm}`;
|
|
42
|
+
const issuer = await openid_client_1.Issuer.discover(issuerUrl);
|
|
43
|
+
this.client = new issuer.Client({
|
|
44
|
+
client_id: keycloak.clientId,
|
|
45
|
+
client_secret: keycloak.clientSecret,
|
|
46
|
+
redirect_uris: [this.getCallbackUrl()],
|
|
47
|
+
response_types: ['code'],
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
getAuthorizationUrl(returnTo) {
|
|
51
|
+
const state = Buffer.from(JSON.stringify({ returnTo: returnTo || '/' })).toString('base64url');
|
|
52
|
+
const codeVerifier = openid_client_1.generators.codeVerifier();
|
|
53
|
+
const codeChallenge = openid_client_1.generators.codeChallenge(codeVerifier);
|
|
54
|
+
let url = this.client.authorizationUrl({
|
|
55
|
+
scope: 'openid email profile',
|
|
56
|
+
state,
|
|
57
|
+
code_challenge: codeChallenge,
|
|
58
|
+
code_challenge_method: 'S256',
|
|
59
|
+
});
|
|
60
|
+
url = this.toPublicKeycloakUrl(url);
|
|
61
|
+
return { url, codeVerifier };
|
|
62
|
+
}
|
|
63
|
+
async handleCallback(code, state, codeVerifier) {
|
|
64
|
+
const tokenSet = await this.client.callback(this.getCallbackUrl(), { code, state, iss: this.client.issuer.metadata.issuer }, { code_verifier: codeVerifier, state });
|
|
65
|
+
const claims = tokenSet.claims();
|
|
66
|
+
const sessionId = (0, uuid_1.v4)();
|
|
67
|
+
const sessionData = {
|
|
68
|
+
accessToken: tokenSet.access_token,
|
|
69
|
+
refreshToken: tokenSet.refresh_token,
|
|
70
|
+
idToken: tokenSet.id_token,
|
|
71
|
+
expiresAt: tokenSet.expires_at ?? Math.floor(Date.now() / 1000) + 300,
|
|
72
|
+
keycloakId: claims.sub,
|
|
73
|
+
email: claims.email ?? '',
|
|
74
|
+
displayName: claims.name ?? claims.preferred_username ?? '',
|
|
75
|
+
roles: claims.realm_access
|
|
76
|
+
? claims.realm_access.roles
|
|
77
|
+
: [],
|
|
78
|
+
username: claims.preferred_username,
|
|
79
|
+
};
|
|
80
|
+
await this.saveSession(sessionId, sessionData);
|
|
81
|
+
let returnTo = '/';
|
|
82
|
+
try {
|
|
83
|
+
const parsed = JSON.parse(Buffer.from(state, 'base64url').toString());
|
|
84
|
+
returnTo = parsed.returnTo || '/';
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
// ignore
|
|
88
|
+
}
|
|
89
|
+
return { sessionId, returnTo };
|
|
90
|
+
}
|
|
91
|
+
async getSession(sessionId) {
|
|
92
|
+
if (!this.redis)
|
|
93
|
+
return null;
|
|
94
|
+
const raw = await this.redis.get(this.sessionPrefix + sessionId);
|
|
95
|
+
if (!raw)
|
|
96
|
+
return null;
|
|
97
|
+
return JSON.parse(raw);
|
|
98
|
+
}
|
|
99
|
+
async getAccessToken(sessionId) {
|
|
100
|
+
const session = await this.getSession(sessionId);
|
|
101
|
+
if (!session)
|
|
102
|
+
return null;
|
|
103
|
+
if (Date.now() / 1000 < session.expiresAt - 30) {
|
|
104
|
+
return session.accessToken;
|
|
105
|
+
}
|
|
106
|
+
return this.refreshSession(sessionId, session);
|
|
107
|
+
}
|
|
108
|
+
async refreshSession(sessionId, session) {
|
|
109
|
+
const sess = session ?? (await this.getSession(sessionId));
|
|
110
|
+
if (!sess)
|
|
111
|
+
return null;
|
|
112
|
+
try {
|
|
113
|
+
const tokenSet = await this.client.refresh(sess.refreshToken);
|
|
114
|
+
const claims = tokenSet.claims();
|
|
115
|
+
const updated = {
|
|
116
|
+
...sess,
|
|
117
|
+
accessToken: tokenSet.access_token,
|
|
118
|
+
refreshToken: tokenSet.refresh_token ?? sess.refreshToken,
|
|
119
|
+
idToken: tokenSet.id_token ?? sess.idToken,
|
|
120
|
+
expiresAt: tokenSet.expires_at ?? Math.floor(Date.now() / 1000) + 300,
|
|
121
|
+
roles: claims.realm_access
|
|
122
|
+
? claims.realm_access.roles
|
|
123
|
+
: sess.roles,
|
|
124
|
+
};
|
|
125
|
+
await this.saveSession(sessionId, updated);
|
|
126
|
+
return updated.accessToken;
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
await this.destroySession(sessionId);
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
async destroySession(sessionId) {
|
|
134
|
+
const session = await this.getSession(sessionId);
|
|
135
|
+
if (!this.redis)
|
|
136
|
+
return;
|
|
137
|
+
if (session?.accessToken) {
|
|
138
|
+
try {
|
|
139
|
+
await this.client.revoke(session.accessToken, 'access_token');
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
// best-effort revocation
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
await this.redis.del(this.sessionPrefix + sessionId);
|
|
146
|
+
}
|
|
147
|
+
async getUserInfo(sessionId) {
|
|
148
|
+
const session = await this.getSession(sessionId);
|
|
149
|
+
if (!session)
|
|
150
|
+
return null;
|
|
151
|
+
return {
|
|
152
|
+
keycloakId: session.keycloakId,
|
|
153
|
+
email: session.email,
|
|
154
|
+
displayName: session.displayName,
|
|
155
|
+
roles: session.roles,
|
|
156
|
+
username: session.username,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
getCookieOptions() {
|
|
160
|
+
return {
|
|
161
|
+
httpOnly: true,
|
|
162
|
+
secure: process.env.NODE_ENV === 'production',
|
|
163
|
+
sameSite: 'lax',
|
|
164
|
+
path: '/',
|
|
165
|
+
maxAge: this.sessionTtl * 1000,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
getCookieName() {
|
|
169
|
+
return this.cookieName;
|
|
170
|
+
}
|
|
171
|
+
async saveSession(sessionId, data) {
|
|
172
|
+
if (!this.redis)
|
|
173
|
+
throw new Error('Redis not available');
|
|
174
|
+
await this.redis.setex(this.sessionPrefix + sessionId, this.sessionTtl, JSON.stringify(data));
|
|
175
|
+
}
|
|
176
|
+
toPublicKeycloakUrl(url) {
|
|
177
|
+
if (this.internalKeycloakBase === this.publicKeycloakBase)
|
|
178
|
+
return url;
|
|
179
|
+
return url.replace(this.internalKeycloakBase, this.publicKeycloakBase);
|
|
180
|
+
}
|
|
181
|
+
getCallbackUrl() {
|
|
182
|
+
const callbackPath = this.options.callbackPath ?? '/auth/callback';
|
|
183
|
+
return `${this.options.publicUrl}${callbackPath}`;
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
exports.BffService = BffService;
|
|
187
|
+
exports.BffService = BffService = __decorate([
|
|
188
|
+
(0, common_1.Injectable)(),
|
|
189
|
+
__param(0, (0, common_1.Inject)(interfaces_1.BFF_OPTIONS)),
|
|
190
|
+
__param(1, (0, common_1.Optional)()),
|
|
191
|
+
__param(1, (0, common_1.Inject)(core_1.REDIS_CLIENT)),
|
|
192
|
+
__metadata("design:paramtypes", [Object, Object])
|
|
193
|
+
], BffService);
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { BffModule } from './bff.module';
|
|
2
|
+
export { BffService } from './bff.service';
|
|
3
|
+
export { BffController } from './bff.controller';
|
|
4
|
+
export { BffMiddleware } from './bff.middleware';
|
|
5
|
+
export { BFF_OPTIONS } from './interfaces';
|
|
6
|
+
export type { BffModuleOptions, SessionData } from './interfaces';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.BFF_OPTIONS = exports.BffMiddleware = exports.BffController = exports.BffService = exports.BffModule = void 0;
|
|
4
|
+
var bff_module_1 = require("./bff.module");
|
|
5
|
+
Object.defineProperty(exports, "BffModule", { enumerable: true, get: function () { return bff_module_1.BffModule; } });
|
|
6
|
+
var bff_service_1 = require("./bff.service");
|
|
7
|
+
Object.defineProperty(exports, "BffService", { enumerable: true, get: function () { return bff_service_1.BffService; } });
|
|
8
|
+
var bff_controller_1 = require("./bff.controller");
|
|
9
|
+
Object.defineProperty(exports, "BffController", { enumerable: true, get: function () { return bff_controller_1.BffController; } });
|
|
10
|
+
var bff_middleware_1 = require("./bff.middleware");
|
|
11
|
+
Object.defineProperty(exports, "BffMiddleware", { enumerable: true, get: function () { return bff_middleware_1.BffMiddleware; } });
|
|
12
|
+
var interfaces_1 = require("./interfaces");
|
|
13
|
+
Object.defineProperty(exports, "BFF_OPTIONS", { enumerable: true, get: function () { return interfaces_1.BFF_OPTIONS; } });
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export interface BffModuleOptions {
|
|
2
|
+
keycloak: {
|
|
3
|
+
url: string;
|
|
4
|
+
internalUrl?: string;
|
|
5
|
+
realm: string;
|
|
6
|
+
clientId: string;
|
|
7
|
+
clientSecret: string;
|
|
8
|
+
};
|
|
9
|
+
session?: {
|
|
10
|
+
ttlSeconds?: number;
|
|
11
|
+
cookieName?: string;
|
|
12
|
+
prefix?: string;
|
|
13
|
+
};
|
|
14
|
+
publicUrl: string;
|
|
15
|
+
callbackPath?: string;
|
|
16
|
+
}
|
|
17
|
+
export interface SessionData {
|
|
18
|
+
accessToken: string;
|
|
19
|
+
refreshToken: string;
|
|
20
|
+
idToken?: string;
|
|
21
|
+
expiresAt: number;
|
|
22
|
+
keycloakId: string;
|
|
23
|
+
email: string;
|
|
24
|
+
displayName: string;
|
|
25
|
+
roles: string[];
|
|
26
|
+
username?: string;
|
|
27
|
+
}
|
|
28
|
+
export declare const BFF_OPTIONS: unique symbol;
|
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@quanticjs/auth-web-bff",
|
|
3
|
+
"version": "4.2.0",
|
|
4
|
+
"description": "BFF authentication module — Keycloak OIDC, Redis sessions, httpOnly cookies",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build": "tsc -p tsconfig.json",
|
|
9
|
+
"clean": "rm -rf dist"
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"@quanticjs/core": "^4.2.0"
|
|
13
|
+
},
|
|
14
|
+
"peerDependencies": {
|
|
15
|
+
"@nestjs/common": "^10.0.0 || ^11.0.0",
|
|
16
|
+
"@nestjs/config": "^3.0.0 || ^4.0.0",
|
|
17
|
+
"@nestjs/core": "^10.0.0 || ^11.0.0",
|
|
18
|
+
"@nestjs/swagger": "^7.0.0 || ^11.0.0",
|
|
19
|
+
"cookie-parser": "^1.4.0",
|
|
20
|
+
"express": "^4.18.0 || ^5.0.0",
|
|
21
|
+
"ioredis": "^5.0.0",
|
|
22
|
+
"openid-client": "^5.7.0",
|
|
23
|
+
"uuid": "^9.0.0"
|
|
24
|
+
},
|
|
25
|
+
"files": [
|
|
26
|
+
"dist"
|
|
27
|
+
],
|
|
28
|
+
"license": "MIT"
|
|
29
|
+
}
|