@eventuras/fides-auth 0.14.1 → 0.15.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.
Files changed (94) hide show
  1. package/README.md +225 -33
  2. package/dist/activity-tracker.js +1 -2
  3. package/dist/cookies.d.ts +6 -0
  4. package/dist/cookies.d.ts.map +1 -1
  5. package/dist/cookies.js +8 -3
  6. package/dist/cookies.js.map +1 -1
  7. package/dist/heartbeat.js +1 -2
  8. package/dist/id-token.d.ts +31 -0
  9. package/dist/id-token.d.ts.map +1 -0
  10. package/dist/id-token.js +60 -0
  11. package/dist/id-token.js.map +1 -0
  12. package/dist/index.js +3 -3
  13. package/dist/logger.js +1 -2
  14. package/dist/oauth-browser.d.ts +13 -10
  15. package/dist/oauth-browser.d.ts.map +1 -1
  16. package/dist/oauth-browser.js +4 -2
  17. package/dist/oauth-browser.js.map +1 -1
  18. package/dist/oauth-config.d.ts +70 -0
  19. package/dist/oauth-config.d.ts.map +1 -0
  20. package/dist/oauth-logging.d.ts +4 -3
  21. package/dist/oauth-logging.d.ts.map +1 -1
  22. package/dist/oauth-logging.js +5 -5
  23. package/dist/oauth-logging.js.map +1 -1
  24. package/dist/oauth.d.ts +47 -45
  25. package/dist/oauth.d.ts.map +1 -1
  26. package/dist/oauth.js +58 -29
  27. package/dist/oauth.js.map +1 -1
  28. package/dist/oidc-discovery.d.ts +13 -4
  29. package/dist/oidc-discovery.d.ts.map +1 -1
  30. package/dist/oidc-discovery.js +82 -15
  31. package/dist/oidc-discovery.js.map +1 -1
  32. package/dist/pkce.d.ts +10 -0
  33. package/dist/pkce.d.ts.map +1 -0
  34. package/dist/pkce.js +23 -0
  35. package/dist/pkce.js.map +1 -0
  36. package/dist/providers/vipps/client.d.ts +3 -2
  37. package/dist/providers/vipps/client.d.ts.map +1 -1
  38. package/dist/providers/vipps/index.js +10 -10
  39. package/dist/providers/vipps/index.js.map +1 -1
  40. package/dist/providers/vipps/types.d.ts +2 -2
  41. package/dist/providers/vipps/types.d.ts.map +1 -1
  42. package/dist/rate-limit.js +1 -2
  43. package/dist/server/authorized-fetch.d.ts +78 -0
  44. package/dist/server/authorized-fetch.d.ts.map +1 -0
  45. package/dist/server/heartbeat-handler.d.ts +6 -4
  46. package/dist/server/heartbeat-handler.d.ts.map +1 -1
  47. package/dist/server/index.d.ts +4 -0
  48. package/dist/server/index.d.ts.map +1 -1
  49. package/dist/server/index.js +744 -94
  50. package/dist/server/index.js.map +1 -1
  51. package/dist/server/login-transaction.d.ts +55 -0
  52. package/dist/server/login-transaction.d.ts.map +1 -0
  53. package/dist/server/oidc-callback.d.ts +13 -4
  54. package/dist/server/oidc-callback.d.ts.map +1 -1
  55. package/dist/server/oidc-login.d.ts +14 -5
  56. package/dist/server/oidc-login.d.ts.map +1 -1
  57. package/dist/server/oidc-logout.d.ts +6 -4
  58. package/dist/server/oidc-logout.d.ts.map +1 -1
  59. package/dist/server/session-persistence.d.ts +102 -0
  60. package/dist/server/session-persistence.d.ts.map +1 -0
  61. package/dist/server/session-store.d.ts +39 -0
  62. package/dist/server/session-store.d.ts.map +1 -0
  63. package/dist/server/session.d.ts +35 -28
  64. package/dist/server/session.d.ts.map +1 -1
  65. package/dist/session-cookies.d.ts +1 -0
  66. package/dist/session-cookies.d.ts.map +1 -1
  67. package/dist/session-cookies.js +4 -3
  68. package/dist/session-cookies.js.map +1 -1
  69. package/dist/session-events.d.ts +10 -3
  70. package/dist/session-events.d.ts.map +1 -1
  71. package/dist/session-events.js +4 -3
  72. package/dist/session-events.js.map +1 -1
  73. package/dist/session-refresh.d.ts +1 -1
  74. package/dist/session-refresh.d.ts.map +1 -1
  75. package/dist/session-refresh.js +1 -2
  76. package/dist/session-refresh.js.map +1 -1
  77. package/dist/session-validation.d.ts.map +1 -1
  78. package/dist/session-validation.js +9 -6
  79. package/dist/session-validation.js.map +1 -1
  80. package/dist/silent-login.d.ts +1 -1
  81. package/dist/silent-login.d.ts.map +1 -1
  82. package/dist/silent-login.js +3 -3
  83. package/dist/silent-login.js.map +1 -1
  84. package/dist/types.d.ts +4 -1
  85. package/dist/types.d.ts.map +1 -1
  86. package/dist/utils.d.ts +0 -14
  87. package/dist/utils.d.ts.map +1 -1
  88. package/dist/utils.js +2 -24
  89. package/dist/utils.js.map +1 -1
  90. package/docs/discovery-cache.md +7 -3
  91. package/docs/rp-initiated-logout.md +6 -3
  92. package/docs/server-sessions.md +154 -0
  93. package/docs/session-events.md +17 -16
  94. package/package.json +14 -6
package/README.md CHANGED
@@ -8,6 +8,7 @@ Framework-agnostic OAuth 2.0 / OpenID Connect library with PKCE, encrypted sessi
8
8
  - **Pushed Authorization Requests** — Opt-in RFC 9126 support (`usePar`) when the provider advertises it
9
9
  - **Dual environment** — Node.js (`openid-client`) and browser (Web Crypto) entry points
10
10
  - **Encrypted sessions** — AES-256-GCM encrypted JWTs for secure session storage
11
+ - **Server-side sessions** — Opt-in store behind an opaque handle cookie, for providers whose tokens don't fit a cookie
11
12
  - **Rate limiting** — Generic token-bucket algorithm, ready for login protection
12
13
  - **RP-initiated logout** — End the provider's session too, with `id_token_hint` (OIDC RP-Initiated Logout 1.0)
13
14
  - **Silent login** — Detect `prompt=none` results without user interaction
@@ -68,37 +69,6 @@ const jwt = await createEncryptedJWT({ user, tokens }, secret);
68
69
  const { status, session } = await validateSessionJwt(jwt, secret);
69
70
  ```
70
71
 
71
- ### Browser — OAuth SPA Flow
72
-
73
- ```typescript
74
- import {
75
- generatePKCE,
76
- buildAuthorizationUrl,
77
- exchangeCodeForTokens,
78
- } from "@eventuras/fides-auth/oauth-browser";
79
-
80
- const config = {
81
- issuer: "https://auth.example.com",
82
- clientId: "my-client-id",
83
- redirect_uri: "https://myapp.com/callback",
84
- scope: "openid profile email",
85
- };
86
-
87
- // Start flow
88
- const pkce = await generatePKCE();
89
- sessionStorage.setItem("code_verifier", pkce.code_verifier);
90
- sessionStorage.setItem("state", pkce.state);
91
- window.location.href = buildAuthorizationUrl(config, pkce);
92
-
93
- // Handle callback
94
- const code = new URLSearchParams(location.search).get("code");
95
- const tokens = await exchangeCodeForTokens(
96
- config,
97
- code,
98
- sessionStorage.getItem("code_verifier")!,
99
- );
100
- ```
101
-
102
72
  ## Modules
103
73
 
104
74
  Each module is available as a separate entry point for tree-shaking:
@@ -107,7 +77,9 @@ Each module is available as a separate entry point for tree-shaking:
107
77
  | ------------------------------------------ | ----------- | ----------------------------------------------------- |
108
78
  | `@eventuras/fides-auth` | Node.js | Main entry — re-exports utils, session, types, logger |
109
79
  | `@eventuras/fides-auth/oauth` | Node.js | OAuth/OIDC flows with `openid-client` |
110
- | `@eventuras/fides-auth/oauth-browser` | Browser | OAuth/OIDC flows with Web Crypto API |
80
+ | `@eventuras/fides-auth/pkce` | Universal | PKCE verifier/challenge and `state` (Web Crypto) |
81
+ | `@eventuras/fides-auth/id-token` | Universal | ID token verification against the provider JWKS |
82
+ | `@eventuras/fides-auth/oauth-browser` | Browser | **Deprecated** — OAuth/OIDC flows with Web Crypto API |
111
83
  | `@eventuras/fides-auth/session-validation` | Node.js | Encrypted JWT session validation |
112
84
  | `@eventuras/fides-auth/session-refresh` | Node.js | Automatic token refresh for sessions |
113
85
  | `@eventuras/fides-auth/silent-login` | Node.js | Silent login (`prompt=none`) utilities |
@@ -138,6 +110,133 @@ import {
138
110
  } from "@eventuras/fides-auth/oauth";
139
111
  ```
