@udibo/oauth2 0.16.0 → 0.17.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/dist/adapters/hono/bff/auth-request-store.d.ts +57 -11
- package/dist/adapters/hono/bff/auth-request-store.js +57 -8
- package/dist/adapters/hono/bff/bff.d.ts +56 -18
- package/dist/adapters/hono/bff/bff.js +41 -6
- package/dist/adapters/hono/bff/mod.d.ts +7 -2
- package/dist/adapters/hono/bff/mod.js +7 -2
- package/package.json +1 -1
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* {@link AuthRequestStorageFactory} implementations for {@link HonoBff}.
|
|
3
3
|
*
|
|
4
4
|
* The pending authorization request — the `state`, its PKCE code verifier, and
|
|
5
5
|
* where to return afterwards — has to survive the round trip to the identity
|
|
6
|
-
* provider.
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* with `unknown state parameter`.
|
|
6
|
+
* provider. {@link MemoryAuthRequestStorageFactory} keeps it in the process
|
|
7
|
+
* that started the flow, which breaks on any platform that spreads requests
|
|
8
|
+
* across isolates or instances: `/auth/login` stores the record on one and
|
|
9
|
+
* `/auth/callback` looks for it on another, so sign-in fails intermittently
|
|
10
|
+
* with `unknown state parameter`. `HonoBff` therefore has no default; the
|
|
11
|
+
* caller chooses.
|
|
11
12
|
*
|
|
12
13
|
* {@link EncryptedCookieAuthRequestStorage} carries the record in an encrypted
|
|
13
14
|
* cookie instead (AES-GCM under a server-held secret, the same construction
|
|
@@ -18,7 +19,7 @@
|
|
|
18
19
|
* **Which storage you pick is a reliability decision, not a security one.**
|
|
19
20
|
* Server-side storage on its own says a `state` is pending somewhere — never
|
|
20
21
|
* that *this* browser is the one that started it, so a process-wide store like
|
|
21
|
-
*
|
|
22
|
+
* {@link MemoryAuthRequestStorageFactory} would otherwise let anyone's browser complete anyone
|
|
22
23
|
* else's pending sign-in (login CSRF). {@link HonoBff} closes that separately
|
|
23
24
|
* and unconditionally: `/auth/login` also binds the `state` to the browser in
|
|
24
25
|
* a short-lived cookie that `/auth/callback` requires, whichever storage is
|
|
@@ -27,7 +28,7 @@
|
|
|
27
28
|
* @module
|
|
28
29
|
*/
|
|
29
30
|
import type { Context } from "hono";
|
|
30
|
-
import type
|
|
31
|
+
import { type AuthRequestStorage, type MemoryAuthRequestStorageOptions } from "../../../client/mod.ts";
|
|
31
32
|
/**
|
|
32
33
|
* Cookie attributes for the pending-authorization cookie.
|
|
33
34
|
*
|
|
@@ -110,8 +111,10 @@ export interface EncryptedCookieAuthRequestStorageOptions {
|
|
|
110
111
|
* {@link HonoBffOptions.authRequestStorage} takes one of these rather than a
|
|
111
112
|
* bare storage because a cookie-backed store can only read and write through
|
|
112
113
|
* the request's {@link Context}. Pass an
|
|
113
|
-
* {@link EncryptedCookieAuthRequestStorage},
|
|
114
|
-
*
|
|
114
|
+
* {@link EncryptedCookieAuthRequestStorage}, a
|
|
115
|
+
* {@link MemoryAuthRequestStorageFactory} for a single process, or implement
|
|
116
|
+
* `forRequest` over storage every instance shares. A storage that needs no
|
|
117
|
+
* request context is wrapped as `{ forRequest: () => storage }`.
|
|
115
118
|
*/
|
|
116
119
|
export interface AuthRequestStorageFactory {
|
|
117
120
|
/** Returns the storage bound to `c`, for the duration of that request. */
|
|
@@ -123,7 +126,7 @@ export interface AuthRequestStorageFactory {
|
|
|
123
126
|
* `/auth/callback` need not be served by the same instance.
|
|
124
127
|
*
|
|
125
128
|
* Reach for it whenever the BFF runs on more than one isolate or instance —
|
|
126
|
-
*
|
|
129
|
+
* {@link MemoryAuthRequestStorageFactory} fails those deployments with
|
|
127
130
|
* `unknown state parameter`. The AES key is derived on first use and shared by
|
|
128
131
|
* every later request.
|
|
129
132
|
*
|
|
@@ -173,3 +176,46 @@ export declare class EncryptedCookieAuthRequestStorage implements AuthRequestSto
|
|
|
173
176
|
* ```
|
|
174
177
|
*/
|
|
175
178
|
export declare function encryptedCookieAuthRequestStorage(options: EncryptedCookieAuthRequestStorageOptions): EncryptedCookieAuthRequestStorage;
|
|
179
|
+
/**
|
|
180
|
+
* In-process {@link AuthRequestStorageFactory}: every request shares one
|
|
181
|
+
* {@link MemoryAuthRequestStorage}, so a sign-in completes only on the process
|
|
182
|
+
* that started it.
|
|
183
|
+
*
|
|
184
|
+
* Pass it explicitly for a single long-lived process, tests and local
|
|
185
|
+
* development. A deployment that can serve `/auth/login` and `/auth/callback`
|
|
186
|
+
* from different instances or isolates fails sign-in with it, intermittently,
|
|
187
|
+
* with an unknown `state`; use {@link EncryptedCookieAuthRequestStorage} or a
|
|
188
|
+
* factory over shared storage there. The options are
|
|
189
|
+
* {@link MemoryAuthRequestStorage}'s: records older than `ttlMs` (10 minutes
|
|
190
|
+
* by default) are pruned and at most `maxEntries` (1000 by default) are held.
|
|
191
|
+
*
|
|
192
|
+
* @example
|
|
193
|
+
* ```ts
|
|
194
|
+
* import {
|
|
195
|
+
* HonoBff,
|
|
196
|
+
* MemoryAuthRequestStorageFactory,
|
|
197
|
+
* MemorySessionStore,
|
|
198
|
+
* } from "@udibo/oauth2/hono/bff";
|
|
199
|
+
* import { DirectClient } from "@udibo/oauth2/client";
|
|
200
|
+
*
|
|
201
|
+
* const bff = new HonoBff({
|
|
202
|
+
* client: new DirectClient({
|
|
203
|
+
* clientId: "local-app",
|
|
204
|
+
* clientSecret: "dev-secret",
|
|
205
|
+
* issuer: "http://localhost:8000",
|
|
206
|
+
* }),
|
|
207
|
+
* sessionStore: new MemorySessionStore(),
|
|
208
|
+
* authRequestStorage: new MemoryAuthRequestStorageFactory(),
|
|
209
|
+
* });
|
|
210
|
+
* ```
|
|
211
|
+
*/
|
|
212
|
+
export declare class MemoryAuthRequestStorageFactory implements AuthRequestStorageFactory {
|
|
213
|
+
#private;
|
|
214
|
+
/**
|
|
215
|
+
* @throws {RangeError} When `maxEntries` is neither a positive integer nor
|
|
216
|
+
* `Infinity`.
|
|
217
|
+
*/
|
|
218
|
+
constructor(options?: MemoryAuthRequestStorageOptions);
|
|
219
|
+
/** Returns the process-wide storage; `c` is not consulted. */
|
|
220
|
+
forRequest(_c: Context): AuthRequestStorage;
|
|
221
|
+
}
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* {@link AuthRequestStorageFactory} implementations for {@link HonoBff}.
|
|
3
3
|
*
|
|
4
4
|
* The pending authorization request — the `state`, its PKCE code verifier, and
|
|
5
5
|
* where to return afterwards — has to survive the round trip to the identity
|
|
6
|
-
* provider.
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* with `unknown state parameter`.
|
|
6
|
+
* provider. {@link MemoryAuthRequestStorageFactory} keeps it in the process
|
|
7
|
+
* that started the flow, which breaks on any platform that spreads requests
|
|
8
|
+
* across isolates or instances: `/auth/login` stores the record on one and
|
|
9
|
+
* `/auth/callback` looks for it on another, so sign-in fails intermittently
|
|
10
|
+
* with `unknown state parameter`. `HonoBff` therefore has no default; the
|
|
11
|
+
* caller chooses.
|
|
11
12
|
*
|
|
12
13
|
* {@link EncryptedCookieAuthRequestStorage} carries the record in an encrypted
|
|
13
14
|
* cookie instead (AES-GCM under a server-held secret, the same construction
|
|
@@ -18,7 +19,7 @@
|
|
|
18
19
|
* **Which storage you pick is a reliability decision, not a security one.**
|
|
19
20
|
* Server-side storage on its own says a `state` is pending somewhere — never
|
|
20
21
|
* that *this* browser is the one that started it, so a process-wide store like
|
|
21
|
-
*
|
|
22
|
+
* {@link MemoryAuthRequestStorageFactory} would otherwise let anyone's browser complete anyone
|
|
22
23
|
* else's pending sign-in (login CSRF). {@link HonoBff} closes that separately
|
|
23
24
|
* and unconditionally: `/auth/login` also binds the `state` to the browser in
|
|
24
25
|
* a short-lived cookie that `/auth/callback` requires, whichever storage is
|
|
@@ -27,6 +28,7 @@
|
|
|
27
28
|
* @module
|
|
28
29
|
*/
|
|
29
30
|
import { deleteCookie, getCookie, setCookie } from "hono/cookie";
|
|
31
|
+
import { MemoryAuthRequestStorage, } from "../../../client/mod.js";
|
|
30
32
|
import { deriveAesKey, sealJson, unsealJson } from "../../../utils/crypto.js";
|
|
31
33
|
import { resolveCookieName, resolvePrefixConstrainedAttributes, } from "./cookie-prefix.js";
|
|
32
34
|
const BASE_NAME = "oauth2_auth_request";
|
|
@@ -51,7 +53,7 @@ function cookieAttributes(options, maxAgeSeconds) {
|
|
|
51
53
|
* `/auth/callback` need not be served by the same instance.
|
|
52
54
|
*
|
|
53
55
|
* Reach for it whenever the BFF runs on more than one isolate or instance —
|
|
54
|
-
*
|
|
56
|
+
* {@link MemoryAuthRequestStorageFactory} fails those deployments with
|
|
55
57
|
* `unknown state parameter`. The AES key is derived on first use and shared by
|
|
56
58
|
* every later request.
|
|
57
59
|
*
|
|
@@ -173,3 +175,50 @@ export class EncryptedCookieAuthRequestStorage {
|
|
|
173
175
|
export function encryptedCookieAuthRequestStorage(options) {
|
|
174
176
|
return new EncryptedCookieAuthRequestStorage(options);
|
|
175
177
|
}
|
|
178
|
+
/**
|
|
179
|
+
* In-process {@link AuthRequestStorageFactory}: every request shares one
|
|
180
|
+
* {@link MemoryAuthRequestStorage}, so a sign-in completes only on the process
|
|
181
|
+
* that started it.
|
|
182
|
+
*
|
|
183
|
+
* Pass it explicitly for a single long-lived process, tests and local
|
|
184
|
+
* development. A deployment that can serve `/auth/login` and `/auth/callback`
|
|
185
|
+
* from different instances or isolates fails sign-in with it, intermittently,
|
|
186
|
+
* with an unknown `state`; use {@link EncryptedCookieAuthRequestStorage} or a
|
|
187
|
+
* factory over shared storage there. The options are
|
|
188
|
+
* {@link MemoryAuthRequestStorage}'s: records older than `ttlMs` (10 minutes
|
|
189
|
+
* by default) are pruned and at most `maxEntries` (1000 by default) are held.
|
|
190
|
+
*
|
|
191
|
+
* @example
|
|
192
|
+
* ```ts
|
|
193
|
+
* import {
|
|
194
|
+
* HonoBff,
|
|
195
|
+
* MemoryAuthRequestStorageFactory,
|
|
196
|
+
* MemorySessionStore,
|
|
197
|
+
* } from "@udibo/oauth2/hono/bff";
|
|
198
|
+
* import { DirectClient } from "@udibo/oauth2/client";
|
|
199
|
+
*
|
|
200
|
+
* const bff = new HonoBff({
|
|
201
|
+
* client: new DirectClient({
|
|
202
|
+
* clientId: "local-app",
|
|
203
|
+
* clientSecret: "dev-secret",
|
|
204
|
+
* issuer: "http://localhost:8000",
|
|
205
|
+
* }),
|
|
206
|
+
* sessionStore: new MemorySessionStore(),
|
|
207
|
+
* authRequestStorage: new MemoryAuthRequestStorageFactory(),
|
|
208
|
+
* });
|
|
209
|
+
* ```
|
|
210
|
+
*/
|
|
211
|
+
export class MemoryAuthRequestStorageFactory {
|
|
212
|
+
#storage;
|
|
213
|
+
/**
|
|
214
|
+
* @throws {RangeError} When `maxEntries` is neither a positive integer nor
|
|
215
|
+
* `Infinity`.
|
|
216
|
+
*/
|
|
217
|
+
constructor(options = {}) {
|
|
218
|
+
this.#storage = new MemoryAuthRequestStorage(options);
|
|
219
|
+
}
|
|
220
|
+
/** Returns the process-wide storage; `c` is not consulted. */
|
|
221
|
+
forRequest(_c) {
|
|
222
|
+
return this.#storage;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
@@ -311,10 +311,23 @@ export interface HonoBffOptions {
|
|
|
311
311
|
*/
|
|
312
312
|
client: DirectClient;
|
|
313
313
|
/**
|
|
314
|
-
* Where session data lives.
|
|
315
|
-
*
|
|
316
|
-
|
|
317
|
-
|
|
314
|
+
* Where session data lives. Required, because the right store depends on
|
|
315
|
+
* how many instances serve the app:
|
|
316
|
+
*
|
|
317
|
+
* - {@link EncryptedCookieSessionStore} keeps the session in an encrypted
|
|
318
|
+
* cookie, so every instance that shares its secret can read it and no
|
|
319
|
+
* server-side store is needed. It cannot list sessions, so it does not
|
|
320
|
+
* support {@link HonoBffOptions.backchannelLogout}, and sign-out cannot
|
|
321
|
+
* invalidate a copied cookie: it stays valid until its `maxAgeMs`. Use a
|
|
322
|
+
* shared {@link SessionStore} when you need revocation.
|
|
323
|
+
* - A {@link SessionStore} over a database or cache every instance shares,
|
|
324
|
+
* for server-side revocation and back-channel logout. Check it with
|
|
325
|
+
* `runSessionStoreContractTests`.
|
|
326
|
+
* - {@link MemorySessionStore} for a single process, tests and local
|
|
327
|
+
* development. A session it holds is unknown to every other instance, so
|
|
328
|
+
* behind a load balancer the user appears signed out.
|
|
329
|
+
*/
|
|
330
|
+
sessionStore: SessionStore;
|
|
318
331
|
/**
|
|
319
332
|
* How long a BFF session is meant to live, in milliseconds. Defaults to 14
|
|
320
333
|
* days ({@link DEFAULT_SESSION_MAX_AGE_MS}). Milliseconds match the store's
|
|
@@ -346,19 +359,25 @@ export interface HonoBffOptions {
|
|
|
346
359
|
sessionMaxAgeMs?: number;
|
|
347
360
|
/**
|
|
348
361
|
* Where the pending authorization request (`state` → PKCE verifier,
|
|
349
|
-
* return-to) lives, resolved per request.
|
|
350
|
-
*
|
|
351
|
-
*
|
|
352
|
-
*
|
|
353
|
-
*
|
|
354
|
-
*
|
|
355
|
-
*
|
|
362
|
+
* return-to) lives, resolved per request. Required, because `/auth/login`
|
|
363
|
+
* and `/auth/callback` can be served by different instances or isolates.
|
|
364
|
+
* The BFF always uses this, never the client's own `authRequestStorage`.
|
|
365
|
+
*
|
|
366
|
+
* - {@link EncryptedCookieAuthRequestStorage} carries the record in an
|
|
367
|
+
* encrypted cookie, so any instance sharing its secret completes the
|
|
368
|
+
* sign-in with no server-side store.
|
|
369
|
+
* - An {@link AuthRequestStorageFactory} over storage every instance shares;
|
|
370
|
+
* wrap a context-free `AuthRequestStorage` as
|
|
371
|
+
* `{ forRequest: () => storage }`.
|
|
372
|
+
* - {@link MemoryAuthRequestStorageFactory} for a single process, tests and
|
|
373
|
+
* local development. A callback that reaches another instance fails with
|
|
374
|
+
* an unknown `state`.
|
|
356
375
|
*
|
|
357
376
|
* Whatever this resolves to, the pending request is **also** bound to the
|
|
358
377
|
* browser that started it by the login-state cookie, which no option turns
|
|
359
378
|
* off. See {@link loginStateTtlMs}.
|
|
360
379
|
*/
|
|
361
|
-
authRequestStorage
|
|
380
|
+
authRequestStorage: AuthRequestStorageFactory;
|
|
362
381
|
/**
|
|
363
382
|
* How long, in ms, the browser keeps the login-state cookie `/auth/login`
|
|
364
383
|
* sets and `/auth/callback` requires — the `Max-Age` on that cookie, and so
|
|
@@ -492,8 +511,16 @@ export interface HonoBffOptions {
|
|
|
492
511
|
* @example Pin every sign-in to one organization
|
|
493
512
|
* ```ts
|
|
494
513
|
* declare const client: DirectClient;
|
|
495
|
-
*
|
|
496
|
-
*
|
|
514
|
+
* declare const stores: Pick<
|
|
515
|
+
* HonoBffOptions,
|
|
516
|
+
* "sessionStore" | "authRequestStorage"
|
|
517
|
+
* >;
|
|
518
|
+
*
|
|
519
|
+
* const bff = new HonoBff({
|
|
520
|
+
* client,
|
|
521
|
+
* ...stores,
|
|
522
|
+
* extraParams: { organization: "acme" },
|
|
523
|
+
* });
|
|
497
524
|
* ```
|
|
498
525
|
*/
|
|
499
526
|
extraParams?: Record<string, string>;
|
|
@@ -559,8 +586,16 @@ export interface HonoBffOptions {
|
|
|
559
586
|
* @example Let a sign-in link choose the organization
|
|
560
587
|
* ```ts
|
|
561
588
|
* declare const client: DirectClient;
|
|
562
|
-
*
|
|
563
|
-
*
|
|
589
|
+
* declare const stores: Pick<
|
|
590
|
+
* HonoBffOptions,
|
|
591
|
+
* "sessionStore" | "authRequestStorage"
|
|
592
|
+
* >;
|
|
593
|
+
*
|
|
594
|
+
* const bff = new HonoBff({
|
|
595
|
+
* client,
|
|
596
|
+
* ...stores,
|
|
597
|
+
* forwardedParams: ["organization"],
|
|
598
|
+
* });
|
|
564
599
|
* ```
|
|
565
600
|
*
|
|
566
601
|
* A link to `/auth/login?organization=acme&return_to=/dashboard` then
|
|
@@ -641,9 +676,12 @@ export interface HonoBffOptions {
|
|
|
641
676
|
export declare class HonoBff {
|
|
642
677
|
#private;
|
|
643
678
|
/**
|
|
644
|
-
* Defaults are applied for
|
|
645
|
-
* session
|
|
679
|
+
* Defaults are applied for session mode (`"own"`), cookie name, and CSRF.
|
|
680
|
+
* The session store and auth-request storage have no default.
|
|
646
681
|
*
|
|
682
|
+
* @throws {TypeError} When {@linkcode HonoBffOptions.sessionStore} or
|
|
683
|
+
* {@linkcode HonoBffOptions.authRequestStorage} is missing, or when
|
|
684
|
+
* `authRequestStorage` has no `forRequest` method.
|
|
647
685
|
* @throws {Error} When {@linkcode HonoBffOptions.backchannelLogout} is
|
|
648
686
|
* configured against a store that cannot enumerate sessions (e.g.
|
|
649
687
|
* {@linkcode EncryptedCookieSessionStore}), when an explicit
|
|
@@ -58,7 +58,7 @@ import { sha256Hash } from "../../../server/utils/hash.js";
|
|
|
58
58
|
import { OAUTH2_CONTEXT_KEY } from "../_common.js";
|
|
59
59
|
import { DEFAULT_MAX_RETURN_TO_LENGTH, loginContinuation as computeLoginContinuation, safeReturnTo, } from "../../../utils/url.js";
|
|
60
60
|
import { resolveCookieName, resolvePrefixConstrainedAttributes, } from "./cookie-prefix.js";
|
|
61
|
-
import { DEFAULT_SESSION_MAX_AGE_MS,
|
|
61
|
+
import { DEFAULT_SESSION_MAX_AGE_MS, sessionStoreMaxAgeMs, supportsBackchannelLogout, } from "./session-store.js";
|
|
62
62
|
import { buildDownstreamHeaders, buildUpstreamHeaders, isInvalidTokenChallenge, resolveForwardHeaders, resolveProxyUrl, } from "./proxy.js";
|
|
63
63
|
function isRevokedGrant(error) {
|
|
64
64
|
if (!isOAuth2Error(error))
|
|
@@ -268,6 +268,30 @@ function loginStateCookieOpts(opts, maxAgeSeconds) {
|
|
|
268
268
|
...(maxAgeSeconds === undefined ? {} : { maxAge: maxAgeSeconds }),
|
|
269
269
|
};
|
|
270
270
|
}
|
|
271
|
+
const MISSING_SESSION_STORE = "HonoBff requires options.sessionStore. Pass EncryptedCookieSessionStore " +
|
|
272
|
+
"to keep sessions in an encrypted cookie any instance can read (sign-out " +
|
|
273
|
+
"cannot revoke a copied cookie), a " +
|
|
274
|
+
"SessionStore over a database or cache every instance shares, or " +
|
|
275
|
+
"MemorySessionStore for a single process, tests and local development.";
|
|
276
|
+
const MISSING_AUTH_REQUEST_STORAGE = "HonoBff requires options.authRequestStorage. Pass " +
|
|
277
|
+
"EncryptedCookieAuthRequestStorage to keep pending sign-ins in an " +
|
|
278
|
+
"encrypted cookie any instance can read, an AuthRequestStorageFactory " +
|
|
279
|
+
"over storage every instance shares, or MemoryAuthRequestStorageFactory " +
|
|
280
|
+
"for a single process, tests and local development.";
|
|
281
|
+
function assertStoresConfigured(options) {
|
|
282
|
+
const missing = [
|
|
283
|
+
...(options.sessionStore ? [] : [MISSING_SESSION_STORE]),
|
|
284
|
+
...(options.authRequestStorage ? [] : [MISSING_AUTH_REQUEST_STORAGE]),
|
|
285
|
+
];
|
|
286
|
+
if (missing.length > 0)
|
|
287
|
+
throw new TypeError(missing.join(" "));
|
|
288
|
+
if (typeof options.authRequestStorage?.forRequest !== "function") {
|
|
289
|
+
throw new TypeError("HonoBff options.authRequestStorage must be an " +
|
|
290
|
+
"AuthRequestStorageFactory, an object whose forRequest(c) returns the " +
|
|
291
|
+
"storage for that request. Wrap an AuthRequestStorage that needs no " +
|
|
292
|
+
"request context as { forRequest: () => storage }.");
|
|
293
|
+
}
|
|
294
|
+
}
|
|
271
295
|
/**
|
|
272
296
|
* Backend-for-Frontend adapter for Hono. Construct once per app, mount
|
|
273
297
|
* `routes()` under `/auth`, and use `attachToken()` as middleware on
|
|
@@ -292,9 +316,12 @@ export class HonoBff {
|
|
|
292
316
|
#maxReturnToLength;
|
|
293
317
|
#refreshes = new Map();
|
|
294
318
|
/**
|
|
295
|
-
* Defaults are applied for
|
|
296
|
-
* session
|
|
319
|
+
* Defaults are applied for session mode (`"own"`), cookie name, and CSRF.
|
|
320
|
+
* The session store and auth-request storage have no default.
|
|
297
321
|
*
|
|
322
|
+
* @throws {TypeError} When {@linkcode HonoBffOptions.sessionStore} or
|
|
323
|
+
* {@linkcode HonoBffOptions.authRequestStorage} is missing, or when
|
|
324
|
+
* `authRequestStorage` has no `forRequest` method.
|
|
298
325
|
* @throws {Error} When {@linkcode HonoBffOptions.backchannelLogout} is
|
|
299
326
|
* configured against a store that cannot enumerate sessions (e.g.
|
|
300
327
|
* {@linkcode EncryptedCookieSessionStore}), when an explicit
|
|
@@ -320,9 +347,10 @@ export class HonoBff {
|
|
|
320
347
|
* {@linkcode HonoBffOptions.scope} is also set.
|
|
321
348
|
*/
|
|
322
349
|
constructor(options) {
|
|
350
|
+
assertStoresConfigured(options);
|
|
323
351
|
this.#options = options;
|
|
324
352
|
this.#client = options.client;
|
|
325
|
-
this.#store = options.sessionStore
|
|
353
|
+
this.#store = options.sessionStore;
|
|
326
354
|
this.#paths = resolvePaths(options.paths, {
|
|
327
355
|
backchannelMounted: options.backchannelLogout !== undefined,
|
|
328
356
|
});
|
|
@@ -447,6 +475,13 @@ export class HonoBff {
|
|
|
447
475
|
deleteCookie(c, this.#cookieName, this.#cookieOptions());
|
|
448
476
|
return null;
|
|
449
477
|
}
|
|
478
|
+
#authRequestStorage(c) {
|
|
479
|
+
const storage = this.#options.authRequestStorage.forRequest(c);
|
|
480
|
+
if (!storage) {
|
|
481
|
+
throw new TypeError("HonoBff options.authRequestStorage.forRequest(c) returned no storage.");
|
|
482
|
+
}
|
|
483
|
+
return storage;
|
|
484
|
+
}
|
|
450
485
|
#noStore(c) {
|
|
451
486
|
c.header("Cache-Control", "no-store");
|
|
452
487
|
c.header("Vary", "Cookie");
|
|
@@ -618,7 +653,7 @@ export class HonoBff {
|
|
|
618
653
|
prompt: requested.prompt,
|
|
619
654
|
extraParams: requested.extraParams,
|
|
620
655
|
redirectUri: this.#derivedRedirectUri(c, origin),
|
|
621
|
-
authRequestStorage: this.#
|
|
656
|
+
authRequestStorage: this.#authRequestStorage(c),
|
|
622
657
|
});
|
|
623
658
|
this.#writeLoginStateHashes(c, [
|
|
624
659
|
...this.#loginStateHashes(c),
|
|
@@ -669,7 +704,7 @@ export class HonoBff {
|
|
|
669
704
|
const { tokens, refreshToken, returnTo, raw } = await this.#client.exchangeAuthorizationCode(code, state, {
|
|
670
705
|
origin,
|
|
671
706
|
redirectUri: this.#derivedRedirectUri(c, origin),
|
|
672
|
-
authRequestStorage: this.#
|
|
707
|
+
authRequestStorage: this.#authRequestStorage(c),
|
|
673
708
|
});
|
|
674
709
|
let user = null;
|
|
675
710
|
let sid;
|
|
@@ -4,7 +4,12 @@
|
|
|
4
4
|
* Exposes a {@link HonoBff} class that wraps an
|
|
5
5
|
* `DirectClient` constructed with a client secret, plus the
|
|
6
6
|
* {@link SessionStore} contract and two ready-to-use implementations
|
|
7
|
-
* ({@link MemorySessionStore}, {@link EncryptedCookieSessionStore})
|
|
7
|
+
* ({@link MemorySessionStore}, {@link EncryptedCookieSessionStore}), and the
|
|
8
|
+
* {@link AuthRequestStorageFactory} contract with two of its own
|
|
9
|
+
* ({@link MemoryAuthRequestStorageFactory},
|
|
10
|
+
* {@link EncryptedCookieAuthRequestStorage}). `HonoBff` requires one of each:
|
|
11
|
+
* the encrypted-cookie pair works across instances with no shared server, and
|
|
12
|
+
* the memory pair serves a single process, tests and local development.
|
|
8
13
|
*
|
|
9
14
|
* Need a server-side store (Postgres, Redis, …)? Implement the
|
|
10
15
|
* {@link SessionStore} interface and verify it with
|
|
@@ -13,6 +18,6 @@
|
|
|
13
18
|
* @module
|
|
14
19
|
*/
|
|
15
20
|
export { type BackchannelLogoutSubject, type BffCallbackError, HonoBff, type HonoBffBackchannelOptions, type HonoBffCookieOptions, type HonoBffCsrfOptions, type HonoBffOptions, type HonoBffPaths, type HonoBffResourceServer, } from "./bff.ts";
|
|
16
|
-
export { type AuthRequestCookieOptions, type AuthRequestStorageFactory, EncryptedCookieAuthRequestStorage, encryptedCookieAuthRequestStorage, type EncryptedCookieAuthRequestStorageOptions, } from "./auth-request-store.ts";
|
|
21
|
+
export { type AuthRequestCookieOptions, type AuthRequestStorageFactory, EncryptedCookieAuthRequestStorage, encryptedCookieAuthRequestStorage, type EncryptedCookieAuthRequestStorageOptions, MemoryAuthRequestStorageFactory, } from "./auth-request-store.ts";
|
|
17
22
|
export { DEFAULT_PROXY_FORWARD_HEADERS, type HonoBffProxyOptions, } from "./proxy.ts";
|
|
18
23
|
export { type BackchannelLogoutStore, type BoundedSessionStore, DEFAULT_SESSION_MAX_AGE_MS, EncryptedCookieSessionStore, type EncryptedCookieSessionStoreOptions, MemorySessionStore, type SessionData, type SessionStore, sessionStoreMaxAgeMs, supportsBackchannelLogout, } from "./session-store.ts";
|
|
@@ -4,7 +4,12 @@
|
|
|
4
4
|
* Exposes a {@link HonoBff} class that wraps an
|
|
5
5
|
* `DirectClient` constructed with a client secret, plus the
|
|
6
6
|
* {@link SessionStore} contract and two ready-to-use implementations
|
|
7
|
-
* ({@link MemorySessionStore}, {@link EncryptedCookieSessionStore})
|
|
7
|
+
* ({@link MemorySessionStore}, {@link EncryptedCookieSessionStore}), and the
|
|
8
|
+
* {@link AuthRequestStorageFactory} contract with two of its own
|
|
9
|
+
* ({@link MemoryAuthRequestStorageFactory},
|
|
10
|
+
* {@link EncryptedCookieAuthRequestStorage}). `HonoBff` requires one of each:
|
|
11
|
+
* the encrypted-cookie pair works across instances with no shared server, and
|
|
12
|
+
* the memory pair serves a single process, tests and local development.
|
|
8
13
|
*
|
|
9
14
|
* Need a server-side store (Postgres, Redis, …)? Implement the
|
|
10
15
|
* {@link SessionStore} interface and verify it with
|
|
@@ -13,6 +18,6 @@
|
|
|
13
18
|
* @module
|
|
14
19
|
*/
|
|
15
20
|
export { HonoBff, } from "./bff.js";
|
|
16
|
-
export { EncryptedCookieAuthRequestStorage, encryptedCookieAuthRequestStorage, } from "./auth-request-store.js";
|
|
21
|
+
export { EncryptedCookieAuthRequestStorage, encryptedCookieAuthRequestStorage, MemoryAuthRequestStorageFactory, } from "./auth-request-store.js";
|
|
17
22
|
export { DEFAULT_PROXY_FORWARD_HEADERS, } from "./proxy.js";
|
|
18
23
|
export { DEFAULT_SESSION_MAX_AGE_MS, EncryptedCookieSessionStore, MemorySessionStore, sessionStoreMaxAgeMs, supportsBackchannelLogout, } from "./session-store.js";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@udibo/oauth2",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.0",
|
|
4
4
|
"description": "A flexible OAuth2 toolkit: framework-agnostic authorization & resource servers, a typed client, Hono adapters (including a backend-for-frontend), and React bindings.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"authorization-server",
|