oidc-caep-client 0.2.3 → 0.2.4-next.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/README.md CHANGED
@@ -280,6 +280,7 @@ These are the `OIDCClient` config options:
280
280
  | `clientId` | required |
281
281
  | `redirectUri`, `postLogoutRedirectUri` | — |
282
282
  | `scope` | `openid profile email` + `offline_access` if supported |
283
+ | `signals` | `true`. Omitted is on. `false` turns CAEP and RISC off and drops `ssf.read` and `ssf.manage` from authorization and token requests |
283
284
  | `resource` | — |
284
285
  | `idTokenSignedResponseAlg` | `RS256` |
285
286
  | `par` | `'auto'` |
@@ -294,6 +295,8 @@ These are the `OIDCClient` config options:
294
295
  | `httpTimeoutMs` | 10000 |
295
296
  | `fetch` | global `fetch` |
296
297
 
298
+ `signals` defaults to on. `"signals": false` in the client configuration turns CAEP and RISC off and drops `ssf.read` and `ssf.manage` from the authorization request, and from a refresh that sends a scope, including when `scope` or the demo's `oidc.json` still lists them. Other scopes stay. Sign-in itself is unchanged: discovery reads the OpenID provider metadata only and does not fetch `/.well-known/ssf-configuration`. The demo page does not contact a transmitter when `signals` is false. The demo's existing `caep.enabled: false` still skips the receiver, and a file value `"signals": false` wins over `caep.enabled: true`.
299
+
297
300
  **`TokenManager(client, { store, refreshSkewSec, autoRefresh })`** (refreshes hold a lock shared by the tabs):
298
301
 
299
302
  - `set(key, ts, { schedule })`, `get`, `getTokenSet`, `getAccessToken`, `refresh`, `schedule(key)`, `unschedule(key)`, `delete(key, { revoke })`, `close`
package/docs/spa-guide.md CHANGED
@@ -154,6 +154,8 @@ const client = await OIDCClient.discover({
154
154
  });