140
112
 
113
+ `extractUserFromTokens` and `buildSessionFromTokens` only decode the ID token —
114
+ they don't check its signature, `iss`, `aud` or `exp`. That is safe on tokens
115
+ straight from `exchangeAuthorizationCode`. Verify tokens from anywhere else
116
+ first with `verifyIdToken` from `@eventuras/fides-auth/id-token`.
117
+
118
+ #### Session mapping
119
+
120
+ `buildSessionFromTokens` keeps the standard fields. To keep more of the token
121
+ response or the ID token claims, pass `mapSession`, here or to
122
+ `handleOidcCallback`:
123
+
124
+ ```typescript
125
+ const session = buildSessionFromTokens(tokens, {
126
+ rolesClaim: "groups",
127
+ mapSession: (tokens, claims, session) => ({
128
+ ...session,
129
+ data: { tenant: tokens.tenant },
130
+ }),
131
+ });
132
+ ```
133
+
134
+ `claims` are decoded, not verified. Without an `id_token` (no `openid` scope),
135
+ `claims` is undefined and the session has no `user`. `user.email` is optional.
136
+ Extra fields make the session bigger; large ones need
137
+ [server-side sessions](./docs/server-sessions.md).
138
+
139
+ #### Client authentication
140
+
141
+ `clientAuth` sets how the client authenticates at the token endpoint. Omit it
142
+ for `client_secret_post`:
143
+
144
+ ```typescript
145
+ { clientSecret: "…" } // client_secret_post
146
+ { clientAuth: "client_secret_basic", clientSecret: "…" }
147
+ { clientAuth: "none" } // public client, PKCE only
148
+ { clientAuth: "private_key_jwt", privateKey } // CryptoKey or { key, kid }
149
+ ```
150
+
151
+ `clientCredentialsGrant` takes the same options except `none`.
152
+
153
+ #### Server metadata instead of discovery
154
+
155
+ If the server publishes its metadata somewhere other than
156
+ `/.well-known/openid-configuration` (an RFC 8414 document, say), fetch it
157
+ yourself and pass it as `serverMetadata`. fides-auth then skips discovery:
158
+
159
+ ```typescript
160
+ const oauthConfig: OAuthConfig = {
161
+ issuer: "https://auth.example.com",
162
+ clientId: "your-client-id",
163
+ clientAuth: "none",
164
+ redirect_uri: "https://app.example.com/callback",
165
+ scope: "openid profile",
166
+ serverMetadata: await fetch(metadataUrl).then((r) => r.json()),
167
+ };
168
+ ```
169
+
170
+ With `openid` in the scope the metadata must include `issuer`, so the ID token's
171
+ `iss` can be checked.
172
+
173
+ #### PKCE and state (`/pkce`)
174
+
175
+ For flows that manage their own login transaction. Web Crypto only, so it also
176
+ runs in the browser and on edge runtimes:
177
+
178
+ ```typescript
179
+ import { createPkcePair, createState } from "@eventuras/fides-auth/pkce";
180
+
181
+ const { verifier, challenge } = await createPkcePair(); // S256
182
+ const state = createState();
183
+ ```
184
+
185
+ `buildPKCEOptions` is built on the same two functions.
186
+
187
+ #### ID token verification (`/id-token`)
188
+
189
+ Checks an ID token's signature, `iss`, `aud` and `exp` against the provider's
190
+ JWKS and returns the claims. Also runs in the browser:
191
+
192
+ ```typescript
193
+ import { verifyIdToken } from "@eventuras/fides-auth/id-token";
194
+
195
+ const claims = await verifyIdToken(idToken, {
196
+ issuer: "https://auth.example.com",
197
+ audience: "your-client-id",
198
+ oauthConfig, // or jwksUri: "https://auth.example.com/jwks"
199
+ });
200
+ ```
201
+
202
+ `algorithms` defaults to `RS*`, `PS*`, `ES*` and `EdDSA`. The JWKS is cached per URI.
203
+
204
+ #### Scope and extra authorization parameters
205
+
206
+ `scope` takes a string or an array. Other authorization parameters (`prompt`,
207
+ `login_hint`, `resource`, …) can be set on the config, per call, or per request
208
+ in `handleOidcLogin`. They go through PAR too.
209
+
210
+ ```typescript
211
+ const oauthConfig = {
212
+ // …
213
+ scope: ["openid", "profile", "offline_access"],
214
+ authorizationParameters: { resource: "https://api.example.com" },
215
+ };
216
+
217
+ const pkce = await buildPKCEOptions(oauthConfig, {
218
+ parameters: { login_hint: "user@example.com" },
219
+ });
220
+
221
+ // Or per request in the server handler
222
+ return handleOidcLogin(request, {
223
+ oauthConfig,
224
+ cookies,
225
+ secret: process.env.SESSION_SECRET!,
226
+ authorizationParameters: (request) =>
227
+ new URL(request.url).searchParams.has("reauth") ? { prompt: "login" } : {},
228
+ });
229
+ ```
230
+
231
+ Per-call parameters override the config's, and a `scope` among them replaces
232
+ `OAuthConfig.scope`. These keys are reserved, and passing one throws:
233
+
234
+ - `state`, `code_challenge`, `code_challenge_method` and `redirect_uri`, which
235
+ fides-auth sets.
236
+ - `client_id` and `response_type`, which openid-client sets.
237
+ - `request` and `request_uri`, which would bypass PKCE and `state`.
238
+ - `nonce`, which the callback doesn't check, so the login would fail.
239
+
141
240
  #### Pushed Authorization Requests (PAR, RFC 9126)
142
241
 
143
242
  Opt in by setting `usePar: true` on your `OAuthConfig`. The convenience
