@data-fair/lib-node 2.14.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/events-queue.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { internalError } from '@data-fair/lib-node/observer.js';
2
- import axios from './axios.js';
2
+ // the events service is part of our own infrastructure (configuration, not user input)
3
+ import { privateAxiosInstance as axios } from './axios.js';
3
4
  import Debug from 'debug';
4
5
  const debug = Debug('events-queue');
5
6
  export class EventsQueue {
@@ -1 +1 @@
1
- {"version":3,"file":"events-queue.js","sourceRoot":"","sources":["events-queue.ts"],"names":[],"mappings":"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","sourcesContent":["import { type Event } from '@data-fair/lib-common-types/event/index.js'\nimport { type Notification } from '@data-fair/lib-common-types/notification/index.js'\nimport { internalError } from '@data-fair/lib-node/observer.js'\nimport axios from './axios.js'\nimport Debug from 'debug'\nimport { SessionState } from '@data-fair/lib-common-types/session/index.js'\n\nconst debug = Debug('events-queue')\n\nexport type EventsQueueOptions = {\n eventsUrl: string,\n eventsSecret: string,\n inactive?: boolean\n}\n\nexport type PushEvent = Omit<Event, 'date'>\nexport type PushNotification = Omit<Notification, 'date'>\n\nexport class EventsQueue {\n _options: EventsQueueOptions | undefined\n eventsQueue: Event[] = []\n notificationsQueue: Notification[] = []\n stopped = false\n currentDrainPromise: Promise<void> | undefined\n\n options () {\n if (!this._options) throw new Error('events queue was not initialized')\n return this._options\n }\n\n start = async (options: EventsQueueOptions) => {\n if (this._options) return // already initialized\n this._options = options\n this.loop()\n }\n\n stop = async () => {\n this.stopped = true\n if (this.currentDrainPromise) await this.currentDrainPromise\n await this.drain()\n }\n\n async loop () {\n while (!this.stopped) {\n await new Promise(resolve => setTimeout(resolve, 3000))\n this.currentDrainPromise = this.drain()\n await this.currentDrainPromise\n this.currentDrainPromise = undefined\n }\n }\n\n async drain () {\n if (this.eventsQueue.length) {\n const events = this.eventsQueue\n this.eventsQueue = []\n debug('drain events', events.length)\n try {\n await axios.post(this.options().eventsUrl + '/api/events', events, { headers: { 'x-secret-key': this.options().eventsSecret } })\n } catch (err: any) {\n internalError('events-queue-push-event', err)\n // retry later\n if (err.status >= 500 && events.length < 1000) this.eventsQueue = events.concat(this.eventsQueue)\n }\n }\n if (this.notificationsQueue.length) {\n const notifications = this.notificationsQueue\n this.notificationsQueue = []\n debug('drain notifications', notifications.length)\n for (const notification of notifications) {\n try {\n await axios.post(this.options().eventsUrl + '/api/notifications', notification, { headers: { 'x-secret-key': this.options().eventsSecret } })\n } catch (err: any) {\n internalError('events-queue-push-notif', err)\n // retry later\n if (err.status >= 500 && (notifications.length + this.notificationsQueue.length) < 1000) {\n this.notificationsQueue.unshift(notification)\n }\n }\n }\n }\n }\n\n pushEvent (event: PushEvent, sessionState?: SessionState) {\n const options = this.options()\n if (options.inactive) return\n if (this.stopped) throw new Error('events queue has been stopped');\n (event as Event).date = new Date().toISOString()\n debug('pushEvent', event)\n if (!event.originator && sessionState?.user && sessionState.account) {\n if (sessionState.user.asAdmin) {\n event.originator = {\n user: { ...sessionState.user.asAdmin, admin: true }\n }\n } else if (sessionState.user.adminMode && (sessionState.account.type !== event.sender?.type || sessionState.account.id !== event.sender?.id)) {\n event.originator = {\n user: { id: sessionState.user.id, name: sessionState.user.name, admin: true }\n }\n } else {\n event.originator = {\n user: { id: sessionState.user.id, name: sessionState.user.name }\n }\n if (sessionState.organization) {\n event.originator.organization = {\n id: sessionState.organization.id,\n name: sessionState.organization.name,\n department: sessionState.organization.department,\n departmentName: sessionState.organization.departmentName\n }\n }\n }\n }\n this.eventsQueue.push(event as Event)\n }\n\n pushNotification (notification: PushNotification) {\n const options = this.options()\n if (options.inactive) return\n if (this.stopped) throw new Error('notifications queue has been stopped');\n (notification as Notification).date = new Date().toISOString()\n debug('pushNotification', notification)\n this.notificationsQueue.push(notification as Notification)\n }\n}\n\nconst eventsQueue = new EventsQueue()\n\nexport default eventsQueue\n"]}
1
+ {"version":3,"file":"events-queue.js","sourceRoot":"","sources":["events-queue.ts"],"names":[],"mappings":"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","sourcesContent":["import { type Event } from '@data-fair/lib-common-types/event/index.js'\nimport { type Notification } from '@data-fair/lib-common-types/notification/index.js'\nimport { internalError } from '@data-fair/lib-node/observer.js'\n// the events service is part of our own infrastructure (configuration, not user input)\nimport { privateAxiosInstance as axios } from './axios.js'\nimport Debug from 'debug'\nimport { SessionState } from '@data-fair/lib-common-types/session/index.js'\n\nconst debug = Debug('events-queue')\n\nexport type EventsQueueOptions = {\n eventsUrl: string,\n eventsSecret: string,\n inactive?: boolean\n}\n\nexport type PushEvent = Omit<Event, 'date'>\nexport type PushNotification = Omit<Notification, 'date'>\n\nexport class EventsQueue {\n _options: EventsQueueOptions | undefined\n eventsQueue: Event[] = []\n notificationsQueue: Notification[] = []\n stopped = false\n currentDrainPromise: Promise<void> | undefined\n\n options () {\n if (!this._options) throw new Error('events queue was not initialized')\n return this._options\n }\n\n start = async (options: EventsQueueOptions) => {\n if (this._options) return // already initialized\n this._options = options\n this.loop()\n }\n\n stop = async () => {\n this.stopped = true\n if (this.currentDrainPromise) await this.currentDrainPromise\n await this.drain()\n }\n\n async loop () {\n while (!this.stopped) {\n await new Promise(resolve => setTimeout(resolve, 3000))\n this.currentDrainPromise = this.drain()\n await this.currentDrainPromise\n this.currentDrainPromise = undefined\n }\n }\n\n async drain () {\n if (this.eventsQueue.length) {\n const events = this.eventsQueue\n this.eventsQueue = []\n debug('drain events', events.length)\n try {\n await axios.post(this.options().eventsUrl + '/api/events', events, { headers: { 'x-secret-key': this.options().eventsSecret } })\n } catch (err: any) {\n internalError('events-queue-push-event', err)\n // retry later\n if (err.status >= 500 && events.length < 1000) this.eventsQueue = events.concat(this.eventsQueue)\n }\n }\n if (this.notificationsQueue.length) {\n const notifications = this.notificationsQueue\n this.notificationsQueue = []\n debug('drain notifications', notifications.length)\n for (const notification of notifications) {\n try {\n await axios.post(this.options().eventsUrl + '/api/notifications', notification, { headers: { 'x-secret-key': this.options().eventsSecret } })\n } catch (err: any) {\n internalError('events-queue-push-notif', err)\n // retry later\n if (err.status >= 500 && (notifications.length + this.notificationsQueue.length) < 1000) {\n this.notificationsQueue.unshift(notification)\n }\n }\n }\n }\n }\n\n pushEvent (event: PushEvent, sessionState?: SessionState) {\n const options = this.options()\n if (options.inactive) return\n if (this.stopped) throw new Error('events queue has been stopped');\n (event as Event).date = new Date().toISOString()\n debug('pushEvent', event)\n if (!event.originator && sessionState?.user && sessionState.account) {\n if (sessionState.user.asAdmin) {\n event.originator = {\n user: { ...sessionState.user.asAdmin, admin: true }\n }\n } else if (sessionState.user.adminMode && (sessionState.account.type !== event.sender?.type || sessionState.account.id !== event.sender?.id)) {\n event.originator = {\n user: { id: sessionState.user.id, name: sessionState.user.name, admin: true }\n }\n } else {\n event.originator = {\n user: { id: sessionState.user.id, name: sessionState.user.name }\n }\n if (sessionState.organization) {\n event.originator.organization = {\n id: sessionState.organization.id,\n name: sessionState.organization.name,\n department: sessionState.organization.department,\n departmentName: sessionState.organization.departmentName\n }\n }\n }\n }\n this.eventsQueue.push(event as Event)\n }\n\n pushNotification (notification: PushNotification) {\n const options = this.options()\n if (options.inactive) return\n if (this.stopped) throw new Error('notifications queue has been stopped');\n (notification as Notification).date = new Date().toISOString()\n debug('pushNotification', notification)\n this.notificationsQueue.push(notification as Notification)\n }\n}\n\nconst eventsQueue = new EventsQueue()\n\nexport default eventsQueue\n"]}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,35 @@
1
+ import { describe, it, before, after } from 'node:test';
2
+ import { strict as assert } from 'assert';
3
+ import http from 'node:http';
4
+ import { EventsQueue } from './events-queue.js';
5
+ describe('events queue', () => {
6
+ let server;
7
+ let port;
8
+ const received = [];
9
+ before(async () => {
10
+ server = http.createServer((req, res) => {
11
+ let body = '';
12
+ req.on('data', chunk => { body += chunk; });
13
+ req.on('end', () => {
14
+ received.push({ url: req.url, body: JSON.parse(body) });
15
+ res.end();
16
+ });
17
+ });
18
+ await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
19
+ port = server.address().port;
20
+ });
21
+ after(() => { server.close(); });
22
+ // the events service is configured, not user provided: it must stay reachable on a private address
23
+ // even though the default http agents refuse those (SSRF protection)
24
+ it('should push events to a private events service', async () => {
25
+ assert.ok(!process.env.SSRF_PUBLIC_IPS, 'the test must run without SSRF_PUBLIC_IPS');
26
+ const queue = new EventsQueue();
27
+ queue._options = { eventsUrl: `http://127.0.0.1:${port}`, eventsSecret: 'secret' };
28
+ queue.pushEvent({ title: 'test event', topic: { key: 'test' }, sender: { type: 'user', id: 'user1' } });
29
+ await queue.drain();
30
+ assert.equal(received.length, 1);
31
+ assert.equal(received[0].url, '/api/events');
32
+ assert.equal(received[0].body[0].title, 'test event');
33
+ });
34
+ });
35
+ //# sourceMappingURL=events-queue.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"events-queue.test.js","sourceRoot":"","sources":["events-queue.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,WAAW,CAAA;AACvD,OAAO,EAAE,MAAM,IAAI,MAAM,EAAE,MAAM,QAAQ,CAAA;AACzC,OAAO,IAAI,MAAM,WAAW,CAAA;AAE5B,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAE/C,QAAQ,CAAC,cAAc,EAAE,GAAG,EAAE;IAC5B,IAAI,MAAmB,CAAA;IACvB,IAAI,IAAY,CAAA;IAChB,MAAM,QAAQ,GAAkC,EAAE,CAAA;IAElD,MAAM,CAAC,KAAK,IAAI,EAAE;QAChB,MAAM,GAAG,IAAI,CAAC,YAAY,CAAC,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE;YACtC,IAAI,IAAI,GAAG,EAAE,CAAA;YACb,GAAG,CAAC,EAAE,CAAC,MAAM,EAAE,KAAK,CAAC,EAAE,GAAG,IAAI,IAAI,KAAK,CAAA,CAAC,CAAC,CAAC,CAAA;YAC1C,GAAG,CAAC,EAAE,CAAC,KAAK,EAAE,GAAG,EAAE;gBACjB,QAAQ,CAAC,IAAI,CAAC,EAAE,GAAG,EAAE,GAAG,CAAC,GAAG,EAAE,IAAI,EAAE,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;gBACvD,GAAG,CAAC,GAAG,EAAE,CAAA;YACX,CAAC,CAAC,CAAA;QACJ,CAAC,CAAC,CAAA;QACF,MAAM,IAAI,OAAO,CAAO,OAAO,CAAC,EAAE,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,WAAW,EAAE,OAAO,CAAC,CAAC,CAAA;QAC1E,IAAI,GAAI,MAAM,CAAC,OAAO,EAAkB,CAAC,IAAI,CAAA;IAC/C,CAAC,CAAC,CAAA;IAEF,KAAK,CAAC,GAAG,EAAE,GAAG,MAAM,CAAC,KAAK,EAAE,CAAA,CAAC,CAAC,CAAC,CAAA;IAE/B,mGAAmG;IACnG,qEAAqE;IACrE,EAAE,CAAC,gDAAgD,EAAE,KAAK,IAAI,EAAE;QAC9D,MAAM,CAAC,EAAE,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,eAAe,EAAE,2CAA2C,CAAC,CAAA;QACpF,MAAM,KAAK,GAAG,IAAI,WAAW,EAAE,CAAA;QAC/B,KAAK,CAAC,QAAQ,GAAG,EAAE,SAAS,EAAE,oBAAoB,IAAI,EAAE,EAAE,YAAY,EAAE,QAAQ,EAAE,CAAA;QAClF,KAAK,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE,YAAY,EAAE,KAAK,EAAE,EAAE,GAAG,EAAE,MAAM,EAAE,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,OAAO,EAAE,EAAS,CAAC,CAAA;QAC9G,MAAM,KAAK,CAAC,KAAK,EAAE,CAAA;QACnB,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,MAAM,EAAE,CAAC,CAAC,CAAA;QAChC,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,GAAG,EAAE,aAAa,CAAC,CAAA;QAC5C,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,EAAE,YAAY,CAAC,CAAA;IACvD,CAAC,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA","sourcesContent":["import { describe, it, before, after } from 'node:test'\nimport { strict as assert } from 'assert'\nimport http from 'node:http'\nimport type { AddressInfo } from 'node:net'\nimport { EventsQueue } from './events-queue.js'\n\ndescribe('events queue', () => {\n let server: http.Server\n let port: number\n const received: { url?: string, body: any }[] = []\n\n before(async () => {\n server = http.createServer((req, res) => {\n let body = ''\n req.on('data', chunk => { body += chunk })\n req.on('end', () => {\n received.push({ url: req.url, body: JSON.parse(body) })\n res.end()\n })\n })\n await new Promise<void>(resolve => server.listen(0, '127.0.0.1', resolve))\n port = (server.address() as AddressInfo).port\n })\n\n after(() => { server.close() })\n\n // the events service is configured, not user provided: it must stay reachable on a private address\n // even though the default http agents refuse those (SSRF protection)\n it('should push events to a private events service', async () => {\n assert.ok(!process.env.SSRF_PUBLIC_IPS, 'the test must run without SSRF_PUBLIC_IPS')\n const queue = new EventsQueue()\n queue._options = { eventsUrl: `http://127.0.0.1:${port}`, eventsSecret: 'secret' }\n queue.pushEvent({ title: 'test event', topic: { key: 'test' }, sender: { type: 'user', id: 'user1' } } as any)\n await queue.drain()\n assert.equal(received.length, 1)\n assert.equal(received[0].url, '/api/events')\n assert.equal(received[0].body[0].title, 'test event')\n })\n})\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@data-fair/lib-node",
3
- "version": "2.14.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"]}
package/ssrf.d.ts CHANGED
@@ -19,6 +19,19 @@ export declare const getSsrfRules: (env?: NodeJS.ProcessEnv) => SsrfRules;
19
19
  * hostnames are not checked here, only the addresses they resolve to