155
155
  ```
156
156
 
157
+ `signals` defaults to on (`true`, or omitted). `signals: false` turns CAEP and RISC off and drops `ssf.read` and `ssf.manage` from the authorization request, including when `scope` still lists them. A refresh that sends a scope loses those two tokens as well. Other scopes stay. Sign-in does not fetch transmitter metadata. Omit `signals`, or leave it `true`, to keep receiving events. In the demo, `"signals": false` in `oidc.json` does the same even when that file still lists the SSF scopes, and the page does not contact a transmitter. The demo's `caep.enabled: false` still skips the receiver.
158
+
157
159
  If you already have the metadata document, use `OIDCClient.fromMetadata(metadata, { ... })` instead. The client checks the metadata before using it:
158
160
 
159
161
  - **PKCE:** the provider must offer `S256`, otherwise the client refuses it.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oidc-caep-client",
3
- "version": "0.2.3",
3
+ "version": "0.2.4-next.0",
4
4
  "description": "In-browser OpenID Connect public client (PKCE, PAR, DPoP, RFC 9207) with token caching/auto-refresh and a Shared Signals (SSF) / CAEP event receiver",
5
5
  "homepage": "https://rcbj.github.io/oidc-caep-client/",
6
6
  "repository": {
@@ -24,6 +24,14 @@ const NOT_ID_TOKEN_TYPES = ['at+jwt', 'application/at+jwt', 'logout+jwt', 'secev
24
24
  /** Parameters that must never arrive at a code-flow redirect URI (tokens in the front channel). */
25
25
  const FRONT_CHANNEL_TOKENS = ['access_token', 'id_token', 'refresh_token', 'token_type'];
26
26
 
27
+ /** The only scopes `signals: false` removes. Any other token stays. */
28
+ const SIGNAL_SCOPES = new Set(['ssf.read', 'ssf.manage']);
29
+
30
+ /** @param {string} scope */
31
+ function scopeWithoutSignals(scope) {
32
+ return scope.split(' ').filter((token) => !SIGNAL_SCOPES.has(token)).join(' ');
33
+ }
34
+
27
35
  /** Endpoints whose URLs must use TLS. */
28
36
  const ENDPOINTS = ['issuer', 'authorization_endpoint', 'token_endpoint', 'jwks_uri', 'userinfo_endpoint',
29
37
  'revocation_endpoint', 'end_session_endpoint', 'pushed_authorization_request_endpoint', 'registration_endpoint'];
@@ -36,6 +44,8 @@ const ENDPOINTS = ['issuer', 'authorization_endpoint', 'token_endpoint', 'jwks_u
36
44
  * @property {string} [redirectUri]
37
45
  * @property {string} [postLogoutRedirectUri]
38
46
  * @property {string} [scope] Default "openid profile email offline_access" (offline_access only if supported).
47
+ * @property {boolean} [signals] CAEP and RISC. Default true; omitted is on. `false` drops `ssf.read` and
48
+ * `ssf.manage` from every authorization and token request. Sign-in does not fetch transmitter metadata.
39
49
  * @property {string|string[]} [resource] RFC 8707 resource indicator(s): audience-restricts the tokens.
40
50
  * @property {string} [idTokenSignedResponseAlg] Expected ID token `alg` (default RS256).
41
51
  * @property {'auto'|'always'|'never'} [par] Pushed Authorization Requests (RFC 9126). Default "auto": used when advertised.
@@ -84,6 +94,7 @@ export class OIDCClient extends Emitter {
84
94
  super();
85
95
  if (!config?.issuer && !config?.metadata?.issuer) throw new TypeError('issuer or metadata is required');
86
96
  if (!config.clientId) throw new TypeError('clientId is required');
97
+ if (config.signals !== undefined && typeof config.signals !== 'boolean') throw new TypeError('signals must be true or false');
87
98
  for (const secretOption of ['clientSecret', 'privateJwk', 'tokenEndpointAuthMethod']) {
88
99
  if (config[secretOption] !== undefined && !(secretOption === 'tokenEndpointAuthMethod' && config[secretOption] === 'none')) {
89
100
  throw new TypeError(`${secretOption} is not supported: this client is a public client (token_endpoint_auth_method "none")`);
@@ -103,6 +114,7 @@ export class OIDCClient extends Emitter {
103
114
  ...config,
104
115
  requireDpop: config.requireDpop ?? !!config.dpop,
105
116
  issuer: config.issuer ?? config.metadata.issuer,
117
+ signals: config.signals ?? true,
106
118
  };
107
119
  this.#config.transactionStore ??= new TransactionStore();
108
120
  }
@@ -240,6 +252,7 @@ export class OIDCClient extends Emitter {
240
252
  const offline = m.scopes_supported?.includes('offline_access') ? ' offline_access' : '';
241
253
  c.scope = `openid profile email${offline}`;
242
254
  }
255
+ if (c.signals === false) c.scope = scopeWithoutSignals(c.scope);
243
256
  c.transactionStore.sweep?.(c.transactionTtlSec * 1000);
244
257
  return this;
245
258
  }
@@ -253,6 +266,11 @@ export class OIDCClient extends Emitter {
253
266
  return r === undefined ? [] : [].concat(r);
254
267
  }
255
268
 
269
+ /** Scope placed on an authorization or token request. */
270
+ #scopeForRequest(scope) {
271
+ return this.#config.signals === false ? scopeWithoutSignals(scope) : scope;
272
+ }
273
+
256
274
  // ---------------------------------------------------------------------------
257
275
  // Authorization request
258
276
  // ---------------------------------------------------------------------------
@@ -263,7 +281,7 @@ export class OIDCClient extends Emitter {
263
281
  * Uses PAR when enabled, and binds the authorization code to the DPoP key (`dpop_jkt`).
264
282
  *
265
283
  * @param {object} [opts]
266
- * @param {string} [opts.scope]
284
+ * @param {string} [opts.scope] Replaces the client's scope for this request. `signals: false` still drops `ssf.read` and `ssf.manage`.
267
285
  * @param {string} [opts.redirectUri]
268
286
  * @param {string} [opts.prompt]
269
287
  * @param {number} [opts.maxAge]
@@ -298,7 +316,7 @@ export class OIDCClient extends Emitter {
298
316
  response_type: 'code',
299
317
  client_id: c.clientId,
300
318
  redirect_uri: redirectUri,
301
- scope: opts.scope ?? c.scope,
319
+ scope: this.#scopeForRequest(opts.scope ?? c.scope),
302
320
  state,
303
321
  nonce,
304
322
  code_challenge: await codeChallengeS256(codeVerifier),
@@ -462,7 +480,8 @@ export class OIDCClient extends Emitter {
462
480
  *
463
481
  * With a bare refresh token string there are no previous claims to hold a new ID token to, so pass
464
482
  * `previousClaims` (the claims of the ID token from the original sign-in); without them a response that
465
- * carries an ID token is refused.
483
+ * carries an ID token is refused. A scope is sent only when the caller passes one; `signals: false`
484
+ * removes `ssf.read` and `ssf.manage` from it.
466
485
  *
467
486
  * @param {TokenSet|string} tokenSetOrRefreshToken
468
487
  * @param {{ scope?: string, previousClaims?: object }} [opts]
@@ -473,7 +492,7 @@ export class OIDCClient extends Emitter {
473
492
  const refreshToken = previous ? previous.refresh_token : tokenSetOrRefreshToken;
474
493
  if (!refreshToken) throw new OIDCClientError('No refresh_token available', { code: 'no_refresh_token' });
475
494
  const body = new URLSearchParams({ grant_type: 'refresh_token', refresh_token: refreshToken });
476
- if (scope) body.set('scope', scope);
495
+ if (scope) body.set('scope', this.#scopeForRequest(scope));
477
496
  for (const r of this.#resources()) body.append('resource', r);
478
497
 
479
498
  const tokens = await this.#tokenRequest(body);
@@ -21,6 +21,11 @@ export type OIDCClientConfig = {
21
21
  * Default "openid profile email offline_access" (offline_access only if supported).
22
22
  */
23
23
  scope?: string;
24
+ /**
25
+ * CAEP and RISC. Default true; omitted is on. `false` drops `ssf.read` and
26
+ * `ssf.manage` from every authorization and token request. Sign-in does not fetch transmitter metadata.
27
+ */
28
+ signals?: boolean;
24
29
  /**
25
30
  * RFC 8707 resource indicator(s): audience-restricts the tokens.
26
31
  */
@@ -101,6 +106,8 @@ export type OIDCClientConfig = {
101
106
  * @property {string} [redirectUri]
102
107
  * @property {string} [postLogoutRedirectUri]
103
108
  * @property {string} [scope] Default "openid profile email offline_access" (offline_access only if supported).
109
+ * @property {boolean} [signals] CAEP and RISC. Default true; omitted is on. `false` drops `ssf.read` and
110
+ * `ssf.manage` from every authorization and token request. Sign-in does not fetch transmitter metadata.
104
111
  * @property {string|string[]} [resource] RFC 8707 resource indicator(s): audience-restricts the tokens.
105
112
  * @property {string} [idTokenSignedResponseAlg] Expected ID token `alg` (default RS256).
106
113
  * @property {'auto'|'always'|'never'} [par] Pushed Authorization Requests (RFC 9126). Default "auto": used when advertised.
@@ -185,6 +192,7 @@ export declare class OIDCClient extends Emitter {
185
192
  allowInsecureRequests: boolean;
186
193
  requireDpop: boolean;
187
194
  issuer: any;
195
+ signals: boolean;
188
196
  tokenEndpointAuthMethod: string;
189
197
  dpop: boolean;
190
198
  par: boolean;
@@ -211,7 +219,7 @@ export declare class OIDCClient extends Emitter {
211
219
  * Uses PAR when enabled, and binds the authorization code to the DPoP key (`dpop_jkt`).
212
220
  *
213
221
  * @param {object} [opts]
214
- * @param {string} [opts.scope]
222
+ * @param {string} [opts.scope] Replaces the client's scope for this request. `signals: false` still drops `ssf.read` and `ssf.manage`.
215
223
  * @param {string} [opts.redirectUri]
216
224
  * @param {string} [opts.prompt]
217
225
  * @param {number} [opts.maxAge]
@@ -268,7 +276,8 @@ export declare class OIDCClient extends Emitter {
268
276
  *
269
277
  * With a bare refresh token string there are no previous claims to hold a new ID token to, so pass
270
278
  * `previousClaims` (the claims of the ID token from the original sign-in); without them a response that
271
- * carries an ID token is refused.
279
+ * carries an ID token is refused. A scope is sent only when the caller passes one; `signals: false`
280
+ * removes `ssf.read` and `ssf.manage` from it.
272
281
  *
273
282
  * @param {TokenSet|string} tokenSetOrRefreshToken
274
283
  * @param {{ scope?: string, previousClaims?: object }} [opts]