@@ -190,7 +289,7 @@ const logoutUrl = await buildOidcLogoutUrl(oauthConfig, {
190
289
  ```
191
290
 
192
291
  The `/server` entry point wraps the whole dance — read the hint, clear the cookies,
193
- redirect:
292
+ redirect. Its handlers never read the environment; pass the session `secret` in:
194
293
 
195
294
  ```typescript
196
295
  import { handleOidcLogout } from "@eventuras/fides-auth/server";
@@ -198,6 +297,7 @@ import { handleOidcLogout } from "@eventuras/fides-auth/server";
198
297
  return handleOidcLogout(request, {
199
298
  oauthConfig,
200
299
  cookies,
300
+ secret: process.env.SESSION_SECRET!,
201
301
  postLogoutRedirectUri: "https://app.example.com/",
202
302
  });
203
303
  ```
@@ -205,8 +305,100 @@ return handleOidcLogout(request, {
205
305
  See [docs/rp-initiated-logout.md](./docs/rp-initiated-logout.md) for the full
206
306
  parameter set, where the ID token is stored, and why `client_id` is still sent.
207
307
 
308
+ #### Server-side sessions
309
+
310
+ By default the session is encrypted into cookies. When a provider's tokens are
311
+ too large for that (OpenIddict's encrypted JWE tokens, for instance), keep the
312
+ session in a store and give the browser only a handle:
313
+
314
+ ```typescript
315
+ import { serverPersistence, createMemorySessionStore } from "@eventuras/fides-auth/server";
316
+
317
+ const persistence = serverPersistence({
318
+ store: createMemorySessionStore(), // or your own SessionStore (Mongo, Redis, …)
319
+ secret: process.env.SESSION_SECRET!,
320
+ });
321
+
322
+ return handleHeartbeat(request, { oauthConfig, cookies, persistence });
323
+ ```
324
+
325
+ Logout, heartbeat and the session helpers take `persistence` in place of
326
+ `secret`. The callback takes both: `secret` also opens the login transaction.
327
+ The store gets encrypted records keyed by a hash of the handle; `clearSession`
328
+ deletes the record, so logout revokes a copied cookie too. See
329
+ [docs/server-sessions.md](./docs/server-sessions.md).
330
+
331
+ #### Login transaction
332
+
333
+ `handleOidcLogin` seals each login (state, PKCE verifier, returnTo, start time)
334
+ into one of three `__Host-oauth_tx_<n>` cookies, so parallel logins don't
335
+ collide; a fourth evicts the oldest, and a `returnTo` over 1 KB as JSON is dropped. With
336
+ an `http:` `redirect_uri` (plain-http development) the cookie is a plain,
337
+ non-Secure `oauth_tx_<n>`. `handleOidcCallback` rejects it after 10 minutes and
338
+ deletes it whatever the outcome. Pass both handlers the same `secret`. A denied
339
+ or cancelled login redirects to `defaultRedirectPath` with
340
+ `?login=error&error=<code>`, or `error=login_expired` once the login has ended.
341
+
342
+ Custom flows use the same helpers. Pass them the handlers' cookie options, so a
343
+ custom login and the built-in callback (or the reverse) agree on the cookie:
344
+
345
+ ```typescript
346
+ import {
347
+ createTransaction,
348
+ persistTransaction,
349
+ readTransaction,
350
+ clearTransaction,
351
+ validateCallback,
352
+ transactionCookieOptions,
353
+ } from "@eventuras/fides-auth/server";
354
+
355
+ const txCookie = transactionCookieOptions(oauthConfig.redirect_uri);
356
+
357
+ // Login
358
+ const pkce = await buildPKCEOptions(oauthConfig);
359
+ await persistTransaction(cookies, createTransaction(pkce, { returnTo }), secret, txCookie);
360
+
361
+ // Callback
362
+ try {
363
+ const transaction = await readTransaction(cookies, state, secret, txCookie);
364
+ if (!transaction) throw new Error("No login in progress");
365
+ validateCallback(query, transaction); // throws CallbackValidationError
366
+ // … exchangeAuthorizationCode(oauthConfig, url, transaction.codeVerifier, transaction.state)
367
+ } finally {
368
+ await clearTransaction(cookies, state, secret, txCookie);
369
+ }
370
+ ```
371
+
372
+ #### API calls with the access token
373
+
374
+ `authorizedFetch` sends the session's access token only to URLs under
375
+ `allowedBase` (same origin, and a path under its path), never follows a
376
+ redirect, and times out after 10 seconds by default. A refused URL, non-2xx
377
+ response, timeout or failed `parse` throws an `AuthorizedFetchError` with a
378
+ `reason`, the status, and the URL without its query. On a 401 it calls `refresh`
379
+ once and retries once:
380
+
381
+ ```typescript
382
+ import { authorizedFetch, tryRefreshSessionInStore } from "@eventuras/fides-auth/server";
383
+
384
+ const { data, session: current } = await authorizedFetch(session, `users/${id}`, {
385
+ allowedBase: "https://api.example.com/v1/",
386
+ parse: async (response) => parseUser(await response.json()),
387
+ refresh: () => tryRefreshSessionInStore(cookies, oauthConfig, secret),
388
+ });
389
+ ```
390
+
391
+ `refresh` may also return `refreshSessionInStore`'s result. Both persist the new
392
+ session; use the returned `session` for later calls. When the refresh fails, the
393
+ error is `refresh_failed`, and `error.refreshFailure.cause === "invalid_grant"`
394
+ means the user must log in again.
395
+
208
396
  ### OAuth — Browser (`/oauth-browser`)
209
397
 
398
+ > **Deprecated.** It hardcodes the provider endpoints and doesn't validate the
399
+ > token response. Use the server flow instead (`handleOidcLogin` /
400
+ > `handleOidcCallback` from `/server`). It will be removed in a later minor.
401
+
210
402
  Lightweight browser-only flows using Web Crypto:
211
403
 
212
404
  ```typescript
@@ -1,4 +1,4 @@
1
- //#region src/activity-tracker.ts
1
+ // src/activity-tracker.ts
2
2
  /**
3
3
  * Records the most recent user activity timestamp by listening to DOM events.
4
4
  *
@@ -69,7 +69,6 @@ function createActivityTracker(options = {}) {
69
69
  }
70
70
  };
71
71
  }
72
- //#endregion
73
72
  export { DEFAULT_ACTIVITY_EVENTS, createActivityTracker };
74
73
 
75
74
  //# sourceMappingURL=activity-tracker.js.map
package/dist/cookies.d.ts CHANGED
@@ -89,6 +89,12 @@ export declare const ID_TOKEN_COOKIE_NAME = "session_it";
89
89
  * apart.
90
90
  */
91
91
  export declare const SESSION_COOKIE_NAME = "session";
92
+ /**
93
+ * Name of the cookie holding the opaque session handle under server
94
+ * persistence, where the session itself lives in a `SessionStore` and no token
95
+ * reaches the browser. See `server/session-persistence.ts`.
96
+ */
97
+ export declare const SESSION_REF_COOKIE_NAME = "session_ref";
92
98
  /**
93
99
  * Serialized byte size of a `name=value` cookie pair. The browser per-cookie
94
100
  * limit applies to the serialized pair, so the `=` separator is counted too —
@@ -1 +1 @@
1
- {"version":3,"file":"cookies.d.ts","sourceRoot":"","sources":["../src/cookies.ts"],"names":[],"mappings":"AAUA;;;;;GAKG;AACH,eAAO,MAAM,gBAAgB,OAAO,CAAC;AAErC;;;;GAIG;AACH,eAAO,MAAM,iBAAiB,OAAO,CAAC;AAEtC;;;;;GAKG;AACH,qBAAa,mBAAoB,SAAQ,KAAK;aAE1B,UAAU,EAAE,MAAM;aAClB,IAAI,EAAE,MAAM;aACZ,KAAK,EAAE,MAAM;IAH/B,YACkB,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE,MAAM,EACZ,KAAK,GAAE,MAAyB,EAMjD;CACF;AAED,MAAM,WAAW,aAAa;IAC5B,iCAAiC;IACjC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,yBAAyB;IACzB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,uCAAuC;IACvC,QAAQ,CAAC,EAAE,QAAQ,GAAG,KAAK,GAAG,MAAM,CAAC;IACrC,qCAAqC;IACrC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB;;;;OAIG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,2BAA2B,EAAE,aAMzC,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,yBAAyB,EAAE,aAMvC,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,wBAAwB,eAAe,CAAC;AAErD;;;;;;;;GAQG;AACH,eAAO,MAAM,oBAAoB,eAAe,CAAC;AAEjD;;;;GAIG;AACH,eAAO,MAAM,mBAAmB,YAAY,CAAC;AAE7C;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAGlE;AAED;;;;GAIG;AACH,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAM3E"}
1
+ {"version":3,"file":"cookies.d.ts","sourceRoot":"","sources":["../src/cookies.ts"],"names":[],"mappings":"AAUA;;;;;GAKG;AACH,eAAO,MAAM,gBAAgB,OAAO,CAAC;AAErC;;;;GAIG;AACH,eAAO,MAAM,iBAAiB,OAAO,CAAC;AAEtC;;;;;GAKG;AACH,qBAAa,mBAAoB,SAAQ,KAAK;aAE1B,UAAU,EAAE,MAAM;aAClB,IAAI,EAAE,MAAM;aACZ,KAAK,EAAE,MAAM;IAH/B,YACkB,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE,MAAM,EACZ,KAAK,GAAE,MAAyB,EAMjD;CACF;AAED,MAAM,WAAW,aAAa;IAC5B,iCAAiC;IACjC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,yBAAyB;IACzB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,uCAAuC;IACvC,QAAQ,CAAC,EAAE,QAAQ,GAAG,KAAK,GAAG,MAAM,CAAC;IACrC,qCAAqC;IACrC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB;;;;OAIG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,2BAA2B,EAAE,aAMzC,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,yBAAyB,EAAE,aAMvC,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,wBAAwB,eAAe,CAAC;AAErD;;;;;;;;GAQG;AACH,eAAO,MAAM,oBAAoB,eAAe,CAAC;AAEjD;;;;GAIG;AACH,eAAO,MAAM,mBAAmB,YAAY,CAAC;AAE7C;;;;GAIG;AACH,eAAO,MAAM,uBAAuB,gBAAgB,CAAC;AAErD;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAGlE;AAED;;;;GAIG;AACH,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAM3E"}
package/dist/cookies.js CHANGED
@@ -1,4 +1,4 @@
1
- //#region src/cookies.ts
1
+ // src/cookies.ts
2
2
  /**
3
3
  * Browser per-cookie size limit (RFC 6265: browsers must support at least
4
4
  * 4096 bytes for the sum of the cookie's name and value). Cookies at or above
@@ -93,6 +93,12 @@ var ID_TOKEN_COOKIE_NAME = "session_it";
93
93
  */
94
94
  var SESSION_COOKIE_NAME = "session";
95
95
  /**
96
+ * Name of the cookie holding the opaque session handle under server
97
+ * persistence, where the session itself lives in a `SessionStore` and no token
98
+ * reaches the browser. See `server/session-persistence.ts`.
99
+ */
100
+ var SESSION_REF_COOKIE_NAME = "session_ref";
101
+ /**
96
102
  * Serialized byte size of a `name=value` cookie pair. The browser per-cookie
97
103
  * limit applies to the serialized pair, so the `=` separator is counted too —
98
104
  * otherwise a borderline pair (name+value === 4095) would slip through only to be
@@ -112,7 +118,6 @@ function assertCookieWithinLimit(name, value) {
112
118
  if (size >= 4096) throw new CookieTooLargeError(name, size);
113
119
  return size;
114
120
  }
115
- //#endregion
116
- export { ACCESS_TOKEN_COOKIE_NAME, COOKIE_INFO_BYTES, COOKIE_MAX_BYTES, CookieTooLargeError, ID_TOKEN_COOKIE_NAME, SESSION_COOKIE_NAME, assertCookieWithinLimit, cookieByteSize, defaultOAuthCookieOptions, defaultSessionCookieOptions };
121
+ export { ACCESS_TOKEN_COOKIE_NAME, COOKIE_INFO_BYTES, COOKIE_MAX_BYTES, CookieTooLargeError, ID_TOKEN_COOKIE_NAME, SESSION_COOKIE_NAME, SESSION_REF_COOKIE_NAME, assertCookieWithinLimit, cookieByteSize, defaultOAuthCookieOptions, defaultSessionCookieOptions };
117
122
 
118
123
  //# sourceMappingURL=cookies.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"cookies.js","names":[],"sources":["../src/cookies.ts"],"sourcesContent":["// cookies.ts\n//\n// Framework-agnostic cookie attributes, size limits, and the access-token cookie\n// name. This module performs no I/O — it never reads or writes cookies. Framework\n// adapters (Next.js, React Router, …) own the actual cookie store and use these\n// values and helpers to configure and size-check the cookies they set.\n//\n// The companion module `session-cookies.ts` encodes/decodes a session into cookie\n// *values*; this module is about the cookie *attributes* and *limits*.\n\n/**\n * Browser per-cookie size limit (RFC 6265: browsers must support at least\n * 4096 bytes for the sum of the cookie's name and value). Cookies at or above\n * this size are silently dropped by the browser, which manifests as a broken\n * login (the cookie is never stored, so the user appears unauthenticated).\n */\nexport const COOKIE_MAX_BYTES = 4096;\n\n/**\n * Threshold at which an adapter should emit an informational log that a cookie is\n * getting large. This is not an error — a session with many scopes/roles can\n * legitimately approach this size — it just gives visibility before the hard limit.\n */\nexport const COOKIE_INFO_BYTES = 3500;\n\n/**\n * Thrown when a cookie's name + value would meet or exceed the browser's\n * per-cookie size limit. Setting such a cookie would be silently dropped by the\n * browser, so adapters should fail loudly instead. Callers (e.g. an OIDC callback)\n * can catch this to surface a clear error rather than producing a broken login.\n */\nexport class CookieTooLargeError extends Error {\n constructor(\n public readonly cookieName: string,\n public readonly size: number,\n public readonly limit: number = COOKIE_MAX_BYTES,\n ) {\n super(\n `Cookie \"${cookieName}\" is ${size} bytes, which meets or exceeds the browser limit of ${limit} bytes and would be silently dropped`,\n );\n this.name = 'CookieTooLargeError';\n }\n}\n\nexport interface CookieOptions {\n /** Cookie path (default: '/') */\n path?: string;\n /** Max age in seconds */\n maxAge?: number;\n /** SameSite policy (default: 'lax') */\n sameSite?: 'strict' | 'lax' | 'none';\n /** HTTP only flag (default: true) */\n httpOnly?: boolean;\n /**\n * Secure flag. `true` in {@link defaultSessionCookieOptions} and\n * {@link defaultOAuthCookieOptions}; when omitted from a hand-built options\n * object, the adapter's own default applies.\n */\n secure?: boolean;\n}\n\n/**\n * Default cookie options for session cookies.\n * - Secure\n * - HTTP only\n * - Lax same-site policy\n * - 30 days max age\n *\n * `secure` is unconditional rather than derived from NODE_ENV: a deployment that\n * doesn't set it — staging, a container, any non-Next server — would otherwise put\n * the session cookie on the wire in the clear, silently.\n *\n * The cookie spec exempts localhost from the https requirement, so a `Secure`\n * cookie is still set and sent over `http://localhost` and local development is\n * unaffected. Plain http on any other host — a LAN address, a custom dev hostname —\n * needs an explicit `secure: false`.\n */\nexport const defaultSessionCookieOptions: CookieOptions = {\n path: '/',\n maxAge: 60 * 60 * 24 * 30, // 30 days\n sameSite: 'lax',\n httpOnly: true,\n secure: true,\n};\n\n/**\n * Default cookie options for OAuth state/PKCE cookies.\n * - Secure — see {@link defaultSessionCookieOptions}\n * - HTTP only\n * - Lax same-site policy\n * - 10 minutes max age (short-lived for security)\n */\nexport const defaultOAuthCookieOptions: CookieOptions = {\n path: '/',\n maxAge: 60 * 10, // 10 minutes\n sameSite: 'lax',\n httpOnly: true,\n secure: true,\n};\n\n/**\n * Name of the cookie holding the (split-out) access token.\n *\n * The access token — typically a large JWT carrying scopes/roles — is stored in\n * its own cookie so it gets a full per-cookie byte budget, instead of competing\n * for space with the rest of the session inside a single \"session\" cookie. See\n * {@link CookieTooLargeError} for the limit this works around.\n */\nexport const ACCESS_TOKEN_COOKIE_NAME = 'session_at';\n\n/**\n * Name of the cookie holding the (split-out) ID token, kept for `id_token_hint`\n * on RP-initiated logout.\n *\n * It gets its own cookie for two reasons: it is a second large JWT, which would\n * eat into the access token's byte budget, and it must outlive access-token\n * expiry — a logout after the session has gone stale is exactly when the hint is\n * needed.\n */\nexport const ID_TOKEN_COOKIE_NAME = 'session_it';\n\n/**\n * Name of the main session cookie (everything except the split-out access and ID\n * tokens). See {@link ACCESS_TOKEN_COOKIE_NAME} for why the raw tokens are stored\n * apart.\n */\nexport const SESSION_COOKIE_NAME = 'session';\n\n/**\n * Serialized byte size of a `name=value` cookie pair. The browser per-cookie\n * limit applies to the serialized pair, so the `=` separator is counted too —\n * otherwise a borderline pair (name+value === 4095) would slip through only to be\n * dropped at 4096 once serialized.\n */\nexport function cookieByteSize(name: string, value: string): number {\n const encoder = new TextEncoder();\n return encoder.encode(name).length + 1 + encoder.encode(value).length;\n}\n\n/**\n * Asserts that a `name=value` cookie pair is within the browser's per-cookie\n * limit, throwing {@link CookieTooLargeError} if not. Returns the measured byte\n * size so callers can additionally log when it crosses {@link COOKIE_INFO_BYTES}.\n */\nexport function assertCookieWithinLimit(name: string, value: string): number {\n const size = cookieByteSize(name, value);\n if (size >= COOKIE_MAX_BYTES) {\n throw new CookieTooLargeError(name, size);\n }\n return size;\n}\n"],"mappings":";;;;;;;AAgBA,IAAa,mBAAmB;;;;;;AAOhC,IAAa,oBAAoB;;;;;;;AAQjC,IAAa,sBAAb,cAAyC,MAAM;CAE3B;CACA;CACA;CAHlB,YACE,YACA,MACA,QAAgC,kBAChC;EACA,MACE,WAAW,WAAW,OAAO,KAAK,sDAAsD,MAAM,qCAChG;EANgB,KAAA,aAAA;EACA,KAAA,OAAA;EACA,KAAA,QAAA;EAKhB,KAAK,OAAO;CACd;AACF;;;;;;;;;;;;;;;;;AAmCA,IAAa,8BAA6C;CACxD,MAAM;CACN,QAAQ;CACR,UAAU;CACV,UAAU;CACV,QAAQ;AACV;;;;;;;;AASA,IAAa,4BAA2C;CACtD,MAAM;CACN,QAAQ;CACR,UAAU;CACV,UAAU;CACV,QAAQ;AACV;;;;;;;;;AAUA,IAAa,2BAA2B;;;;;;;;;;AAWxC,IAAa,uBAAuB;;;;;;AAOpC,IAAa,sBAAsB;;;;;;;AAQnC,SAAgB,eAAe,MAAc,OAAuB;CAClE,MAAM,UAAU,IAAI,YAAY;CAChC,OAAO,QAAQ,OAAO,IAAI,CAAC,CAAC,SAAS,IAAI,QAAQ,OAAO,KAAK,CAAC,CAAC;AACjE;;;;;;AAOA,SAAgB,wBAAwB,MAAc,OAAuB;CAC3E,MAAM,OAAO,eAAe,MAAM,KAAK;CACvC,IAAI,QAAA,MACF,MAAM,IAAI,oBAAoB,MAAM,IAAI;CAE1C,OAAO;AACT"}
1
+ {"version":3,"file":"cookies.js","names":[],"sources":["../src/cookies.ts"],"sourcesContent":["// cookies.ts\n//\n// Framework-agnostic cookie attributes, size limits, and the access-token cookie\n// name. This module performs no I/O — it never reads or writes cookies. Framework\n// adapters (Next.js, React Router, …) own the actual cookie store and use these\n// values and helpers to configure and size-check the cookies they set.\n//\n// The companion module `session-cookies.ts` encodes/decodes a session into cookie\n// *values*; this module is about the cookie *attributes* and *limits*.\n\n/**\n * Browser per-cookie size limit (RFC 6265: browsers must support at least\n * 4096 bytes for the sum of the cookie's name and value). Cookies at or above\n * this size are silently dropped by the browser, which manifests as a broken\n * login (the cookie is never stored, so the user appears unauthenticated).\n */\nexport const COOKIE_MAX_BYTES = 4096;\n\n/**\n * Threshold at which an adapter should emit an informational log that a cookie is\n * getting large. This is not an error — a session with many scopes/roles can\n * legitimately approach this size — it just gives visibility before the hard limit.\n */\nexport const COOKIE_INFO_BYTES = 3500;\n\n/**\n * Thrown when a cookie's name + value would meet or exceed the browser's\n * per-cookie size limit. Setting such a cookie would be silently dropped by the\n * browser, so adapters should fail loudly instead. Callers (e.g. an OIDC callback)\n * can catch this to surface a clear error rather than producing a broken login.\n */\nexport class CookieTooLargeError extends Error {\n constructor(\n public readonly cookieName: string,\n public readonly size: number,\n public readonly limit: number = COOKIE_MAX_BYTES,\n ) {\n super(\n `Cookie \"${cookieName}\" is ${size} bytes, which meets or exceeds the browser limit of ${limit} bytes and would be silently dropped`,\n );\n this.name = 'CookieTooLargeError';\n }\n}\n\nexport interface CookieOptions {\n /** Cookie path (default: '/') */\n path?: string;\n /** Max age in seconds */\n maxAge?: number;\n /** SameSite policy (default: 'lax') */\n sameSite?: 'strict' | 'lax' | 'none';\n /** HTTP only flag (default: true) */\n httpOnly?: boolean;\n /**\n * Secure flag. `true` in {@link defaultSessionCookieOptions} and\n * {@link defaultOAuthCookieOptions}; when omitted from a hand-built options\n * object, the adapter's own default applies.\n */\n secure?: boolean;\n}\n\n/**\n * Default cookie options for session cookies.\n * - Secure\n * - HTTP only\n * - Lax same-site policy\n * - 30 days max age\n *\n * `secure` is unconditional rather than derived from NODE_ENV: a deployment that\n * doesn't set it — staging, a container, any non-Next server — would otherwise put\n * the session cookie on the wire in the clear, silently.\n *\n * The cookie spec exempts localhost from the https requirement, so a `Secure`\n * cookie is still set and sent over `http://localhost` and local development is\n * unaffected. Plain http on any other host — a LAN address, a custom dev hostname —\n * needs an explicit `secure: false`.\n */\nexport const defaultSessionCookieOptions: CookieOptions = {\n path: '/',\n maxAge: 60 * 60 * 24 * 30, // 30 days\n sameSite: 'lax',\n httpOnly: true,\n secure: true,\n};\n\n/**\n * Default cookie options for OAuth state/PKCE cookies.\n * - Secure — see {@link defaultSessionCookieOptions}\n * - HTTP only\n * - Lax same-site policy\n * - 10 minutes max age (short-lived for security)\n */\nexport const defaultOAuthCookieOptions: CookieOptions = {\n path: '/',\n maxAge: 60 * 10, // 10 minutes\n sameSite: 'lax',\n httpOnly: true,\n secure: true,\n};\n\n/**\n * Name of the cookie holding the (split-out) access token.\n *\n * The access token — typically a large JWT carrying scopes/roles — is stored in\n * its own cookie so it gets a full per-cookie byte budget, instead of competing\n * for space with the rest of the session inside a single \"session\" cookie. See\n * {@link CookieTooLargeError} for the limit this works around.\n */\nexport const ACCESS_TOKEN_COOKIE_NAME = 'session_at';\n\n/**\n * Name of the cookie holding the (split-out) ID token, kept for `id_token_hint`\n * on RP-initiated logout.\n *\n * It gets its own cookie for two reasons: it is a second large JWT, which would\n * eat into the access token's byte budget, and it must outlive access-token\n * expiry — a logout after the session has gone stale is exactly when the hint is\n * needed.\n */\nexport const ID_TOKEN_COOKIE_NAME = 'session_it';\n\n/**\n * Name of the main session cookie (everything except the split-out access and ID\n * tokens). See {@link ACCESS_TOKEN_COOKIE_NAME} for why the raw tokens are stored\n * apart.\n */\nexport const SESSION_COOKIE_NAME = 'session';\n\n/**\n * Name of the cookie holding the opaque session handle under server\n * persistence, where the session itself lives in a `SessionStore` and no token\n * reaches the browser. See `server/session-persistence.ts`.\n */\nexport const SESSION_REF_COOKIE_NAME = 'session_ref';\n\n/**\n * Serialized byte size of a `name=value` cookie pair. The browser per-cookie\n * limit applies to the serialized pair, so the `=` separator is counted too —\n * otherwise a borderline pair (name+value === 4095) would slip through only to be\n * dropped at 4096 once serialized.\n */\nexport function cookieByteSize(name: string, value: string): number {\n const encoder = new TextEncoder();\n return encoder.encode(name).length + 1 + encoder.encode(value).length;\n}\n\n/**\n * Asserts that a `name=value` cookie pair is within the browser's per-cookie\n * limit, throwing {@link CookieTooLargeError} if not. Returns the measured byte\n * size so callers can additionally log when it crosses {@link COOKIE_INFO_BYTES}.\n */\nexport function assertCookieWithinLimit(name: string, value: string): number {\n const size = cookieByteSize(name, value);\n if (size >= COOKIE_MAX_BYTES) {\n throw new CookieTooLargeError(name, size);\n }\n return size;\n}\n"],"mappings":";;;;;;;AAgBA,IAAa,mBAAmB;;;;;;AAOhC,IAAa,oBAAoB;;;;;;;AAQjC,IAAa,sBAAb,cAAyC,MAAM;CAE3B;CACA;CACA;CAHlB,YACE,YACA,MACA,QAAgC,kBAChC;EACA,MACE,WAAW,WAAW,OAAO,KAAK,sDAAsD,MAAM,qCAChG;EANgB,KAAA,aAAA;EACA,KAAA,OAAA;EACA,KAAA,QAAA;EAKhB,KAAK,OAAO;CACd;AACF;;;;;;;;;;;;;;;;;AAmCA,IAAa,8BAA6C;CACxD,MAAM;CACN,QAAQ;CACR,UAAU;CACV,UAAU;CACV,QAAQ;AACV;;;;;;;;AASA,IAAa,4BAA2C;CACtD,MAAM;CACN,QAAQ;CACR,UAAU;CACV,UAAU;CACV,QAAQ;AACV;;;;;;;;;AAUA,IAAa,2BAA2B;;;;;;;;;;AAWxC,IAAa,uBAAuB;;;;;;AAOpC,IAAa,sBAAsB;;;;;;AAOnC,IAAa,0BAA0B;;;;;;;AAQvC,SAAgB,eAAe,MAAc,OAAuB;CAClE,MAAM,UAAU,IAAI,YAAY;CAChC,OAAO,QAAQ,OAAO,IAAI,CAAC,CAAC,SAAS,IAAI,QAAQ,OAAO,KAAK,CAAC,CAAC;AACjE;;;;;;AAOA,SAAgB,wBAAwB,MAAc,OAAuB;CAC3E,MAAM,OAAO,eAAe,MAAM,KAAK;CACvC,IAAI,QAAA,MACF,MAAM,IAAI,oBAAoB,MAAM,IAAI;CAE1C,OAAO;AACT"}
package/dist/heartbeat.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { DEFAULT_ACTIVITY_EVENTS, createActivityTracker } from "./activity-tracker.js";
2
2
  import { createLogger } from "./logger.js";
3
3
  import { SESSION_EVENT, isSessionRejectedReason } from "./session-events.js";
4
- //#region src/heartbeat.ts
4
+ // src/heartbeat.ts
5
5
  var DEFAULTS = {
6
6
  endpoint: "/api/auth/heartbeat",
7
7
  fraction: .3,
@@ -197,7 +197,6 @@ function createHeartbeat(config = {}) {
197
197
  }, "Heartbeat started");
198
198
  return { stop: teardown };
199
199
  }
200
- //#endregion
201
200
  export { createHeartbeat };
202
201
 
203
202
  //# sourceMappingURL=heartbeat.js.map
@@ -0,0 +1,31 @@
1
+ import { type JWTPayload } from 'jose';
2
+ import type { OAuthConfig } from './oauth-config';
3
+ interface VerifyIdTokenBaseOptions {
4
+ /** Expected `iss`. */
5
+ issuer: string;
6
+ /** Expected `aud`, usually the client ID. */
7
+ audience: string | string[];
8
+ /** Accepted `alg` values. Defaults to `RS*`, `PS*`, `ES*` and `EdDSA`. */
9
+ algorithms?: string[];
10
+ }
11
+ /** Options for {@link verifyIdToken}. Pass `jwksUri` or `oauthConfig`. */
12
+ export type VerifyIdTokenOptions = VerifyIdTokenBaseOptions & ({
13
+ /** The provider's JWKS URL. */
14
+ jwksUri: string | URL;
15
+ oauthConfig?: never;
16
+ } | {
17
+ /** Reads `jwks_uri` from discovery, or from `serverMetadata`. */
18
+ oauthConfig: OAuthConfig;
19
+ jwksUri?: never;
20
+ });
21
+ /** Drops every cached JWKS. Tests should call it between cases. */
22
+ export declare function clearJwksCache(): void;
23
+ /**
24
+ * Verifies an ID token's signature, `iss`, `aud` and `exp` against the provider's JWKS.
25
+ *
26
+ * @returns The verified claims.
27
+ * @throws A jose error if the token is invalid, or a `TypeError` if no JWKS URI is found.
28
+ */
29
+ export declare function verifyIdToken(idToken: string, options: VerifyIdTokenOptions): Promise<JWTPayload>;
30
+ export {};
31
+ //# sourceMappingURL=id-token.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"id-token.d.ts","sourceRoot":"","sources":["../src/id-token.ts"],"names":[],"mappings":"AAIA,OAAO,EAAsB,KAAK,UAAU,EAAa,MAAM,MAAM,CAAC;AAEtE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAsBlD,UAAU,wBAAwB;IAChC,sBAAsB;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,6CAA6C;IAC7C,QAAQ,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IAC5B,0EAA0E;IAC1E,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;CACvB;AAED,0EAA0E;AAC1E,MAAM,MAAM,oBAAoB,GAAG,wBAAwB,GACzD,CACI;IACE,+BAA+B;IAC/B,OAAO,EAAE,MAAM,GAAG,GAAG,CAAC;IACtB,WAAW,CAAC,EAAE,KAAK,CAAC;CACrB,GACD;IACE,iEAAiE;IACjE,WAAW,EAAE,WAAW,CAAC;IACzB,OAAO,CAAC,EAAE,KAAK,CAAC;CACjB,CACJ,CAAC;AAEJ,mEAAmE;AACnE,wBAAgB,cAAc,IAAI,IAAI,CAErC;AAqBD;;;;;GAKG;AACH,wBAAsB,aAAa,CACjC,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,oBAAoB,GAC5B,OAAO,CAAC,UAAU,CAAC,CAUrB"}
@@ -0,0 +1,60 @@
1
+ import { discoverConfiguration } from "./oidc-discovery.js";
2
+ import { createRemoteJWKSet, jwtVerify } from "jose";
3
+ // src/id-token.ts
4
+ /** Asymmetric only, so `none` and `HS*` are rejected. */
5
+ var DEFAULT_ALGORITHMS = [
6
+ "RS256",
7
+ "RS384",
8
+ "RS512",
9
+ "PS256",
10
+ "PS384",
11
+ "PS512",
12
+ "ES256",
13
+ "ES384",
14
+ "ES512",
15
+ "EdDSA"
16
+ ];
17
+ /** One getter per JWKS URI; jose refetches it on an unknown `kid`. */
18
+ var jwksCache = /* @__PURE__ */ new Map();
19
+ /** Drops every cached JWKS. Tests should call it between cases. */
20
+ function clearJwksCache() {
21
+ jwksCache.clear();
22
+ }
23
+ function getJwks(uri) {
24
+ const url = new URL(uri);
25
+ let jwks = jwksCache.get(url.href);
26
+ if (!jwks) {
27
+ jwks = createRemoteJWKSet(url);
28
+ jwksCache.set(url.href, jwks);
29
+ }
30
+ return jwks;
31
+ }
32
+ async function resolveJwksUri(options) {
33
+ if (options.jwksUri !== void 0) return options.jwksUri;
34
+ const { jwks_uri } = (await discoverConfiguration(options.oauthConfig)).serverMetadata();
35
+ if (!jwks_uri) throw new TypeError("The provider metadata has no jwks_uri");
36
+ return jwks_uri;
37
+ }
38
+ /**
39
+ * Verifies an ID token's signature, `iss`, `aud` and `exp` against the provider's JWKS.
40
+ *
41
+ * @returns The verified claims.
42
+ * @throws A jose error if the token is invalid, or a `TypeError` if no JWKS URI is found.
43
+ */
44
+ async function verifyIdToken(idToken, options) {
45
+ const jwks = getJwks(await resolveJwksUri(options));
46
+ const { payload } = await jwtVerify(idToken, jwks, {
47
+ issuer: options.issuer,
48
+ audience: options.audience,
49
+ algorithms: options.algorithms ?? DEFAULT_ALGORITHMS,
50
+ requiredClaims: [
51
+ "sub",
52
+ "exp",
53
+ "iat"
54
+ ]
55
+ });
56
+ return payload;
57
+ }
58
+ export { clearJwksCache, verifyIdToken };
59
+
60
+ //# sourceMappingURL=id-token.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"id-token.js","names":[],"sources":["../src/id-token.ts"],"sourcesContent":["// id-token.ts\n//\n// Verifies ID tokens against the provider's JWKS. Universal: jose uses Web Crypto and fetch.\n\nimport { createRemoteJWKSet, type JWTPayload, jwtVerify } from 'jose';\n\nimport type { OAuthConfig } from './oauth-config';\nimport { discoverConfiguration } from './oidc-discovery';\n\n/** Asymmetric only, so `none` and `HS*` are rejected. */\nconst DEFAULT_ALGORITHMS = [\n 'RS256',\n 'RS384',\n 'RS512',\n 'PS256',\n 'PS384',\n 'PS512',\n 'ES256',\n 'ES384',\n 'ES512',\n 'EdDSA',\n];\n\ntype RemoteJwks = ReturnType<typeof createRemoteJWKSet>;\n\n/** One getter per JWKS URI; jose refetches it on an unknown `kid`. */\nconst jwksCache = new Map<string, RemoteJwks>();\n\ninterface VerifyIdTokenBaseOptions {\n /** Expected `iss`. */\n issuer: string;\n /** Expected `aud`, usually the client ID. */\n audience: string | string[];\n /** Accepted `alg` values. Defaults to `RS*`, `PS*`, `ES*` and `EdDSA`. */\n algorithms?: string[];\n}\n\n/** Options for {@link verifyIdToken}. Pass `jwksUri` or `oauthConfig`. */\nexport type VerifyIdTokenOptions = VerifyIdTokenBaseOptions &\n (\n | {\n /** The provider's JWKS URL. */\n jwksUri: string | URL;\n oauthConfig?: never;\n }\n | {\n /** Reads `jwks_uri` from discovery, or from `serverMetadata`. */\n oauthConfig: OAuthConfig;\n jwksUri?: never;\n }\n );\n\n/** Drops every cached JWKS. Tests should call it between cases. */\nexport function clearJwksCache(): void {\n jwksCache.clear();\n}\n\nfunction getJwks(uri: string | URL): RemoteJwks {\n const url = new URL(uri);\n let jwks = jwksCache.get(url.href);\n if (!jwks) {\n jwks = createRemoteJWKSet(url);\n jwksCache.set(url.href, jwks);\n }\n return jwks;\n}\n\nasync function resolveJwksUri(options: VerifyIdTokenOptions): Promise<string | URL> {\n if (options.jwksUri !== undefined) return options.jwksUri;\n\n const configuration = await discoverConfiguration(options.oauthConfig);\n const { jwks_uri } = configuration.serverMetadata();\n if (!jwks_uri) throw new TypeError('The provider metadata has no jwks_uri');\n return jwks_uri;\n}\n\n/**\n * Verifies an ID token's signature, `iss`, `aud` and `exp` against the provider's JWKS.\n *\n * @returns The verified claims.\n * @throws A jose error if the token is invalid, or a `TypeError` if no JWKS URI is found.\n */\nexport async function verifyIdToken(\n idToken: string,\n options: VerifyIdTokenOptions,\n): Promise<JWTPayload> {\n const jwks = getJwks(await resolveJwksUri(options));\n const { payload } = await jwtVerify(idToken, jwks, {\n issuer: options.issuer,\n audience: options.audience,\n algorithms: options.algorithms ?? DEFAULT_ALGORITHMS,\n // jose checks `exp` only when present; OIDC requires it.\n requiredClaims: ['sub', 'exp', 'iat'],\n });\n return payload;\n}\n"],"mappings":";;;;AAUA,IAAM,qBAAqB;CACzB;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF;;AAKA,IAAM,4BAAY,IAAI,IAAwB;;AA2B9C,SAAgB,iBAAuB;CACrC,UAAU,MAAM;AAClB;AAEA,SAAS,QAAQ,KAA+B;CAC9C,MAAM,MAAM,IAAI,IAAI,GAAG;CACvB,IAAI,OAAO,UAAU,IAAI,IAAI,IAAI;CACjC,IAAI,CAAC,MAAM;EACT,OAAO,mBAAmB,GAAG;EAC7B,UAAU,IAAI,IAAI,MAAM,IAAI;CAC9B;CACA,OAAO;AACT;AAEA,eAAe,eAAe,SAAsD;CAClF,IAAI,QAAQ,YAAY,KAAA,GAAW,OAAO,QAAQ;CAGlD,MAAM,EAAE,cAAa,MADO,sBAAsB,QAAQ,WAAW,EAAA,CAClC,eAAe;CAClD,IAAI,CAAC,UAAU,MAAM,IAAI,UAAU,uCAAuC;CAC1E,OAAO;AACT;;;;;;;AAQA,eAAsB,cACpB,SACA,SACqB;CACrB,MAAM,OAAO,QAAQ,MAAM,eAAe,OAAO,CAAC;CAClD,MAAM,EAAE,YAAY,MAAM,UAAU,SAAS,MAAM;EACjD,QAAQ,QAAQ;EAChB,UAAU,QAAQ;EAClB,YAAY,QAAQ,cAAc;EAElC,gBAAgB;GAAC;GAAO;GAAO;EAAK;CACtC,CAAC;CACD,OAAO;AACT"}
package/dist/index.js CHANGED
@@ -1,10 +1,10 @@
1
1
  import { DEFAULT_ACTIVITY_EVENTS, createActivityTracker } from "./activity-tracker.js";
2
- import { ACCESS_TOKEN_COOKIE_NAME, COOKIE_INFO_BYTES, COOKIE_MAX_BYTES, CookieTooLargeError, ID_TOKEN_COOKIE_NAME, SESSION_COOKIE_NAME, assertCookieWithinLimit, cookieByteSize, defaultOAuthCookieOptions, defaultSessionCookieOptions } from "./cookies.js";
2
+ import { ACCESS_TOKEN_COOKIE_NAME, COOKIE_INFO_BYTES, COOKIE_MAX_BYTES, CookieTooLargeError, ID_TOKEN_COOKIE_NAME, SESSION_COOKIE_NAME, SESSION_REF_COOKIE_NAME, assertCookieWithinLimit, cookieByteSize, defaultOAuthCookieOptions, defaultSessionCookieOptions } from "./cookies.js";
3
3
  import { configureLogger, createLogger } from "./logger.js";
4
4
  import { classifyRefreshFailure, getOAuthConfigLogContext, getOAuthErrorLogContext } from "./oauth-logging.js";
5
5
  import { SESSION_EVENT, SESSION_REJECTED_REASONS, isSessionRejectedReason, logSessionEvent, newSessionId, sessionEventLevel } from "./session-events.js";
6
6
  import { refreshSession } from "./session-refresh.js";
7
- import { accessTokenExpires, createEncryptedJWT, decrypt, decryptJWT, encrypt, generateToken, getSessionSecret, getSessionSecretUint8Array, hasScope, hexToUint8Array, sha256, sha512, toHex } from "./utils.js";
7
+ import { accessTokenExpires, createEncryptedJWT, decrypt, decryptJWT, encrypt, generateToken, hasScope, hexToUint8Array, sha256, sha512, toHex } from "./utils.js";
8
8
  import { isSession, validateSessionJwt } from "./session-validation.js";
9
9
  import "./types.js";
10
- export { ACCESS_TOKEN_COOKIE_NAME, COOKIE_INFO_BYTES, COOKIE_MAX_BYTES, CookieTooLargeError, DEFAULT_ACTIVITY_EVENTS, ID_TOKEN_COOKIE_NAME, SESSION_COOKIE_NAME, SESSION_EVENT, SESSION_REJECTED_REASONS, accessTokenExpires, assertCookieWithinLimit, classifyRefreshFailure, configureLogger, cookieByteSize, createActivityTracker, createEncryptedJWT, createLogger, decrypt, decryptJWT, defaultOAuthCookieOptions, defaultSessionCookieOptions, encrypt, generateToken, getOAuthConfigLogContext, getOAuthErrorLogContext, getSessionSecret, getSessionSecretUint8Array, hasScope, hexToUint8Array, isSession, isSessionRejectedReason, logSessionEvent, newSessionId, refreshSession, sessionEventLevel, sha256, sha512, toHex, validateSessionJwt };
10
+ export { ACCESS_TOKEN_COOKIE_NAME, COOKIE_INFO_BYTES, COOKIE_MAX_BYTES, CookieTooLargeError, DEFAULT_ACTIVITY_EVENTS, ID_TOKEN_COOKIE_NAME, SESSION_COOKIE_NAME, SESSION_EVENT, SESSION_REF_COOKIE_NAME, SESSION_REJECTED_REASONS, accessTokenExpires, assertCookieWithinLimit, classifyRefreshFailure, configureLogger, cookieByteSize, createActivityTracker, createEncryptedJWT, createLogger, decrypt, decryptJWT, defaultOAuthCookieOptions, defaultSessionCookieOptions, encrypt, generateToken, getOAuthConfigLogContext, getOAuthErrorLogContext, hasScope, hexToUint8Array, isSession, isSessionRejectedReason, logSessionEvent, newSessionId, refreshSession, sessionEventLevel, sha256, sha512, toHex, validateSessionJwt };
package/dist/logger.js CHANGED
@@ -1,4 +1,4 @@
1
- //#region src/logger.ts
1
+ // src/logger.ts
2
2
  function serializeError(value) {
3
3
  if (value instanceof Error) return {
4
4
  name: value.name,
@@ -129,7 +129,6 @@ function createLogger(options) {
129
129
  }
130
130
  };
131
131
  }
132
- //#endregion
133
132
  export { configureLogger, createLogger };
134
133
 
135
134
  //# sourceMappingURL=logger.js.map
@@ -1,14 +1,8 @@
1
1
  /**
2
2
  * Browser-compatible OAuth 2.0 + PKCE helpers
3
3
  *
4
- * This module provides OAuth 2.0 helpers that work in browser environments.
5
- * Unlike `oauth.ts` which uses openid-client (Node.js only), this uses
6
- * browser-native Web Crypto API for PKCE generation.
7
- *
8
- * **Use this in:**
9
- * - Vite/React SPAs
10
- * - Browser-only applications
11
- * - Any frontend that can't use Node.js APIs
4
+ * Deprecated and will be removed in a later minor. Use the server flow instead:
5
+ * `handleOidcLogin` / `handleOidcCallback` from `@eventuras/fides-auth/server`.
12
6
  *
13
7
  * **Usage:**
14
8
  * ```typescript
@@ -29,14 +23,20 @@
29
23
  * const tokens = await exchangeCodeForTokens(config, code, verifier);
30
24
  * ```
31
25
  */
32
- /** Configuration for browser-side OAuth / OIDC flows (no client secret). */
26
+ /**
27
+ * Configuration for browser-side OAuth / OIDC flows (no client secret).
28
+ * @deprecated Only used by the deprecated `oauth-browser` helpers. Use `handleOidcLogin` / `handleOidcCallback`.
29
+ */
33
30
  export interface OAuthConfig {
34
31
  issuer: string;
35
32
  clientId: string;
36
33
  redirect_uri: string;
37
34
  scope: string;
38
35
  }
39
- /** PKCE parameters for a browser-initiated authorization request. */
36
+ /**
37
+ * PKCE parameters for a browser-initiated authorization request.
38
+ * @deprecated Only used by the deprecated `oauth-browser` helpers. Use `handleOidcLogin`.
39
+ */
40
40
  export interface PKCEParams {
41
41
  code_verifier: string;
42
42
  code_challenge: string;
@@ -44,14 +44,17 @@ export interface PKCEParams {
44
44
  }
45
45
  /**
46
46
  * Generate PKCE parameters for OAuth flow
47
+ * @deprecated The random strings have modulo bias. Use `handleOidcLogin`, which builds PKCE server-side.
47
48
  */
48
49
  export declare function generatePKCE(): Promise<PKCEParams>;
49
50
  /**
50
51
  * Build authorization URL for OAuth flow
52
+ * @deprecated Hardcodes `${issuer}/auth` instead of using discovery. Use `handleOidcLogin`.
51
53
  */
52
54
  export declare function buildAuthorizationUrl(config: OAuthConfig, pkce: PKCEParams): string;
53
55
  /**
54
56
  * Exchange authorization code for tokens
57
+ * @deprecated Hardcodes `${issuer}/token` and doesn't validate the response. Use `handleOidcCallback`.
55
58
  */
56
59
  export declare function exchangeCodeForTokens(config: OAuthConfig, code: string, codeVerifier: string): Promise<{
57
60
  access_token: string;
@@ -1 +1 @@
1
- {"version":3,"file":"oauth-browser.d.ts","sourceRoot":"","sources":["../src/oauth-browser.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAEH,4EAA4E;AAC5E,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,MAAM,CAAC;CACf;AAED,qEAAqE;AACrE,MAAM,WAAW,UAAU;IACzB,aAAa,EAAE,MAAM,CAAC;IACtB,cAAc,EAAE,MAAM,CAAC;IACvB,KAAK,EAAE,MAAM,CAAC;CACf;AA+BD;;GAEG;AACH,wBAAsB,YAAY,IAAI,OAAO,CAAC,UAAU,CAAC,CAUxD;AAED;;GAEG;AACH,wBAAgB,qBAAqB,CACnC,MAAM,EAAE,WAAW,EACnB,IAAI,EAAE,UAAU,GACf,MAAM,CAYR;AAED;;GAEG;AACH,wBAAsB,qBAAqB,CACzC,MAAM,EAAE,WAAW,EACnB,IAAI,EAAE,MAAM,EACZ,YAAY,EAAE,MAAM,GACnB,OAAO,CAAC;IACT,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,UAAU,EAAE,MAAM,CAAC;CACpB,CAAC,CAqBD"}
1
+ {"version":3,"file":"oauth-browser.d.ts","sourceRoot":"","sources":["../src/oauth-browser.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH;;;GAGG;AACH,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,MAAM,CAAC;CACf;AAED;;;GAGG;AACH,MAAM,WAAW,UAAU;IACzB,aAAa,EAAE,MAAM,CAAC;IACtB,cAAc,EAAE,MAAM,CAAC;IACvB,KAAK,EAAE,MAAM,CAAC;CACf;AA+BD;;;GAGG;AACH,wBAAsB,YAAY,IAAI,OAAO,CAAC,UAAU,CAAC,CAUxD;AAED;;;GAGG;AACH,wBAAgB,qBAAqB,CACnC,MAAM,EAAE,WAAW,EACnB,IAAI,EAAE,UAAU,GACf,MAAM,CAYR;AAED;;;GAGG;AACH,wBAAsB,qBAAqB,CACzC,MAAM,EAAE,WAAW,EACnB,IAAI,EAAE,MAAM,EACZ,YAAY,EAAE,MAAM,GACnB,OAAO,CAAC;IACT,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,UAAU,EAAE,MAAM,CAAC;CACpB,CAAC,CAqBD"}