20
20
  */
21
21
  export declare const checkAddress: (address: string, host: string | undefined, rules: SsrfRules) => SsrfError | undefined;
22
+ /**
23
+ * Resolve a host and check all its addresses, returns the address to connect to.
24
+ * Connect to the returned address (with the original host as TLS servername), not to the host:
25
+ * resolving the host again could give a different address (DNS rebinding).
26
+ * Throws a SsrfError if the host is or resolves to a non public address.
27
+ */
28
+ export declare const resolvePublicAddress: (host: string, rules?: SsrfRules) => Promise<string>;
29
+ /**
30
+ * A dns.lookup compatible function that refuses non public addresses, for any client accepting a
31
+ * `lookup` option (net.connect, tls.connect, undici connect options, got dnsLookup...).
32
+ * WARNING: node does not call lookup for IP literals, check those with resolvePublicAddress.
33
+ */
34
+ export declare const publicLookup: (hostname: string, options: any, callback?: (...args: any[]) => void) => void;
22
35
  export declare class SsrfHttpAgent extends HttpAgent {
23
36
  constructor(opts: HttpAgent.HttpOptions, rules: SsrfRules);
24
37
  }
package/ssrf.js CHANGED
@@ -82,6 +82,46 @@ const guardLookup = (lookup, rules) => {
82
82
  });
