@quanticjs/auth-web-bff 5.7.0 → 5.9.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.
@@ -81,13 +81,17 @@ let BffController = class BffController {
81
81
  }
82
82
  const cookieName = this.bffService.getCookieName();
83
83
  const sessionId = req.cookies?.[cookieName];
84
+ let idToken;
84
85
  if (sessionId) {
86
+ const session = await this.bffService.getSession(sessionId);
87
+ idToken = session?.idToken;
85
88
  await this.bffService.destroySession(sessionId);
86
89
  }
90
+ const endSessionUrl = this.bffService.getEndSessionUrl(idToken);
87
91
  res
88
92
  .clearCookie(cookieName, this.bffService.getCookieOptions())
89
93
  .clearCookie(this.bffService.getCsrfCookieName(), this.bffService.getCsrfCookieOptions())
90
- .json({ success: true });
94
+ .json({ success: true, endSessionUrl });
91
95
  }
92
96
  async me(req, res) {
93
97
  const cookieName = this.bffService.getCookieName();
@@ -25,6 +25,7 @@ export declare class BffService implements OnModuleInit {
25
25
  getAccessToken(sessionId: string): Promise<string | null>;
26
26
  refreshSession(sessionId: string, session?: SessionData): Promise<string | null>;
27
27
  destroySession(sessionId: string): Promise<void>;
28
+ getEndSessionUrl(idToken?: string): string | null;
28
29
  getUserInfo(sessionId: string): Promise<Record<string, unknown> | null>;
29
30
  getCookieOptions(): Record<string, unknown>;
30
31
  getCookieName(): string;
@@ -19,15 +19,20 @@ const uuid_1 = require("uuid");
19
19
  const crypto_1 = require("crypto");
20
20
  const core_1 = require("@quanticjs/core");
21
21
  const interfaces_1 = require("./interfaces");
22
- function extractRealmRoles(accessToken) {
22
+ function extractTokenClaims(accessToken, clientId) {
23
23
  try {
24
24
  const payload = JSON.parse(Buffer.from(accessToken.split('.')[1], 'base64url').toString());
25
- return Array.isArray(payload.realm_access?.roles)
26
- ? payload.realm_access.roles
27
- : [];
25
+ return {
26
+ realmRoles: Array.isArray(payload.realm_access?.roles)
27
+ ? payload.realm_access.roles
28
+ : [],
29
+ clientRoles: Array.isArray(payload.resource_access?.[clientId]?.roles)
30
+ ? payload.resource_access[clientId].roles
31
+ : [],
32
+ };
28
33
  }
29
34
  catch {
30
- return [];
35
+ return { realmRoles: [], clientRoles: [] };
31
36
  }
32
37
  }
33
38
  let BffService = class BffService {
@@ -78,6 +83,11 @@ let BffService = class BffService {
78
83
  const tokenSet = await this.client.callback(this.getCallbackUrl(), { code, state, iss: this.client.issuer.metadata.issuer }, { code_verifier: codeVerifier, state });
79
84
  const claims = tokenSet.claims();
80
85
  const sessionId = (0, uuid_1.v4)();
86
+ const { realmRoles, clientRoles } = extractTokenClaims(tokenSet.access_token, this.options.keycloak.clientId);
87
+ let permissions = clientRoles;
88
+ if (this.options.permissionResolver) {
89
+ permissions = await this.options.permissionResolver(realmRoles, clientRoles);
90
+ }
81
91
  const sessionData = {
82
92
  accessToken: tokenSet.access_token,
83
93
  refreshToken: tokenSet.refresh_token,
@@ -86,7 +96,8 @@ let BffService = class BffService {
86
96
  id: claims.sub,
87
97
  email: claims.email ?? '',
88
98
  displayName: claims.name ?? claims.preferred_username ?? '',
89
- roles: extractRealmRoles(tokenSet.access_token),
99
+ roles: realmRoles,
100
+ permissions,
90
101
  username: claims.preferred_username,
91
102
  };
92
103
  await this.saveSession(sessionId, sessionData);
@@ -123,13 +134,19 @@ let BffService = class BffService {
123
134
  return null;
124
135
  try {
125
136
  const tokenSet = await this.client.refresh(sess.refreshToken);
137
+ const { realmRoles, clientRoles } = extractTokenClaims(tokenSet.access_token, this.options.keycloak.clientId);
138
+ let permissions = clientRoles;
139
+ if (this.options.permissionResolver) {
140
+ permissions = await this.options.permissionResolver(realmRoles, clientRoles);
141
+ }
126
142
  const updated = {
127
143
  ...sess,
128
144
  accessToken: tokenSet.access_token,
129
145
  refreshToken: tokenSet.refresh_token ?? sess.refreshToken,
130
146
  idToken: tokenSet.id_token ?? sess.idToken,
131
147
  expiresAt: tokenSet.expires_at ?? Math.floor(Date.now() / 1000) + 300,
132
- roles: extractRealmRoles(tokenSet.access_token),
148
+ roles: realmRoles,
149
+ permissions,
133
150
  };
134
151
  await this.saveSession(sessionId, updated);
135
152
  return updated.accessToken;
@@ -143,9 +160,19 @@ let BffService = class BffService {
143
160
  const session = await this.getSession(sessionId);
144
161
  if (!this.redis)
145
162
  return;
146
- if (session?.accessToken) {
163
+ if (session) {
164
+ try {
165
+ if (session.refreshToken) {
166
+ await this.client.revoke(session.refreshToken, 'refresh_token');
167
+ }
168
+ }
169
+ catch {
170
+ // best-effort revocation
171
+ }
147
172
  try {
148
- await this.client.revoke(session.accessToken, 'access_token');
173
+ if (session.accessToken) {
174
+ await this.client.revoke(session.accessToken, 'access_token');
175
+ }
149
176
  }
150
177
  catch {
151
178
  // best-effort revocation
@@ -153,6 +180,20 @@ let BffService = class BffService {
153
180
  }
154
181
  await this.redis.del(this.sessionPrefix + sessionId);
155
182
  }
183
+ getEndSessionUrl(idToken) {
184
+ const endSessionEndpoint = this.client.issuer.metadata.end_session_endpoint;
185
+ if (!endSessionEndpoint)
186
+ return null;
187
+ const publicUrl = endSessionEndpoint.replace(this.internalKeycloakBase, this.publicKeycloakBase);
188
+ const redirectUri = this.options.publicUrl ?? 'http://localhost:5173';
189
+ const params = new URLSearchParams({
190
+ client_id: this.options.keycloak.clientId,
191
+ post_logout_redirect_uri: redirectUri,
192
+ });
193
+ if (idToken)
194
+ params.set('id_token_hint', idToken);
195
+ return `${publicUrl}?${params.toString()}`;
196
+ }
156
197
  async getUserInfo(sessionId) {
157
198
  const session = await this.getSession(sessionId);
158
199
  if (!session)
@@ -162,6 +203,7 @@ let BffService = class BffService {
162
203
  email: session.email,
163
204
  displayName: session.displayName,
164
205
  roles: session.roles,
206
+ permissions: session.permissions,
165
207
  username: session.username,
166
208
  };
167
209
  }
@@ -16,6 +16,7 @@ export interface BffModuleOptions {
16
16
  };
17
17
  publicUrl: string;
18
18
  callbackPath?: string;
19
+ permissionResolver?: (realmRoles: string[], clientRoles: string[]) => string[] | Promise<string[]>;
19
20
  }
20
21
  export interface SessionData {
21
22
  accessToken: string;
@@ -26,6 +27,7 @@ export interface SessionData {
26
27
  email: string;
27
28
  displayName: string;
28
29
  roles: string[];
30
+ permissions: string[];
29
31
  username?: string;
30
32
  }
31
33
  export declare const BFF_OPTIONS: unique symbol;
package/package.json CHANGED
@@ -1,15 +1,16 @@
1
1
  {
2
2
  "name": "@quanticjs/auth-web-bff",
3
- "version": "5.7.0",
3
+ "version": "5.9.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",
7
7
  "scripts": {
8
8
  "build": "tsc -p tsconfig.json",
9
+ "test": "jest --passWithNoTests",
9
10
  "clean": "rm -rf dist"
10
11
  },
11
12
  "dependencies": {
12
- "@quanticjs/core": "^5.7.0"
13
+ "@quanticjs/core": "^5.9.0"
13
14
  },
14
15
  "peerDependencies": {
15
16
  "@nestjs/common": "^10.0.0 || ^11.0.0",