@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,703 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-memory implementations of every OAuth2 service interface.
|
|
3
|
+
*
|
|
4
|
+
* The `Memory*Service` family is the public, generic version of the fixtures
|
|
5
|
+
* the library uses for its own tests. They satisfy the
|
|
6
|
+
* `UserServiceInterface`, `ClientServiceInterface`, `TokenServiceInterface`,
|
|
7
|
+
* `AuthorizationCodeServiceInterface`, and `DeviceAuthorizationServiceInterface`
|
|
8
|
+
* contracts using `Map`-backed storage that lives only for the process /
|
|
9
|
+
* test lifetime.
|
|
10
|
+
*
|
|
11
|
+
* They are deliberately **not** for production: tokens and codes vanish on
|
|
12
|
+
* restart, there is no concurrency control, and nothing is ever reclaimed — an
|
|
13
|
+
* expired record stays in its `Map` until something revokes it, and the
|
|
14
|
+
* revoked-refresh-token tombstones that reuse detection reads are kept for the
|
|
15
|
+
* process lifetime, so memory grows with every token issued. Port the shape to
|
|
16
|
+
* a database and it needs the expiry sweep or row TTL these fixtures skip. Use
|
|
17
|
+
* them to:
|
|
18
|
+
*
|
|
19
|
+
* - Spin up a stand-in authorization server in tests of consumer apps.
|
|
20
|
+
* - Prototype an OAuth2 integration before the database schema lands.
|
|
21
|
+
* - Drive examples that need realistic data without DB infrastructure.
|
|
22
|
+
*
|
|
23
|
+
* Pair with `createMemoryAuthorizationServer` for the one-shot factory
|
|
24
|
+
* variant, or compose directly when you need finer control.
|
|
25
|
+
*
|
|
26
|
+
* @module
|
|
27
|
+
*/
|
|
28
|
+
import { snapshotAuthenticationContext } from "../utils/authentication-context.js";
|
|
29
|
+
import { PasswordIdentityService, } from "../identity/password.js";
|
|
30
|
+
import { sha256Hash } from "../server/utils/hash.js";
|
|
31
|
+
const FIXTURE_PBKDF2_ITERATIONS = 1_000;
|
|
32
|
+
/**
|
|
33
|
+
* In-memory `UserServiceInterface` implementation.
|
|
34
|
+
*
|
|
35
|
+
* Generic over the user type — supply your real app's `User` interface
|
|
36
|
+
* (extending {@link MemoryUserShape}) so fixtures match production data.
|
|
37
|
+
*/
|
|
38
|
+
export class MemoryUserService {
|
|
39
|
+
#usersById = new Map();
|
|
40
|
+
#usersByUsername = new Map();
|
|
41
|
+
#credentialsByUserId = new Map();
|
|
42
|
+
#passwords;
|
|
43
|
+
/** Applies the password-hasher default; construct with no arguments for it. */
|
|
44
|
+
constructor(options = {}) {
|
|
45
|
+
this.#passwords = options.passwords ??
|
|
46
|
+
new PasswordIdentityService({ iterations: FIXTURE_PBKDF2_ITERATIONS });
|
|
47
|
+
}
|
|
48
|
+
/** Adds a user with the given password. Throws on duplicate id or username. */
|
|
49
|
+
async add(user, password) {
|
|
50
|
+
if (this.#usersById.has(user.id)) {
|
|
51
|
+
throw new Error(`User with id "${user.id}" already exists`);
|
|
52
|
+
}
|
|
53
|
+
if (this.#usersByUsername.has(user.username)) {
|
|
54
|
+
throw new Error(`User with username "${user.username}" already exists`);
|
|
55
|
+
}
|
|
56
|
+
this.#usersById.set(user.id, user);
|
|
57
|
+
this.#usersByUsername.set(user.username, user);
|
|
58
|
+
this.#credentialsByUserId.set(user.id, await this.#passwords.hash(password));
|
|
59
|
+
}
|
|
60
|
+
/** Looks up a user by id, or resolves `undefined` when none is stored. */
|
|
61
|
+
get(id) {
|
|
62
|
+
return Promise.resolve(this.#usersById.get(id));
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Resolves the user when the username exists and the password verifies
|
|
66
|
+
* against its stored salt/hash, otherwise `undefined`. Backs the
|
|
67
|
+
* resource-owner password-credentials grant.
|
|
68
|
+
*/
|
|
69
|
+
async getAuthenticated(username, password) {
|
|
70
|
+
const user = this.#usersByUsername.get(username);
|
|
71
|
+
if (!user)
|
|
72
|
+
return undefined;
|
|
73
|
+
const credential = this.#credentialsByUserId.get(user.id);
|
|
74
|
+
if (!credential)
|
|
75
|
+
return undefined;
|
|
76
|
+
return await this.#passwords.verify(password, credential)
|
|
77
|
+
? user
|
|
78
|
+
: undefined;
|
|
79
|
+
}
|
|
80
|
+
/** Looks up a user by username without a password check, or `undefined`. */
|
|
81
|
+
findByUsername(username) {
|
|
82
|
+
return Promise.resolve(this.#usersByUsername.get(username));
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* The user's stored password credential, or `undefined`. Shape-compatible
|
|
86
|
+
* with the identity layer's `IdentityUserStore.getCredential`, so a
|
|
87
|
+
* memory-backed store can drive the own-auth flows (sign-in, password
|
|
88
|
+
* reset) in examples and tests.
|
|
89
|
+
*/
|
|
90
|
+
getCredential(userId) {
|
|
91
|
+
return Promise.resolve(this.#credentialsByUserId.get(userId));
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Replace the user's stored password credential (e.g. after a password
|
|
95
|
+
* reset). Matches `IdentityUserStore.setCredential`.
|
|
96
|
+
* @throws Error when no user with `userId` exists.
|
|
97
|
+
*/
|
|
98
|
+
setCredential(userId, credential) {
|
|
99
|
+
if (!this.#usersById.has(userId)) {
|
|
100
|
+
throw new Error(`User with id "${userId}" does not exist`);
|
|
101
|
+
}
|
|
102
|
+
this.#credentialsByUserId.set(userId, credential);
|
|
103
|
+
return Promise.resolve();
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* In-memory `ClientServiceInterface` implementation.
|
|
108
|
+
*
|
|
109
|
+
* A client is treated as confidential when {@link add} is called with a
|
|
110
|
+
* secret, and public when it is not — there is no separate flag to
|
|
111
|
+
* remember. `getAuthenticated` requires the secret for confidential
|
|
112
|
+
* clients and accepts a missing secret for public clients.
|
|
113
|
+
*/
|
|
114
|
+
export class MemoryClientService {
|
|
115
|
+
#clientsById = new Map();
|
|
116
|
+
#secretHashByClientId = new Map();
|
|
117
|
+
#ownerUserIdByClientId = new Map();
|
|
118
|
+
#userService;
|
|
119
|
+
/**
|
|
120
|
+
* Creates an empty client store backed by the given user service.
|
|
121
|
+
*
|
|
122
|
+
* @param userService Resolves the owner returned by {@linkcode getUser}
|
|
123
|
+
* for the client-credentials grant.
|
|
124
|
+
*/
|
|
125
|
+
constructor(userService) {
|
|
126
|
+
this.#userService = userService;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Registers a client. Pass `secret` to make it confidential; omit for a
|
|
130
|
+
* public client. `ownerUserId` is the user returned by {@link getUser},
|
|
131
|
+
* used by the client-credentials grant.
|
|
132
|
+
* @throws Error when a client with the same id is already registered.
|
|
133
|
+
*/
|
|
134
|
+
async add(client, secret, ownerUserId) {
|
|
135
|
+
if (this.#clientsById.has(client.id)) {
|
|
136
|
+
throw new Error(`Client with id "${client.id}" already exists`);
|
|
137
|
+
}
|
|
138
|
+
this.#clientsById.set(client.id, client);
|
|
139
|
+
if (secret) {
|
|
140
|
+
this.#secretHashByClientId.set(client.id, await sha256Hash(secret));
|
|
141
|
+
}
|
|
142
|
+
if (ownerUserId) {
|
|
143
|
+
this.#ownerUserIdByClientId.set(client.id, ownerUserId);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
/** Looks up a client by id, or resolves `undefined` when none is stored. */
|
|
147
|
+
get(id) {
|
|
148
|
+
return Promise.resolve(this.#clientsById.get(id));
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Resolves the client when authentication succeeds, otherwise `undefined`.
|
|
152
|
+
* Confidential clients (registered with a secret) require a matching
|
|
153
|
+
* `secret`; public clients (registered without one) ignore it.
|
|
154
|
+
*/
|
|
155
|
+
async getAuthenticated(id, secret) {
|
|
156
|
+
const client = this.#clientsById.get(id);
|
|
157
|
+
if (!client)
|
|
158
|
+
return undefined;
|
|
159
|
+
const storedHash = this.#secretHashByClientId.get(id);
|
|
160
|
+
if (storedHash) {
|
|
161
|
+
if (!secret)
|
|
162
|
+
return undefined;
|
|
163
|
+
if (await sha256Hash(secret) !== storedHash)
|
|
164
|
+
return undefined;
|
|
165
|
+
}
|
|
166
|
+
return client;
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Resolves the user registered as the client's owner via the `ownerUserId`
|
|
170
|
+
* passed to {@linkcode add}, or `undefined` when none was set. Accepts the
|
|
171
|
+
* client or its id.
|
|
172
|
+
*/
|
|
173
|
+
getUser(client) {
|
|
174
|
+
const id = typeof client === "string" ? client : client.id;
|
|
175
|
+
const userId = this.#ownerUserIdByClientId.get(id);
|
|
176
|
+
if (!userId)
|
|
177
|
+
return Promise.resolve(undefined);
|
|
178
|
+
return this.#userService.get(userId);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* In-memory `TokenServiceInterface` implementation. Tokens are hashed on
|
|
183
|
+
* write and looked up by hash, mirroring how a real database-backed token
|
|
184
|
+
* service would store them.
|
|
185
|
+
*
|
|
186
|
+
* Per-client lifetime overrides are supported via three optional fields the
|
|
187
|
+
* client may declare: `accessTokenLifetime`, `refreshTokenLifetime`, and
|
|
188
|
+
* `refreshTokenMaxLifetime`. Each takes precedence over the service-wide
|
|
189
|
+
* default when present.
|
|
190
|
+
*/
|
|
191
|
+
export class MemoryTokenService {
|
|
192
|
+
/** Service-wide access-token lifetime in seconds when a client sets none. */
|
|
193
|
+
accessTokenLifetime;
|
|
194
|
+
/** Service-wide refresh-token lifetime in seconds when a client sets none. */
|
|
195
|
+
refreshTokenLifetime;
|
|
196
|
+
/**
|
|
197
|
+
* Service-wide absolute refresh-token family lifetime in seconds when a
|
|
198
|
+
* client sets none. Undefined leaves families uncapped.
|
|
199
|
+
*/
|
|
200
|
+
refreshTokenMaxLifetime;
|
|
201
|
+
#clientService;
|
|
202
|
+
#userService;
|
|
203
|
+
#tokensByAccessHash = new Map();
|
|
204
|
+
#tokensByRefreshHash = new Map();
|
|
205
|
+
#tokensByCodeHash = new Map();
|
|
206
|
+
#revokedByRefreshHash = new Map();
|
|
207
|
+
/**
|
|
208
|
+
* Creates an empty token store.
|
|
209
|
+
*
|
|
210
|
+
* @param options Backing client/user services and optional default token
|
|
211
|
+
* lifetimes.
|
|
212
|
+
*/
|
|
213
|
+
constructor(options) {
|
|
214
|
+
this.#clientService = options.clientService;
|
|
215
|
+
this.#userService = options.userService;
|
|
216
|
+
this.accessTokenLifetime = options.accessTokenLifetime ?? 60 * 60;
|
|
217
|
+
this.refreshTokenLifetime = options.refreshTokenLifetime ??
|
|
218
|
+
14 * 24 * 60 * 60;
|
|
219
|
+
this.refreshTokenMaxLifetime = options.refreshTokenMaxLifetime;
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Accepts the requested scope verbatim; the stub performs no scope
|
|
223
|
+
* narrowing. Return `false` from a subclass to reject a request.
|
|
224
|
+
*/
|
|
225
|
+
acceptedScope(_client, _user, scope) {
|
|
226
|
+
return Promise.resolve(scope);
|
|
227
|
+
}
|
|
228
|
+
/** Mints a random opaque access token. */
|
|
229
|
+
generateAccessToken(_client, _user, _scope) {
|
|
230
|
+
return Promise.resolve(crypto.randomUUID());
|
|
231
|
+
}
|
|
232
|
+
/** Mints a random opaque refresh token. */
|
|
233
|
+
generateRefreshToken(_client, _user, _scope) {
|
|
234
|
+
return Promise.resolve(crypto.randomUUID());
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Returns now plus the client's `accessTokenLifetime` override, falling back
|
|
238
|
+
* to {@linkcode accessTokenLifetime}.
|
|
239
|
+
*/
|
|
240
|
+
accessTokenExpiresAt(client, _user, _scope) {
|
|
241
|
+
const lifetime = client.accessTokenLifetime ??
|
|
242
|
+
this.accessTokenLifetime;
|
|
243
|
+
return Promise.resolve(new Date(Date.now() + lifetime * 1000));
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Returns now plus the client's `refreshTokenLifetime` override, falling
|
|
247
|
+
* back to {@linkcode refreshTokenLifetime}.
|
|
248
|
+
*/
|
|
249
|
+
refreshTokenExpiresAt(client, _user, _scope) {
|
|
250
|
+
const lifetime = client.refreshTokenLifetime ??
|
|
251
|
+
this.refreshTokenLifetime;
|
|
252
|
+
return Promise.resolve(new Date(Date.now() + lifetime * 1000));
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Returns the family anchor plus the client's `refreshTokenMaxLifetime`
|
|
256
|
+
* override, falling back to {@linkcode refreshTokenMaxLifetime}, or
|
|
257
|
+
* `undefined` when neither caps the family.
|
|
258
|
+
*/
|
|
259
|
+
refreshTokenFamilyExpiresAt(client, _user, familyCreatedAt, _scope) {
|
|
260
|
+
const maxLifetime = client
|
|
261
|
+
.refreshTokenMaxLifetime ??
|
|
262
|
+
this.refreshTokenMaxLifetime;
|
|
263
|
+
return Promise.resolve(maxLifetime == null
|
|
264
|
+
? undefined
|
|
265
|
+
: new Date(familyCreatedAt.getTime() + maxLifetime * 1000));
|
|
266
|
+
}
|
|
267
|
+
async #hydrate(stored, accessToken, refreshToken) {
|
|
268
|
+
const client = await this.#clientService.get(stored.clientId);
|
|
269
|
+
if (!client)
|
|
270
|
+
return undefined;
|
|
271
|
+
const user = stored.userId
|
|
272
|
+
? await this.#userService.get(stored.userId)
|
|
273
|
+
: undefined;
|
|
274
|
+
if (stored.userId && !user)
|
|
275
|
+
return undefined;
|
|
276
|
+
const token = {
|
|
277
|
+
accessToken,
|
|
278
|
+
accessTokenExpiresAt: stored.accessTokenExpiresAt,
|
|
279
|
+
client,
|
|
280
|
+
user,
|
|
281
|
+
scope: stored.scope,
|
|
282
|
+
authenticationContext: snapshotAuthenticationContext(stored.authenticationContext),
|
|
283
|
+
};
|
|
284
|
+
if (stored.refreshTokenHash) {
|
|
285
|
+
return {
|
|
286
|
+
...token,
|
|
287
|
+
refreshToken: refreshToken ?? "",
|
|
288
|
+
refreshTokenExpiresAt: stored.refreshTokenExpiresAt,
|
|
289
|
+
familyId: stored.familyId,
|
|
290
|
+
familyCreatedAt: stored.familyCreatedAt,
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
return token;
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* Resolves the token whose access token hashes to the given value, with its
|
|
297
|
+
* client and user rehydrated, or `undefined` when not found.
|
|
298
|
+
*/
|
|
299
|
+
async getToken(accessToken) {
|
|
300
|
+
const stored = this.#tokensByAccessHash.get(await sha256Hash(accessToken));
|
|
301
|
+
if (!stored)
|
|
302
|
+
return undefined;
|
|
303
|
+
return this.#hydrate(stored, accessToken);
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* Resolves the refresh token whose value hashes to the given input, with its
|
|
307
|
+
* client and user rehydrated, or `undefined` when not found.
|
|
308
|
+
*/
|
|
309
|
+
async getRefreshToken(refreshToken) {
|
|
310
|
+
const stored = this.#tokensByRefreshHash.get(await sha256Hash(refreshToken));
|
|
311
|
+
if (!stored)
|
|
312
|
+
return undefined;
|
|
313
|
+
return this.#hydrate(stored, "", refreshToken);
|
|
314
|
+
}
|
|
315
|
+
async save(token) {
|
|
316
|
+
const accessTokenHash = await sha256Hash(token.accessToken);
|
|
317
|
+
const stored = {
|
|
318
|
+
accessTokenHash,
|
|
319
|
+
accessTokenExpiresAt: token.accessTokenExpiresAt,
|
|
320
|
+
clientId: token.client.id,
|
|
321
|
+
userId: token.user?.id,
|
|
322
|
+
scope: token.scope,
|
|
323
|
+
authenticationContext: snapshotAuthenticationContext(token.authenticationContext),
|
|
324
|
+
};
|
|
325
|
+
if (token.code) {
|
|
326
|
+
stored.codeHash = await sha256Hash(token.code);
|
|
327
|
+
let bucket = this.#tokensByCodeHash.get(stored.codeHash);
|
|
328
|
+
if (!bucket) {
|
|
329
|
+
bucket = new Set();
|
|
330
|
+
this.#tokensByCodeHash.set(stored.codeHash, bucket);
|
|
331
|
+
}
|
|
332
|
+
bucket.add(accessTokenHash);
|
|
333
|
+
}
|
|
334
|
+
if ("refreshToken" in token) {
|
|
335
|
+
const rt = token;
|
|
336
|
+
stored.refreshTokenHash = await sha256Hash(rt.refreshToken);
|
|
337
|
+
stored.refreshTokenExpiresAt = rt.refreshTokenExpiresAt;
|
|
338
|
+
stored.familyId = rt.familyId;
|
|
339
|
+
stored.familyCreatedAt = rt.familyCreatedAt;
|
|
340
|
+
this.#tokensByRefreshHash.set(stored.refreshTokenHash, stored);
|
|
341
|
+
}
|
|
342
|
+
this.#tokensByAccessHash.set(accessTokenHash, stored);
|
|
343
|
+
return token;
|
|
344
|
+
}
|
|
345
|
+
async revoke(token, hint) {
|
|
346
|
+
if (typeof token !== "string") {
|
|
347
|
+
if ("refreshToken" in token && token.refreshToken) {
|
|
348
|
+
hint = "refresh_token";
|
|
349
|
+
token = token.refreshToken;
|
|
350
|
+
}
|
|
351
|
+
else {
|
|
352
|
+
token = token.accessToken;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
const hashed = await sha256Hash(token);
|
|
356
|
+
const stored = hint === "refresh_token"
|
|
357
|
+
? this.#tokensByRefreshHash.get(hashed)
|
|
358
|
+
: this.#tokensByAccessHash.get(hashed);
|
|
359
|
+
if (!stored)
|
|
360
|
+
return false;
|
|
361
|
+
this.#tokensByAccessHash.delete(stored.accessTokenHash);
|
|
362
|
+
if (stored.refreshTokenHash) {
|
|
363
|
+
this.#tombstone(stored.refreshTokenHash, stored);
|
|
364
|
+
this.#tokensByRefreshHash.delete(stored.refreshTokenHash);
|
|
365
|
+
}
|
|
366
|
+
return true;
|
|
367
|
+
}
|
|
368
|
+
#tombstone(refreshTokenHash, stored) {
|
|
369
|
+
this.#revokedByRefreshHash.set(refreshTokenHash, stored);
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* Looks up a revoked/rotated-out refresh token for the refresh grant's
|
|
373
|
+
* reuse detection: revoked refresh records are kept as tombstones instead
|
|
374
|
+
* of deleted.
|
|
375
|
+
*/
|
|
376
|
+
async getRevokedRefreshToken(refreshToken) {
|
|
377
|
+
const stored = this.#revokedByRefreshHash.get(await sha256Hash(refreshToken));
|
|
378
|
+
if (!stored)
|
|
379
|
+
return undefined;
|
|
380
|
+
return this.#hydrate(stored, "", refreshToken);
|
|
381
|
+
}
|
|
382
|
+
/**
|
|
383
|
+
* Revokes every live token in a rotation family (reuse detection response).
|
|
384
|
+
* Resolves `true` when at least one live token was removed.
|
|
385
|
+
*/
|
|
386
|
+
revokeFamily(familyId) {
|
|
387
|
+
let revoked = false;
|
|
388
|
+
for (const [hash, stored] of this.#tokensByRefreshHash) {
|
|
389
|
+
if (stored.familyId === familyId) {
|
|
390
|
+
this.#tombstone(hash, stored);
|
|
391
|
+
this.#tokensByRefreshHash.delete(hash);
|
|
392
|
+
this.#tokensByAccessHash.delete(stored.accessTokenHash);
|
|
393
|
+
revoked = true;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
for (const [hash, stored] of this.#tokensByAccessHash) {
|
|
397
|
+
if (stored.familyId === familyId) {
|
|
398
|
+
this.#tokensByAccessHash.delete(hash);
|
|
399
|
+
revoked = true;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
return Promise.resolve(revoked);
|
|
403
|
+
}
|
|
404
|
+
/**
|
|
405
|
+
* Removes every token issued from the given authorization code, used when a
|
|
406
|
+
* code is replayed. Resolves `true` when at least one token was removed.
|
|
407
|
+
*/
|
|
408
|
+
async revokeCode(code) {
|
|
409
|
+
const codeHash = await sha256Hash(code);
|
|
410
|
+
const tokenHashes = this.#tokensByCodeHash.get(codeHash);
|
|
411
|
+
if (!tokenHashes || tokenHashes.size === 0)
|
|
412
|
+
return false;
|
|
413
|
+
for (const accessTokenHash of tokenHashes) {
|
|
414
|
+
const stored = this.#tokensByAccessHash.get(accessTokenHash);
|
|
415
|
+
if (stored) {
|
|
416
|
+
this.#tokensByAccessHash.delete(accessTokenHash);
|
|
417
|
+
if (stored.refreshTokenHash) {
|
|
418
|
+
this.#tokensByRefreshHash.delete(stored.refreshTokenHash);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
this.#tokensByCodeHash.delete(codeHash);
|
|
423
|
+
return true;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
/** In-memory `AuthorizationCodeServiceInterface` implementation. */
|
|
427
|
+
export class MemoryAuthorizationCodeService {
|
|
428
|
+
/** Authorization-code lifetime in seconds applied by {@linkcode expiresAt}. */
|
|
429
|
+
lifetime;
|
|
430
|
+
#clientService;
|
|
431
|
+
#userService;
|
|
432
|
+
#codes = new Map();
|
|
433
|
+
/**
|
|
434
|
+
* Creates an empty authorization-code store.
|
|
435
|
+
*
|
|
436
|
+
* @param options Backing client/user services and optional code lifetime.
|
|
437
|
+
*/
|
|
438
|
+
constructor(options) {
|
|
439
|
+
this.#clientService = options.clientService;
|
|
440
|
+
this.#userService = options.userService;
|
|
441
|
+
this.lifetime = options.lifetime ?? 600;
|
|
442
|
+
}
|
|
443
|
+
/** Mints a random opaque authorization code. */
|
|
444
|
+
generateCode(_client, _user, _scope) {
|
|
445
|
+
return Promise.resolve(crypto.randomUUID());
|
|
446
|
+
}
|
|
447
|
+
/** Returns now plus {@linkcode lifetime}. */
|
|
448
|
+
expiresAt(_client, _user, _scope) {
|
|
449
|
+
return Promise.resolve(new Date(Date.now() + this.lifetime * 1000));
|
|
450
|
+
}
|
|
451
|
+
/**
|
|
452
|
+
* Resolves the code whose value hashes to the given input, with its client
|
|
453
|
+
* and user rehydrated, or `undefined` when not found or either is missing.
|
|
454
|
+
*/
|
|
455
|
+
async get(code) {
|
|
456
|
+
const stored = this.#codes.get(await sha256Hash(code));
|
|
457
|
+
if (!stored)
|
|
458
|
+
return undefined;
|
|
459
|
+
const client = await this.#clientService.get(stored.clientId);
|
|
460
|
+
const user = await this.#userService.get(stored.userId);
|
|
461
|
+
if (!client || !user)
|
|
462
|
+
return undefined;
|
|
463
|
+
return {
|
|
464
|
+
code,
|
|
465
|
+
expiresAt: stored.expiresAt,
|
|
466
|
+
client,
|
|
467
|
+
user,
|
|
468
|
+
scope: stored.scope,
|
|
469
|
+
redirectUri: stored.redirectUri,
|
|
470
|
+
challenge: stored.challenge,
|
|
471
|
+
challengeMethod: stored.challengeMethod,
|
|
472
|
+
nonce: stored.nonce,
|
|
473
|
+
authenticationContext: snapshotAuthenticationContext(stored.authenticationContext),
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
/**
|
|
477
|
+
* Persists an authorization code indexed by its hash, including the PKCE
|
|
478
|
+
* challenge and redirect URI. Returns the input unchanged.
|
|
479
|
+
*/
|
|
480
|
+
async save(authorizationCode) {
|
|
481
|
+
const hashed = await sha256Hash(authorizationCode.code);
|
|
482
|
+
this.#codes.set(hashed, {
|
|
483
|
+
codeHash: hashed,
|
|
484
|
+
expiresAt: authorizationCode.expiresAt,
|
|
485
|
+
clientId: authorizationCode.client.id,
|
|
486
|
+
userId: authorizationCode.user.id,
|
|
487
|
+
scope: authorizationCode.scope,
|
|
488
|
+
redirectUri: authorizationCode.redirectUri,
|
|
489
|
+
challenge: authorizationCode.challenge,
|
|
490
|
+
challengeMethod: authorizationCode.challengeMethod,
|
|
491
|
+
nonce: authorizationCode.nonce,
|
|
492
|
+
authenticationContext: snapshotAuthenticationContext(authorizationCode.authenticationContext),
|
|
493
|
+
});
|
|
494
|
+
return authorizationCode;
|
|
495
|
+
}
|
|
496
|
+
/**
|
|
497
|
+
* Removes a code, accepting the record or its raw string. Resolves `true`
|
|
498
|
+
* when a code was removed, `false` otherwise.
|
|
499
|
+
*/
|
|
500
|
+
async revoke(authorizationCode) {
|
|
501
|
+
const code = typeof authorizationCode === "string"
|
|
502
|
+
? authorizationCode
|
|
503
|
+
: authorizationCode.code;
|
|
504
|
+
return this.#codes.delete(await sha256Hash(code));
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
/** In-memory `DeviceAuthorizationServiceInterface` implementation. */
|
|
508
|
+
export class MemoryDeviceAuthorizationService {
|
|
509
|
+
/** Device-authorization lifetime in seconds applied by {@linkcode expiresAt}. */
|
|
510
|
+
lifetime;
|
|
511
|
+
/** Minimum seconds clients must wait between token-endpoint polls. */
|
|
512
|
+
interval;
|
|
513
|
+
#clientService;
|
|
514
|
+
#userService;
|
|
515
|
+
#byDeviceCode = new Map();
|
|
516
|
+
#byUserCode = new Map();
|
|
517
|
+
/**
|
|
518
|
+
* Creates an empty device-authorization store.
|
|
519
|
+
*
|
|
520
|
+
* @param options Backing client/user services and optional lifetime and
|
|
521
|
+
* polling interval.
|
|
522
|
+
*/
|
|
523
|
+
constructor(options) {
|
|
524
|
+
this.#clientService = options.clientService;
|
|
525
|
+
this.#userService = options.userService;
|
|
526
|
+
this.lifetime = options.lifetime ?? 1800;
|
|
527
|
+
this.interval = options.interval ?? 5;
|
|
528
|
+
}
|
|
529
|
+
/** Mints a random opaque device code (polled by the device). */
|
|
530
|
+
generateDeviceCode(_client, _scope) {
|
|
531
|
+
return Promise.resolve(crypto.randomUUID());
|
|
532
|
+
}
|
|
533
|
+
/**
|
|
534
|
+
* Mints a short, human-enterable user code in `XXXX-XXXX` form drawn from an
|
|
535
|
+
* unambiguous alphabet (no vowels or look-alike digits).
|
|
536
|
+
*/
|
|
537
|
+
generateUserCode(_client, _scope) {
|
|
538
|
+
const chars = "BCDFGHJKLMNPQRSTVWXYZ23456789";
|
|
539
|
+
let code = "";
|
|
540
|
+
const array = new Uint8Array(8);
|
|
541
|
+
crypto.getRandomValues(array);
|
|
542
|
+
for (let i = 0; i < 8; i++) {
|
|
543
|
+
code += chars[array[i] % chars.length];
|
|
544
|
+
if (i === 3)
|
|
545
|
+
code += "-";
|
|
546
|
+
}
|
|
547
|
+
return Promise.resolve(code);
|
|
548
|
+
}
|
|
549
|
+
/** Returns now plus {@linkcode lifetime}. */
|
|
550
|
+
expiresAt(_client, _scope) {
|
|
551
|
+
return Promise.resolve(new Date(Date.now() + this.lifetime * 1000));
|
|
552
|
+
}
|
|
553
|
+
async #hydrate(stored, deviceCode, userCode) {
|
|
554
|
+
const client = await this.#clientService.get(stored.clientId);
|
|
555
|
+
if (!client)
|
|
556
|
+
return undefined;
|
|
557
|
+
const user = stored.userId
|
|
558
|
+
? await this.#userService.get(stored.userId)
|
|
559
|
+
: undefined;
|
|
560
|
+
if (stored.userId && !user)
|
|
561
|
+
return undefined;
|
|
562
|
+
return {
|
|
563
|
+
deviceCode,
|
|
564
|
+
userCode,
|
|
565
|
+
expiresAt: stored.expiresAt,
|
|
566
|
+
client,
|
|
567
|
+
scope: stored.scope,
|
|
568
|
+
interval: stored.interval,
|
|
569
|
+
authorized: stored.authorized,
|
|
570
|
+
user,
|
|
571
|
+
denied: stored.denied,
|
|
572
|
+
lastPolled: stored.lastPolled,
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
/**
|
|
576
|
+
* Persists a device authorization indexed by both its hashed device code and
|
|
577
|
+
* hashed user code. Returns the input unchanged.
|
|
578
|
+
*/
|
|
579
|
+
async save(authorization) {
|
|
580
|
+
const deviceCodeHash = await sha256Hash(authorization.deviceCode);
|
|
581
|
+
const userCodeHash = await sha256Hash(authorization.userCode);
|
|
582
|
+
const stored = {
|
|
583
|
+
deviceCodeHash,
|
|
584
|
+
userCodeHash,
|
|
585
|
+
expiresAt: authorization.expiresAt,
|
|
586
|
+
clientId: authorization.client.id,
|
|
587
|
+
scope: authorization.scope,
|
|
588
|
+
interval: authorization.interval,
|
|
589
|
+
authorized: authorization.authorized,
|
|
590
|
+
userId: authorization.user?.id,
|
|
591
|
+
denied: authorization.denied,
|
|
592
|
+
lastPolled: authorization.lastPolled,
|
|
593
|
+
};
|
|
594
|
+
this.#byDeviceCode.set(deviceCodeHash, stored);
|
|
595
|
+
this.#byUserCode.set(userCodeHash, stored);
|
|
596
|
+
return authorization;
|
|
597
|
+
}
|
|
598
|
+
/**
|
|
599
|
+
* Resolves the authorization for a device code (the value the device polls
|
|
600
|
+
* with), client and user rehydrated, or `undefined` when not found.
|
|
601
|
+
*/
|
|
602
|
+
async getByDeviceCode(deviceCode) {
|
|
603
|
+
const stored = this.#byDeviceCode.get(await sha256Hash(deviceCode));
|
|
604
|
+
if (!stored)
|
|
605
|
+
return undefined;
|
|
606
|
+
return this.#hydrate(stored, deviceCode, "");
|
|
607
|
+
}
|
|
608
|
+
/**
|
|
609
|
+
* Resolves the authorization for a user code (the value the user enters in a
|
|
610
|
+
* browser), client and user rehydrated, or `undefined` when not found.
|
|
611
|
+
*/
|
|
612
|
+
async getByUserCode(userCode) {
|
|
613
|
+
const stored = this.#byUserCode.get(await sha256Hash(userCode));
|
|
614
|
+
if (!stored)
|
|
615
|
+
return undefined;
|
|
616
|
+
return this.#hydrate(stored, "", userCode);
|
|
617
|
+
}
|
|
618
|
+
async #findStored(authorization) {
|
|
619
|
+
if (authorization.deviceCode) {
|
|
620
|
+
const stored = this.#byDeviceCode.get(await sha256Hash(authorization.deviceCode));
|
|
621
|
+
if (stored)
|
|
622
|
+
return stored;
|
|
623
|
+
}
|
|
624
|
+
if (authorization.userCode) {
|
|
625
|
+
const stored = this.#byUserCode.get(await sha256Hash(authorization.userCode));
|
|
626
|
+
if (stored)
|
|
627
|
+
return stored;
|
|
628
|
+
}
|
|
629
|
+
return undefined;
|
|
630
|
+
}
|
|
631
|
+
/**
|
|
632
|
+
* Marks the authorization granted by `user`, optionally narrowing `scope`,
|
|
633
|
+
* so a subsequent device-code poll can exchange it for a token. Returns the
|
|
634
|
+
* updated record. Throws when the authorization is not stored.
|
|
635
|
+
*/
|
|
636
|
+
async approve(authorization, user, scope) {
|
|
637
|
+
const stored = await this.#findStored(authorization);
|
|
638
|
+
if (!stored) {
|
|
639
|
+
throw new Error("approve: device authorization not found");
|
|
640
|
+
}
|
|
641
|
+
stored.authorized = true;
|
|
642
|
+
stored.userId = user.id;
|
|
643
|
+
if (scope !== undefined)
|
|
644
|
+
stored.scope = scope;
|
|
645
|
+
return {
|
|
646
|
+
...authorization,
|
|
647
|
+
authorized: true,
|
|
648
|
+
user,
|
|
649
|
+
scope: stored.scope,
|
|
650
|
+
};
|
|
651
|
+
}
|
|
652
|
+
/**
|
|
653
|
+
* Marks the authorization denied so a subsequent poll is rejected. Returns
|
|
654
|
+
* the updated record. Throws when the authorization is not stored.
|
|
655
|
+
*/
|
|
656
|
+
async deny(authorization) {
|
|
657
|
+
const stored = await this.#findStored(authorization);
|
|
658
|
+
if (!stored) {
|
|
659
|
+
throw new Error("deny: device authorization not found");
|
|
660
|
+
}
|
|
661
|
+
stored.denied = true;
|
|
662
|
+
return { ...authorization, denied: true };
|
|
663
|
+
}
|
|
664
|
+
/**
|
|
665
|
+
* Records that the device just polled (and stores its current interval) so
|
|
666
|
+
* `slow_down` rate limiting can be enforced. Returns the updated record.
|
|
667
|
+
* Throws when the authorization is not stored.
|
|
668
|
+
*/
|
|
669
|
+
async updateLastPolled(authorization) {
|
|
670
|
+
const stored = await this.#findStored(authorization);
|
|
671
|
+
if (!stored) {
|
|
672
|
+
throw new Error("updateLastPolled: device authorization not found");
|
|
673
|
+
}
|
|
674
|
+
stored.lastPolled = new Date();
|
|
675
|
+
stored.interval = authorization.interval;
|
|
676
|
+
return { ...authorization, lastPolled: stored.lastPolled };
|
|
677
|
+
}
|
|
678
|
+
/**
|
|
679
|
+
* Removes a device authorization, accepting a raw device code or any record
|
|
680
|
+
* this store returns — including the partially hydrated ones from
|
|
681
|
+
* {@linkcode getByDeviceCode} and {@linkcode getByUserCode}, which are
|
|
682
|
+
* resolved back to the stored entry so both its device-code and user-code
|
|
683
|
+
* index entries are dropped. Resolves `true` only for the call that removed
|
|
684
|
+
* a live authorization, `false` otherwise.
|
|
685
|
+
*/
|
|
686
|
+
async revoke(authorization) {
|
|
687
|
+
if (typeof authorization === "string") {
|
|
688
|
+
const hashed = await sha256Hash(authorization);
|
|
689
|
+
const stored = this.#byDeviceCode.get(hashed);
|
|
690
|
+
if (!stored)
|
|
691
|
+
return false;
|
|
692
|
+
this.#byDeviceCode.delete(hashed);
|
|
693
|
+
this.#byUserCode.delete(stored.userCodeHash);
|
|
694
|
+
return true;
|
|
695
|
+
}
|
|
696
|
+
const stored = await this.#findStored(authorization);
|
|
697
|
+
if (!stored)
|
|
698
|
+
return false;
|
|
699
|
+
const deleted = this.#byDeviceCode.delete(stored.deviceCodeHash);
|
|
700
|
+
this.#byUserCode.delete(stored.userCodeHash);
|
|
701
|
+
return deleted;
|
|
702
|
+
}
|
|
703
|
+
}
|