83
83
  };
84
84
  };
85
+ // helpers for clients that do not use our http agents (ftp, sftp/ssh2, raw sockets, SDKs, undici...)
86
+ // unlike the agents they are not disabled by a proxy env variable: these clients connect directly
87
+ let envRules;
88
+ const getEnvRules = () => {
89
+ envRules = envRules ?? getSsrfRules();
90
+ return envRules;
91
+ };
92
+ /**
93
+ * Resolve a host and check all its addresses, returns the address to connect to.
94
+ * Connect to the returned address (with the original host as TLS servername), not to the host:
95
+ * resolving the host again could give a different address (DNS rebinding).
96
+ * Throws a SsrfError if the host is or resolves to a non public address.
97
+ */
98
+ export const resolvePublicAddress = async (host, rules = getEnvRules()) => {
99
+ const bareHost = host.replace(/^\[(.*)\]$/, '$1');
100
+ if (net.isIP(bareHost)) {
101
+ const ssrfError = checkAddress(bareHost, undefined, rules);
102
+ if (ssrfError)
103
+ throw ssrfError;
104
+ return bareHost;
105
+ }
106
+ const addresses = await dns.promises.lookup(bareHost, { all: true });
107
+ for (const { address } of addresses) {
108
+ const ssrfError = checkAddress(address, bareHost, rules);
109
+ if (ssrfError)
110
+ throw ssrfError;
111
+ }
112
+ return addresses[0].address;
113
+ };
114
+ /**
115
+ * A dns.lookup compatible function that refuses non public addresses, for any client accepting a
116
+ * `lookup` option (net.connect, tls.connect, undici connect options, got dnsLookup...).
117
+ * WARNING: node does not call lookup for IP literals, check those with resolvePublicAddress.
118
+ */
119
+ export const publicLookup = (hostname, options, callback) => {
120
+ // support the dns.lookup(hostname, callback) signature
121
+ if (typeof options === 'function')
122
+ return publicLookup(hostname, {}, options);
123
+ return guardLookup(dns.lookup, getEnvRules())(hostname, options ?? {}, callback);
124
+ };
85
125
  // the check happens when the connection is created, on the exact address that will be used:
86
126
  // IP literals are checked directly (no DNS lookup happens for them),
87
127
  // hostnames are checked on every address returned by the lookup (no DNS rebinding),
