@udibo/oauth2 0.1.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/LICENSE +20 -0
- package/README.md +174 -0
- package/SECURITY.md +77 -0
- package/esm/_dnt.polyfills.d.ts +17 -0
- package/esm/_dnt.polyfills.js +15 -0
- package/esm/adapters/hono/_common.d.ts +66 -0
- package/esm/adapters/hono/_common.js +108 -0
- package/esm/adapters/hono/authorization-server.d.ts +211 -0
- package/esm/adapters/hono/authorization-server.js +214 -0
- package/esm/adapters/hono/bff/auth-request-store.d.ts +164 -0
- package/esm/adapters/hono/bff/auth-request-store.js +154 -0
- package/esm/adapters/hono/bff/bff.d.ts +852 -0
- package/esm/adapters/hono/bff/bff.js +1194 -0
- package/esm/adapters/hono/bff/cookie-prefix.d.ts +72 -0
- package/esm/adapters/hono/bff/cookie-prefix.js +85 -0
- package/esm/adapters/hono/bff/mod.d.ts +19 -0
- package/esm/adapters/hono/bff/mod.js +19 -0
- package/esm/adapters/hono/bff/proxy.d.ts +108 -0
- package/esm/adapters/hono/bff/proxy.js +314 -0
- package/esm/adapters/hono/bff/session-store.d.ts +250 -0
- package/esm/adapters/hono/bff/session-store.js +216 -0
- package/esm/adapters/hono/bff/testing.d.ts +165 -0
- package/esm/adapters/hono/bff/testing.js +254 -0
- package/esm/adapters/hono/identity.d.ts +117 -0
- package/esm/adapters/hono/identity.js +215 -0
- package/esm/adapters/hono/log.d.ts +49 -0
- package/esm/adapters/hono/log.js +66 -0
- package/esm/adapters/hono/resource-server.d.ts +101 -0
- package/esm/adapters/hono/resource-server.js +108 -0
- package/esm/client/_http.d.ts +91 -0
- package/esm/client/_http.js +303 -0
- package/esm/client/base.d.ts +184 -0
- package/esm/client/base.js +93 -0
- package/esm/client/bff-client.d.ts +235 -0
- package/esm/client/bff-client.js +280 -0
- package/esm/client/browser-storage.d.ts +152 -0
- package/esm/client/browser-storage.js +246 -0
- package/esm/client/check.d.ts +114 -0
- package/esm/client/check.js +97 -0
- package/esm/client/direct-client.d.ts +707 -0
- package/esm/client/direct-client.js +1237 -0
- package/esm/client/discovery-cache.d.ts +138 -0
- package/esm/client/discovery-cache.js +124 -0
- package/esm/client/events.d.ts +54 -0
- package/esm/client/events.js +37 -0
- package/esm/client/mod.d.ts +35 -0
- package/esm/client/mod.js +33 -0
- package/esm/client/storage.d.ts +127 -0
- package/esm/client/storage.js +81 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/almost_equals.d.ts +29 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/almost_equals.js +47 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/array_includes.d.ts +28 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/array_includes.js +65 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/assert.d.ts +16 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/assert.js +23 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/assertion_error.d.ts +25 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/assertion_error.js +30 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/equal.d.ts +24 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/equal.js +193 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/equals.d.ts +36 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/equals.js +61 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/exists.d.ts +17 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/exists.js +27 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/fail.d.ts +14 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/fail.js +20 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/false.d.ts +18 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/false.js +23 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/greater.d.ts +19 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/greater.js +29 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/greater_or_equal.d.ts +19 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/greater_or_equal.js +29 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/instance_of.d.ts +22 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/instance_of.js +52 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/is_error.d.ts +24 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/is_error.js +53 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/less.d.ts +18 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/less.js +28 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/less_or_equal.d.ts +19 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/less_or_equal.js +29 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/match.d.ts +17 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/match.js +26 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/mod.d.ts +43 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/mod.js +45 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/not_equals.d.ts +26 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/not_equals.js +39 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/not_instance_of.d.ts +19 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/not_instance_of.js +29 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/not_match.d.ts +17 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/not_match.js +26 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/not_strict_equals.d.ts +22 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/not_strict_equals.js +32 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/object_match.d.ts +28 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/object_match.js +177 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/rejects.d.ts +41 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/rejects.js +53 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/strict_equals.d.ts +23 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/strict_equals.js +58 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/string_includes.d.ts +17 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/string_includes.js +26 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/throws.d.ts +44 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/throws.js +44 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/unimplemented.d.ts +14 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/unimplemented.js +20 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/unreachable.d.ts +14 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/unreachable.js +20 -0
- package/esm/deps/jsr.io/@std/async/1.5.0/delay.d.ts +44 -0
- package/esm/deps/jsr.io/@std/async/1.5.0/delay.js +84 -0
- package/esm/deps/jsr.io/@std/crypto/1.1.0/timing_safe_equal.d.ts +39 -0
- package/esm/deps/jsr.io/@std/crypto/1.1.0/timing_safe_equal.js +61 -0
- package/esm/deps/jsr.io/@std/encoding/1.0.11/_common16.d.ts +22 -0
- package/esm/deps/jsr.io/@std/encoding/1.0.11/_common16.js +51 -0
- package/esm/deps/jsr.io/@std/encoding/1.0.11/_common32.d.ts +34 -0
- package/esm/deps/jsr.io/@std/encoding/1.0.11/_common32.js +192 -0
- package/esm/deps/jsr.io/@std/encoding/1.0.11/_common64.d.ts +34 -0
- package/esm/deps/jsr.io/@std/encoding/1.0.11/_common64.js +113 -0
- package/esm/deps/jsr.io/@std/encoding/1.0.11/_common_detach.d.ts +3 -0
- package/esm/deps/jsr.io/@std/encoding/1.0.11/_common_detach.js +13 -0
- package/esm/deps/jsr.io/@std/encoding/1.0.11/_types.d.ts +8 -0
- package/esm/deps/jsr.io/@std/encoding/1.0.11/_types.js +2 -0
- package/esm/deps/jsr.io/@std/encoding/1.0.11/_validate_binary_like.d.ts +1 -0
- package/esm/deps/jsr.io/@std/encoding/1.0.11/_validate_binary_like.js +26 -0
- package/esm/deps/jsr.io/@std/encoding/1.0.11/ascii85.d.ts +60 -0
- package/esm/deps/jsr.io/@std/encoding/1.0.11/ascii85.js +152 -0
- package/esm/deps/jsr.io/@std/encoding/1.0.11/base32.d.ts +39 -0
- package/esm/deps/jsr.io/@std/encoding/1.0.11/base32.js +87 -0
- package/esm/deps/jsr.io/@std/encoding/1.0.11/base58.d.ts +39 -0
- package/esm/deps/jsr.io/@std/encoding/1.0.11/base58.js +131 -0
- package/esm/deps/jsr.io/@std/encoding/1.0.11/base64.d.ts +39 -0
- package/esm/deps/jsr.io/@std/encoding/1.0.11/base64.js +82 -0
- package/esm/deps/jsr.io/@std/encoding/1.0.11/base64url.d.ts +39 -0
- package/esm/deps/jsr.io/@std/encoding/1.0.11/base64url.js +72 -0
- package/esm/deps/jsr.io/@std/encoding/1.0.11/hex.d.ts +38 -0
- package/esm/deps/jsr.io/@std/encoding/1.0.11/hex.js +87 -0
- package/esm/deps/jsr.io/@std/encoding/1.0.11/mod.d.ts +97 -0
- package/esm/deps/jsr.io/@std/encoding/1.0.11/mod.js +99 -0
- package/esm/deps/jsr.io/@std/encoding/1.0.11/varint.d.ts +119 -0
- package/esm/deps/jsr.io/@std/encoding/1.0.11/varint.js +209 -0
- package/esm/deps/jsr.io/@std/http/1.1.3/status.d.ts +343 -0
- package/esm/deps/jsr.io/@std/http/1.1.3/status.js +343 -0
- package/esm/deps/jsr.io/@std/internal/1.0.14/assertion_state.d.ts +159 -0
- package/esm/deps/jsr.io/@std/internal/1.0.14/assertion_state.js +223 -0
- package/esm/deps/jsr.io/@std/internal/1.0.14/build_message.d.ts +79 -0
- package/esm/deps/jsr.io/@std/internal/1.0.14/build_message.js +116 -0
- package/esm/deps/jsr.io/@std/internal/1.0.14/diff.d.ts +139 -0
- package/esm/deps/jsr.io/@std/internal/1.0.14/diff.js +277 -0
- package/esm/deps/jsr.io/@std/internal/1.0.14/diff_str.d.ts +98 -0
- package/esm/deps/jsr.io/@std/internal/1.0.14/diff_str.js +181 -0
- package/esm/deps/jsr.io/@std/internal/1.0.14/format.d.ts +29 -0
- package/esm/deps/jsr.io/@std/internal/1.0.14/format.js +68 -0
- package/esm/deps/jsr.io/@std/internal/1.0.14/styles.d.ts +158 -0
- package/esm/deps/jsr.io/@std/internal/1.0.14/styles.js +206 -0
- package/esm/deps/jsr.io/@std/internal/1.0.14/types.d.ts +24 -0
- package/esm/deps/jsr.io/@std/internal/1.0.14/types.js +3 -0
- package/esm/deps/jsr.io/@std/testing/1.0.20/_test_suite.d.ts +91 -0
- package/esm/deps/jsr.io/@std/testing/1.0.20/_test_suite.js +347 -0
- package/esm/deps/jsr.io/@std/testing/1.0.20/bdd.d.ts +382 -0
- package/esm/deps/jsr.io/@std/testing/1.0.20/bdd.js +1110 -0
- package/esm/deps/jsr.io/@udibo/http-error/0.11.1/mod.d.ts +363 -0
- package/esm/deps/jsr.io/@udibo/http-error/0.11.1/mod.js +616 -0
- package/esm/errors.d.ts +203 -0
- package/esm/errors.js +287 -0
- package/esm/identity/captcha.d.ts +147 -0
- package/esm/identity/captcha.js +64 -0
- package/esm/identity/delivery.d.ts +96 -0
- package/esm/identity/delivery.js +41 -0
- package/esm/identity/errors.d.ts +39 -0
- package/esm/identity/errors.js +46 -0
- package/esm/identity/events.d.ts +242 -0
- package/esm/identity/events.js +39 -0
- package/esm/identity/external/_shared.d.ts +86 -0
- package/esm/identity/external/_shared.js +107 -0
- package/esm/identity/external/_token-exchange.d.ts +61 -0
- package/esm/identity/external/_token-exchange.js +179 -0
- package/esm/identity/external/apple-client-secret.d.ts +82 -0
- package/esm/identity/external/apple-client-secret.js +133 -0
- package/esm/identity/external/apple.d.ts +64 -0
- package/esm/identity/external/apple.js +248 -0
- package/esm/identity/external/discord.d.ts +38 -0
- package/esm/identity/external/discord.js +62 -0
- package/esm/identity/external/errors.d.ts +48 -0
- package/esm/identity/external/errors.js +36 -0
- package/esm/identity/external/flow.d.ts +107 -0
- package/esm/identity/external/flow.js +158 -0
- package/esm/identity/external/github.d.ts +42 -0
- package/esm/identity/external/github.js +164 -0
- package/esm/identity/external/google.d.ts +52 -0
- package/esm/identity/external/google.js +38 -0
- package/esm/identity/external/mod.d.ts +63 -0
- package/esm/identity/external/mod.js +61 -0
- package/esm/identity/external/oauth2.d.ts +110 -0
- package/esm/identity/external/oauth2.js +158 -0
- package/esm/identity/external/oidc.d.ts +81 -0
- package/esm/identity/external/oidc.js +250 -0
- package/esm/identity/external/provider.d.ts +132 -0
- package/esm/identity/external/provider.js +12 -0
- package/esm/identity/hibp.d.ts +67 -0
- package/esm/identity/hibp.js +87 -0
- package/esm/identity/identifier.d.ts +44 -0
- package/esm/identity/identifier.js +47 -0
- package/esm/identity/lockout.d.ts +155 -0
- package/esm/identity/lockout.js +117 -0
- package/esm/identity/mfa/mod.d.ts +42 -0
- package/esm/identity/mfa/mod.js +42 -0
- package/esm/identity/mfa/recovery-codes.d.ts +96 -0
- package/esm/identity/mfa/recovery-codes.js +80 -0
- package/esm/identity/mfa/service.d.ts +338 -0
- package/esm/identity/mfa/service.js +351 -0
- package/esm/identity/mfa/totp.d.ts +186 -0
- package/esm/identity/mfa/totp.js +194 -0
- package/esm/identity/migration.d.ts +155 -0
- package/esm/identity/migration.js +243 -0
- package/esm/identity/mod.d.ts +65 -0
- package/esm/identity/mod.js +64 -0
- package/esm/identity/otp.d.ts +255 -0
- package/esm/identity/otp.js +245 -0
- package/esm/identity/password-policy.d.ts +45 -0
- package/esm/identity/password-policy.js +60 -0
- package/esm/identity/password.d.ts +171 -0
- package/esm/identity/password.js +160 -0
- package/esm/identity/rate-limit.d.ts +192 -0
- package/esm/identity/rate-limit.js +168 -0
- package/esm/identity/service.d.ts +579 -0
- package/esm/identity/service.js +835 -0
- package/esm/identity/session.d.ts +122 -0
- package/esm/identity/session.js +48 -0
- package/esm/identity/token-flow.d.ts +214 -0
- package/esm/identity/token-flow.js +173 -0
- package/esm/models/authentication.d.ts +24 -0
- package/esm/models/authentication.js +1 -0
- package/esm/models/authorization-code.d.ts +63 -0
- package/esm/models/authorization-code.js +1 -0
- package/esm/models/authorization.d.ts +165 -0
- package/esm/models/authorization.js +160 -0
- package/esm/models/client.d.ts +78 -0
- package/esm/models/client.js +1 -0
- package/esm/models/device-authorization.d.ts +30 -0
- package/esm/models/device-authorization.js +1 -0
- package/esm/models/responses.d.ts +93 -0
- package/esm/models/responses.js +1 -0
- package/esm/models/scope.d.ts +81 -0
- package/esm/models/scope.js +125 -0
- package/esm/models/token.d.ts +135 -0
- package/esm/models/token.js +1 -0
- package/esm/package.json +3 -0
- package/esm/react/callback.d.ts +52 -0
- package/esm/react/callback.js +71 -0
- package/esm/react/components/class-names.d.ts +91 -0
- package/esm/react/components/class-names.js +118 -0
- package/esm/react/components/internal.d.ts +62 -0
- package/esm/react/components/internal.js +102 -0
- package/esm/react/components/mfa-challenge-form.d.ts +66 -0
- package/esm/react/components/mfa-challenge-form.js +46 -0
- package/esm/react/components/mfa-enrollment-form.d.ts +83 -0
- package/esm/react/components/mfa-enrollment-form.js +41 -0
- package/esm/react/components/mod.d.ts +103 -0
- package/esm/react/components/mod.js +93 -0
- package/esm/react/components/request-password-reset-form.d.ts +61 -0
- package/esm/react/components/request-password-reset-form.js +41 -0
- package/esm/react/components/reset-password-form.d.ts +66 -0
- package/esm/react/components/reset-password-form.js +56 -0
- package/esm/react/components/sign-in-form.d.ts +74 -0
- package/esm/react/components/sign-in-form.js +43 -0
- package/esm/react/components/sign-up-form.d.ts +85 -0
- package/esm/react/components/sign-up-form.js +57 -0
- package/esm/react/components/types.d.ts +208 -0
- package/esm/react/components/types.js +14 -0
- package/esm/react/components/use-auth-form.d.ts +118 -0
- package/esm/react/components/use-auth-form.js +172 -0
- package/esm/react/components/user-menu.d.ts +79 -0
- package/esm/react/components/user-menu.js +73 -0
- package/esm/react/context.d.ts +51 -0
- package/esm/react/context.js +16 -0
- package/esm/react/mod.d.ts +26 -0
- package/esm/react/mod.js +26 -0
- package/esm/react/provider.d.ts +40 -0
- package/esm/react/provider.js +189 -0
- package/esm/react/require-auth.d.ts +31 -0
- package/esm/react/require-auth.js +37 -0
- package/esm/react/testing.d.ts +322 -0
- package/esm/react/testing.js +461 -0
- package/esm/react/use-authorization.d.ts +31 -0
- package/esm/react/use-authorization.js +36 -0
- package/esm/react/use-oauth2.d.ts +112 -0
- package/esm/react/use-oauth2.js +127 -0
- package/esm/server/authorization-server.d.ts +736 -0
- package/esm/server/authorization-server.js +1304 -0
- package/esm/server/client-authentication.d.ts +28 -0
- package/esm/server/client-authentication.js +51 -0
- package/esm/server/grants/authorization-code.d.ts +179 -0
- package/esm/server/grants/authorization-code.js +232 -0
- package/esm/server/grants/client-credentials.d.ts +52 -0
- package/esm/server/grants/client-credentials.js +53 -0
- package/esm/server/grants/device-authorization.d.ts +103 -0
- package/esm/server/grants/device-authorization.js +156 -0
- package/esm/server/grants/grant.d.ts +165 -0
- package/esm/server/grants/grant.js +173 -0
- package/esm/server/grants/password.d.ts +80 -0
- package/esm/server/grants/password.js +92 -0
- package/esm/server/grants/refresh-token.d.ts +80 -0
- package/esm/server/grants/refresh-token.js +136 -0
- package/esm/server/introspection-token-reader.d.ts +153 -0
- package/esm/server/introspection-token-reader.js +164 -0
- package/esm/server/jwks-token-reader.d.ts +263 -0
- package/esm/server/jwks-token-reader.js +508 -0
- package/esm/server/mod.d.ts +23 -0
- package/esm/server/mod.js +15 -0
- package/esm/server/public-suffix/mod.d.ts +62 -0
- package/esm/server/public-suffix/mod.js +103 -0
- package/esm/server/public-suffix/rules.d.ts +23 -0
- package/esm/server/public-suffix/rules.js +23 -0
- package/esm/server/redirect-uri.d.ts +187 -0
- package/esm/server/redirect-uri.js +331 -0
- package/esm/server/resource-server.d.ts +286 -0
- package/esm/server/resource-server.js +365 -0
- package/esm/server/services/authorization-code.d.ts +84 -0
- package/esm/server/services/authorization-code.js +35 -0
- package/esm/server/services/client.d.ts +35 -0
- package/esm/server/services/client.js +1 -0
- package/esm/server/services/device-authorization.d.ts +134 -0
- package/esm/server/services/device-authorization.js +7 -0
- package/esm/server/services/token.d.ts +242 -0
- package/esm/server/services/token.js +78 -0
- package/esm/server/services/user.d.ts +29 -0
- package/esm/server/services/user.js +7 -0
- package/esm/server/signing-keys.d.ts +201 -0
- package/esm/server/signing-keys.js +225 -0
- package/esm/server/utils/hash.d.ts +6 -0
- package/esm/server/utils/hash.js +11 -0
- package/esm/testing/contract/authorization-code.d.ts +37 -0
- package/esm/testing/contract/authorization-code.js +93 -0
- package/esm/testing/contract/client.d.ts +85 -0
- package/esm/testing/contract/client.js +126 -0
- package/esm/testing/contract/device-authorization.d.ts +37 -0
- package/esm/testing/contract/device-authorization.js +193 -0
- package/esm/testing/contract/lockout-store.d.ts +47 -0
- package/esm/testing/contract/lockout-store.js +122 -0
- package/esm/testing/contract/mfa-store.d.ts +50 -0
- package/esm/testing/contract/mfa-store.js +226 -0
- package/esm/testing/contract/mod.d.ts +39 -0
- package/esm/testing/contract/mod.js +39 -0
- package/esm/testing/contract/otp-store.d.ts +48 -0
- package/esm/testing/contract/otp-store.js +197 -0
- package/esm/testing/contract/rate-limit-store.d.ts +52 -0
- package/esm/testing/contract/rate-limit-store.js +105 -0
- package/esm/testing/contract/token-flow-store.d.ts +54 -0
- package/esm/testing/contract/token-flow-store.js +179 -0
- package/esm/testing/contract/token-reader.d.ts +77 -0
- package/esm/testing/contract/token-reader.js +78 -0
- package/esm/testing/contract/token.d.ts +61 -0
- package/esm/testing/contract/token.js +287 -0
- package/esm/testing/contract/user.d.ts +63 -0
- package/esm/testing/contract/user.js +75 -0
- package/esm/testing/mod.d.ts +34 -0
- package/esm/testing/mod.js +34 -0
- package/esm/testing/server.d.ts +197 -0
- package/esm/testing/server.js +193 -0
- package/esm/testing/services.d.ts +373 -0
- package/esm/testing/services.js +703 -0
- package/esm/utils/_buffer.d.ts +10 -0
- package/esm/utils/_buffer.js +12 -0
- package/esm/utils/_timing-safe.d.ts +13 -0
- package/esm/utils/_timing-safe.js +22 -0
- package/esm/utils/authentication-context.d.ts +3 -0
- package/esm/utils/authentication-context.js +14 -0
- package/esm/utils/basic-auth.d.ts +50 -0
- package/esm/utils/basic-auth.js +106 -0
- package/esm/utils/crypto.d.ts +97 -0
- package/esm/utils/crypto.js +162 -0
- package/esm/utils/pkce.d.ts +151 -0
- package/esm/utils/pkce.js +179 -0
- package/esm/utils/text.d.ts +24 -0
- package/esm/utils/text.js +28 -0
- package/esm/utils/url.d.ts +66 -0
- package/esm/utils/url.js +90 -0
- package/package.json +92 -0
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser client for an app fronted by a Backend-for-Frontend.
|
|
3
|
+
*
|
|
4
|
+
* The BFF holds the tokens; this client only ever talks to the BFF's
|
|
5
|
+
* `/auth/*` routes with the session cookie attached, so no token reaches the
|
|
6
|
+
* browser. Pair it with the `@udibo/oauth2/hono/bff` adapter, or any BFF
|
|
7
|
+
* serving the same four routes.
|
|
8
|
+
*
|
|
9
|
+
* @module
|
|
10
|
+
*/
|
|
11
|
+
import { loginContinuation as computeLoginContinuation } from "../utils/url.js";
|
|
12
|
+
import { receiveJson, sendGuarded } from "./_http.js";
|
|
13
|
+
import { mergedHeaders, OAuth2ClientBase, SIGNED_OUT, } from "./base.js";
|
|
14
|
+
/**
|
|
15
|
+
* OAuth2 client for a browser app whose sessions live in a
|
|
16
|
+
* Backend-for-Frontend.
|
|
17
|
+
*
|
|
18
|
+
* Every method here is a same-origin call to the BFF with `credentials:
|
|
19
|
+
* "include"`, never a call to the authorization server. There is no token
|
|
20
|
+
* storage, no PKCE, and no access token to read — if you need one, the app
|
|
21
|
+
* wants a {@link DirectClient} instead.
|
|
22
|
+
*
|
|
23
|
+
* @example Same-origin BFF
|
|
24
|
+
* ```ts
|
|
25
|
+
* import { BffClient } from "@udibo/oauth2/client";
|
|
26
|
+
*
|
|
27
|
+
* const client = new BffClient();
|
|
28
|
+
*
|
|
29
|
+
* const session = await client.getSession();
|
|
30
|
+
* if (!session.isAuthenticated) {
|
|
31
|
+
* const { url } = await client.login({ returnTo: location.pathname });
|
|
32
|
+
* location.assign(url);
|
|
33
|
+
* }
|
|
34
|
+
*
|
|
35
|
+
* const res = await client.fetch("/api/items");
|
|
36
|
+
* ```
|
|
37
|
+
*
|
|
38
|
+
* @example BFF mounted elsewhere, with a custom CSRF header
|
|
39
|
+
* ```ts
|
|
40
|
+
* import { BffClient } from "@udibo/oauth2/client";
|
|
41
|
+
*
|
|
42
|
+
* const client = new BffClient({
|
|
43
|
+
* endpoints: { baseUrl: "https://api.example.com", login: "/session/start" },
|
|
44
|
+
* csrfHeader: { name: "x-requested-with", value: "spa" },
|
|
45
|
+
* });
|
|
46
|
+
* ```
|
|
47
|
+
*/
|
|
48
|
+
export class BffClient extends OAuth2ClientBase {
|
|
49
|
+
#endpoints;
|
|
50
|
+
#authorizePath;
|
|
51
|
+
#csrfHeader;
|
|
52
|
+
/**
|
|
53
|
+
* Builds a client against the BFF's `/auth/*` routes.
|
|
54
|
+
*
|
|
55
|
+
* @param options Endpoint overrides, the CSRF header to send, and the
|
|
56
|
+
* `fetch` to use. Every field is optional; the defaults match `HonoBff`
|
|
57
|
+
* mounted at `/auth` on the same origin.
|
|
58
|
+
*/
|
|
59
|
+
constructor(options = {}) {
|
|
60
|
+
super(options);
|
|
61
|
+
const endpoints = options.endpoints ?? {};
|
|
62
|
+
this.#endpoints = {
|
|
63
|
+
baseUrl: endpoints.baseUrl ?? "",
|
|
64
|
+
login: endpoints.login ?? "/auth/login",
|
|
65
|
+
logout: endpoints.logout ?? "/auth/logout",
|
|
66
|
+
session: endpoints.session ?? "/auth/session",
|
|
67
|
+
};
|
|
68
|
+
this.#authorizePath = options.authorizePath;
|
|
69
|
+
this.#csrfHeader = options.csrfHeader === false ? {} : {
|
|
70
|
+
[options.csrfHeader?.name ?? "x-csrf"]: options.csrfHeader?.value ?? "1",
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
#url(path) {
|
|
74
|
+
const origin = typeof location !== "undefined"
|
|
75
|
+
? location.origin
|
|
76
|
+
: "http://localhost";
|
|
77
|
+
return new URL(`${this.#endpoints.baseUrl}${path}`, origin).toString();
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Builds the BFF login URL. Navigate the browser there; the BFF runs the
|
|
81
|
+
* authorization-code flow and drops the session cookie before redirecting
|
|
82
|
+
* back to `returnTo`.
|
|
83
|
+
*
|
|
84
|
+
* @param options `returnTo` is passed through as the `return_to` query
|
|
85
|
+
* parameter. The BFF is responsible for validating it against open
|
|
86
|
+
* redirects.
|
|
87
|
+
* @returns The URL to navigate to.
|
|
88
|
+
*
|
|
89
|
+
* @example
|
|
90
|
+
* ```ts
|
|
91
|
+
* import { BffClient } from "@udibo/oauth2/client";
|
|
92
|
+
*
|
|
93
|
+
* const client = new BffClient();
|
|
94
|
+
* const { url } = await client.login({ returnTo: "/dashboard" });
|
|
95
|
+
* location.assign(url);
|
|
96
|
+
* ```
|
|
97
|
+
*/
|
|
98
|
+
login(options = {}) {
|
|
99
|
+
const url = new URL(this.#url(this.#endpoints.login));
|
|
100
|
+
if (options.returnTo)
|
|
101
|
+
url.searchParams.set("return_to", options.returnTo);
|
|
102
|
+
return Promise.resolve({ url: url.toString() });
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Builds the BFF logout URL and emits `logged_out` so the UI can render
|
|
106
|
+
* signed-out immediately. Navigate the browser to the returned URL; the BFF
|
|
107
|
+
* destroys the session and clears the cookie.
|
|
108
|
+
*
|
|
109
|
+
* @param options `returnTo` is passed through as the `return_to` query
|
|
110
|
+
* parameter.
|
|
111
|
+
* @returns The URL to navigate to.
|
|
112
|
+
*
|
|
113
|
+
* @example
|
|
114
|
+
* ```ts
|
|
115
|
+
* import { BffClient } from "@udibo/oauth2/client";
|
|
116
|
+
*
|
|
117
|
+
* const client = new BffClient();
|
|
118
|
+
* const { url } = await client.logout({ returnTo: "/" });
|
|
119
|
+
* location.assign(url!);
|
|
120
|
+
* ```
|
|
121
|
+
*/
|
|
122
|
+
logout(options = {}) {
|
|
123
|
+
const url = new URL(this.#url(this.#endpoints.logout));
|
|
124
|
+
if (options.returnTo)
|
|
125
|
+
url.searchParams.set("return_to", options.returnTo);
|
|
126
|
+
this.emit({ type: "logged_out", reason: "user" });
|
|
127
|
+
return Promise.resolve({ url: url.toString() });
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Probes `GET /auth/session` and returns the full {@link SessionState} —
|
|
131
|
+
* `isAuthenticated`, `user`, plus the `sessionExpiresIn` and `logoutUrl` a
|
|
132
|
+
* SPA uses to schedule a re-probe and render a sign-out link.
|
|
133
|
+
*
|
|
134
|
+
* **Never rejects**, per {@link OAuth2ClientBase.getSession}: an
|
|
135
|
+
* unreachable BFF, a non-OK status, and a body that is not a JSON object
|
|
136
|
+
* (an HTML error page from a misrouted path is the common one) all read as
|
|
137
|
+
* signed out. A failure that was not simply "nobody is signed in" — anything
|
|
138
|
+
* other than a `401` — also emits an `error` event so a UI can tell the two
|
|
139
|
+
* apart. The read is bounded and deadlined like every other call this
|
|
140
|
+
* package makes.
|
|
141
|
+
*
|
|
142
|
+
* Only `isAuthenticated: true` carries `user` / `sessionExpiresIn` /
|
|
143
|
+
* `logoutUrl` through; an anonymous payload always reports them as `null`,
|
|
144
|
+
* whatever the BFF put in the body.
|
|
145
|
+
*
|
|
146
|
+
* @returns The session as the BFF reports it. Tokens are never included.
|
|
147
|
+
*
|
|
148
|
+
* @example
|
|
149
|
+
* ```ts
|
|
150
|
+
* import { BffClient } from "@udibo/oauth2/client";
|
|
151
|
+
*
|
|
152
|
+
* const { isAuthenticated, logoutUrl } = await new BffClient().getSession();
|
|
153
|
+
* ```
|
|
154
|
+
*/
|
|
155
|
+
async getSession() {
|
|
156
|
+
return await this.#probe(false);
|
|
157
|
+
}
|
|
158
|
+
async #probe(quiet) {
|
|
159
|
+
let body;
|
|
160
|
+
try {
|
|
161
|
+
const res = await sendGuarded((input, init) => this.fetchImpl(input, init), this.#url(this.#endpoints.session), {
|
|
162
|
+
credentials: "include",
|
|
163
|
+
headers: { Accept: "application/json", ...this.#csrfHeader },
|
|
164
|
+
}, "session endpoint");
|
|
165
|
+
if (res.status === 401) {
|
|
166
|
+
await res.body?.cancel();
|
|
167
|
+
return SIGNED_OUT;
|
|
168
|
+
}
|
|
169
|
+
body = await receiveJson(res, "session endpoint");
|
|
170
|
+
}
|
|
171
|
+
catch (error) {
|
|
172
|
+
if (!quiet)
|
|
173
|
+
this.emit({ type: "error", error });
|
|
174
|
+
return SIGNED_OUT;
|
|
175
|
+
}
|
|
176
|
+
const session = body;
|
|
177
|
+
if (!session.isAuthenticated)
|
|
178
|
+
return SIGNED_OUT;
|
|
179
|
+
return {
|
|
180
|
+
isAuthenticated: true,
|
|
181
|
+
user: session.user ?? null,
|
|
182
|
+
sessionExpiresIn: session.sessionExpiresIn ?? null,
|
|
183
|
+
logoutUrl: session.logoutUrl ?? null,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* The signed-in user's claims from the session probe, or `null`.
|
|
188
|
+
*
|
|
189
|
+
* @returns The `user` field of {@link getSession}.
|
|
190
|
+
*/
|
|
191
|
+
async getUser() {
|
|
192
|
+
return (await this.getSession()).user;
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Re-probes `GET /auth/session`.
|
|
196
|
+
*
|
|
197
|
+
* The BFF owns credential renewal — its `attachToken` middleware refreshes
|
|
198
|
+
* the access token server-side when a proxied call needs one — so this
|
|
199
|
+
* reports the current state rather than driving a refresh itself. Call it
|
|
200
|
+
* shortly before {@link SessionState.sessionExpiresIn} elapses to keep the
|
|
201
|
+
* UI honest about whether the session is still alive.
|
|
202
|
+
*
|
|
203
|
+
* Emits no `error` event, per {@link OAuth2ClientBase.renewSession}: this
|
|
204
|
+
* runs on a background timer, where a failed probe means "ask again shortly",
|
|
205
|
+
* not "show the user a problem".
|
|
206
|
+
*
|
|
207
|
+
* @returns The refreshed {@link SessionState}.
|
|
208
|
+
*/
|
|
209
|
+
renewSession() {
|
|
210
|
+
return this.#probe(true);
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Fetches with the session cookie and the CSRF header attached, so the BFF
|
|
214
|
+
* authenticates the call and its CSRF guard accepts it.
|
|
215
|
+
*
|
|
216
|
+
* A `401` emits `logged_out` with reason `session_expired` and is returned
|
|
217
|
+
* as-is — the BFF, not the browser, decides whether a refresh is possible.
|
|
218
|
+
*
|
|
219
|
+
* @param input Request target, as for `fetch`. A `Request` keeps the headers
|
|
220
|
+
* and body it was built with.
|
|
221
|
+
* @param init Request options. A caller-set CSRF header is left alone, and
|
|
222
|
+
* `init.headers` override same-named headers on a `Request` input.
|
|
223
|
+
* @returns The BFF's response, unmodified.
|
|
224
|
+
*
|
|
225
|
+
* @example
|
|
226
|
+
* ```ts
|
|
227
|
+
* import { BffClient } from "@udibo/oauth2/client";
|
|
228
|
+
*
|
|
229
|
+
* const res = await new BffClient().fetch("/api/items");
|
|
230
|
+
* ```
|
|
231
|
+
*/
|
|
232
|
+
async fetch(input, init = {}) {
|
|
233
|
+
const headers = mergedHeaders(input, init);
|
|
234
|
+
for (const [name, value] of Object.entries(this.#csrfHeader)) {
|
|
235
|
+
if (!headers.has(name))
|
|
236
|
+
headers.set(name, value);
|
|
237
|
+
}
|
|
238
|
+
const res = await this.fetchImpl(input, {
|
|
239
|
+
...init,
|
|
240
|
+
credentials: "include",
|
|
241
|
+
headers,
|
|
242
|
+
});
|
|
243
|
+
if (res.status === 401) {
|
|
244
|
+
this.emit({ type: "logged_out", reason: "session_expired" });
|
|
245
|
+
}
|
|
246
|
+
return res;
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Builds the URL that (re)starts sign-in toward `returnTo`, for an app whose
|
|
250
|
+
* own login surface (a sign-in form, create-account, forgot-password) hands
|
|
251
|
+
* back to the BFF flow after authenticating a user: resume an in-flight
|
|
252
|
+
* authorize URL, or start a fresh BFF login.
|
|
253
|
+
*
|
|
254
|
+
* Pure and synchronous — safe in render. The mirror of
|
|
255
|
+
* `HonoBff.loginContinuation`, so both sides of the app agree on where
|
|
256
|
+
* "sign in" goes.
|
|
257
|
+
*
|
|
258
|
+
* Recognizing an in-flight authorize URL requires `authorizePath` in the
|
|
259
|
+
* constructor options; without it every `returnTo` starts a fresh login.
|
|
260
|
+
*
|
|
261
|
+
* @param returnTo Where the user was headed. `null` / `undefined` fall back
|
|
262
|
+
* to `/`.
|
|
263
|
+
* @returns The URL to redirect to.
|
|
264
|
+
*
|
|
265
|
+
* @example
|
|
266
|
+
* ```ts
|
|
267
|
+
* import { BffClient } from "@udibo/oauth2/client";
|
|
268
|
+
*
|
|
269
|
+
* const client = new BffClient({ authorizePath: "/api/oauth2/authorize" });
|
|
270
|
+
* const href = client.loginContinuation("/api/oauth2/authorize?client_id=a");
|
|
271
|
+
* ```
|
|
272
|
+
*/
|
|
273
|
+
loginContinuation(returnTo) {
|
|
274
|
+
return computeLoginContinuation(returnTo, {
|
|
275
|
+
authorizeEndpoint: this.#authorizePath,
|
|
276
|
+
loginPath: `${this.#endpoints.baseUrl}${this.#endpoints.login}`,
|
|
277
|
+
defaultReturnTo: "/",
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser-storage implementations of {@link RefreshTokenStorage} and
|
|
3
|
+
* {@link AuthRequestStorage}.
|
|
4
|
+
*
|
|
5
|
+
* Both ship from the `@udibo/oauth2/client` subpath alongside the
|
|
6
|
+
* `Memory*` defaults so non-React browser consumers (vanilla JS,
|
|
7
|
+
* Vue, Svelte, …) can use them too.
|
|
8
|
+
*
|
|
9
|
+
* Both implementations are **SSR-safe**: if `window` / `indexedDB` /
|
|
10
|
+
* `sessionStorage` is undefined at construction, the storage degrades
|
|
11
|
+
* to a no-op (`get` returns `null`, `set`/`clear` do nothing). The
|
|
12
|
+
* client constructs the real backing on first use in the browser.
|
|
13
|
+
*
|
|
14
|
+
* @module
|
|
15
|
+
*/
|
|
16
|
+
import type { AuthRequestRecord, AuthRequestStorage, RefreshTokenStorage } from "./storage.js";
|
|
17
|
+
/** Options for {@link IndexedDBRefreshTokenStorage}. */
|
|
18
|
+
export interface IndexedDBRefreshTokenStorageOptions {
|
|
19
|
+
/**
|
|
20
|
+
* Discriminates this client's refresh token from every other OAuth2
|
|
21
|
+
* client's on the same origin, which all share one IndexedDB namespace.
|
|
22
|
+
* Pass the `client_id` handed to the client; when one client id is
|
|
23
|
+
* registered at more than one issuer, include the issuer too so the two
|
|
24
|
+
* registrations do not share a slot.
|
|
25
|
+
*/
|
|
26
|
+
clientId: string;
|
|
27
|
+
/** IndexedDB database name. Defaults to `"@udibo/oauth2"`. */
|
|
28
|
+
databaseName?: string;
|
|
29
|
+
/** Object store name. Defaults to `"refresh-token"`. */
|
|
30
|
+
storeName?: string;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Refresh token storage backed by IndexedDB.
|
|
34
|
+
*
|
|
35
|
+
* IndexedDB is preferred over `localStorage` because the latter is
|
|
36
|
+
* synchronously readable from any script that runs in the document
|
|
37
|
+
* (including injected XSS payloads). IndexedDB's same-origin guarantees
|
|
38
|
+
* are similar but its async API and per-database isolation make
|
|
39
|
+
* exfiltration somewhat harder. Combined with refresh-token rotation +
|
|
40
|
+
* reuse detection on the authorization server, it's the recommended
|
|
41
|
+
* default for a browser `DirectClient`.
|
|
42
|
+
*
|
|
43
|
+
* No `localStorage`-backed variant is shipped: it is synchronously
|
|
44
|
+
* readable by any in-document script (including injected XSS), which
|
|
45
|
+
* defeats the protection above.
|
|
46
|
+
*
|
|
47
|
+
* Every instance needs a
|
|
48
|
+
* {@link IndexedDBRefreshTokenStorageOptions.clientId} — the token is stored
|
|
49
|
+
* under it, so two clients on one origin (two APIs, two issuers) keep
|
|
50
|
+
* separate refresh tokens instead of overwriting each other's.
|
|
51
|
+
*
|
|
52
|
+
* @example
|
|
53
|
+
* ```ts
|
|
54
|
+
* import {
|
|
55
|
+
* DirectClient,
|
|
56
|
+
* IndexedDBRefreshTokenStorage,
|
|
57
|
+
* } from "@udibo/oauth2/client";
|
|
58
|
+
*
|
|
59
|
+
* const client = new DirectClient({
|
|
60
|
+
* clientId: "web-app",
|
|
61
|
+
* issuer: "https://sso.example",
|
|
62
|
+
* refreshTokenStorage: new IndexedDBRefreshTokenStorage({
|
|
63
|
+
* clientId: "web-app",
|
|
64
|
+
* }),
|
|
65
|
+
* });
|
|
66
|
+
* ```
|
|
67
|
+
*/
|
|
68
|
+
export declare class IndexedDBRefreshTokenStorage implements RefreshTokenStorage {
|
|
69
|
+
#private;
|
|
70
|
+
/**
|
|
71
|
+
* Names the slot this instance owns.
|
|
72
|
+
*
|
|
73
|
+
* @param options Must carry `clientId`; `databaseName` and `storeName`
|
|
74
|
+
* isolate further when one client id needs more than one slot.
|
|
75
|
+
* @throws {TypeError} when `clientId` is empty, which would put every
|
|
76
|
+
* client on the origin back in one slot.
|
|
77
|
+
*/
|
|
78
|
+
constructor(options: IndexedDBRefreshTokenStorageOptions);
|
|
79
|
+
/**
|
|
80
|
+
* Returns the stored refresh token, or `null` when none is stored, when
|
|
81
|
+
* `indexedDB` is unavailable (SSR / non-browser), or when another tab is
|
|
82
|
+
* blocking the store's creation.
|
|
83
|
+
*
|
|
84
|
+
* @throws {DOMException} when IndexedDB rejects the read.
|
|
85
|
+
*/
|
|
86
|
+
get(): Promise<string | null>;
|
|
87
|
+
/**
|
|
88
|
+
* Persists {@linkcode value} as the refresh token. A no-op when `indexedDB`
|
|
89
|
+
* is unavailable (SSR / non-browser).
|
|
90
|
+
*
|
|
91
|
+
* @throws {DOMException} when IndexedDB rejects the write.
|
|
92
|
+
*/
|
|
93
|
+
set(value: string): Promise<void>;
|
|
94
|
+
/**
|
|
95
|
+
* Discards the stored refresh token. A no-op when `indexedDB` is unavailable
|
|
96
|
+
* (SSR / non-browser).
|
|
97
|
+
*
|
|
98
|
+
* @throws {DOMException} when IndexedDB rejects the delete.
|
|
99
|
+
*/
|
|
100
|
+
clear(): Promise<void>;
|
|
101
|
+
}
|
|
102
|
+
/** Options for {@link SessionStorageAuthRequestStorage}. */
|
|
103
|
+
export interface SessionStorageAuthRequestStorageOptions {
|
|
104
|
+
/** sessionStorage key prefix. Defaults to `"oauth2:auth-req:"`. */
|
|
105
|
+
keyPrefix?: string;
|
|
106
|
+
/**
|
|
107
|
+
* Maximum age of an auth request before it's treated as expired
|
|
108
|
+
* (epoch-ms). Reads of older entries return `null` and the entry is
|
|
109
|
+
* removed. Defaults to 10 minutes — long enough for a slow
|
|
110
|
+
* authorize-redirect round-trip, short enough that stale state from a
|
|
111
|
+
* previous abandoned login doesn't accumulate.
|
|
112
|
+
*/
|
|
113
|
+
ttlMs?: number;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Auth-request storage backed by `sessionStorage`.
|
|
117
|
+
*
|
|
118
|
+
* Survives the authorize-redirect round-trip (sessionStorage persists
|
|
119
|
+
* across navigations within the same tab) but doesn't outlive the tab,
|
|
120
|
+
* so a forgotten in-flight authorization doesn't leak across browser
|
|
121
|
+
* sessions.
|
|
122
|
+
*
|
|
123
|
+
* Stale entries past {@link SessionStorageAuthRequestStorageOptions.ttlMs}
|
|
124
|
+
* are pruned on read.
|
|
125
|
+
*/
|
|
126
|
+
export declare class SessionStorageAuthRequestStorage implements AuthRequestStorage {
|
|
127
|
+
#private;
|
|
128
|
+
/** Configures the key prefix and TTL; both default per option. */
|
|
129
|
+
constructor(options?: SessionStorageAuthRequestStorageOptions);
|
|
130
|
+
/**
|
|
131
|
+
* Returns the record for {@linkcode state}, or `null` when it is absent,
|
|
132
|
+
* unparseable, expired past the configured TTL, or `sessionStorage` is
|
|
133
|
+
* unavailable (SSR / non-browser). Expired or unparseable entries are pruned.
|
|
134
|
+
*/
|
|
135
|
+
get(state: string): AuthRequestRecord | null;
|
|
136
|
+
/**
|
|
137
|
+
* Stores {@linkcode value} under {@linkcode state}. A no-op when
|
|
138
|
+
* `sessionStorage` is unavailable (SSR / non-browser).
|
|
139
|
+
*/
|
|
140
|
+
set(state: string, value: AuthRequestRecord): void;
|
|
141
|
+
/**
|
|
142
|
+
* Removes the record for {@linkcode state}. A no-op when `sessionStorage` is
|
|
143
|
+
* unavailable (SSR / non-browser).
|
|
144
|
+
*/
|
|
145
|
+
delete(state: string): void;
|
|
146
|
+
/**
|
|
147
|
+
* Removes every record under the configured key prefix, leaving unrelated
|
|
148
|
+
* `sessionStorage` entries intact. A no-op when `sessionStorage` is
|
|
149
|
+
* unavailable (SSR / non-browser).
|
|
150
|
+
*/
|
|
151
|
+
clear(): void;
|
|
152
|
+
}
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser-storage implementations of {@link RefreshTokenStorage} and
|
|
3
|
+
* {@link AuthRequestStorage}.
|
|
4
|
+
*
|
|
5
|
+
* Both ship from the `@udibo/oauth2/client` subpath alongside the
|
|
6
|
+
* `Memory*` defaults so non-React browser consumers (vanilla JS,
|
|
7
|
+
* Vue, Svelte, …) can use them too.
|
|
8
|
+
*
|
|
9
|
+
* Both implementations are **SSR-safe**: if `window` / `indexedDB` /
|
|
10
|
+
* `sessionStorage` is undefined at construction, the storage degrades
|
|
11
|
+
* to a no-op (`get` returns `null`, `set`/`clear` do nothing). The
|
|
12
|
+
* client constructs the real backing on first use in the browser.
|
|
13
|
+
*
|
|
14
|
+
* @module
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* Refresh token storage backed by IndexedDB.
|
|
18
|
+
*
|
|
19
|
+
* IndexedDB is preferred over `localStorage` because the latter is
|
|
20
|
+
* synchronously readable from any script that runs in the document
|
|
21
|
+
* (including injected XSS payloads). IndexedDB's same-origin guarantees
|
|
22
|
+
* are similar but its async API and per-database isolation make
|
|
23
|
+
* exfiltration somewhat harder. Combined with refresh-token rotation +
|
|
24
|
+
* reuse detection on the authorization server, it's the recommended
|
|
25
|
+
* default for a browser `DirectClient`.
|
|
26
|
+
*
|
|
27
|
+
* No `localStorage`-backed variant is shipped: it is synchronously
|
|
28
|
+
* readable by any in-document script (including injected XSS), which
|
|
29
|
+
* defeats the protection above.
|
|
30
|
+
*
|
|
31
|
+
* Every instance needs a
|
|
32
|
+
* {@link IndexedDBRefreshTokenStorageOptions.clientId} — the token is stored
|
|
33
|
+
* under it, so two clients on one origin (two APIs, two issuers) keep
|
|
34
|
+
* separate refresh tokens instead of overwriting each other's.
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* ```ts
|
|
38
|
+
* import {
|
|
39
|
+
* DirectClient,
|
|
40
|
+
* IndexedDBRefreshTokenStorage,
|
|
41
|
+
* } from "@udibo/oauth2/client";
|
|
42
|
+
*
|
|
43
|
+
* const client = new DirectClient({
|
|
44
|
+
* clientId: "web-app",
|
|
45
|
+
* issuer: "https://sso.example",
|
|
46
|
+
* refreshTokenStorage: new IndexedDBRefreshTokenStorage({
|
|
47
|
+
* clientId: "web-app",
|
|
48
|
+
* }),
|
|
49
|
+
* });
|
|
50
|
+
* ```
|
|
51
|
+
*/
|
|
52
|
+
export class IndexedDBRefreshTokenStorage {
|
|
53
|
+
#databaseName;
|
|
54
|
+
#storeName;
|
|
55
|
+
#key;
|
|
56
|
+
/**
|
|
57
|
+
* Names the slot this instance owns.
|
|
58
|
+
*
|
|
59
|
+
* @param options Must carry `clientId`; `databaseName` and `storeName`
|
|
60
|
+
* isolate further when one client id needs more than one slot.
|
|
61
|
+
* @throws {TypeError} when `clientId` is empty, which would put every
|
|
62
|
+
* client on the origin back in one slot.
|
|
63
|
+
*/
|
|
64
|
+
constructor(options) {
|
|
65
|
+
if (!options.clientId) {
|
|
66
|
+
throw new TypeError("`clientId` is required so clients on one origin do not share a " +
|
|
67
|
+
"refresh token slot");
|
|
68
|
+
}
|
|
69
|
+
this.#databaseName = options.databaseName ?? "@udibo/oauth2";
|
|
70
|
+
this.#storeName = options.storeName ?? "refresh-token";
|
|
71
|
+
this.#key = options.clientId;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Returns the stored refresh token, or `null` when none is stored, when
|
|
75
|
+
* `indexedDB` is unavailable (SSR / non-browser), or when another tab is
|
|
76
|
+
* blocking the store's creation.
|
|
77
|
+
*
|
|
78
|
+
* @throws {DOMException} when IndexedDB rejects the read.
|
|
79
|
+
*/
|
|
80
|
+
async get() {
|
|
81
|
+
const db = await this.#openDb();
|
|
82
|
+
if (!db)
|
|
83
|
+
return null;
|
|
84
|
+
return await new Promise((resolve, reject) => {
|
|
85
|
+
const tx = db.transaction(this.#storeName, "readonly");
|
|
86
|
+
const req = tx.objectStore(this.#storeName).get(this.#key);
|
|
87
|
+
req.onsuccess = () => resolve(req.result ?? null);
|
|
88
|
+
req.onerror = () => reject(req.error);
|
|
89
|
+
tx.oncomplete = () => db.close();
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Persists {@linkcode value} as the refresh token. A no-op when `indexedDB`
|
|
94
|
+
* is unavailable (SSR / non-browser).
|
|
95
|
+
*
|
|
96
|
+
* @throws {DOMException} when IndexedDB rejects the write.
|
|
97
|
+
*/
|
|
98
|
+
async set(value) {
|
|
99
|
+
const db = await this.#openDb();
|
|
100
|
+
if (!db)
|
|
101
|
+
return;
|
|
102
|
+
await new Promise((resolve, reject) => {
|
|
103
|
+
const tx = db.transaction(this.#storeName, "readwrite");
|
|
104
|
+
const req = tx.objectStore(this.#storeName).put(value, this.#key);
|
|
105
|
+
req.onerror = () => reject(req.error);
|
|
106
|
+
tx.oncomplete = () => {
|
|
107
|
+
db.close();
|
|
108
|
+
resolve();
|
|
109
|
+
};
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Discards the stored refresh token. A no-op when `indexedDB` is unavailable
|
|
114
|
+
* (SSR / non-browser).
|
|
115
|
+
*
|
|
116
|
+
* @throws {DOMException} when IndexedDB rejects the delete.
|
|
117
|
+
*/
|
|
118
|
+
async clear() {
|
|
119
|
+
const db = await this.#openDb();
|
|
120
|
+
if (!db)
|
|
121
|
+
return;
|
|
122
|
+
await new Promise((resolve, reject) => {
|
|
123
|
+
const tx = db.transaction(this.#storeName, "readwrite");
|
|
124
|
+
const req = tx.objectStore(this.#storeName).delete(this.#key);
|
|
125
|
+
req.onerror = () => reject(req.error);
|
|
126
|
+
tx.oncomplete = () => {
|
|
127
|
+
db.close();
|
|
128
|
+
resolve();
|
|
129
|
+
};
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
async #openDb() {
|
|
133
|
+
if (typeof indexedDB === "undefined")
|
|
134
|
+
return null;
|
|
135
|
+
const db = await this.#open();
|
|
136
|
+
if (!db || db.objectStoreNames.contains(this.#storeName))
|
|
137
|
+
return db;
|
|
138
|
+
const nextVersion = db.version + 1;
|
|
139
|
+
db.close();
|
|
140
|
+
return await this.#open(nextVersion);
|
|
141
|
+
}
|
|
142
|
+
#open(version) {
|
|
143
|
+
return new Promise((resolve, reject) => {
|
|
144
|
+
const req = version === undefined
|
|
145
|
+
? indexedDB.open(this.#databaseName)
|
|
146
|
+
: indexedDB.open(this.#databaseName, version);
|
|
147
|
+
req.onupgradeneeded = () => {
|
|
148
|
+
const db = req.result;
|
|
149
|
+
if (!db.objectStoreNames.contains(this.#storeName)) {
|
|
150
|
+
db.createObjectStore(this.#storeName);
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
req.onsuccess = () => resolve(req.result);
|
|
154
|
+
req.onerror = () => reject(req.error);
|
|
155
|
+
req.onblocked = () => resolve(null);
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Auth-request storage backed by `sessionStorage`.
|
|
161
|
+
*
|
|
162
|
+
* Survives the authorize-redirect round-trip (sessionStorage persists
|
|
163
|
+
* across navigations within the same tab) but doesn't outlive the tab,
|
|
164
|
+
* so a forgotten in-flight authorization doesn't leak across browser
|
|
165
|
+
* sessions.
|
|
166
|
+
*
|
|
167
|
+
* Stale entries past {@link SessionStorageAuthRequestStorageOptions.ttlMs}
|
|
168
|
+
* are pruned on read.
|
|
169
|
+
*/
|
|
170
|
+
export class SessionStorageAuthRequestStorage {
|
|
171
|
+
#keyPrefix;
|
|
172
|
+
#ttlMs;
|
|
173
|
+
/** Configures the key prefix and TTL; both default per option. */
|
|
174
|
+
constructor(options = {}) {
|
|
175
|
+
this.#keyPrefix = options.keyPrefix ?? "oauth2:auth-req:";
|
|
176
|
+
this.#ttlMs = options.ttlMs ?? 10 * 60 * 1000;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Returns the record for {@linkcode state}, or `null` when it is absent,
|
|
180
|
+
* unparseable, expired past the configured TTL, or `sessionStorage` is
|
|
181
|
+
* unavailable (SSR / non-browser). Expired or unparseable entries are pruned.
|
|
182
|
+
*/
|
|
183
|
+
get(state) {
|
|
184
|
+
const storage = this.#storage();
|
|
185
|
+
if (!storage)
|
|
186
|
+
return null;
|
|
187
|
+
const raw = storage.getItem(this.#keyPrefix + state);
|
|
188
|
+
if (!raw)
|
|
189
|
+
return null;
|
|
190
|
+
try {
|
|
191
|
+
const parsed = JSON.parse(raw);
|
|
192
|
+
if (Date.now() - parsed.createdAt > this.#ttlMs) {
|
|
193
|
+
storage.removeItem(this.#keyPrefix + state);
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
return parsed;
|
|
197
|
+
}
|
|
198
|
+
catch {
|
|
199
|
+
storage.removeItem(this.#keyPrefix + state);
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Stores {@linkcode value} under {@linkcode state}. A no-op when
|
|
205
|
+
* `sessionStorage` is unavailable (SSR / non-browser).
|
|
206
|
+
*/
|
|
207
|
+
set(state, value) {
|
|
208
|
+
const storage = this.#storage();
|
|
209
|
+
if (!storage)
|
|
210
|
+
return;
|
|
211
|
+
storage.setItem(this.#keyPrefix + state, JSON.stringify(value));
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Removes the record for {@linkcode state}. A no-op when `sessionStorage` is
|
|
215
|
+
* unavailable (SSR / non-browser).
|
|
216
|
+
*/
|
|
217
|
+
delete(state) {
|
|
218
|
+
const storage = this.#storage();
|
|
219
|
+
if (!storage)
|
|
220
|
+
return;
|
|
221
|
+
storage.removeItem(this.#keyPrefix + state);
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Removes every record under the configured key prefix, leaving unrelated
|
|
225
|
+
* `sessionStorage` entries intact. A no-op when `sessionStorage` is
|
|
226
|
+
* unavailable (SSR / non-browser).
|
|
227
|
+
*/
|
|
228
|
+
clear() {
|
|
229
|
+
const storage = this.#storage();
|
|
230
|
+
if (!storage)
|
|
231
|
+
return;
|
|
232
|
+
const keysToRemove = [];
|
|
233
|
+
for (let i = 0; i < storage.length; i++) {
|
|
234
|
+
const key = storage.key(i);
|
|
235
|
+
if (key && key.startsWith(this.#keyPrefix))
|
|
236
|
+
keysToRemove.push(key);
|
|
237
|
+
}
|
|
238
|
+
for (const key of keysToRemove)
|
|
239
|
+
storage.removeItem(key);
|
|
240
|
+
}
|
|
241
|
+
#storage() {
|
|
242
|
+
if (typeof sessionStorage === "undefined")
|
|
243
|
+
return null;
|
|
244
|
+
return sessionStorage;
|
|
245
|
+
}
|
|
246
|
+
}
|