@data-fair/lib-node 2.15.0 → 2.16.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@data-fair/lib-node",
3
- "version": "2.15.0",
3
+ "version": "2.16.0",
4
4
  "description": "Small nodejs related utility functions for the data-fair stack.",
5
5
  "main": "index.js",
6
6
  "files": [
@@ -15,7 +15,7 @@
15
15
  "build": "cd .. && npm run build"
16
16
  },
17
17
  "dependencies": {
18
- "@data-fair/lib-common-types": "^1.8.4",
18
+ "@data-fair/lib-common-types": "^1.25.0",
19
19
  "@data-fair/lib-utils": "^1.1.0",
20
20
  "@data-fair/lib-validation": "^1.0.0",
21
21
  "agentkeepalive": "^4.5.0",
package/session.d.ts CHANGED
@@ -1,13 +1,26 @@
1
1
  import type { SessionState } from '@data-fair/lib-common-types/session/index.js';
2
+ import { type KeyObject } from 'node:crypto';
3
+ import type { JwtHeader } from 'jsonwebtoken';
2
4
  import JwksClient from 'jwks-rsa';
5
+ export type KeySet = 'session' | 'scoped';
6
+ export declare const keySetOfKid: (kid?: string) => KeySet;
3
7
  export declare class SessionHandler {
4
- private _jwksClient;
8
+ private _jwksClients;
5
9
  defaultLang: string;
6
- get jwksClient(): JwksClient.JwksClient;
10
+ scopeAware: boolean;
11
+ get jwksClients(): Record<KeySet, JwksClient.JwksClient>;
7
12
  initJWKS(directoryUrl?: string): void;
8
13
  private _signingKeyObjects;
9
- private getSigningKeyObject;
14
+ protected getSigningKeyObject(keySet: KeySet, kid?: string): Promise<KeyObject>;
15
+ verifyTokenKeySet(token: string): Promise<{
16
+ payload: any;
17
+ header: JwtHeader;
18
+ keySet: KeySet;
19
+ }>;
10
20
  verifyToken(token: string): Promise<any>;
21
+ private assertTokenKind;
11
22
  private buildTokenSessionState;
23
+ private assertScopeAware;
12
24
  readStateFromCookie(rawCookies: string | Record<string, string | undefined> | undefined, onlyDecode?: boolean, clearBrokenSession?: () => void): Promise<SessionState>;
25
+ readStateFromBearer(token: string, lang?: string): Promise<SessionState>;
13
26
  }
package/session.js CHANGED
@@ -1,33 +1,67 @@
1
+ import { parseScope } from '@data-fair/lib-common-types/session/index.js';
1
2
  import { createPublicKey } from 'node:crypto';
2
3
  import jwt from 'jsonwebtoken';
3
4
  import JwksClient from 'jwks-rsa';
4
5
  import cookie from 'cookie';
5
6
  import { httpError } from '@data-fair/lib-utils/http-errors.js';
7
+ // simple-directory names the keys of its scoped key set scoped-<kid>; the signature still has to
8
+ // verify against that set, so a forged kid gains nothing
9
+ export const keySetOfKid = (kid) => kid?.startsWith('scoped-') ? 'scoped' : 'session';
10
+ // only a genuine token failure means the credential is bad; anything else (jwks unreachable,
11
+ // jwks-rsa rate limit, network error, unexpected bug) says nothing about the token and must
12
+ // neither refuse it as invalid nor clear the session of the user
13
+ const tokenFailureNames = new Set(['JsonWebTokenError', 'TokenExpiredError', 'NotBeforeError', 'SigningKeyNotFoundError']);
14
+ const isTokenFailure = (err) => err?.status === 401 || tokenFailureNames.has(err?.name) ||
15
+ err?.message === 'failed to decode token' || err?.message === 'not a session token';
16
+ // jwt.decode throws a raw SyntaxError on a payload that is not JSON: that is a malformed token,
17
+ // not an infrastructure failure
18
+ const decodeToken = (token) => {
19
+ let decoded;
20
+ try {
21
+ decoded = jwt.decode(token, { complete: true });
22
+ }
23
+ catch {
24
+ throw new Error('failed to decode token');
25
+ }
26
+ if (!decoded)
27
+ throw new Error('failed to decode token');
28
+ return decoded;
29
+ };
30
+ // RFC 7515 4.1.9: the media type comparison is case-insensitive and the application/ prefix is optional
31
+ const isAccessTokenTyp = (typ) => typeof typ === 'string' && typ.toLowerCase().replace(/^application\//, '') === 'at+jwt';
6
32
  export class SessionHandler {
7
- _jwksClient;
33
+ _jwksClients;
8
34
  defaultLang = 'fr';
9
- get jwksClient() {
10
- if (!this._jwksClient)
35
+ // a service declares itself scope-aware once it enforces scopes on its routes; until then a
36
+ // scope-restricted session (an NHI or OAuth access token with a scope claim) is refused with
37
+ // a 403 instead of acting with the full role of its identity
38
+ scopeAware = false;
39
+ get jwksClients() {
40
+ if (!this._jwksClients)
11
41
  throw new Error('session management was not initialized');
12
- return this._jwksClient;
42
+ return this._jwksClients;
13
43
  }
14
44
  initJWKS(directoryUrl = 'http://simple-directory:8080') {
15
- this._jwksClient = JwksClient({ jwksUri: directoryUrl + '/.well-known/jwks.json' });
16
- // the parsed key cache is derived from this jwks client (which enforces key rotation
17
- // through its own ttl), so it must be dropped whenever the client is recreated
45
+ const client = (path) => JwksClient({ jwksUri: directoryUrl + path });
46
+ this._jwksClients = {
47
+ session: client('/.well-known/jwks.json'),
48
+ scoped: client('/.well-known/scoped-jwks.json')
49
+ };
50
+ // the parsed key cache is derived from these jwks clients (which enforce key rotation
51
+ // through their own ttl), so it must be dropped whenever the clients are recreated
18
52
  this._signingKeyObjects.clear();
19
53
  }
20
54
  // jwks-rsa caches the JWK per kid but it exports a new PEM string at every access
21
55
  // and jsonwebtoken parses that PEM again at every verification: caching the parsed
22
- // KeyObject per kid skips both costs.
56
+ // KeyObject per key set and kid skips both costs.
23
57
  // IMPORTANT: getSigningKey must still be awaited on every verification. It is the only
24
58
  // thing that re-checks the kid against the current keyset, so a key rotated out of the
25
59
  // JWKS stops being accepted. Short-circuiting it from a persistent cache would accept a
26
60
  // rotated-out key forever (the lib#41 regression).
27
61
  _signingKeyObjects = new Map();
28
- async getSigningKeyObject(kid) {
29
- const signingKey = await this.jwksClient.getSigningKey(kid);
30
- const cacheKey = kid ?? '';
62
+ async getSigningKeyObject(keySet, kid) {
63
+ const signingKey = await this.jwksClients[keySet].getSigningKey(kid);
64
+ const cacheKey = keySet + ':' + (kid ?? '');
31
65
  let keyObject = this._signingKeyObjects.get(cacheKey);
32
66
  if (!keyObject) {
33
67
  keyObject = createPublicKey(signingKey.getPublicKey());
@@ -35,13 +69,47 @@ export class SessionHandler {
35
69
  }
36
70
  return keyObject;
37
71
  }
72
+ // verifies against the key set designated by the kid: a token claiming a scoped kid must be
73
+ // signed by a key of the scoped set, any other must be signed by a key of the session set
74
+ async verifyTokenKeySet(token) {
75
+ const decoded = decodeToken(token);
76
+ const keySet = keySetOfKid(decoded.header.kid);
77
+ const signingKeyObject = await this.getSigningKeyObject(keySet, decoded.header.kid);
78
+ const payload = jwt.verify(token, signingKeyObject, { algorithms: ['RS256'] });
79
+ return { payload, header: decoded.header, keySet };
80
+ }
38
81
  // used for the main session token, but can also be used to use the jwks to verify other types of tokens
82
+ // (simple-directory verifies its exchange tokens with it): only the session key set is accepted
39
83
  async verifyToken(token) {
40
- const decoded = jwt.decode(token, { complete: true });
41
- if (!decoded)
42
- throw new Error('failed to decode token');
43
- const signingKeyObject = await this.getSigningKeyObject(decoded.header.kid);
44
- return jwt.verify(token, signingKeyObject);
84
+ const { payload, keySet } = await this.verifyTokenKeySet(token);
85
+ if (keySet !== 'session')
86
+ throw new Error('not a session token');
87
+ return payload;
88
+ }
89
+ assertTokenKind(path, header, payload, keySet) {
90
+ if (path === 'bearer') {
91
+ if (!isAccessTokenTyp(header.typ))
92
+ throw httpError(401, 'bearer token is not an access token');
93
+ if (keySet !== 'scoped')
94
+ throw httpError(401, 'access token must be signed by the scoped key set');
95
+ }
96
+ else {
97
+ if (isAccessTokenTyp(header.typ))
98
+ throw httpError(401, 'an access token is not a session token');
99
+ const hasScope = payload && typeof payload === 'object' && 'scope' in payload;
100
+ if (keySet === 'session' && hasScope)
101
+ throw httpError(401, 'session key set token must not carry a scope');
102
+ if (keySet === 'scoped' && !hasScope)
103
+ throw httpError(401, 'scoped key set token must carry a scope');
104
+ if (keySet === 'scoped' && payload?.organizations?.length > 1)
105
+ throw httpError(401, 'scoped token must be bound to at most one organization');
106
+ }
107
+ if (keySet === 'scoped') {
108
+ if (payload && ('adminMode' in payload || 'asAdmin' in payload || 'isAdmin' in payload))
109
+ throw httpError(401, 'scoped token cannot carry superadmin claims');
110
+ if (typeof payload?.exp !== 'number')
111
+ throw httpError(401, 'scoped token must expire');
112
+ }
45
113
  }
46
114
  buildTokenSessionState(user, organizationId, departmentId, switchedRole) {
47
115
  const state = {};
@@ -53,6 +121,13 @@ export class SessionHandler {
53
121
  }
54
122
  }
55
123
  state.user = user;
124
+ // absent claim = unrestricted first party session; anything but a string is malformed
125
+ // (null must not silently become unrestricted)
126
+ if (user.scope !== undefined) {
127
+ if (typeof user.scope !== 'string')
128
+ throw httpError(401, 'invalid scope claim');
129
+ state.scopes = parseScope(user.scope);
130
+ }
56
131
  if (organizationId) {
57
132
  state.organization = user.organizations.find(o => {
58
133
  if (o.id !== organizationId)
@@ -93,9 +168,15 @@ export class SessionHandler {
93
168
  }
94
169
  return state;
95
170
  }
171
+ assertScopeAware(state) {
172
+ if (state.scopes && !this.scopeAware)
173
+ throw httpError(403, 'this service does not accept scope-restricted sessions');
174
+ }
96
175
  // accepts either the raw Cookie header or an already-parsed cookies object
97
176
  // (e.g. req.cookies when a cookie-parser middleware already ran upstream) to
98
177
  // avoid re-parsing and allocating a second cookies object per request
178
+ // raw reader, applies neither the route scope guard nor the session cache: not for authorization,
179
+ // use session.req() / reqSession(). It only refuses scoped states when the handler is not scope-aware.
99
180
  async readStateFromCookie(rawCookies, onlyDecode, clearBrokenSession) {
100
181
  if (!rawCookies)
101
182
  return { lang: this.defaultLang };
@@ -112,29 +193,80 @@ export class SessionHandler {
112
193
  const switchedRole = cookies.id_token_role ?? '';
113
194
  let user;
114
195
  if (onlyDecode) {
115
- user = jwt.decode(token);
196
+ let decoded;
197
+ try {
198
+ decoded = decodeToken(token);
199
+ this.assertTokenKind('cookie', decoded.header, decoded.payload, keySetOfKid(decoded.header.kid));
200
+ }
201
+ catch (err) {
202
+ if (clearBrokenSession)
203
+ clearBrokenSession();
204
+ throw err.status === 401 ? err : httpError(401);
205
+ }
206
+ user = decoded.payload;
116
207
  }
117
208
  else {
118
209
  try {
119
- user = await this.verifyToken(token);
210
+ const verified = await this.verifyTokenKeySet(token);
211
+ user = verified.payload;
212
+ this.assertTokenKind('cookie', verified.header, verified.payload, verified.keySet);
120
213
  }
121
214
  catch (err) {
122
215
  console.warn(err);
123
- if (err.name === 'JwksError') {
124
- // happens in case of temporary unavailability if SD
216
+ if (!isTokenFailure(err)) {
217
+ // happens in case of temporary unavailability of SD
125
218
  // better not to disconnect user in this case
126
219
  throw httpError(500, 'Session token public keys not initialized');
127
220
  }
128
- else {
129
- if (clearBrokenSession)
130
- clearBrokenSession();
131
- throw httpError(401);
132
- }
221
+ if (clearBrokenSession)
222
+ clearBrokenSession();
223
+ throw err.status === 401 ? err : httpError(401);
133
224
  }
134
225
  }
135
226
  if (!user)
136
227
  return { lang, dark };
137
- return Object.assign({ lang, dark }, this.buildTokenSessionState(user, organizationId, departmentId, switchedRole));
228
+ const state = Object.assign({ lang, dark }, this.buildTokenSessionState(user, organizationId, departmentId, switchedRole));
229
+ this.assertScopeAware(state);
230
+ return state;
231
+ }
232
+ // Only tokens minted as access tokens (RFC 9068 header typ at+jwt) are bearer credentials.
233
+ // Session cookie tokens and the long lived exchange token are signed by the same keys and
234
+ // must never be usable this way (cf simple-directory docs/architecture/session-theft-protections.md).
235
+ // An access token is bound to one account: its organizations array holds the one membership it
236
+ // acts in (1 = that organization), or nothing for the personal account (0 memberships). More than
237
+ // one is refused because the full array stays in user.organizations and consumers filter on it.
238
+ // A missing organizations key is not an access token either.
239
+ // raw reader, applies neither the route scope guard nor the session cache: not for authorization,
240
+ // use session.req() / reqSession(). It only refuses scoped states when the handler is not scope-aware.
241
+ async readStateFromBearer(token, lang) {
242
+ let decoded;
243
+ try {
244
+ decoded = decodeToken(token);
245
+ }
246
+ catch {
247
+ throw httpError(401, 'bearer token is not an access token');
248
+ }
249
+ if (!isAccessTokenTyp(decoded.header.typ))
250
+ throw httpError(401, 'bearer token is not an access token');
251
+ let user;
252
+ try {
253
+ const verified = await this.verifyTokenKeySet(token);
254
+ this.assertTokenKind('bearer', verified.header, verified.payload, verified.keySet);
255
+ user = verified.payload;
256
+ }
257
+ catch (err) {
258
+ if (!isTokenFailure(err))
259
+ throw httpError(500, 'Session token public keys not initialized');
260
+ throw err.status === 401 ? err : httpError(401);
261
+ }
262
+ if (!user?.organizations)
263
+ throw httpError(401, 'bearer token is not an access token');
264
+ if (user.organizations.length > 1)
265
+ throw httpError(401, 'access token must be bound to at most one organization');
266
+ const org = user.organizations[0];
267
+ const state = Object.assign({ lang: lang ?? this.defaultLang }, this.buildTokenSessionState(user, org?.id ?? '', org?.department ?? '', ''));
268
+ this.assertScopeAware(state);
269
+ return state;
138
270
  }
139
271
  }
140
272
  //# sourceMappingURL=session.js.map
package/session.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"session.js","sourceRoot":"","sources":["session.ts"],"names":[],"mappings":"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","sourcesContent":["import type { SessionState, User } from '@data-fair/lib-common-types/session/index.js'\nimport { createPublicKey, type KeyObject } from 'node:crypto'\nimport jwt from 'jsonwebtoken'\nimport JwksClient from 'jwks-rsa'\nimport cookie from 'cookie'\nimport { httpError } from '@data-fair/lib-utils/http-errors.js'\n\ntype TokenSessionState = Omit<SessionState, 'lang' | 'dark'>\n\nexport class SessionHandler {\n private _jwksClient: JwksClient.JwksClient | undefined\n public defaultLang: string = 'fr'\n\n get jwksClient () {\n if (!this._jwksClient) throw new Error('session management was not initialized')\n return this._jwksClient\n }\n\n initJWKS (directoryUrl = 'http://simple-directory:8080') {\n this._jwksClient = JwksClient({ jwksUri: directoryUrl + '/.well-known/jwks.json' })\n // the parsed key cache is derived from this jwks client (which enforces key rotation\n // through its own ttl), so it must be dropped whenever the client is recreated\n this._signingKeyObjects.clear()\n }\n\n // jwks-rsa caches the JWK per kid but it exports a new PEM string at every access\n // and jsonwebtoken parses that PEM again at every verification: caching the parsed\n // KeyObject per kid skips both costs.\n // IMPORTANT: getSigningKey must still be awaited on every verification. It is the only\n // thing that re-checks the kid against the current keyset, so a key rotated out of the\n // JWKS stops being accepted. Short-circuiting it from a persistent cache would accept a\n // rotated-out key forever (the lib#41 regression).\n private _signingKeyObjects = new Map<string, KeyObject>()\n private async getSigningKeyObject (kid?: string): Promise<KeyObject> {\n const signingKey = await this.jwksClient.getSigningKey(kid)\n const cacheKey = kid ?? ''\n let keyObject = this._signingKeyObjects.get(cacheKey)\n if (!keyObject) {\n keyObject = createPublicKey(signingKey.getPublicKey())\n this._signingKeyObjects.set(cacheKey, keyObject)\n }\n return keyObject\n }\n\n // used for the main session token, but can also be used to use the jwks to verify other types of tokens\n async verifyToken (token: string): Promise<any> {\n const decoded = jwt.decode(token, { complete: true })\n if (!decoded) throw new Error('failed to decode token')\n const signingKeyObject = await this.getSigningKeyObject(decoded.header.kid)\n return jwt.verify(token, signingKeyObject)\n }\n\n private buildTokenSessionState (user: User, organizationId: string, departmentId: string, switchedRole: string): TokenSessionState {\n const state: TokenSessionState = {}\n\n // this is to prevent null values that are put by SD versions that do not strictly respect their schema\n for (const org of user.organizations) {\n if (!org.department) {\n delete org.department\n delete org.departmentName\n }\n }\n\n state.user = user\n\n if (organizationId) {\n state.organization = user.organizations.find(o => {\n if (o.id !== organizationId) return false\n if (departmentId && departmentId !== o.department) return false\n if (switchedRole && switchedRole !== o.role) return false\n return true\n })\n }\n if (state.organization) {\n state.account = {\n type: 'organization',\n id: state.organization.id,\n name: state.organization.name,\n department: state.organization.department,\n departmentName: state.organization.departmentName\n }\n state.accountRole = state.organization.role\n } else {\n state.account = {\n type: 'user',\n id: user.id,\n name: user.name\n }\n state.accountRole = 'admin'\n return state\n }\n\n if (state.user?.siteOwner) {\n if (state.user.siteOwner.type === 'user' && state.user.siteOwner.id === state.user.id) {\n state.siteRole = 'admin'\n }\n if (state.user.siteOwner.type === 'organization' && state.user.siteOwner.id === state.organization?.id) {\n state.siteRole = state.organization.role\n }\n }\n return state\n }\n\n // accepts either the raw Cookie header or an already-parsed cookies object\n // (e.g. req.cookies when a cookie-parser middleware already ran upstream) to\n // avoid re-parsing and allocating a second cookies object per request\n async readStateFromCookie (rawCookies: string | Record<string, string | undefined> | undefined, onlyDecode?: boolean, clearBrokenSession?: () => void): Promise<SessionState> {\n if (!rawCookies) return { lang: this.defaultLang }\n const cookies = typeof rawCookies === 'string' ? cookie.parse(rawCookies) : rawCookies\n\n const lang = cookies.i18n_lang || this.defaultLang\n const dark = cookies.theme_dark === '1' || cookies.theme_dark === 'true'\n\n if (!cookies.id_token) return { lang, dark }\n if (!cookies.id_token_sign && !onlyDecode) return { lang, dark }\n const token = cookies.id_token + '.' + (cookies.id_token_sign ?? '')\n const organizationId = cookies.id_token_org ?? ''\n const departmentId = cookies.id_token_dep ?? ''\n const switchedRole = cookies.id_token_role ?? ''\n\n let user: User\n if (onlyDecode) {\n user = jwt.decode(token) as User\n } else {\n try {\n user = await this.verifyToken(token) as User\n } catch (err: any) {\n console.warn(err)\n if (err.name === 'JwksError') {\n // happens in case of temporary unavailability if SD\n // better not to disconnect user in this case\n throw httpError(500, 'Session token public keys not initialized')\n } else {\n if (clearBrokenSession) clearBrokenSession()\n throw httpError(401)\n }\n }\n }\n if (!user) return { lang, dark }\n return Object.assign({ lang, dark } as SessionState, this.buildTokenSessionState(user, organizationId, departmentId, switchedRole))\n }\n}\n"]}
1
+ {"version":3,"file":"session.js","sourceRoot":"","sources":["session.ts"],"names":[],"mappings":"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","sourcesContent":["import type { SessionState, User } from '@data-fair/lib-common-types/session/index.js'\nimport { parseScope } from '@data-fair/lib-common-types/session/index.js'\nimport { createPublicKey, type KeyObject } from 'node:crypto'\nimport type { JwtHeader } from 'jsonwebtoken'\nimport jwt from 'jsonwebtoken'\nimport JwksClient from 'jwks-rsa'\nimport cookie from 'cookie'\nimport { httpError } from '@data-fair/lib-utils/http-errors.js'\n\nexport type KeySet = 'session' | 'scoped'\n// simple-directory names the keys of its scoped key set scoped-<kid>; the signature still has to\n// verify against that set, so a forged kid gains nothing\nexport const keySetOfKid = (kid?: string): KeySet => kid?.startsWith('scoped-') ? 'scoped' : 'session'\n\n// only a genuine token failure means the credential is bad; anything else (jwks unreachable,\n// jwks-rsa rate limit, network error, unexpected bug) says nothing about the token and must\n// neither refuse it as invalid nor clear the session of the user\nconst tokenFailureNames = new Set(['JsonWebTokenError', 'TokenExpiredError', 'NotBeforeError', 'SigningKeyNotFoundError'])\nconst isTokenFailure = (err: any) =>\n err?.status === 401 || tokenFailureNames.has(err?.name) ||\n err?.message === 'failed to decode token' || err?.message === 'not a session token'\n\n// jwt.decode throws a raw SyntaxError on a payload that is not JSON: that is a malformed token,\n// not an infrastructure failure\nconst decodeToken = (token: string) => {\n let decoded\n try { decoded = jwt.decode(token, { complete: true }) } catch { throw new Error('failed to decode token') }\n if (!decoded) throw new Error('failed to decode token')\n return decoded\n}\n// RFC 7515 4.1.9: the media type comparison is case-insensitive and the application/ prefix is optional\nconst isAccessTokenTyp = (typ?: string) => typeof typ === 'string' && typ.toLowerCase().replace(/^application\\//, '') === 'at+jwt'\n\ntype TokenSessionState = Omit<SessionState, 'lang' | 'dark'>\n\nexport class SessionHandler {\n private _jwksClients: Record<KeySet, JwksClient.JwksClient> | undefined\n public defaultLang: string = 'fr'\n // a service declares itself scope-aware once it enforces scopes on its routes; until then a\n // scope-restricted session (an NHI or OAuth access token with a scope claim) is refused with\n // a 403 instead of acting with the full role of its identity\n public scopeAware = false\n\n get jwksClients () {\n if (!this._jwksClients) throw new Error('session management was not initialized')\n return this._jwksClients\n }\n\n initJWKS (directoryUrl = 'http://simple-directory:8080') {\n const client = (path: string) => JwksClient({ jwksUri: directoryUrl + path })\n this._jwksClients = {\n session: client('/.well-known/jwks.json'),\n scoped: client('/.well-known/scoped-jwks.json')\n }\n // the parsed key cache is derived from these jwks clients (which enforce key rotation\n // through their own ttl), so it must be dropped whenever the clients are recreated\n this._signingKeyObjects.clear()\n }\n\n // jwks-rsa caches the JWK per kid but it exports a new PEM string at every access\n // and jsonwebtoken parses that PEM again at every verification: caching the parsed\n // KeyObject per key set and kid skips both costs.\n // IMPORTANT: getSigningKey must still be awaited on every verification. It is the only\n // thing that re-checks the kid against the current keyset, so a key rotated out of the\n // JWKS stops being accepted. Short-circuiting it from a persistent cache would accept a\n // rotated-out key forever (the lib#41 regression).\n private _signingKeyObjects = new Map<string, KeyObject>()\n protected async getSigningKeyObject (keySet: KeySet, kid?: string): Promise<KeyObject> {\n const signingKey = await this.jwksClients[keySet].getSigningKey(kid)\n const cacheKey = keySet + ':' + (kid ?? '')\n let keyObject = this._signingKeyObjects.get(cacheKey)\n if (!keyObject) {\n keyObject = createPublicKey(signingKey.getPublicKey())\n this._signingKeyObjects.set(cacheKey, keyObject)\n }\n return keyObject\n }\n\n // verifies against the key set designated by the kid: a token claiming a scoped kid must be\n // signed by a key of the scoped set, any other must be signed by a key of the session set\n async verifyTokenKeySet (token: string): Promise<{ payload: any, header: JwtHeader, keySet: KeySet }> {\n const decoded = decodeToken(token)\n const keySet = keySetOfKid(decoded.header.kid)\n const signingKeyObject = await this.getSigningKeyObject(keySet, decoded.header.kid)\n const payload = jwt.verify(token, signingKeyObject, { algorithms: ['RS256'] })\n return { payload, header: decoded.header, keySet }\n }\n\n // used for the main session token, but can also be used to use the jwks to verify other types of tokens\n // (simple-directory verifies its exchange tokens with it): only the session key set is accepted\n async verifyToken (token: string): Promise<any> {\n const { payload, keySet } = await this.verifyTokenKeySet(token)\n if (keySet !== 'session') throw new Error('not a session token')\n return payload\n }\n\n private assertTokenKind (path: 'bearer' | 'cookie', header: JwtHeader, payload: any, keySet: KeySet) {\n if (path === 'bearer') {\n if (!isAccessTokenTyp(header.typ)) throw httpError(401, 'bearer token is not an access token')\n if (keySet !== 'scoped') throw httpError(401, 'access token must be signed by the scoped key set')\n } else {\n if (isAccessTokenTyp(header.typ)) throw httpError(401, 'an access token is not a session token')\n const hasScope = payload && typeof payload === 'object' && 'scope' in payload\n if (keySet === 'session' && hasScope) throw httpError(401, 'session key set token must not carry a scope')\n if (keySet === 'scoped' && !hasScope) throw httpError(401, 'scoped key set token must carry a scope')\n if (keySet === 'scoped' && payload?.organizations?.length > 1) throw httpError(401, 'scoped token must be bound to at most one organization')\n }\n if (keySet === 'scoped') {\n if (payload && ('adminMode' in payload || 'asAdmin' in payload || 'isAdmin' in payload)) throw httpError(401, 'scoped token cannot carry superadmin claims')\n if (typeof payload?.exp !== 'number') throw httpError(401, 'scoped token must expire')\n }\n }\n\n private buildTokenSessionState (user: User, organizationId: string, departmentId: string, switchedRole: string): TokenSessionState {\n const state: TokenSessionState = {}\n\n // this is to prevent null values that are put by SD versions that do not strictly respect their schema\n for (const org of user.organizations) {\n if (!org.department) {\n delete org.department\n delete org.departmentName\n }\n }\n\n state.user = user\n // absent claim = unrestricted first party session; anything but a string is malformed\n // (null must not silently become unrestricted)\n if (user.scope !== undefined) {\n if (typeof user.scope !== 'string') throw httpError(401, 'invalid scope claim')\n state.scopes = parseScope(user.scope)\n }\n\n if (organizationId) {\n state.organization = user.organizations.find(o => {\n if (o.id !== organizationId) return false\n if (departmentId && departmentId !== o.department) return false\n if (switchedRole && switchedRole !== o.role) return false\n return true\n })\n }\n if (state.organization) {\n state.account = {\n type: 'organization',\n id: state.organization.id,\n name: state.organization.name,\n department: state.organization.department,\n departmentName: state.organization.departmentName\n }\n state.accountRole = state.organization.role\n } else {\n state.account = {\n type: 'user',\n id: user.id,\n name: user.name\n }\n state.accountRole = 'admin'\n return state\n }\n\n if (state.user?.siteOwner) {\n if (state.user.siteOwner.type === 'user' && state.user.siteOwner.id === state.user.id) {\n state.siteRole = 'admin'\n }\n if (state.user.siteOwner.type === 'organization' && state.user.siteOwner.id === state.organization?.id) {\n state.siteRole = state.organization.role\n }\n }\n return state\n }\n\n private assertScopeAware (state: SessionState) {\n if (state.scopes && !this.scopeAware) throw httpError(403, 'this service does not accept scope-restricted sessions')\n }\n\n // accepts either the raw Cookie header or an already-parsed cookies object\n // (e.g. req.cookies when a cookie-parser middleware already ran upstream) to\n // avoid re-parsing and allocating a second cookies object per request\n // raw reader, applies neither the route scope guard nor the session cache: not for authorization,\n // use session.req() / reqSession(). It only refuses scoped states when the handler is not scope-aware.\n async readStateFromCookie (rawCookies: string | Record<string, string | undefined> | undefined, onlyDecode?: boolean, clearBrokenSession?: () => void): Promise<SessionState> {\n if (!rawCookies) return { lang: this.defaultLang }\n const cookies = typeof rawCookies === 'string' ? cookie.parse(rawCookies) : rawCookies\n\n const lang = cookies.i18n_lang || this.defaultLang\n const dark = cookies.theme_dark === '1' || cookies.theme_dark === 'true'\n\n if (!cookies.id_token) return { lang, dark }\n if (!cookies.id_token_sign && !onlyDecode) return { lang, dark }\n const token = cookies.id_token + '.' + (cookies.id_token_sign ?? '')\n const organizationId = cookies.id_token_org ?? ''\n const departmentId = cookies.id_token_dep ?? ''\n const switchedRole = cookies.id_token_role ?? ''\n\n let user: User\n if (onlyDecode) {\n let decoded\n try {\n decoded = decodeToken(token)\n this.assertTokenKind('cookie', decoded.header, decoded.payload, keySetOfKid(decoded.header.kid))\n } catch (err: any) {\n if (clearBrokenSession) clearBrokenSession()\n throw err.status === 401 ? err : httpError(401)\n }\n user = decoded.payload as User\n } else {\n try {\n const verified = await this.verifyTokenKeySet(token)\n user = verified.payload as User\n this.assertTokenKind('cookie', verified.header, verified.payload, verified.keySet)\n } catch (err: any) {\n console.warn(err)\n if (!isTokenFailure(err)) {\n // happens in case of temporary unavailability of SD\n // better not to disconnect user in this case\n throw httpError(500, 'Session token public keys not initialized')\n }\n if (clearBrokenSession) clearBrokenSession()\n throw err.status === 401 ? err : httpError(401)\n }\n }\n if (!user) return { lang, dark }\n const state = Object.assign({ lang, dark } as SessionState, this.buildTokenSessionState(user, organizationId, departmentId, switchedRole))\n this.assertScopeAware(state)\n return state\n }\n\n // Only tokens minted as access tokens (RFC 9068 header typ at+jwt) are bearer credentials.\n // Session cookie tokens and the long lived exchange token are signed by the same keys and\n // must never be usable this way (cf simple-directory docs/architecture/session-theft-protections.md).\n // An access token is bound to one account: its organizations array holds the one membership it\n // acts in (1 = that organization), or nothing for the personal account (0 memberships). More than\n // one is refused because the full array stays in user.organizations and consumers filter on it.\n // A missing organizations key is not an access token either.\n // raw reader, applies neither the route scope guard nor the session cache: not for authorization,\n // use session.req() / reqSession(). It only refuses scoped states when the handler is not scope-aware.\n async readStateFromBearer (token: string, lang?: string): Promise<SessionState> {\n let decoded\n try { decoded = decodeToken(token) } catch { throw httpError(401, 'bearer token is not an access token') }\n if (!isAccessTokenTyp(decoded.header.typ)) throw httpError(401, 'bearer token is not an access token')\n let user: User\n try {\n const verified = await this.verifyTokenKeySet(token)\n this.assertTokenKind('bearer', verified.header, verified.payload, verified.keySet)\n user = verified.payload as User\n } catch (err: any) {\n if (!isTokenFailure(err)) throw httpError(500, 'Session token public keys not initialized')\n throw err.status === 401 ? err : httpError(401)\n }\n if (!user?.organizations) throw httpError(401, 'bearer token is not an access token')\n if (user.organizations.length > 1) throw httpError(401, 'access token must be bound to at most one organization')\n const org = user.organizations[0]\n const state = Object.assign({ lang: lang ?? this.defaultLang } as SessionState, this.buildTokenSessionState(user, org?.id ?? '', org?.department ?? '', ''))\n this.assertScopeAware(state)\n return state\n }\n}\n"]}
package/session.test.js CHANGED
@@ -2,7 +2,7 @@ import { describe, it } from 'node:test';
2
2
  import { strict as assert } from 'assert';
3
3
  import { generateKeyPairSync } from 'node:crypto';
4
4
  import jwt from 'jsonwebtoken';
5
- import { SessionHandler } from './session.js';
5
+ import { SessionHandler, keySetOfKid } from './session.js';
6
6
  const testUser = {
7
7
  email: 'test@example.com',
8
8
  id: 'user1',
@@ -16,9 +16,9 @@ const testUser = {
16
16
  const stubbedHandler = () => {
17
17
  const handler = new SessionHandler();
18
18
  const calls = { count: 0 };
19
- handler.verifyToken = async () => {
19
+ handler.verifyTokenKeySet = async () => {
20
20
  calls.count++;
21
- return structuredClone(testUser);
21
+ return { payload: structuredClone(testUser), header: { alg: 'RS256' }, keySet: 'session' };
22
22
  };
23
23
  return { handler, calls };
24
24
  };
@@ -114,14 +114,14 @@ describe('SessionHandler.readStateFromCookie', () => {
114
114
  });
115
115
  it('should reject and clear the session when the token is invalid', async () => {
116
116
  const handler = new SessionHandler();
117
- handler.verifyToken = async () => { throw Object.assign(new Error('jwt expired'), { name: 'TokenExpiredError' }); };
117
+ handler.verifyTokenKeySet = async () => { throw Object.assign(new Error('jwt expired'), { name: 'TokenExpiredError' }); };
118
118
  let cleared = false;
119
119
  await assert.rejects(handler.readStateFromCookie('id_token=h.p; id_token_sign=s', undefined, () => { cleared = true; }), (err) => err.status === 401);
120
120
  assert.equal(cleared, true);
121
121
  });
122
122
  it('should surface a 500 without clearing the session on a jwks error', async () => {
123
123
  const handler = new SessionHandler();
124
- handler.verifyToken = async () => { throw Object.assign(new Error('jwks down'), { name: 'JwksError' }); };
124
+ handler.verifyTokenKeySet = async () => { throw Object.assign(new Error('jwks down'), { name: 'JwksError' }); };
125
125
  let cleared = false;
126
126
  await assert.rejects(handler.readStateFromCookie('id_token=h.p; id_token_sign=s', undefined, () => { cleared = true; }), (err) => err.status === 500);
127
127
  assert.equal(cleared, false);
@@ -134,10 +134,12 @@ describe('SessionHandler.verifyToken', () => {
134
134
  const handler = new SessionHandler();
135
135
  let getSigningKeyCalls = 0;
136
136
  let getPublicKeyCalls = 0;
137
- handler._jwksClient = {
138
- getSigningKey: async () => {
139
- getSigningKeyCalls++;
140
- return { getPublicKey: () => { getPublicKeyCalls++; return pem; } };
137
+ handler._jwksClients = {
138
+ session: {
139
+ getSigningKey: async () => {
140
+ getSigningKeyCalls++;
141
+ return { getPublicKey: () => { getPublicKeyCalls++; return pem; } };
142
+ }
141
143
  }
142
144
  };
143
145
  const token = jwt.sign({ id: 'user1' }, privateKey, { algorithm: 'RS256', keyid: 'kid1' });
@@ -156,11 +158,13 @@ describe('SessionHandler.verifyToken', () => {
156
158
  const pem = publicKey.export({ format: 'pem', type: 'spki' });
157
159
  const handler = new SessionHandler();
158
160
  let keyPresent = true;
159
- handler._jwksClient = {
160
- getSigningKey: async () => {
161
- if (!keyPresent)
162
- throw Object.assign(new Error('key not found'), { name: 'SigningKeyNotFoundError' });
163
- return { getPublicKey: () => pem };
161
+ handler._jwksClients = {
162
+ session: {
163
+ getSigningKey: async () => {
164
+ if (!keyPresent)
165
+ throw Object.assign(new Error('key not found'), { name: 'SigningKeyNotFoundError' });
166
+ return { getPublicKey: () => pem };
167
+ }
164
168
  }
165
169
  };
166
170
  const token = jwt.sign({ id: 'user1' }, privateKey, { algorithm: 'RS256', keyid: 'kid1' });
@@ -172,4 +176,206 @@ describe('SessionHandler.verifyToken', () => {
172
176
  await assert.rejects(handler.verifyToken(token));
173
177
  });
174
178
  });
179
+ const sessionKeys = generateKeyPairSync('rsa', { modulusLength: 2048 });
180
+ const scopedKeys = generateKeyPairSync('rsa', { modulusLength: 2048 });
181
+ const signWith = (privateKey, kid, payload, header = {}, opts = {}) => jwt.sign(payload, privateKey, { algorithm: 'RS256', expiresIn: '5m', keyid: kid, header: { alg: 'RS256', ...header }, ...opts });
182
+ const signSession = (payload, header, opts) => signWith(sessionKeys.privateKey, 'sd-1', payload, header, opts);
183
+ const signScoped = (payload, header, opts) => signWith(scopedKeys.privateKey, 'scoped-sd-1', payload, header, opts);
184
+ // real jwt verification against two local key pairs, one per key set
185
+ const realHandler = (scopeAware = true) => {
186
+ const handler = new SessionHandler();
187
+ handler.scopeAware = scopeAware;
188
+ handler.getSigningKeyObject = async (keySet) => keySet === 'scoped' ? scopedKeys.publicKey : sessionKeys.publicKey;
189
+ return handler;
190
+ };
191
+ const toCookies = (token) => {
192
+ const [h, p, s] = token.split('.');
193
+ return `id_token=${h}.${p}; id_token_sign=${s}`;
194
+ };
195
+ const rejects401 = (p) => assert.rejects(p, (err) => err.status === 401);
196
+ describe('SessionHandler.readStateFromBearer', () => {
197
+ const nhiUser = { ...testUser, organizations: [testUser.organizations[0]], nhi: 1, scope: 'datasets:read organizations:read' };
198
+ const bearer = (payload, header = { typ: 'at+jwt' }, opts) => signScoped(payload, header, opts);
199
+ it('reads an at+jwt access token, bound to its single organization, with its scopes', async () => {
200
+ const state = await realHandler().readStateFromBearer(bearer(nhiUser));
201
+ assert.equal(state.user?.id, 'user1');
202
+ assert.equal(state.account?.type, 'organization');
203
+ assert.equal(state.account?.id, 'org1');
204
+ assert.equal(state.accountRole, 'admin');
205
+ assert.deepEqual(state.scopes, ['datasets:read', 'organizations:read']);
206
+ });
207
+ it('refuses (401) a token bound to several organizations', async () => {
208
+ await rejects401(realHandler().readStateFromBearer(bearer(testUser)));
209
+ });
210
+ it('gives a personal account session to a token with no organization membership', async () => {
211
+ const state = await realHandler().readStateFromBearer(bearer({ ...testUser, organizations: [], scope: 'datasets' }));
212
+ assert.equal(state.account?.type, 'user');
213
+ assert.equal(state.account?.id, 'user1');
214
+ assert.equal(state.accountRole, 'admin');
215
+ });
216
+ it('refuses a token without an organizations key', async () => {
217
+ const { organizations, ...noOrgs } = testUser;
218
+ await rejects401(realHandler().readStateFromBearer(bearer({ ...noOrgs, scope: 'datasets' })));
219
+ });
220
+ it('refuses (401) an invalid scope claim on the bearer and cookie paths', async () => {
221
+ for (const scope of [null, 12, ['a']]) {
222
+ await rejects401(realHandler().readStateFromBearer(bearer({ ...nhiUser, scope })));
223
+ const handler = new SessionHandler();
224
+ handler.scopeAware = true;
225
+ handler.verifyTokenKeySet = async () => ({ payload: structuredClone({ ...testUser, scope, exp: 4102444800 }), header: { alg: 'RS256' }, keySet: 'scoped' });
226
+ await rejects401(handler.readStateFromCookie('id_token=a.b; id_token_sign=c'));
227
+ }
228
+ });
229
+ it('refuses a scoped session unless the handler is scope-aware', async () => {
230
+ const handler = realHandler(false);
231
+ assert.equal(handler.scopeAware, false);
232
+ await assert.rejects(handler.readStateFromBearer(bearer(nhiUser)), (err) => err.status === 403);
233
+ const cookies = toCookies(signScoped({ ...testUser, organizations: [testUser.organizations[0]], scope: 'datasets' }));
234
+ await assert.rejects(handler.readStateFromCookie(cookies), (err) => err.status === 403);
235
+ handler.scopeAware = true;
236
+ assert.deepEqual((await handler.readStateFromCookie(cookies)).scopes, ['datasets']);
237
+ assert.deepEqual((await handler.readStateFromBearer(bearer(nhiUser))).scopes, ['datasets:read', 'organizations:read']);
238
+ });
239
+ it('refuses an expired token or one signed by another key when really verifying', async () => {
240
+ const { privateKey: otherKey } = generateKeyPairSync('rsa', { modulusLength: 2048 });
241
+ const handler = realHandler();
242
+ assert.equal((await handler.readStateFromBearer(bearer(nhiUser))).user?.id, 'user1');
243
+ await rejects401(handler.readStateFromBearer(bearer(nhiUser, undefined, { expiresIn: -10 })));
244
+ await rejects401(handler.readStateFromBearer(signWith(otherKey, 'scoped-sd-1', nhiUser, { typ: 'at+jwt' })));
245
+ });
246
+ it('refuses a session token or an exchange token presented as bearer', async () => {
247
+ const handler = realHandler();
248
+ await rejects401(handler.readStateFromBearer(signSession(testUser)));
249
+ await rejects401(handler.readStateFromBearer(signSession({ user: 'user1', session: 'abc' }, { typ: 'JWT' })));
250
+ await rejects401(handler.readStateFromBearer('not-a-jwt'));
251
+ });
252
+ it('fills scopes from a cookie session too', async () => {
253
+ const state = await realHandler().readStateFromCookie(toCookies(signScoped({ ...testUser, organizations: [testUser.organizations[0]], scope: 'datasets' })));
254
+ assert.deepEqual(state.scopes, ['datasets']);
255
+ });
256
+ });
257
+ describe('key sets and token kinds', () => {
258
+ const nhiUser = { ...testUser, organizations: [testUser.organizations[0]], nhi: 1, scope: 'datasets:read' };
259
+ it('keySetOfKid', () => {
260
+ assert.equal(keySetOfKid('scoped-sd-1'), 'scoped');
261
+ assert.equal(keySetOfKid('sd-1'), 'session');
262
+ assert.equal(keySetOfKid(undefined), 'session');
263
+ });
264
+ it('bearer requires an at+jwt from the scoped set', async () => {
265
+ const handler = realHandler();
266
+ assert.equal((await handler.readStateFromBearer(signScoped(nhiUser, { typ: 'at+jwt' }))).user?.id, 'user1');
267
+ await rejects401(handler.readStateFromBearer(signSession(nhiUser, { typ: 'at+jwt' })));
268
+ await rejects401(handler.readStateFromBearer(signScoped(nhiUser, {})));
269
+ });
270
+ it('a token signed by one set is not verified by the other', async () => {
271
+ const handler = realHandler();
272
+ // claims a scoped kid but is signed with the session private key
273
+ await rejects401(handler.readStateFromBearer(signWith(sessionKeys.privateKey, 'scoped-sd-1', nhiUser, { typ: 'at+jwt' })));
274
+ // and the other way round, on the cookie path
275
+ await rejects401(handler.readStateFromCookie(toCookies(signWith(scopedKeys.privateKey, 'sd-1', testUser))));
276
+ });
277
+ it('cookie path refuses access tokens and enforces the scope/key-set pairing', async () => {
278
+ const handler = realHandler();
279
+ await rejects401(handler.readStateFromCookie(toCookies(signScoped(nhiUser, { typ: 'at+jwt' }))));
280
+ await rejects401(handler.readStateFromCookie(toCookies(signSession({ ...testUser, scope: 'datasets' }))));
281
+ await rejects401(handler.readStateFromCookie(toCookies(signScoped(testUser))));
282
+ assert.deepEqual((await handler.readStateFromCookie(toCookies(signScoped({ ...testUser, organizations: [testUser.organizations[0]], scope: 'datasets' })))).scopes, ['datasets']);
283
+ assert.equal((await handler.readStateFromCookie(toCookies(signSession(testUser)))).scopes, undefined);
284
+ });
285
+ it('applies the same kind rules when only decoding', async () => {
286
+ const handler = realHandler();
287
+ await rejects401(handler.readStateFromCookie(toCookies(signScoped(nhiUser, { typ: 'at+jwt' })), true));
288
+ await rejects401(handler.readStateFromCookie(toCookies(signSession({ ...testUser, scope: 'datasets' })), true));
289
+ await rejects401(handler.readStateFromCookie(toCookies(signScoped(testUser)), true));
290
+ assert.equal((await handler.readStateFromCookie(toCookies(signSession(testUser)), true)).user?.id, 'user1');
291
+ });
292
+ it('scoped-set tokens cannot carry superadmin claims and must expire', async () => {
293
+ const handler = realHandler();
294
+ for (const claims of [{ adminMode: 1 }, { asAdmin: { id: 'a', name: 'A' } }, { isAdmin: 1 }, { isAdmin: 0 }, { adminMode: false }]) {
295
+ await rejects401(handler.readStateFromBearer(signScoped({ ...nhiUser, ...claims }, { typ: 'at+jwt' })));
296
+ }
297
+ // no exp claim at all
298
+ const noExp = jwt.sign(nhiUser, scopedKeys.privateKey, { algorithm: 'RS256', keyid: 'scoped-sd-1', header: { alg: 'RS256', typ: 'at+jwt' }, noTimestamp: true });
299
+ assert.equal(jwt.decode(noExp).exp, undefined);
300
+ await rejects401(handler.readStateFromBearer(noExp));
301
+ });
302
+ it('verifyToken only accepts the session key set', async () => {
303
+ const handler = realHandler();
304
+ assert.equal((await handler.verifyToken(signSession({ id: 'user1' }))).id, 'user1');
305
+ await assert.rejects(handler.verifyToken(signScoped({ id: 'user1', scope: 'datasets' })));
306
+ });
307
+ it('only RS256 is accepted', async () => {
308
+ const handler = realHandler();
309
+ const token = jwt.sign(testUser, sessionKeys.privateKey, { algorithm: 'PS256', keyid: 'sd-1', expiresIn: '5m' });
310
+ await rejects401(handler.readStateFromCookie(toCookies(token)));
311
+ await assert.rejects(handler.verifyToken(token));
312
+ });
313
+ });
314
+ describe('verification errors', () => {
315
+ const stubbed = (err) => {
316
+ const handler = realHandler();
317
+ handler.getSigningKeyObject = async () => { throw err; };
318
+ return handler;
319
+ };
320
+ const named = (name, message = name) => Object.assign(new Error(message), { name });
321
+ const token = () => signScoped({ ...testUser, organizations: [testUser.organizations[0]], scope: 'datasets' }, { typ: 'at+jwt' });
322
+ it('infrastructure failures are a 500 and never clear the cookies', async () => {
323
+ for (const err of [named('JwksRateLimitError'), new Error('connect ECONNREFUSED'), named('JwksError')]) {
324
+ const handler = stubbed(err);
325
+ let cleared = false;
326
+ await assert.rejects(handler.readStateFromCookie(toCookies(signScoped({ ...testUser, organizations: [testUser.organizations[0]], scope: 'datasets' })), false, () => { cleared = true; }), (e) => e.status === 500);
327
+ assert.equal(cleared, false);
328
+ await assert.rejects(handler.readStateFromBearer(token()), (e) => e.status === 500);
329
+ }
330
+ });
331
+ it('a kid absent from the jwks is a 401 and clears the cookies', async () => {
332
+ const handler = stubbed(named('SigningKeyNotFoundError'));
333
+ let cleared = false;
334
+ await assert.rejects(handler.readStateFromCookie(toCookies(signScoped({ ...testUser, organizations: [testUser.organizations[0]], scope: 'datasets' })), false, () => { cleared = true; }), (e) => e.status === 401);
335
+ assert.equal(cleared, true);
336
+ await rejects401(handler.readStateFromBearer(token()));
337
+ });
338
+ it('looks a scoped kid up in the scoped client and another kid in the session client', async () => {
339
+ const handler = new SessionHandler();
340
+ const calls = [];
341
+ const client = (name, key) => ({ getSigningKey: async (kid) => { calls.push(name + ':' + kid); return { getPublicKey: () => key.export({ format: 'pem', type: 'spki' }) }; } });
342
+ handler._jwksClients = { session: client('session', sessionKeys.publicKey), scoped: client('scoped', scopedKeys.publicKey) };
343
+ await handler.verifyTokenKeySet(signSession({ id: 'u' }));
344
+ await handler.verifyTokenKeySet(signScoped({ id: 'u' }));
345
+ assert.deepEqual(calls, ['session:sd-1', 'scoped:scoped-sd-1']);
346
+ });
347
+ });
348
+ describe('malformed tokens, typ normalization, one organization per scoped cookie token', () => {
349
+ const b64 = (o) => Buffer.from(typeof o === 'string' ? o : JSON.stringify(o)).toString('base64url');
350
+ const malformed = (typ, kid) => b64({ alg: 'RS256', typ, kid }) + '.' + b64('{not json') + '.sig';
351
+ const oneOrg = [testUser.organizations[0]];
352
+ it('a token whose payload is not JSON is a 401 and clears the cookies', async () => {
353
+ for (const onlyDecode of [false, true]) {
354
+ const handler = realHandler();
355
+ let cleared = false;
356
+ await rejects401(handler.readStateFromCookie(toCookies(malformed('JWT', 'sd-1')), onlyDecode, () => { cleared = true; }));
357
+ assert.equal(cleared, true);
358
+ }
359
+ });
360
+ it('a bearer whose payload is not JSON is a 401', async () => {
361
+ const handler = realHandler();
362
+ await rejects401(handler.readStateFromBearer(malformed('JWT', 'sd-1')));
363
+ await rejects401(handler.readStateFromBearer(malformed('at+jwt', 'scoped-sd-1')));
364
+ });
365
+ it('a scoped cookie token carrying more than one organization is a 401', async () => {
366
+ const handler = realHandler();
367
+ await rejects401(handler.readStateFromCookie(toCookies(signScoped({ ...testUser, scope: 'datasets' }))));
368
+ await rejects401(handler.readStateFromCookie(toCookies(signScoped({ ...testUser, scope: 'datasets' })), true));
369
+ assert.equal((await handler.readStateFromCookie(toCookies(signScoped({ ...testUser, organizations: oneOrg, scope: 'datasets' })))).organization, undefined);
370
+ });
371
+ it('typ at+jwt is compared case-insensitively with an optional application/ prefix', async () => {
372
+ const handler = realHandler();
373
+ const nhi = { ...testUser, organizations: oneOrg, scope: 'datasets:read' };
374
+ for (const typ of ['AT+JWT', 'application/at+jwt', 'Application/AT+JWT']) {
375
+ assert.equal((await handler.readStateFromBearer(signScoped(nhi, { typ }))).user?.id, 'user1');
376
+ await rejects401(handler.readStateFromCookie(toCookies(signScoped(nhi, { typ }))));
377
+ await rejects401(handler.readStateFromCookie(toCookies(signScoped(nhi, { typ })), true));
378
+ }
379
+ });
380
+ });
175
381
  //# sourceMappingURL=session.test.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"session.test.js","sourceRoot":"","sources":["session.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { describe, it } from 'node:test'\nimport { strict as assert } from 'assert'\nimport { generateKeyPairSync } from 'node:crypto'\nimport jwt from 'jsonwebtoken'\nimport { SessionHandler } from './session.js'\n\nconst testUser = {\n email: 'test@example.com',\n id: 'user1',\n name: 'Test User',\n organizations: [\n { id: 'org1', name: 'Test Org', role: 'admin' },\n { id: 'org2', name: 'Other Org', role: 'user', department: 'dep1', departmentName: 'Department 1' }\n ]\n}\n\n// a handler whose token verification is stubbed, counts the number of verifications\nconst stubbedHandler = () => {\n const handler = new SessionHandler()\n const calls = { count: 0 }\n handler.verifyToken = async () => {\n calls.count++\n return structuredClone(testUser)\n }\n return { handler, calls }\n}\n\ndescribe('SessionHandler.readStateFromCookie', () => {\n it('should return default session with no cookies', async () => {\n const handler = new SessionHandler()\n const state = await handler.readStateFromCookie(undefined)\n assert.equal(state.lang, 'fr')\n assert.equal(state.user, undefined)\n assert.equal(state.dark, undefined)\n })\n\n it('should parse dark mode cookie', async () => {\n const handler = new SessionHandler()\n const state = await handler.readStateFromCookie('theme_dark=true')\n assert.equal(state.dark, true)\n })\n\n it('should parse i18n_lang cookie', async () => {\n const handler = new SessionHandler()\n const state = await handler.readStateFromCookie('i18n_lang=en')\n assert.equal(state.lang, 'en')\n })\n\n it('should use defaultLang when no i18n_lang cookie', async () => {\n const handler = new SessionHandler()\n handler.defaultLang = 'de'\n const state = await handler.readStateFromCookie('')\n assert.equal(state.lang, 'de')\n })\n\n it('should return early when no id_token', async () => {\n const handler = new SessionHandler()\n const state = await handler.readStateFromCookie('i18n_lang=en; theme_dark=1')\n assert.equal(state.lang, 'en')\n assert.equal(state.dark, true)\n assert.equal(state.user, undefined)\n })\n\n it('should return early when no id_token_sign and not onlyDecode', async () => {\n const handler = new SessionHandler()\n const state = await handler.readStateFromCookie('id_token=header.payload')\n assert.equal(state.user, undefined)\n })\n\n it('should select the organization account from cookies', async () => {\n const { handler } = stubbedHandler()\n const state = await handler.readStateFromCookie('id_token=h.p; id_token_sign=s; id_token_org=org2; id_token_dep=dep1')\n assert.equal(state.account?.type, 'organization')\n assert.equal(state.account?.id, 'org2')\n assert.equal(state.account?.department, 'dep1')\n assert.equal(state.accountRole, 'user')\n })\n\n it('should build a user account when no org cookie', async () => {\n const { handler } = stubbedHandler()\n const state = await handler.readStateFromCookie('id_token=h.p; id_token_sign=s')\n assert.equal(state.account?.type, 'user')\n assert.equal(state.account?.id, 'user1')\n assert.equal(state.accountRole, 'admin')\n })\n\n it('should accept an already-parsed cookies object without re-parsing', async () => {\n const { handler, calls } = stubbedHandler()\n const parsed = { id_token: 'h.p', id_token_sign: 's', id_token_org: 'org2', id_token_dep: 'dep1', i18n_lang: 'en', theme_dark: '1' }\n const state = await handler.readStateFromCookie(parsed)\n assert.equal(state.lang, 'en')\n assert.equal(state.dark, true)\n assert.equal(state.account?.type, 'organization')\n assert.equal(state.account?.id, 'org2')\n assert.equal(calls.count, 1)\n })\n\n it('should produce equivalent state from string and object cookie inputs', async () => {\n const { handler } = stubbedHandler()\n const fromString = await handler.readStateFromCookie('id_token=h.p; id_token_sign=s; i18n_lang=en')\n const fromObject = await handler.readStateFromCookie({ id_token: 'h.p', id_token_sign: 's', i18n_lang: 'en' })\n assert.deepEqual(fromString, fromObject)\n })\n\n it('should verify the token on every request', async () => {\n const { handler, calls } = stubbedHandler()\n const cookies = 'id_token=h.p; id_token_sign=s'\n await handler.readStateFromCookie(cookies)\n await handler.readStateFromCookie(cookies)\n assert.equal(calls.count, 2)\n })\n\n it('should return a mutable state that callers can adjust', async () => {\n const { handler } = stubbedHandler()\n const state = await handler.readStateFromCookie('id_token=h.p; id_token_sign=s')\n state.user!.name = 'mutated' // must not throw\n state.user!.organizations[0].role = 'other'\n assert.equal(state.user!.name, 'mutated')\n assert.equal(state.user!.organizations[0].role, 'other')\n })\n\n it('should decode without verifying in onlyDecode mode', async () => {\n const handler = new SessionHandler()\n const token = jwt.sign(structuredClone(testUser), 'secret')\n const [header, payload, sign] = token.split('.')\n const state = await handler.readStateFromCookie(`id_token=${header}.${payload}; id_token_sign=${sign}`, true)\n assert.equal(state.user?.id, 'user1')\n assert.equal(state.account?.type, 'user')\n })\n\n it('should reject and clear the session when the token is invalid', async () => {\n const handler = new SessionHandler()\n handler.verifyToken = async () => { throw Object.assign(new Error('jwt expired'), { name: 'TokenExpiredError' }) }\n let cleared = false\n await assert.rejects(\n handler.readStateFromCookie('id_token=h.p; id_token_sign=s', undefined, () => { cleared = true }),\n (err: any) => err.status === 401\n )\n assert.equal(cleared, true)\n })\n\n it('should surface a 500 without clearing the session on a jwks error', async () => {\n const handler = new SessionHandler()\n handler.verifyToken = async () => { throw Object.assign(new Error('jwks down'), { name: 'JwksError' }) }\n let cleared = false\n await assert.rejects(\n handler.readStateFromCookie('id_token=h.p; id_token_sign=s', undefined, () => { cleared = true }),\n (err: any) => err.status === 500\n )\n assert.equal(cleared, false)\n })\n})\n\ndescribe('SessionHandler.verifyToken', () => {\n it('should consult the JWKS on every verification but cache the parsed key per kid', async () => {\n const { privateKey, publicKey } = generateKeyPairSync('rsa', { modulusLength: 2048 })\n const pem = publicKey.export({ format: 'pem', type: 'spki' }) as string\n const handler = new SessionHandler()\n let getSigningKeyCalls = 0\n let getPublicKeyCalls = 0\n ;(handler as any)._jwksClient = {\n getSigningKey: async () => {\n getSigningKeyCalls++\n return { getPublicKey: () => { getPublicKeyCalls++; return pem } }\n }\n }\n const token = jwt.sign({ id: 'user1' }, privateKey, { algorithm: 'RS256', keyid: 'kid1' })\n const payload1 = await handler.verifyToken(token)\n assert.equal(payload1.id, 'user1')\n const payload2 = await handler.verifyToken(token)\n assert.equal(payload2.id, 'user1')\n // the signing key must be re-checked against the JWKS every time so that a key\n // rotated out of the keyset stops being accepted (lib#41 regression)\n assert.equal(getSigningKeyCalls, 2)\n // but the expensive PEM export + parsing is cached per kid\n assert.equal(getPublicKeyCalls, 1)\n })\n\n it('should reject a token whose signing key was rotated out of the JWKS', async () => {\n const { privateKey, publicKey } = generateKeyPairSync('rsa', { modulusLength: 2048 })\n const pem = publicKey.export({ format: 'pem', type: 'spki' }) as string\n const handler = new SessionHandler()\n let keyPresent = true\n ;(handler as any)._jwksClient = {\n getSigningKey: async () => {\n if (!keyPresent) throw Object.assign(new Error('key not found'), { name: 'SigningKeyNotFoundError' })\n return { getPublicKey: () => pem }\n }\n }\n const token = jwt.sign({ id: 'user1' }, privateKey, { algorithm: 'RS256', keyid: 'kid1' })\n // first verification succeeds and would populate any key cache\n assert.equal((await handler.verifyToken(token)).id, 'user1')\n // the key is rotated out of the published keyset\n keyPresent = false\n // a cached parsed key must not let the rotated-out key keep verifying\n await assert.rejects(handler.verifyToken(token))\n })\n})\n"]}
1
+ {"version":3,"file":"session.test.js","sourceRoot":"","sources":["session.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { describe, it } from 'node:test'\nimport { strict as assert } from 'assert'\nimport { generateKeyPairSync } from 'node:crypto'\nimport jwt from 'jsonwebtoken'\nimport { SessionHandler, keySetOfKid } from './session.js'\n\nconst testUser = {\n email: 'test@example.com',\n id: 'user1',\n name: 'Test User',\n organizations: [\n { id: 'org1', name: 'Test Org', role: 'admin' },\n { id: 'org2', name: 'Other Org', role: 'user', department: 'dep1', departmentName: 'Department 1' }\n ]\n}\n\n// a handler whose token verification is stubbed, counts the number of verifications\nconst stubbedHandler = () => {\n const handler = new SessionHandler()\n const calls = { count: 0 }\n handler.verifyTokenKeySet = async () => {\n calls.count++\n return { payload: structuredClone(testUser), header: { alg: 'RS256' }, keySet: 'session' }\n }\n return { handler, calls }\n}\n\ndescribe('SessionHandler.readStateFromCookie', () => {\n it('should return default session with no cookies', async () => {\n const handler = new SessionHandler()\n const state = await handler.readStateFromCookie(undefined)\n assert.equal(state.lang, 'fr')\n assert.equal(state.user, undefined)\n assert.equal(state.dark, undefined)\n })\n\n it('should parse dark mode cookie', async () => {\n const handler = new SessionHandler()\n const state = await handler.readStateFromCookie('theme_dark=true')\n assert.equal(state.dark, true)\n })\n\n it('should parse i18n_lang cookie', async () => {\n const handler = new SessionHandler()\n const state = await handler.readStateFromCookie('i18n_lang=en')\n assert.equal(state.lang, 'en')\n })\n\n it('should use defaultLang when no i18n_lang cookie', async () => {\n const handler = new SessionHandler()\n handler.defaultLang = 'de'\n const state = await handler.readStateFromCookie('')\n assert.equal(state.lang, 'de')\n })\n\n it('should return early when no id_token', async () => {\n const handler = new SessionHandler()\n const state = await handler.readStateFromCookie('i18n_lang=en; theme_dark=1')\n assert.equal(state.lang, 'en')\n assert.equal(state.dark, true)\n assert.equal(state.user, undefined)\n })\n\n it('should return early when no id_token_sign and not onlyDecode', async () => {\n const handler = new SessionHandler()\n const state = await handler.readStateFromCookie('id_token=header.payload')\n assert.equal(state.user, undefined)\n })\n\n it('should select the organization account from cookies', async () => {\n const { handler } = stubbedHandler()\n const state = await handler.readStateFromCookie('id_token=h.p; id_token_sign=s; id_token_org=org2; id_token_dep=dep1')\n assert.equal(state.account?.type, 'organization')\n assert.equal(state.account?.id, 'org2')\n assert.equal(state.account?.department, 'dep1')\n assert.equal(state.accountRole, 'user')\n })\n\n it('should build a user account when no org cookie', async () => {\n const { handler } = stubbedHandler()\n const state = await handler.readStateFromCookie('id_token=h.p; id_token_sign=s')\n assert.equal(state.account?.type, 'user')\n assert.equal(state.account?.id, 'user1')\n assert.equal(state.accountRole, 'admin')\n })\n\n it('should accept an already-parsed cookies object without re-parsing', async () => {\n const { handler, calls } = stubbedHandler()\n const parsed = { id_token: 'h.p', id_token_sign: 's', id_token_org: 'org2', id_token_dep: 'dep1', i18n_lang: 'en', theme_dark: '1' }\n const state = await handler.readStateFromCookie(parsed)\n assert.equal(state.lang, 'en')\n assert.equal(state.dark, true)\n assert.equal(state.account?.type, 'organization')\n assert.equal(state.account?.id, 'org2')\n assert.equal(calls.count, 1)\n })\n\n it('should produce equivalent state from string and object cookie inputs', async () => {\n const { handler } = stubbedHandler()\n const fromString = await handler.readStateFromCookie('id_token=h.p; id_token_sign=s; i18n_lang=en')\n const fromObject = await handler.readStateFromCookie({ id_token: 'h.p', id_token_sign: 's', i18n_lang: 'en' })\n assert.deepEqual(fromString, fromObject)\n })\n\n it('should verify the token on every request', async () => {\n const { handler, calls } = stubbedHandler()\n const cookies = 'id_token=h.p; id_token_sign=s'\n await handler.readStateFromCookie(cookies)\n await handler.readStateFromCookie(cookies)\n assert.equal(calls.count, 2)\n })\n\n it('should return a mutable state that callers can adjust', async () => {\n const { handler } = stubbedHandler()\n const state = await handler.readStateFromCookie('id_token=h.p; id_token_sign=s')\n state.user!.name = 'mutated' // must not throw\n state.user!.organizations[0].role = 'other'\n assert.equal(state.user!.name, 'mutated')\n assert.equal(state.user!.organizations[0].role, 'other')\n })\n\n it('should decode without verifying in onlyDecode mode', async () => {\n const handler = new SessionHandler()\n const token = jwt.sign(structuredClone(testUser), 'secret')\n const [header, payload, sign] = token.split('.')\n const state = await handler.readStateFromCookie(`id_token=${header}.${payload}; id_token_sign=${sign}`, true)\n assert.equal(state.user?.id, 'user1')\n assert.equal(state.account?.type, 'user')\n })\n\n it('should reject and clear the session when the token is invalid', async () => {\n const handler = new SessionHandler()\n handler.verifyTokenKeySet = async () => { throw Object.assign(new Error('jwt expired'), { name: 'TokenExpiredError' }) }\n let cleared = false\n await assert.rejects(\n handler.readStateFromCookie('id_token=h.p; id_token_sign=s', undefined, () => { cleared = true }),\n (err: any) => err.status === 401\n )\n assert.equal(cleared, true)\n })\n\n it('should surface a 500 without clearing the session on a jwks error', async () => {\n const handler = new SessionHandler()\n handler.verifyTokenKeySet = async () => { throw Object.assign(new Error('jwks down'), { name: 'JwksError' }) }\n let cleared = false\n await assert.rejects(\n handler.readStateFromCookie('id_token=h.p; id_token_sign=s', undefined, () => { cleared = true }),\n (err: any) => err.status === 500\n )\n assert.equal(cleared, false)\n })\n})\n\ndescribe('SessionHandler.verifyToken', () => {\n it('should consult the JWKS on every verification but cache the parsed key per kid', async () => {\n const { privateKey, publicKey } = generateKeyPairSync('rsa', { modulusLength: 2048 })\n const pem = publicKey.export({ format: 'pem', type: 'spki' }) as string\n const handler = new SessionHandler()\n let getSigningKeyCalls = 0\n let getPublicKeyCalls = 0\n ;(handler as any)._jwksClients = {\n session: {\n getSigningKey: async () => {\n getSigningKeyCalls++\n return { getPublicKey: () => { getPublicKeyCalls++; return pem } }\n }\n }\n }\n const token = jwt.sign({ id: 'user1' }, privateKey, { algorithm: 'RS256', keyid: 'kid1' })\n const payload1 = await handler.verifyToken(token)\n assert.equal(payload1.id, 'user1')\n const payload2 = await handler.verifyToken(token)\n assert.equal(payload2.id, 'user1')\n // the signing key must be re-checked against the JWKS every time so that a key\n // rotated out of the keyset stops being accepted (lib#41 regression)\n assert.equal(getSigningKeyCalls, 2)\n // but the expensive PEM export + parsing is cached per kid\n assert.equal(getPublicKeyCalls, 1)\n })\n\n it('should reject a token whose signing key was rotated out of the JWKS', async () => {\n const { privateKey, publicKey } = generateKeyPairSync('rsa', { modulusLength: 2048 })\n const pem = publicKey.export({ format: 'pem', type: 'spki' }) as string\n const handler = new SessionHandler()\n let keyPresent = true\n ;(handler as any)._jwksClients = {\n session: {\n getSigningKey: async () => {\n if (!keyPresent) throw Object.assign(new Error('key not found'), { name: 'SigningKeyNotFoundError' })\n return { getPublicKey: () => pem }\n }\n }\n }\n const token = jwt.sign({ id: 'user1' }, privateKey, { algorithm: 'RS256', keyid: 'kid1' })\n // first verification succeeds and would populate any key cache\n assert.equal((await handler.verifyToken(token)).id, 'user1')\n // the key is rotated out of the published keyset\n keyPresent = false\n // a cached parsed key must not let the rotated-out key keep verifying\n await assert.rejects(handler.verifyToken(token))\n })\n})\n\nconst sessionKeys = generateKeyPairSync('rsa', { modulusLength: 2048 })\nconst scopedKeys = generateKeyPairSync('rsa', { modulusLength: 2048 })\nconst signWith = (privateKey: any, kid: string, payload: any, header: any = {}, opts: any = {}) =>\n jwt.sign(payload, privateKey, { algorithm: 'RS256', expiresIn: '5m', keyid: kid, header: { alg: 'RS256', ...header }, ...opts })\nconst signSession = (payload: any, header?: any, opts?: any) => signWith(sessionKeys.privateKey, 'sd-1', payload, header, opts)\nconst signScoped = (payload: any, header?: any, opts?: any) => signWith(scopedKeys.privateKey, 'scoped-sd-1', payload, header, opts)\n// real jwt verification against two local key pairs, one per key set\nconst realHandler = (scopeAware = true) => {\n const handler = new SessionHandler()\n handler.scopeAware = scopeAware\n ;(handler as any).getSigningKeyObject = async (keySet: string) => keySet === 'scoped' ? scopedKeys.publicKey : sessionKeys.publicKey\n return handler\n}\nconst toCookies = (token: string) => {\n const [h, p, s] = token.split('.')\n return `id_token=${h}.${p}; id_token_sign=${s}`\n}\nconst rejects401 = (p: Promise<any>) => assert.rejects(p, (err: any) => err.status === 401)\n\ndescribe('SessionHandler.readStateFromBearer', () => {\n const nhiUser = { ...testUser, organizations: [testUser.organizations[0]], nhi: 1, scope: 'datasets:read organizations:read' }\n const bearer = (payload: any, header: any = { typ: 'at+jwt' }, opts?: any) => signScoped(payload, header, opts)\n\n it('reads an at+jwt access token, bound to its single organization, with its scopes', async () => {\n const state = await realHandler().readStateFromBearer(bearer(nhiUser))\n assert.equal(state.user?.id, 'user1')\n assert.equal(state.account?.type, 'organization')\n assert.equal(state.account?.id, 'org1')\n assert.equal(state.accountRole, 'admin')\n assert.deepEqual(state.scopes, ['datasets:read', 'organizations:read'])\n })\n\n it('refuses (401) a token bound to several organizations', async () => {\n await rejects401(realHandler().readStateFromBearer(bearer(testUser)))\n })\n\n it('gives a personal account session to a token with no organization membership', async () => {\n const state = await realHandler().readStateFromBearer(bearer({ ...testUser, organizations: [], scope: 'datasets' }))\n assert.equal(state.account?.type, 'user')\n assert.equal(state.account?.id, 'user1')\n assert.equal(state.accountRole, 'admin')\n })\n\n it('refuses a token without an organizations key', async () => {\n const { organizations, ...noOrgs } = testUser\n await rejects401(realHandler().readStateFromBearer(bearer({ ...noOrgs, scope: 'datasets' })))\n })\n\n it('refuses (401) an invalid scope claim on the bearer and cookie paths', async () => {\n for (const scope of [null, 12, ['a']]) {\n await rejects401(realHandler().readStateFromBearer(bearer({ ...nhiUser, scope })))\n const handler = new SessionHandler()\n handler.scopeAware = true\n handler.verifyTokenKeySet = async () => ({ payload: structuredClone({ ...testUser, scope, exp: 4102444800 }), header: { alg: 'RS256' }, keySet: 'scoped' })\n await rejects401(handler.readStateFromCookie('id_token=a.b; id_token_sign=c'))\n }\n })\n\n it('refuses a scoped session unless the handler is scope-aware', async () => {\n const handler = realHandler(false)\n assert.equal(handler.scopeAware, false)\n await assert.rejects(handler.readStateFromBearer(bearer(nhiUser)), (err: any) => err.status === 403)\n const cookies = toCookies(signScoped({ ...testUser, organizations: [testUser.organizations[0]], scope: 'datasets' }))\n await assert.rejects(handler.readStateFromCookie(cookies), (err: any) => err.status === 403)\n handler.scopeAware = true\n assert.deepEqual((await handler.readStateFromCookie(cookies)).scopes, ['datasets'])\n assert.deepEqual((await handler.readStateFromBearer(bearer(nhiUser))).scopes, ['datasets:read', 'organizations:read'])\n })\n\n it('refuses an expired token or one signed by another key when really verifying', async () => {\n const { privateKey: otherKey } = generateKeyPairSync('rsa', { modulusLength: 2048 })\n const handler = realHandler()\n assert.equal((await handler.readStateFromBearer(bearer(nhiUser))).user?.id, 'user1')\n await rejects401(handler.readStateFromBearer(bearer(nhiUser, undefined, { expiresIn: -10 })))\n await rejects401(handler.readStateFromBearer(signWith(otherKey, 'scoped-sd-1', nhiUser, { typ: 'at+jwt' })))\n })\n\n it('refuses a session token or an exchange token presented as bearer', async () => {\n const handler = realHandler()\n await rejects401(handler.readStateFromBearer(signSession(testUser)))\n await rejects401(handler.readStateFromBearer(signSession({ user: 'user1', session: 'abc' }, { typ: 'JWT' })))\n await rejects401(handler.readStateFromBearer('not-a-jwt'))\n })\n\n it('fills scopes from a cookie session too', async () => {\n const state = await realHandler().readStateFromCookie(toCookies(signScoped({ ...testUser, organizations: [testUser.organizations[0]], scope: 'datasets' })))\n assert.deepEqual(state.scopes, ['datasets'])\n })\n})\n\ndescribe('key sets and token kinds', () => {\n const nhiUser = { ...testUser, organizations: [testUser.organizations[0]], nhi: 1, scope: 'datasets:read' }\n\n it('keySetOfKid', () => {\n assert.equal(keySetOfKid('scoped-sd-1'), 'scoped')\n assert.equal(keySetOfKid('sd-1'), 'session')\n assert.equal(keySetOfKid(undefined), 'session')\n })\n\n it('bearer requires an at+jwt from the scoped set', async () => {\n const handler = realHandler()\n assert.equal((await handler.readStateFromBearer(signScoped(nhiUser, { typ: 'at+jwt' }))).user?.id, 'user1')\n await rejects401(handler.readStateFromBearer(signSession(nhiUser, { typ: 'at+jwt' })))\n await rejects401(handler.readStateFromBearer(signScoped(nhiUser, {})))\n })\n\n it('a token signed by one set is not verified by the other', async () => {\n const handler = realHandler()\n // claims a scoped kid but is signed with the session private key\n await rejects401(handler.readStateFromBearer(signWith(sessionKeys.privateKey, 'scoped-sd-1', nhiUser, { typ: 'at+jwt' })))\n // and the other way round, on the cookie path\n await rejects401(handler.readStateFromCookie(toCookies(signWith(scopedKeys.privateKey, 'sd-1', testUser))))\n })\n\n it('cookie path refuses access tokens and enforces the scope/key-set pairing', async () => {\n const handler = realHandler()\n await rejects401(handler.readStateFromCookie(toCookies(signScoped(nhiUser, { typ: 'at+jwt' }))))\n await rejects401(handler.readStateFromCookie(toCookies(signSession({ ...testUser, scope: 'datasets' }))))\n await rejects401(handler.readStateFromCookie(toCookies(signScoped(testUser))))\n assert.deepEqual((await handler.readStateFromCookie(toCookies(signScoped({ ...testUser, organizations: [testUser.organizations[0]], scope: 'datasets' })))).scopes, ['datasets'])\n assert.equal((await handler.readStateFromCookie(toCookies(signSession(testUser)))).scopes, undefined)\n })\n\n it('applies the same kind rules when only decoding', async () => {\n const handler = realHandler()\n await rejects401(handler.readStateFromCookie(toCookies(signScoped(nhiUser, { typ: 'at+jwt' })), true))\n await rejects401(handler.readStateFromCookie(toCookies(signSession({ ...testUser, scope: 'datasets' })), true))\n await rejects401(handler.readStateFromCookie(toCookies(signScoped(testUser)), true))\n assert.equal((await handler.readStateFromCookie(toCookies(signSession(testUser)), true)).user?.id, 'user1')\n })\n\n it('scoped-set tokens cannot carry superadmin claims and must expire', async () => {\n const handler = realHandler()\n for (const claims of [{ adminMode: 1 }, { asAdmin: { id: 'a', name: 'A' } }, { isAdmin: 1 }, { isAdmin: 0 }, { adminMode: false }]) {\n await rejects401(handler.readStateFromBearer(signScoped({ ...nhiUser, ...claims }, { typ: 'at+jwt' })))\n }\n // no exp claim at all\n const noExp = jwt.sign(nhiUser, scopedKeys.privateKey, { algorithm: 'RS256', keyid: 'scoped-sd-1', header: { alg: 'RS256', typ: 'at+jwt' }, noTimestamp: true })\n assert.equal((jwt.decode(noExp) as any).exp, undefined)\n await rejects401(handler.readStateFromBearer(noExp))\n })\n\n it('verifyToken only accepts the session key set', async () => {\n const handler = realHandler()\n assert.equal((await handler.verifyToken(signSession({ id: 'user1' }))).id, 'user1')\n await assert.rejects(handler.verifyToken(signScoped({ id: 'user1', scope: 'datasets' })))\n })\n\n it('only RS256 is accepted', async () => {\n const handler = realHandler()\n const token = jwt.sign(testUser, sessionKeys.privateKey, { algorithm: 'PS256', keyid: 'sd-1', expiresIn: '5m' })\n await rejects401(handler.readStateFromCookie(toCookies(token)))\n await assert.rejects(handler.verifyToken(token))\n })\n})\n\ndescribe('verification errors', () => {\n const stubbed = (err: Error) => {\n const handler = realHandler()\n ;(handler as any).getSigningKeyObject = async () => { throw err }\n return handler\n }\n const named = (name: string, message = name) => Object.assign(new Error(message), { name })\n const token = () => signScoped({ ...testUser, organizations: [testUser.organizations[0]], scope: 'datasets' }, { typ: 'at+jwt' })\n\n it('infrastructure failures are a 500 and never clear the cookies', async () => {\n for (const err of [named('JwksRateLimitError'), new Error('connect ECONNREFUSED'), named('JwksError')]) {\n const handler = stubbed(err)\n let cleared = false\n await assert.rejects(\n handler.readStateFromCookie(toCookies(signScoped({ ...testUser, organizations: [testUser.organizations[0]], scope: 'datasets' })), false, () => { cleared = true }),\n (e: any) => e.status === 500\n )\n assert.equal(cleared, false)\n await assert.rejects(handler.readStateFromBearer(token()), (e: any) => e.status === 500)\n }\n })\n\n it('a kid absent from the jwks is a 401 and clears the cookies', async () => {\n const handler = stubbed(named('SigningKeyNotFoundError'))\n let cleared = false\n await assert.rejects(\n handler.readStateFromCookie(toCookies(signScoped({ ...testUser, organizations: [testUser.organizations[0]], scope: 'datasets' })), false, () => { cleared = true }),\n (e: any) => e.status === 401\n )\n assert.equal(cleared, true)\n await rejects401(handler.readStateFromBearer(token()))\n })\n\n it('looks a scoped kid up in the scoped client and another kid in the session client', async () => {\n const handler = new SessionHandler()\n const calls: string[] = []\n const client = (name: string, key: any) => ({ getSigningKey: async (kid: string) => { calls.push(name + ':' + kid); return { getPublicKey: () => key.export({ format: 'pem', type: 'spki' }) } } })\n ;(handler as any)._jwksClients = { session: client('session', sessionKeys.publicKey), scoped: client('scoped', scopedKeys.publicKey) }\n await handler.verifyTokenKeySet(signSession({ id: 'u' }))\n await handler.verifyTokenKeySet(signScoped({ id: 'u' }))\n assert.deepEqual(calls, ['session:sd-1', 'scoped:scoped-sd-1'])\n })\n})\n\ndescribe('malformed tokens, typ normalization, one organization per scoped cookie token', () => {\n const b64 = (o: any) => Buffer.from(typeof o === 'string' ? o : JSON.stringify(o)).toString('base64url')\n const malformed = (typ: string, kid: string) => b64({ alg: 'RS256', typ, kid }) + '.' + b64('{not json') + '.sig'\n const oneOrg = [testUser.organizations[0]]\n\n it('a token whose payload is not JSON is a 401 and clears the cookies', async () => {\n for (const onlyDecode of [false, true]) {\n const handler = realHandler()\n let cleared = false\n await rejects401(handler.readStateFromCookie(toCookies(malformed('JWT', 'sd-1')), onlyDecode, () => { cleared = true }))\n assert.equal(cleared, true)\n }\n })\n\n it('a bearer whose payload is not JSON is a 401', async () => {\n const handler = realHandler()\n await rejects401(handler.readStateFromBearer(malformed('JWT', 'sd-1')))\n await rejects401(handler.readStateFromBearer(malformed('at+jwt', 'scoped-sd-1')))\n })\n\n it('a scoped cookie token carrying more than one organization is a 401', async () => {\n const handler = realHandler()\n await rejects401(handler.readStateFromCookie(toCookies(signScoped({ ...testUser, scope: 'datasets' }))))\n await rejects401(handler.readStateFromCookie(toCookies(signScoped({ ...testUser, scope: 'datasets' })), true))\n assert.equal((await handler.readStateFromCookie(toCookies(signScoped({ ...testUser, organizations: oneOrg, scope: 'datasets' })))).organization, undefined)\n })\n\n it('typ at+jwt is compared case-insensitively with an optional application/ prefix', async () => {\n const handler = realHandler()\n const nhi = { ...testUser, organizations: oneOrg, scope: 'datasets:read' }\n for (const typ of ['AT+JWT', 'application/at+jwt', 'Application/AT+JWT']) {\n assert.equal((await handler.readStateFromBearer(signScoped(nhi, { typ }))).user?.id, 'user1')\n await rejects401(handler.readStateFromCookie(toCookies(signScoped(nhi, { typ }))))\n await rejects401(handler.readStateFromCookie(toCookies(signScoped(nhi, { typ })), true))\n }\n })\n})\n"]}