package/ssrf.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"ssrf.js","sourceRoot":"","sources":["ssrf.ts"],"names":[],"mappings":"AAAA,4FAA4F;AAC5F,qFAAqF;AACrF,kFAAkF;AAElF,OAAO,GAAG,MAAM,UAAU,CAAA;AAC1B,OAAO,GAAG,MAAM,UAAU,CAAA;AAC1B,OAAO,MAAM,MAAM,WAAW,CAAA;AAC9B,OAAO,SAAS,EAAE,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAA;AAStD,MAAM,OAAO,SAAU,SAAQ,KAAK;IAClC,IAAI,GAAG,kBAAkB,CAAA;IACzB,YAAa,OAAe;QAC1B,KAAK,CAAC,OAAO,CAAC,CAAA;QACd,IAAI,CAAC,IAAI,GAAG,WAAW,CAAA;IACzB,CAAC;CACF;AAED;;GAEG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,KAAyB,EAAE,IAAY,EAAa,EAAE;IAChF,IAAI,CAAC,KAAK;QAAE,OAAO,EAAE,CAAA;IACrB,OAAO,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE;QACjE,IAAI,CAAC;YACH,IAAI,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC;gBAChB,MAAM,IAAI,GAAG,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,CAAA;gBAC9B,OAAO,CAAC,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,KAAK,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,CAAY,CAAA;YAC7D,CAAC;YACD,MAAM,CAAC,IAAI,EAAE,IAAI,CAAC,GAAG,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,CAAA;YACxC,IAAI,IAAI,CAAC,IAAI,EAAE,KAAK,MAAM,IAAK,IAAoB,CAAC,mBAAmB,EAAE,EAAE,CAAC;gBAC1E,OAAO,CAAE,IAAoB,CAAC,aAAa,EAAE,EAAE,IAAI,GAAG,EAAE,CAAY,CAAA;YACtE,CAAC;YACD,OAAO,CAAC,IAAI,EAAE,IAAI,CAAY,CAAA;QAChC,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,MAAM,IAAI,KAAK,CAAC,qCAAqC,CAAC,QAAQ,IAAI,EAAE,EAAE,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC,CAAA;QACvF,CAAC;IACH,CAAC,CAAC,CAAA;AACJ,CAAC,CAAA;AAED,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,MAAyB,OAAO,CAAC,GAAG,EAAa,EAAE,CAAC,CAAC;IAChF,SAAS,EAAE,WAAW,CAAC,GAAG,CAAC,eAAe,EAAE,iBAAiB,CAAC;IAC9D,UAAU,EAAE,WAAW,CAAC,GAAG,CAAC,gBAAgB,EAAE,kBAAkB,CAAC;CAClE,CAAC,CAAA;AAEF,MAAM,SAAS,GAAG,CAAC,IAA+B,EAAE,IAAe,EAAE,EAAE;IACrE,OAAO,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,EAAE,IAAI,CAAC,EAAE,EAAE;QACjC,IAAI,IAAI,CAAC,IAAI,EAAE,KAAK,KAAK,CAAC,IAAI,EAAE;YAAE,OAAO,KAAK,CAAA;QAC9C,IAAI,IAAI,CAAC,IAAI,EAAE,KAAK,MAAM;YAAE,OAAQ,IAAoB,CAAC,KAAK,CAAC,KAAoB,EAAE,IAAI,CAAC,CAAA;QAC1F,OAAQ,IAAoB,CAAC,KAAK,CAAC,KAAoB,EAAE,IAAI,CAAC,CAAA;IAChE,CAAC,CAAC,CAAA;AACJ,CAAC,CAAA;AAED;;;GAGG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,OAAe,EAAE,IAAwB,EAAE,KAAgB,EAAyB,EAAE;IACjH,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC;QAAE,OAAM;IAC9B,+FAA+F;IAC/F,MAAM,IAAI,GAAG,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,CAAA;IACpC,MAAM,MAAM,GAAG,IAAI,IAAI,IAAI,KAAK,OAAO,CAAC,CAAC,CAAC,GAAG,IAAI,KAAK,IAAI,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,QAAQ,EAAE,CAAA;IAC1F,IAAI,SAAS,CAAC,IAAI,EAAE,KAAK,CAAC,SAAS,CAAC;QAAE,OAAM;IAC5C,IAAI,IAAI,CAAC,KAAK,EAAE,KAAK,SAAS,EAAE,CAAC;QAC/B,OAAO,IAAI,SAAS,CAAC,iBAAiB,MAAM,yCAAyC,IAAI,CAAC,KAAK,EAAE,2DAA2D,CAAC,CAAA;IAC/J,CAAC;IACD,IAAI,SAAS,CAAC,IAAI,EAAE,KAAK,CAAC,UAAU,CAAC,EAAE,CAAC;QACtC,OAAO,IAAI,SAAS,CAAC,iBAAiB,MAAM,+DAA+D,CAAC,CAAA;IAC9G,CAAC;AACH,CAAC,CAAA;AAID,MAAM,WAAW,GAAG,CAAC,MAAsB,EAAE,KAAgB,EAAkB,EAAE;IAC/E,OAAO,CAAC,QAAQ,EAAE,OAAO,EAAE,QAAQ,EAAE,EAAE;QACrC,MAAM,CAAC,QAAQ,EAAE,OAAO,EAAE,CAAC,GAAiB,EAAE,OAAqC,EAAE,MAAe,EAAE,EAAE;YACtG,IAAI,GAAG;gBAAE,OAAO,QAAQ,CAAC,GAAG,CAAC,CAAA;YAC7B,MAAM,SAAS,GAAG,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAA;YAClF,KAAK,MAAM,CAAC,IAAI,SAAS,EAAE,CAAC;gBAC1B,MAAM,SAAS,GAAG,YAAY,CAAC,CAAC,EAAE,QAAQ,EAAE,KAAK,CAAC,CAAA;gBAClD,IAAI,SAAS;oBAAE,OAAO,QAAQ,CAAC,SAAS,CAAC,CAAA;YAC3C,CAAC;YACD,QAAQ,CAAC,IAAI,EAAE,OAAO,EAAE,MAAM,CAAC,CAAA;QACjC,CAAC,CAAC,CAAA;IACJ,CAAC,CAAA;AACH,CAAC,CAAA;AAED,4FAA4F;AAC5F,qEAAqE;AACrE,oFAAoF;AACpF,sEAAsE;AACtE,MAAM,uBAAuB,GAAG,CAAC,KAAgB,EAAE,gBAAsD,EAAE,EAAE;IAC3G,OAAO,CAAC,OAAY,EAAE,QAA0D,EAAE,EAAE;QAClF,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,IAAI,OAAO,CAAC,QAAQ,CAAA;QAC7C,IAAI,IAAI,EAAE,CAAC;YACT,4FAA4F;YAC5F,MAAM,SAAS,GAAG,YAAY,CAAC,IAAI,CAAC,OAAO,CAAC,YAAY,EAAE,IAAI,CAAC,EAAE,SAAS,EAAE,KAAK,CAAC,CAAA;YAClF,IAAI,SAAS,EAAE,CAAC;gBACd,QAAQ,CAAC,SAAS,CAAC,CAAA;gBACnB,OAAM;YACR,CAAC;QACH,CAAC;QACD,OAAO,gBAAgB,CAAC,EAAE,GAAG,OAAO,EAAE,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,MAAM,IAAI,GAAG,CAAC,MAAM,EAAE,KAAK,CAAC,EAAE,EAAE,QAAQ,CAAC,CAAA;IAC7G,CAAC,CAAA;AACH,CAAC,CAAA;AAED,MAAM,OAAO,aAAc,SAAQ,SAAS;IAC1C,YAAa,IAA2B,EAAE,KAAgB;QACxD,KAAK,CAAC,IAAI,CAAC,CAAA;QACX,4EAA4E;QAC5E,IAAI,CAAC,gBAAgB,GAAG,uBAAuB,CAAC,KAAK,EAAE,KAAK,CAAC,gBAAgB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAA;IAC3F,CAAC;CACF;AAED,MAAM,OAAO,cAAe,SAAQ,UAAU;IAC5C,YAAa,IAA4B,EAAE,KAAgB;QACzD,KAAK,CAAC,IAAI,CAAC,CAAA;QACX,4EAA4E;QAC5E,IAAI,CAAC,gBAAgB,GAAG,uBAAuB,CAAC,KAAK,EAAE,KAAK,CAAC,gBAAgB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAA;IAC3F,CAAC;CACF;AAED,MAAM,aAAa,GAAG,CAAC,YAAY,EAAE,aAAa,EAAE,WAAW,EAAE,YAAY,EAAE,aAAa,EAAE,WAAW,CAAC,CAAA;AAE1G;;;GAGG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,MAAyB,OAAO,CAAC,GAAG,EAAE,EAAE,CAAC,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAA","sourcesContent":["// protection against server side request forgery: refuse to connect to non public addresses\n// the address checks are adapted from https://github.com/azu/request-filtering-agent\n// they are applied on top of our keepalive agents instead of node's default agent\n\nimport net from 'node:net'\nimport dns from 'node:dns'\nimport ipaddr from 'ipaddr.js'\nimport HttpAgent, { HttpsAgent } from 'agentkeepalive'\n\ntype IPRange = [ipaddr.IPv4 | ipaddr.IPv6, number]\n\nexport type SsrfRules = {\n publicIPs: IPRange[],\n privateIPs: IPRange[]\n}\n\nexport class SsrfError extends Error {\n code = 'ERR_SSRF_BLOCKED'\n constructor (message: string) {\n super(message)\n this.name = 'SsrfError'\n }\n}\n\n/**\n * Parse a comma separated list of IP addresses and CIDR ranges, as found in SSRF_PUBLIC_IPS and SSRF_PRIVATE_IPS\n */\nexport const parseIPList = (value: string | undefined, name: string): IPRange[] => {\n if (!value) return []\n return value.split(',').map(s => s.trim()).filter(Boolean).map(s => {\n try {\n if (net.isIP(s)) {\n const addr = ipaddr.process(s)\n return [addr, addr.kind() === 'ipv4' ? 32 : 128] as IPRange\n }\n const [addr, bits] = ipaddr.parseCIDR(s)\n if (addr.kind() === 'ipv6' && (addr as ipaddr.IPv6).isIPv4MappedAddress()) {\n return [(addr as ipaddr.IPv6).toIPv4Address(), bits - 96] as IPRange\n }\n return [addr, bits] as IPRange\n } catch (err) {\n throw new Error(`invalid IP address or CIDR range \"${s}\" in ${name}`, { cause: err })\n }\n })\n}\n\nexport const getSsrfRules = (env: NodeJS.ProcessEnv = process.env): SsrfRules => ({\n publicIPs: parseIPList(env.SSRF_PUBLIC_IPS, 'SSRF_PUBLIC_IPS'),\n privateIPs: parseIPList(env.SSRF_PRIVATE_IPS, 'SSRF_PRIVATE_IPS')\n})\n\nconst matchList = (addr: ipaddr.IPv4 | ipaddr.IPv6, list: IPRange[]) => {\n return list.some(([range, bits]) => {\n if (addr.kind() !== range.kind()) return false\n if (addr.kind() === 'ipv4') return (addr as ipaddr.IPv4).match(range as ipaddr.IPv4, bits)\n return (addr as ipaddr.IPv6).match(range as ipaddr.IPv6, bits)\n })\n}\n\n/**\n * Check that an address is public, returns an error if it is not\n * hostnames are not checked here, only the addresses they resolve to\n */\nexport const checkAddress = (address: string, host: string | undefined, rules: SsrfRules): SsrfError | undefined => {\n if (!net.isIP(address)) return\n // ipv4 mapped ipv6 addresses (::ffff:127.0.0.1) are checked as the ipv4 address they represent\n const addr = ipaddr.process(address)\n const target = host && host !== address ? `${host} (${addr.toString()})` : addr.toString()\n if (matchList(addr, rules.publicIPs)) return\n if (addr.range() !== 'unicast') {\n return new SsrfError(`connection to ${target} refused, it is not a public address (${addr.range()}), if this target is legitimate add it to SSRF_PUBLIC_IPS`)\n }\n if (matchList(addr, rules.privateIPs)) {\n return new SsrfError(`connection to ${target} refused, the address is declared private in SSRF_PRIVATE_IPS`)\n }\n}\n\ntype LookupFunction = (hostname: string, options: any, callback: (...args: any[]) => void) => void\n\nconst guardLookup = (lookup: LookupFunction, rules: SsrfRules): LookupFunction => {\n return (hostname, options, callback) => {\n lookup(hostname, options, (err: Error | null, address: string | dns.LookupAddress[], family?: number) => {\n if (err) return callback(err)\n const addresses = Array.isArray(address) ? address.map(a => a.address) : [address]\n for (const a of addresses) {\n const ssrfError = checkAddress(a, hostname, rules)\n if (ssrfError) return callback(ssrfError)\n }\n callback(null, address, family)\n })\n }\n}\n\n// the check happens when the connection is created, on the exact address that will be used:\n// IP literals are checked directly (no DNS lookup happens for them),\n// hostnames are checked on every address returned by the lookup (no DNS rebinding),\n// and each redirection creates a new connection that is checked again\nconst guardedCreateConnection = (rules: SsrfRules, createConnection: (options: any, oncreate: any) => any) => {\n return (options: any, oncreate: (err: Error | null, socket?: net.Socket) => void) => {\n const host = options.host ?? options.hostname\n if (host) {\n // brackets of ipv6 literals are removed by node before reaching the agent, but be defensive\n const ssrfError = checkAddress(host.replace(/^\\[(.*)\\]$/, '$1'), undefined, rules)\n if (ssrfError) {\n oncreate(ssrfError)\n return\n }\n }\n return createConnection({ ...options, lookup: guardLookup(options.lookup ?? dns.lookup, rules) }, oncreate)\n }\n}\n\nexport class SsrfHttpAgent extends HttpAgent {\n constructor (opts: HttpAgent.HttpOptions, rules: SsrfRules) {\n super(opts)\n // @ts-ignore createConnection is not declared in the agentkeepalive typings\n this.createConnection = guardedCreateConnection(rules, super.createConnection.bind(this))\n }\n}\n\nexport class SsrfHttpsAgent extends HttpsAgent {\n constructor (opts: HttpAgent.HttpsOptions, rules: SsrfRules) {\n super(opts)\n // @ts-ignore createConnection is not declared in the agentkeepalive typings\n this.createConnection = guardedCreateConnection(rules, super.createConnection.bind(this))\n }\n}\n\nconst proxyEnvNames = ['HTTP_PROXY', 'HTTPS_PROXY', 'ALL_PROXY', 'http_proxy', 'https_proxy', 'all_proxy']\n\n/**\n * The name of the proxy env variable that disables the SSRF protection, if any\n * behind a proxy our agents connect to the proxy, not to the target, so they cannot check the target\n */\nexport const getProxyEnvName = (env: NodeJS.ProcessEnv = process.env) => proxyEnvNames.find(name => !!env[name])\n"]}
1
+ {"version":3,"file":"ssrf.js","sourceRoot":"","sources":["ssrf.ts"],"names":[],"mappings":"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","sourcesContent":["// protection against server side request forgery: refuse to connect to non public addresses\n// the address checks are adapted from https://github.com/azu/request-filtering-agent\n// they are applied on top of our keepalive agents instead of node's default agent\n\nimport net from 'node:net'\nimport dns from 'node:dns'\nimport ipaddr from 'ipaddr.js'\nimport HttpAgent, { HttpsAgent } from 'agentkeepalive'\n\ntype IPRange = [ipaddr.IPv4 | ipaddr.IPv6, number]\n\nexport type SsrfRules = {\n publicIPs: IPRange[],\n privateIPs: IPRange[]\n}\n\nexport class SsrfError extends Error {\n code = 'ERR_SSRF_BLOCKED'\n constructor (message: string) {\n super(message)\n this.name = 'SsrfError'\n }\n}\n\n/**\n * Parse a comma separated list of IP addresses and CIDR ranges, as found in SSRF_PUBLIC_IPS and SSRF_PRIVATE_IPS\n */\nexport const parseIPList = (value: string | undefined, name: string): IPRange[] => {\n if (!value) return []\n return value.split(',').map(s => s.trim()).filter(Boolean).map(s => {\n try {\n if (net.isIP(s)) {\n const addr = ipaddr.process(s)\n return [addr, addr.kind() === 'ipv4' ? 32 : 128] as IPRange\n }\n const [addr, bits] = ipaddr.parseCIDR(s)\n if (addr.kind() === 'ipv6' && (addr as ipaddr.IPv6).isIPv4MappedAddress()) {\n return [(addr as ipaddr.IPv6).toIPv4Address(), bits - 96] as IPRange\n }\n return [addr, bits] as IPRange\n } catch (err) {\n throw new Error(`invalid IP address or CIDR range \"${s}\" in ${name}`, { cause: err })\n }\n })\n}\n\nexport const getSsrfRules = (env: NodeJS.ProcessEnv = process.env): SsrfRules => ({\n publicIPs: parseIPList(env.SSRF_PUBLIC_IPS, 'SSRF_PUBLIC_IPS'),\n privateIPs: parseIPList(env.SSRF_PRIVATE_IPS, 'SSRF_PRIVATE_IPS')\n})\n\nconst matchList = (addr: ipaddr.IPv4 | ipaddr.IPv6, list: IPRange[]) => {\n return list.some(([range, bits]) => {\n if (addr.kind() !== range.kind()) return false\n if (addr.kind() === 'ipv4') return (addr as ipaddr.IPv4).match(range as ipaddr.IPv4, bits)\n return (addr as ipaddr.IPv6).match(range as ipaddr.IPv6, bits)\n })\n}\n\n/**\n * Check that an address is public, returns an error if it is not\n * hostnames are not checked here, only the addresses they resolve to\n */\nexport const checkAddress = (address: string, host: string | undefined, rules: SsrfRules): SsrfError | undefined => {\n if (!net.isIP(address)) return\n // ipv4 mapped ipv6 addresses (::ffff:127.0.0.1) are checked as the ipv4 address they represent\n const addr = ipaddr.process(address)\n const target = host && host !== address ? `${host} (${addr.toString()})` : addr.toString()\n if (matchList(addr, rules.publicIPs)) return\n if (addr.range() !== 'unicast') {\n return new SsrfError(`connection to ${target} refused, it is not a public address (${addr.range()}), if this target is legitimate add it to SSRF_PUBLIC_IPS`)\n }\n if (matchList(addr, rules.privateIPs)) {\n return new SsrfError(`connection to ${target} refused, the address is declared private in SSRF_PRIVATE_IPS`)\n }\n}\n\ntype LookupFunction = (hostname: string, options: any, callback: (...args: any[]) => void) => void\n\nconst guardLookup = (lookup: LookupFunction, rules: SsrfRules): LookupFunction => {\n return (hostname, options, callback) => {\n lookup(hostname, options, (err: Error | null, address: string | dns.LookupAddress[], family?: number) => {\n if (err) return callback(err)\n const addresses = Array.isArray(address) ? address.map(a => a.address) : [address]\n for (const a of addresses) {\n const ssrfError = checkAddress(a, hostname, rules)\n if (ssrfError) return callback(ssrfError)\n }\n callback(null, address, family)\n })\n }\n}\n\n// helpers for clients that do not use our http agents (ftp, sftp/ssh2, raw sockets, SDKs, undici...)\n// unlike the agents they are not disabled by a proxy env variable: these clients connect directly\n\nlet envRules: SsrfRules | undefined\nconst getEnvRules = () => {\n envRules = envRules ?? getSsrfRules()\n return envRules\n}\n\n/**\n * Resolve a host and check all its addresses, returns the address to connect to.\n * Connect to the returned address (with the original host as TLS servername), not to the host:\n * resolving the host again could give a different address (DNS rebinding).\n * Throws a SsrfError if the host is or resolves to a non public address.\n */\nexport const resolvePublicAddress = async (host: string, rules: SsrfRules = getEnvRules()): Promise<string> => {\n const bareHost = host.replace(/^\\[(.*)\\]$/, '$1')\n if (net.isIP(bareHost)) {\n const ssrfError = checkAddress(bareHost, undefined, rules)\n if (ssrfError) throw ssrfError\n return bareHost\n }\n const addresses = await dns.promises.lookup(bareHost, { all: true })\n for (const { address } of addresses) {\n const ssrfError = checkAddress(address, bareHost, rules)\n if (ssrfError) throw ssrfError\n }\n return addresses[0].address\n}\n\n/**\n * A dns.lookup compatible function that refuses non public addresses, for any client accepting a\n * `lookup` option (net.connect, tls.connect, undici connect options, got dnsLookup...).\n * WARNING: node does not call lookup for IP literals, check those with resolvePublicAddress.\n */\nexport const publicLookup = (hostname: string, options: any, callback?: (...args: any[]) => void): void => {\n // support the dns.lookup(hostname, callback) signature\n if (typeof options === 'function') return publicLookup(hostname, {}, options)\n return guardLookup(dns.lookup as LookupFunction, getEnvRules())(hostname, options ?? {}, callback as (...args: any[]) => void)\n}\n\n// the check happens when the connection is created, on the exact address that will be used:\n// IP literals are checked directly (no DNS lookup happens for them),\n// hostnames are checked on every address returned by the lookup (no DNS rebinding),\n// and each redirection creates a new connection that is checked again\nconst guardedCreateConnection = (rules: SsrfRules, createConnection: (options: any, oncreate: any) => any) => {\n return (options: any, oncreate: (err: Error | null, socket?: net.Socket) => void) => {\n const host = options.host ?? options.hostname\n if (host) {\n // brackets of ipv6 literals are removed by node before reaching the agent, but be defensive\n const ssrfError = checkAddress(host.replace(/^\\[(.*)\\]$/, '$1'), undefined, rules)\n if (ssrfError) {\n oncreate(ssrfError)\n return\n }\n }\n return createConnection({ ...options, lookup: guardLookup(options.lookup ?? dns.lookup, rules) }, oncreate)\n }\n}\n\nexport class SsrfHttpAgent extends HttpAgent {\n constructor (opts: HttpAgent.HttpOptions, rules: SsrfRules) {\n super(opts)\n // @ts-ignore createConnection is not declared in the agentkeepalive typings\n this.createConnection = guardedCreateConnection(rules, super.createConnection.bind(this))\n }\n}\n\nexport class SsrfHttpsAgent extends HttpsAgent {\n constructor (opts: HttpAgent.HttpsOptions, rules: SsrfRules) {\n super(opts)\n // @ts-ignore createConnection is not declared in the agentkeepalive typings\n this.createConnection = guardedCreateConnection(rules, super.createConnection.bind(this))\n }\n}\n\nconst proxyEnvNames = ['HTTP_PROXY', 'HTTPS_PROXY', 'ALL_PROXY', 'http_proxy', 'https_proxy', 'all_proxy']\n\n/**\n * The name of the proxy env variable that disables the SSRF protection, if any\n * behind a proxy our agents connect to the proxy, not to the target, so they cannot check the target\n */\nexport const getProxyEnvName = (env: NodeJS.ProcessEnv = process.env) => proxyEnvNames.find(name => !!env[name])\n"]}
package/ssrf.test.js CHANGED
@@ -6,7 +6,8 @@ import { promisify } from 'node:util';
6
6
  import { fileURLToPath } from 'node:url';
7
7
  import axios from 'axios';
8
8
  import CacheableLookup from 'cacheable-lookup';
9
- import { checkAddress, parseIPList, getSsrfRules, getProxyEnvName, SsrfHttpAgent, SsrfHttpsAgent } from './ssrf.js';
9
+ import net from 'node:net';
10
+ import { checkAddress, parseIPList, getSsrfRules, getProxyEnvName, resolvePublicAddress, publicLookup, SsrfHttpAgent, SsrfHttpsAgent } from './ssrf.js';
10
11
  const execFileAsync = promisify(execFile);
11
12
  const noRules = { publicIPs: [], privateIPs: [] };
12
13
  describe('ssrf address checks', () => {
@@ -110,4 +111,27 @@ describe('ssrf agents', () => {
110
111
  assert.match(stderr, /WARNING: HTTPS_PROXY is defined, the protection against server side request forgery \(SSRF\) is disabled/);
111
112
  });
112
113
  });
114
+ describe('ssrf helpers for other clients', () => {
115
+ it('should resolve and check hosts', async () => {
116
+ await assert.rejects(resolvePublicAddress('localhost', noRules), { code: 'ERR_SSRF_BLOCKED' });
117
+ await assert.rejects(resolvePublicAddress('127.0.0.1', noRules), { code: 'ERR_SSRF_BLOCKED' });
118
+ await assert.rejects(resolvePublicAddress('[::1]', noRules), { code: 'ERR_SSRF_BLOCKED' });
119
+ await assert.rejects(resolvePublicAddress('169.254.169.254'), { code: 'ERR_SSRF_BLOCKED' });
120
+ assert.equal(await resolvePublicAddress('8.8.8.8', noRules), '8.8.8.8');
121
+ assert.equal(await resolvePublicAddress('[2001:4860:4860::8888]', noRules), '2001:4860:4860::8888');
122
+ const loopback = getSsrfRules({ SSRF_PUBLIC_IPS: '127.0.0.1,::1' });
123
+ assert.ok(['127.0.0.1', '::1'].includes(await resolvePublicAddress('localhost', loopback)));
124
+ });
125
+ it('should refuse non public addresses in a lookup option', async () => {
126
+ assert.ok(!process.env.SSRF_PUBLIC_IPS, 'the test must run without SSRF_PUBLIC_IPS');
127
+ const err = await new Promise((resolve) => {
128
+ const socket = net.connect({ host: 'localhost', port: 1, lookup: publicLookup });
129
+ socket.on('error', resolve);
130
+ socket.on('connect', () => { socket.destroy(); resolve(undefined); });
131
+ });
132
+ assert.equal(err?.code, 'ERR_SSRF_BLOCKED');
133
+ const legacyErr = await new Promise(resolve => publicLookup('localhost', resolve));
134
+ assert.equal(legacyErr?.code, 'ERR_SSRF_BLOCKED');
135
+ });
136
+ });
113
137
  //# sourceMappingURL=ssrf.test.js.map
package/ssrf.test.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"ssrf.test.js","sourceRoot":"","sources":["ssrf.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { describe, it, before, after } from 'node:test'\nimport { strict as assert } from 'assert'\nimport http from 'node:http'\nimport type { AddressInfo } from 'node:net'\nimport { execFile } from 'node:child_process'\nimport { promisify } from 'node:util'\nimport { fileURLToPath } from 'node:url'\nimport axios from 'axios'\nimport CacheableLookup from 'cacheable-lookup'\nimport { checkAddress, parseIPList, getSsrfRules, getProxyEnvName, SsrfHttpAgent, SsrfHttpsAgent, type SsrfRules } from './ssrf.js'\n\nconst execFileAsync = promisify(execFile)\n\nconst noRules: SsrfRules = { publicIPs: [], privateIPs: [] }\n\ndescribe('ssrf address checks', () => {\n it('should refuse non public addresses', () => {\n for (const address of [\n '127.0.0.1', '10.1.2.3', '172.16.0.1', '192.168.1.1', '100.64.0.1', '169.254.169.254', '0.0.0.0',\n '255.255.255.255', '224.0.0.1', '::1', '::', 'fd00::1', 'fe80::1', '::ffff:127.0.0.1', '::ffff:169.254.169.254',\n '64:ff9b::a9fe:a9fe', '2002:a9fe:a9fe::1'\n ]) {\n assert.ok(checkAddress(address, undefined, noRules), `${address} should be refused`)\n }\n })\n\n it('should accept public addresses and ignore hostnames', () => {\n for (const address of ['8.8.8.8', '141.95.149.122', '2001:4860:4860::8888', '::ffff:8.8.8.8', 'koumoul.com']) {\n assert.equal(checkAddress(address, undefined, noRules), undefined, `${address} should be accepted`)\n }\n })\n\n it('should extend public and private addresses from env variables', () => {\n const rules = getSsrfRules({ SSRF_PUBLIC_IPS: '10.0.0.0/8, 192.168.1.1', SSRF_PRIVATE_IPS: '141.95.149.120/30,2001:4860:4860::8888' })\n assert.equal(checkAddress('10.1.2.3', undefined, rules), undefined)\n assert.equal(checkAddress('::ffff:10.1.2.3', undefined, rules), undefined)\n assert.equal(checkAddress('192.168.1.1', undefined, rules), undefined)\n assert.ok(checkAddress('192.168.1.2', undefined, rules))\n assert.ok(checkAddress('141.95.149.122', undefined, rules)?.message.includes('SSRF_PRIVATE_IPS'))\n assert.equal(checkAddress('141.95.149.124', undefined, rules), undefined)\n assert.ok(checkAddress('2001:4860:4860::8888', undefined, rules))\n })\n\n it('should name the env variable to change in the error message', () => {\n const err = checkAddress('10.1.2.3', 'internal.example.com', noRules)\n assert.ok(err?.message.includes('internal.example.com (10.1.2.3)'))\n assert.ok(err?.message.includes('SSRF_PUBLIC_IPS'))\n assert.equal(err?.code, 'ERR_SSRF_BLOCKED')\n })\n\n it('should reject invalid lists', () => {\n assert.throws(() => parseIPList('10.0.0.0/8,not-an-ip', 'SSRF_PUBLIC_IPS'), /not-an-ip.*SSRF_PUBLIC_IPS/)\n assert.deepEqual(parseIPList(' , ', 'SSRF_PUBLIC_IPS'), [])\n })\n\n it('should detect proxy env variables', () => {\n assert.equal(getProxyEnvName({}), undefined)\n assert.equal(getProxyEnvName({ https_proxy: 'http://proxy:3128' }), 'https_proxy')\n })\n})\n\ndescribe('ssrf agents', () => {\n let server: http.Server\n let port: number\n\n before(async () => {\n server = http.createServer((req, res) => {\n if (req.url === '/redirect') {\n res.writeHead(302, { location: `http://127.0.0.2:${port}/` })\n return res.end()\n }\n res.end('ok')\n })\n await new Promise<void>(resolve => server.listen(0, '0.0.0.0', resolve))\n port = (server.address() as AddressInfo).port\n })\n\n after(() => { server.close() })\n\n // same setup as http-agents.ts\n const buildAgents = (rules: SsrfRules) => {\n const httpAgent = new SsrfHttpAgent({ keepAlive: true }, rules)\n const httpsAgent = new SsrfHttpsAgent({ keepAlive: true }, rules)\n const cacheableLookup = new CacheableLookup()\n cacheableLookup.install(httpAgent)\n cacheableLookup.install(httpsAgent)\n return { httpAgent, httpsAgent, proxy: false as const }\n }\n\n it('should refuse IP literals before connecting', async () => {\n await assert.rejects(axios.get(`http://127.0.0.1:${port}/`, buildAgents(noRules)), { code: 'ERR_SSRF_BLOCKED' })\n await assert.rejects(axios.get('https://169.254.169.254/', buildAgents(noRules)), { code: 'ERR_SSRF_BLOCKED' })\n })\n\n it('should refuse hostnames resolving to non public addresses', async () => {\n await assert.rejects(axios.get(`http://localhost:${port}/`, buildAgents(noRules)), { code: 'ERR_SSRF_BLOCKED' })\n })\n\n it('should accept addresses declared public', async () => {\n const agents = buildAgents(getSsrfRules({ SSRF_PUBLIC_IPS: '127.0.0.1,::1' }))\n assert.equal((await axios.get(`http://127.0.0.1:${port}/`, agents)).data, 'ok')\n assert.equal((await axios.get(`http://localhost:${port}/`, agents)).data, 'ok')\n })\n\n it('should check every redirection', async () => {\n const agents = buildAgents(getSsrfRules({ SSRF_PUBLIC_IPS: '127.0.0.1' }))\n await assert.rejects(axios.get(`http://127.0.0.1:${port}/redirect`, agents), { code: 'ERR_SSRF_BLOCKED' })\n const allAgents = buildAgents(getSsrfRules({ SSRF_PUBLIC_IPS: '127.0.0.0/8' }))\n assert.equal((await axios.get(`http://127.0.0.1:${port}/redirect`, allAgents)).data, 'ok')\n })\n\n it('should guard the default lib instance, not the private one', async () => {\n const script = `\n import { axiosInstance, privateAxiosInstance } from ${JSON.stringify(fileURLToPath(new URL('./axios.ts', import.meta.url)))}\n const url = 'http://localhost:${port}/'\n try { await axiosInstance.get(url); console.log('public:allowed') } catch (err) { console.log('public:' + err.code) }\n console.log('private:' + (await privateAxiosInstance.get(url)).data)\n `\n const env = { ...process.env, SSRF_PUBLIC_IPS: '', HTTP_PROXY: '', HTTPS_PROXY: '', ALL_PROXY: '', http_proxy: '', https_proxy: '', all_proxy: '' }\n const { stdout } = await execFileAsync(process.execPath, ['--experimental-strip-types', '--input-type=module', '--eval', script], { env, timeout: 10000 })\n assert.match(stdout, /public:ERR_SSRF_BLOCKED/)\n assert.match(stdout, /private:ok/)\n })\n\n it('should disable the protection with a warning behind a proxy', async () => {\n const script = `import ${JSON.stringify(fileURLToPath(new URL('./http-agents.ts', import.meta.url)))}`\n const env = { ...process.env, HTTPS_PROXY: 'http://proxy.example.com:3128' }\n const { stderr } = await execFileAsync(process.execPath, ['--experimental-strip-types', '--input-type=module', '--eval', script], { env, timeout: 10000 })\n assert.match(stderr, /WARNING: HTTPS_PROXY is defined, the protection against server side request forgery \\(SSRF\\) is disabled/)\n })\n})\n"]}
1
+ {"version":3,"file":"ssrf.test.js","sourceRoot":"","sources":["ssrf.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { describe, it, before, after } from 'node:test'\nimport { strict as assert } from 'assert'\nimport http from 'node:http'\nimport type { AddressInfo } from 'node:net'\nimport { execFile } from 'node:child_process'\nimport { promisify } from 'node:util'\nimport { fileURLToPath } from 'node:url'\nimport axios from 'axios'\nimport CacheableLookup from 'cacheable-lookup'\nimport net from 'node:net'\nimport { checkAddress, parseIPList, getSsrfRules, getProxyEnvName, resolvePublicAddress, publicLookup, SsrfHttpAgent, SsrfHttpsAgent, type SsrfRules } from './ssrf.js'\n\nconst execFileAsync = promisify(execFile)\n\nconst noRules: SsrfRules = { publicIPs: [], privateIPs: [] }\n\ndescribe('ssrf address checks', () => {\n it('should refuse non public addresses', () => {\n for (const address of [\n '127.0.0.1', '10.1.2.3', '172.16.0.1', '192.168.1.1', '100.64.0.1', '169.254.169.254', '0.0.0.0',\n '255.255.255.255', '224.0.0.1', '::1', '::', 'fd00::1', 'fe80::1', '::ffff:127.0.0.1', '::ffff:169.254.169.254',\n '64:ff9b::a9fe:a9fe', '2002:a9fe:a9fe::1'\n ]) {\n assert.ok(checkAddress(address, undefined, noRules), `${address} should be refused`)\n }\n })\n\n it('should accept public addresses and ignore hostnames', () => {\n for (const address of ['8.8.8.8', '141.95.149.122', '2001:4860:4860::8888', '::ffff:8.8.8.8', 'koumoul.com']) {\n assert.equal(checkAddress(address, undefined, noRules), undefined, `${address} should be accepted`)\n }\n })\n\n it('should extend public and private addresses from env variables', () => {\n const rules = getSsrfRules({ SSRF_PUBLIC_IPS: '10.0.0.0/8, 192.168.1.1', SSRF_PRIVATE_IPS: '141.95.149.120/30,2001:4860:4860::8888' })\n assert.equal(checkAddress('10.1.2.3', undefined, rules), undefined)\n assert.equal(checkAddress('::ffff:10.1.2.3', undefined, rules), undefined)\n assert.equal(checkAddress('192.168.1.1', undefined, rules), undefined)\n assert.ok(checkAddress('192.168.1.2', undefined, rules))\n assert.ok(checkAddress('141.95.149.122', undefined, rules)?.message.includes('SSRF_PRIVATE_IPS'))\n assert.equal(checkAddress('141.95.149.124', undefined, rules), undefined)\n assert.ok(checkAddress('2001:4860:4860::8888', undefined, rules))\n })\n\n it('should name the env variable to change in the error message', () => {\n const err = checkAddress('10.1.2.3', 'internal.example.com', noRules)\n assert.ok(err?.message.includes('internal.example.com (10.1.2.3)'))\n assert.ok(err?.message.includes('SSRF_PUBLIC_IPS'))\n assert.equal(err?.code, 'ERR_SSRF_BLOCKED')\n })\n\n it('should reject invalid lists', () => {\n assert.throws(() => parseIPList('10.0.0.0/8,not-an-ip', 'SSRF_PUBLIC_IPS'), /not-an-ip.*SSRF_PUBLIC_IPS/)\n assert.deepEqual(parseIPList(' , ', 'SSRF_PUBLIC_IPS'), [])\n })\n\n it('should detect proxy env variables', () => {\n assert.equal(getProxyEnvName({}), undefined)\n assert.equal(getProxyEnvName({ https_proxy: 'http://proxy:3128' }), 'https_proxy')\n })\n})\n\ndescribe('ssrf agents', () => {\n let server: http.Server\n let port: number\n\n before(async () => {\n server = http.createServer((req, res) => {\n if (req.url === '/redirect') {\n res.writeHead(302, { location: `http://127.0.0.2:${port}/` })\n return res.end()\n }\n res.end('ok')\n })\n await new Promise<void>(resolve => server.listen(0, '0.0.0.0', resolve))\n port = (server.address() as AddressInfo).port\n })\n\n after(() => { server.close() })\n\n // same setup as http-agents.ts\n const buildAgents = (rules: SsrfRules) => {\n const httpAgent = new SsrfHttpAgent({ keepAlive: true }, rules)\n const httpsAgent = new SsrfHttpsAgent({ keepAlive: true }, rules)\n const cacheableLookup = new CacheableLookup()\n cacheableLookup.install(httpAgent)\n cacheableLookup.install(httpsAgent)\n return { httpAgent, httpsAgent, proxy: false as const }\n }\n\n it('should refuse IP literals before connecting', async () => {\n await assert.rejects(axios.get(`http://127.0.0.1:${port}/`, buildAgents(noRules)), { code: 'ERR_SSRF_BLOCKED' })\n await assert.rejects(axios.get('https://169.254.169.254/', buildAgents(noRules)), { code: 'ERR_SSRF_BLOCKED' })\n })\n\n it('should refuse hostnames resolving to non public addresses', async () => {\n await assert.rejects(axios.get(`http://localhost:${port}/`, buildAgents(noRules)), { code: 'ERR_SSRF_BLOCKED' })\n })\n\n it('should accept addresses declared public', async () => {\n const agents = buildAgents(getSsrfRules({ SSRF_PUBLIC_IPS: '127.0.0.1,::1' }))\n assert.equal((await axios.get(`http://127.0.0.1:${port}/`, agents)).data, 'ok')\n assert.equal((await axios.get(`http://localhost:${port}/`, agents)).data, 'ok')\n })\n\n it('should check every redirection', async () => {\n const agents = buildAgents(getSsrfRules({ SSRF_PUBLIC_IPS: '127.0.0.1' }))\n await assert.rejects(axios.get(`http://127.0.0.1:${port}/redirect`, agents), { code: 'ERR_SSRF_BLOCKED' })\n const allAgents = buildAgents(getSsrfRules({ SSRF_PUBLIC_IPS: '127.0.0.0/8' }))\n assert.equal((await axios.get(`http://127.0.0.1:${port}/redirect`, allAgents)).data, 'ok')\n })\n\n it('should guard the default lib instance, not the private one', async () => {\n const script = `\n import { axiosInstance, privateAxiosInstance } from ${JSON.stringify(fileURLToPath(new URL('./axios.ts', import.meta.url)))}\n const url = 'http://localhost:${port}/'\n try { await axiosInstance.get(url); console.log('public:allowed') } catch (err) { console.log('public:' + err.code) }\n console.log('private:' + (await privateAxiosInstance.get(url)).data)\n `\n const env = { ...process.env, SSRF_PUBLIC_IPS: '', HTTP_PROXY: '', HTTPS_PROXY: '', ALL_PROXY: '', http_proxy: '', https_proxy: '', all_proxy: '' }\n const { stdout } = await execFileAsync(process.execPath, ['--experimental-strip-types', '--input-type=module', '--eval', script], { env, timeout: 10000 })\n assert.match(stdout, /public:ERR_SSRF_BLOCKED/)\n assert.match(stdout, /private:ok/)\n })\n\n it('should disable the protection with a warning behind a proxy', async () => {\n const script = `import ${JSON.stringify(fileURLToPath(new URL('./http-agents.ts', import.meta.url)))}`\n const env = { ...process.env, HTTPS_PROXY: 'http://proxy.example.com:3128' }\n const { stderr } = await execFileAsync(process.execPath, ['--experimental-strip-types', '--input-type=module', '--eval', script], { env, timeout: 10000 })\n assert.match(stderr, /WARNING: HTTPS_PROXY is defined, the protection against server side request forgery \\(SSRF\\) is disabled/)\n })\n})\n\ndescribe('ssrf helpers for other clients', () => {\n it('should resolve and check hosts', async () => {\n await assert.rejects(resolvePublicAddress('localhost', noRules), { code: 'ERR_SSRF_BLOCKED' })\n await assert.rejects(resolvePublicAddress('127.0.0.1', noRules), { code: 'ERR_SSRF_BLOCKED' })\n await assert.rejects(resolvePublicAddress('[::1]', noRules), { code: 'ERR_SSRF_BLOCKED' })\n await assert.rejects(resolvePublicAddress('169.254.169.254'), { code: 'ERR_SSRF_BLOCKED' })\n assert.equal(await resolvePublicAddress('8.8.8.8', noRules), '8.8.8.8')\n assert.equal(await resolvePublicAddress('[2001:4860:4860::8888]', noRules), '2001:4860:4860::8888')\n const loopback = getSsrfRules({ SSRF_PUBLIC_IPS: '127.0.0.1,::1' })\n assert.ok(['127.0.0.1', '::1'].includes(await resolvePublicAddress('localhost', loopback)))\n })\n\n it('should refuse non public addresses in a lookup option', async () => {\n assert.ok(!process.env.SSRF_PUBLIC_IPS, 'the test must run without SSRF_PUBLIC_IPS')\n const err = await new Promise<any>((resolve) => {\n const socket = net.connect({ host: 'localhost', port: 1, lookup: publicLookup })\n socket.on('error', resolve)\n socket.on('connect', () => { socket.destroy(); resolve(undefined) })\n })\n assert.equal(err?.code, 'ERR_SSRF_BLOCKED')\n const legacyErr = await new Promise<any>(resolve => publicLookup('localhost', resolve))\n assert.equal(legacyErr?.code, 'ERR_SSRF_BLOCKED')\n })\n})\n"]}