@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,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Contract test suite for {@link DeviceAuthorizationServiceInterface}
|
|
3
|
+
* implementations (RFC 8628).
|
|
4
|
+
*
|
|
5
|
+
* @module
|
|
6
|
+
*/
|
|
7
|
+
import { assert, assertEquals, assertStrictEquals } from "../../deps/jsr.io/@std/assert/1.0.19/mod.js";
|
|
8
|
+
import { beforeEach, describe, it } from "../../deps/jsr.io/@std/testing/1.0.20/bdd.js";
|
|
9
|
+
const defaultMakeUser = (seq) => ({ id: `u${seq}`, username: `user${seq}` });
|
|
10
|
+
const defaultMakeClient = (seq) => ({
|
|
11
|
+
id: `client-${seq}`,
|
|
12
|
+
grants: ["urn:ietf:params:oauth:grant-type:device_code"],
|
|
13
|
+
});
|
|
14
|
+
/** Runs the {@link DeviceAuthorizationServiceInterface} contract suite. */
|
|
15
|
+
export function runDeviceAuthorizationServiceContractTests(options) {
|
|
16
|
+
const makeUser = options.makeUser ?? (defaultMakeUser);
|
|
17
|
+
const makeClient = options.makeClient ?? (defaultMakeClient);
|
|
18
|
+
describe(options.describeName ??
|
|
19
|
+
"DeviceAuthorizationServiceInterface contract", () => {
|
|
20
|
+
let userService;
|
|
21
|
+
let clientService;
|
|
22
|
+
let deviceService;
|
|
23
|
+
let user;
|
|
24
|
+
let client;
|
|
25
|
+
beforeEach(async () => {
|
|
26
|
+
const services = await options.makeServices();
|
|
27
|
+
userService = services.userService;
|
|
28
|
+
clientService = services.clientService;
|
|
29
|
+
deviceService = services.deviceAuthorizationService;
|
|
30
|
+
user = makeUser(1);
|
|
31
|
+
client = makeClient(1);
|
|
32
|
+
await options.addUser(userService, user, "pw");
|
|
33
|
+
await options.addClient(clientService, client);
|
|
34
|
+
});
|
|
35
|
+
async function makeDeviceAuth() {
|
|
36
|
+
return {
|
|
37
|
+
deviceCode: await deviceService.generateDeviceCode(client),
|
|
38
|
+
userCode: await deviceService.generateUserCode(client),
|
|
39
|
+
expiresAt: await deviceService.expiresAt(client),
|
|
40
|
+
client,
|
|
41
|
+
interval: deviceService.interval,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
it("generateDeviceCode and generateUserCode produce unique strings", async () => {
|
|
45
|
+
const d1 = await deviceService.generateDeviceCode(client);
|
|
46
|
+
const d2 = await deviceService.generateDeviceCode(client);
|
|
47
|
+
const u1 = await deviceService.generateUserCode(client);
|
|
48
|
+
const u2 = await deviceService.generateUserCode(client);
|
|
49
|
+
assert(d1 !== d2, "device codes should not collide");
|
|
50
|
+
assert(u1 !== u2, "user codes should not collide");
|
|
51
|
+
});
|
|
52
|
+
it("save + lookup by device code", async () => {
|
|
53
|
+
const auth = await makeDeviceAuth();
|
|
54
|
+
await deviceService.save(auth);
|
|
55
|
+
const fetched = await deviceService.getByDeviceCode(auth.deviceCode);
|
|
56
|
+
assertEquals(fetched?.deviceCode, auth.deviceCode);
|
|
57
|
+
assertEquals(fetched?.client.id, client.id);
|
|
58
|
+
});
|
|
59
|
+
it("save + lookup by user code", async () => {
|
|
60
|
+
const auth = await makeDeviceAuth();
|
|
61
|
+
await deviceService.save(auth);
|
|
62
|
+
const fetched = await deviceService.getByUserCode(auth.userCode);
|
|
63
|
+
assertEquals(fetched?.userCode, auth.userCode);
|
|
64
|
+
});
|
|
65
|
+
it("getByDeviceCode / getByUserCode return undefined for unknown codes", async () => {
|
|
66
|
+
assertStrictEquals(await deviceService.getByDeviceCode("missing"), undefined);
|
|
67
|
+
assertStrictEquals(await deviceService.getByUserCode("missing"), undefined);
|
|
68
|
+
});
|
|
69
|
+
it("approve marks the authorization with the approving user", async () => {
|
|
70
|
+
const auth = await makeDeviceAuth();
|
|
71
|
+
await deviceService.save(auth);
|
|
72
|
+
const approved = await deviceService.approve(auth, user);
|
|
73
|
+
assertStrictEquals(approved.authorized, true);
|
|
74
|
+
assertEquals(approved.user.id, user.id);
|
|
75
|
+
const refetched = await deviceService.getByDeviceCode(auth.deviceCode);
|
|
76
|
+
assertStrictEquals(refetched?.authorized, true);
|
|
77
|
+
});
|
|
78
|
+
it("deny marks the authorization as denied", async () => {
|
|
79
|
+
const auth = await makeDeviceAuth();
|
|
80
|
+
await deviceService.save(auth);
|
|
81
|
+
const denied = await deviceService.deny(auth);
|
|
82
|
+
assertStrictEquals(denied.denied, true);
|
|
83
|
+
const refetched = await deviceService.getByDeviceCode(auth.deviceCode);
|
|
84
|
+
assertStrictEquals(refetched?.denied, true);
|
|
85
|
+
});
|
|
86
|
+
it("updateLastPolled records a poll time", async () => {
|
|
87
|
+
const auth = await makeDeviceAuth();
|
|
88
|
+
await deviceService.save(auth);
|
|
89
|
+
const updated = await deviceService.updateLastPolled(auth);
|
|
90
|
+
assert(updated.lastPolled instanceof Date);
|
|
91
|
+
const refetched = await deviceService.getByDeviceCode(auth.deviceCode);
|
|
92
|
+
assert(refetched?.lastPolled instanceof Date);
|
|
93
|
+
});
|
|
94
|
+
it("revoke deletes by object and by string", async () => {
|
|
95
|
+
const auth1 = await makeDeviceAuth();
|
|
96
|
+
await deviceService.save(auth1);
|
|
97
|
+
assertStrictEquals(await deviceService.revoke(auth1), true);
|
|
98
|
+
assertStrictEquals(await deviceService.getByDeviceCode(auth1.deviceCode), undefined);
|
|
99
|
+
const auth2 = await makeDeviceAuth();
|
|
100
|
+
await deviceService.save(auth2);
|
|
101
|
+
assertStrictEquals(await deviceService.revoke(auth2.deviceCode), true);
|
|
102
|
+
assertStrictEquals(await deviceService.getByDeviceCode(auth2.deviceCode), undefined);
|
|
103
|
+
});
|
|
104
|
+
it("revoke answers true only for the call that removed a live authorization", async () => {
|
|
105
|
+
const auth = await makeDeviceAuth();
|
|
106
|
+
await deviceService.save(auth);
|
|
107
|
+
assertStrictEquals(await deviceService.revoke(auth), true);
|
|
108
|
+
assertStrictEquals(await deviceService.revoke(auth), false);
|
|
109
|
+
assertStrictEquals(await deviceService.revoke(auth.deviceCode), false);
|
|
110
|
+
});
|
|
111
|
+
async function saveDeviceAuth() {
|
|
112
|
+
const auth = await makeDeviceAuth();
|
|
113
|
+
await deviceService.save(auth);
|
|
114
|
+
const byDeviceCode = await deviceService.getByDeviceCode(auth.deviceCode);
|
|
115
|
+
const byUserCode = await deviceService.getByUserCode(auth.userCode);
|
|
116
|
+
assert(byDeviceCode, "getByDeviceCode should resolve what was saved");
|
|
117
|
+
assert(byUserCode, "getByUserCode should resolve what was saved");
|
|
118
|
+
return { auth, byDeviceCode, byUserCode };
|
|
119
|
+
}
|
|
120
|
+
async function assertUnreachableByEitherCode(auth) {
|
|
121
|
+
assertStrictEquals(await deviceService.getByDeviceCode(auth.deviceCode), undefined, "a revoked authorization must not resolve by its device code");
|
|
122
|
+
assertStrictEquals(await deviceService.getByUserCode(auth.userCode), undefined, "a revoked authorization must not resolve by its user code");
|
|
123
|
+
}
|
|
124
|
+
it("revoke by full record clears both the device-code and user-code lookups", async () => {
|
|
125
|
+
const { auth } = await saveDeviceAuth();
|
|
126
|
+
assertStrictEquals(await deviceService.revoke(auth), true);
|
|
127
|
+
await assertUnreachableByEitherCode(auth);
|
|
128
|
+
});
|
|
129
|
+
it("revoke by raw device code clears both the device-code and user-code lookups", async () => {
|
|
130
|
+
const { auth } = await saveDeviceAuth();
|
|
131
|
+
assertStrictEquals(await deviceService.revoke(auth.deviceCode), true);
|
|
132
|
+
await assertUnreachableByEitherCode(auth);
|
|
133
|
+
});
|
|
134
|
+
it("revoke accepts the record getByDeviceCode returns and clears both lookups", async () => {
|
|
135
|
+
const { auth, byDeviceCode } = await saveDeviceAuth();
|
|
136
|
+
assertStrictEquals(await deviceService.revoke(byDeviceCode), true);
|
|
137
|
+
await assertUnreachableByEitherCode(auth);
|
|
138
|
+
});
|
|
139
|
+
it("revoke accepts the record getByUserCode returns and clears both lookups", async () => {
|
|
140
|
+
const { auth, byUserCode } = await saveDeviceAuth();
|
|
141
|
+
assertStrictEquals(await deviceService.revoke(byUserCode), true);
|
|
142
|
+
await assertUnreachableByEitherCode(auth);
|
|
143
|
+
});
|
|
144
|
+
it("revoke answers true once across the hydrated shapes of one authorization", async () => {
|
|
145
|
+
const { byDeviceCode, byUserCode } = await saveDeviceAuth();
|
|
146
|
+
assertStrictEquals(await deviceService.revoke(byDeviceCode), true);
|
|
147
|
+
assertStrictEquals(await deviceService.revoke(byUserCode), false);
|
|
148
|
+
});
|
|
149
|
+
it("revoke answers true to exactly one of two concurrent claims", async () => {
|
|
150
|
+
const { auth, byDeviceCode } = await saveDeviceAuth();
|
|
151
|
+
const claims = await Promise.all([
|
|
152
|
+
deviceService.revoke(byDeviceCode),
|
|
153
|
+
deviceService.revoke(byDeviceCode),
|
|
154
|
+
]);
|
|
155
|
+
assertEquals(claims.filter(Boolean).length, 1, "concurrent polls of one device code must not both be told they claimed it");
|
|
156
|
+
await assertUnreachableByEitherCode(auth);
|
|
157
|
+
});
|
|
158
|
+
it("revoke of a user-code-hydrated record consumes the device code", async () => {
|
|
159
|
+
const { auth, byUserCode } = await saveDeviceAuth();
|
|
160
|
+
await deviceService.approve(byUserCode, user);
|
|
161
|
+
await deviceService.revoke(byUserCode);
|
|
162
|
+
assertStrictEquals(await deviceService.getByDeviceCode(auth.deviceCode), undefined, "a cancelled authorization must not stay redeemable by its device code");
|
|
163
|
+
});
|
|
164
|
+
it("approve accepts the record getByUserCode returns", async () => {
|
|
165
|
+
const { auth, byUserCode } = await saveDeviceAuth();
|
|
166
|
+
const approved = await deviceService.approve(byUserCode, user);
|
|
167
|
+
assertStrictEquals(approved.authorized, true);
|
|
168
|
+
const refetched = await deviceService.getByDeviceCode(auth.deviceCode);
|
|
169
|
+
assertStrictEquals(refetched?.authorized, true);
|
|
170
|
+
assertEquals(refetched?.user?.id, user.id);
|
|
171
|
+
});
|
|
172
|
+
it("approve accepts the record getByDeviceCode returns", async () => {
|
|
173
|
+
const { auth, byDeviceCode } = await saveDeviceAuth();
|
|
174
|
+
await deviceService.approve(byDeviceCode, user);
|
|
175
|
+
const refetched = await deviceService.getByUserCode(auth.userCode);
|
|
176
|
+
assertStrictEquals(refetched?.authorized, true);
|
|
177
|
+
});
|
|
178
|
+
it("deny accepts the record getByUserCode returns", async () => {
|
|
179
|
+
const { auth, byUserCode } = await saveDeviceAuth();
|
|
180
|
+
const denied = await deviceService.deny(byUserCode);
|
|
181
|
+
assertStrictEquals(denied.denied, true);
|
|
182
|
+
const refetched = await deviceService.getByDeviceCode(auth.deviceCode);
|
|
183
|
+
assertStrictEquals(refetched?.denied, true);
|
|
184
|
+
});
|
|
185
|
+
it("updateLastPolled accepts the record getByDeviceCode returns", async () => {
|
|
186
|
+
const { auth, byDeviceCode } = await saveDeviceAuth();
|
|
187
|
+
const updated = await deviceService.updateLastPolled(byDeviceCode);
|
|
188
|
+
assert(updated.lastPolled instanceof Date);
|
|
189
|
+
const refetched = await deviceService.getByDeviceCode(auth.deviceCode);
|
|
190
|
+
assert(refetched?.lastPolled instanceof Date, "the poll time must be visible to the next read");
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Contract test suite for {@link LockoutStore} implementations — the
|
|
3
|
+
* per-account failure counter behind `AccountLockout`.
|
|
4
|
+
*
|
|
5
|
+
* `AccountLockout` locks on the failure count `increment` returns, so a store
|
|
6
|
+
* that lets concurrent failed sign-ins share a count undershoots the threshold:
|
|
7
|
+
* the brute-force floor the lockout is meant to be moves up by however many
|
|
8
|
+
* attempts an attacker can land at once.
|
|
9
|
+
*
|
|
10
|
+
* @example Verify a database-backed store
|
|
11
|
+
* ```ts
|
|
12
|
+
* import { runLockoutStoreContractTests } from "@udibo/oauth2/testing/contract";
|
|
13
|
+
* import type { LockoutStore } from "@udibo/oauth2/identity";
|
|
14
|
+
*
|
|
15
|
+
* declare function freshLockoutStore(): Promise<LockoutStore>;
|
|
16
|
+
*
|
|
17
|
+
* runLockoutStoreContractTests({
|
|
18
|
+
* describeName: "DrizzleLockoutStore satisfies LockoutStore contract",
|
|
19
|
+
* makeStore: freshLockoutStore,
|
|
20
|
+
* });
|
|
21
|
+
* ```
|
|
22
|
+
*
|
|
23
|
+
* @module
|
|
24
|
+
*/
|
|
25
|
+
import type { LockoutStore } from "../../identity/lockout.js";
|
|
26
|
+
/** Options for {@link runLockoutStoreContractTests}. */
|
|
27
|
+
export interface LockoutStoreContractOptions {
|
|
28
|
+
/**
|
|
29
|
+
* Returns a fresh, empty store for each test. Isolation is required — a
|
|
30
|
+
* failure count surviving into the next test shifts every count the suite
|
|
31
|
+
* asserts.
|
|
32
|
+
*/
|
|
33
|
+
makeStore(): Promise<LockoutStore> | LockoutStore;
|
|
34
|
+
/**
|
|
35
|
+
* Overrides the name passed to the outer `describe` block. Defaults to
|
|
36
|
+
* `"LockoutStore contract"`.
|
|
37
|
+
*/
|
|
38
|
+
describeName?: string;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Runs the {@link LockoutStore} contract suite. Call it from your own test
|
|
42
|
+
* file — it registers `describe` / `it` blocks the test runner picks up.
|
|
43
|
+
*
|
|
44
|
+
* `now` is passed explicitly on every call, so the lock-expiry cases are
|
|
45
|
+
* clock-free and never need a timer.
|
|
46
|
+
*/
|
|
47
|
+
export declare function runLockoutStoreContractTests(options: LockoutStoreContractOptions): void;
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Contract test suite for {@link LockoutStore} implementations — the
|
|
3
|
+
* per-account failure counter behind `AccountLockout`.
|
|
4
|
+
*
|
|
5
|
+
* `AccountLockout` locks on the failure count `increment` returns, so a store
|
|
6
|
+
* that lets concurrent failed sign-ins share a count undershoots the threshold:
|
|
7
|
+
* the brute-force floor the lockout is meant to be moves up by however many
|
|
8
|
+
* attempts an attacker can land at once.
|
|
9
|
+
*
|
|
10
|
+
* @example Verify a database-backed store
|
|
11
|
+
* ```ts
|
|
12
|
+
* import { runLockoutStoreContractTests } from "@udibo/oauth2/testing/contract";
|
|
13
|
+
* import type { LockoutStore } from "@udibo/oauth2/identity";
|
|
14
|
+
*
|
|
15
|
+
* declare function freshLockoutStore(): Promise<LockoutStore>;
|
|
16
|
+
*
|
|
17
|
+
* runLockoutStoreContractTests({
|
|
18
|
+
* describeName: "DrizzleLockoutStore satisfies LockoutStore contract",
|
|
19
|
+
* makeStore: freshLockoutStore,
|
|
20
|
+
* });
|
|
21
|
+
* ```
|
|
22
|
+
*
|
|
23
|
+
* @module
|
|
24
|
+
*/
|
|
25
|
+
import { assertEquals, assertStrictEquals } from "../../deps/jsr.io/@std/assert/1.0.19/mod.js";
|
|
26
|
+
import { beforeEach, describe, it } from "../../deps/jsr.io/@std/testing/1.0.20/bdd.js";
|
|
27
|
+
const USER = "user-1";
|
|
28
|
+
const OTHER_USER = "user-2";
|
|
29
|
+
/**
|
|
30
|
+
* Runs the {@link LockoutStore} contract suite. Call it from your own test
|
|
31
|
+
* file — it registers `describe` / `it` blocks the test runner picks up.
|
|
32
|
+
*
|
|
33
|
+
* `now` is passed explicitly on every call, so the lock-expiry cases are
|
|
34
|
+
* clock-free and never need a timer.
|
|
35
|
+
*/
|
|
36
|
+
export function runLockoutStoreContractTests(options) {
|
|
37
|
+
describe(options.describeName ?? "LockoutStore contract", () => {
|
|
38
|
+
let store;
|
|
39
|
+
let now;
|
|
40
|
+
beforeEach(async () => {
|
|
41
|
+
store = await options.makeStore();
|
|
42
|
+
now = Date.now();
|
|
43
|
+
});
|
|
44
|
+
describe("get", () => {
|
|
45
|
+
it("returns undefined for an account with no record", async () => {
|
|
46
|
+
assertStrictEquals(await store.get(USER), undefined);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
describe("increment", () => {
|
|
50
|
+
it("records the first failure as one", async () => {
|
|
51
|
+
const result = await store.increment(USER, now);
|
|
52
|
+
assertEquals(result.failures, 1);
|
|
53
|
+
assertStrictEquals(result.lockedUntil, undefined);
|
|
54
|
+
assertEquals((await store.get(USER))?.failures, 1);
|
|
55
|
+
});
|
|
56
|
+
it("adds one failure per call", async () => {
|
|
57
|
+
await store.increment(USER, now);
|
|
58
|
+
await store.increment(USER, now);
|
|
59
|
+
assertEquals((await store.increment(USER, now)).failures, 3);
|
|
60
|
+
});
|
|
61
|
+
it("counts each account on its own", async () => {
|
|
62
|
+
await store.increment(USER, now);
|
|
63
|
+
await store.increment(USER, now);
|
|
64
|
+
assertEquals((await store.increment(OTHER_USER, now)).failures, 1);
|
|
65
|
+
});
|
|
66
|
+
it("keeps an active lock while counting the failure", async () => {
|
|
67
|
+
const lockedUntil = now + 60_000;
|
|
68
|
+
await store.set(USER, { failures: 10, lockedUntil });
|
|
69
|
+
const result = await store.increment(USER, now);
|
|
70
|
+
assertEquals(result.failures, 11);
|
|
71
|
+
assertStrictEquals(result.lockedUntil, lockedUntil, "a failed attempt against a locked account must not extend or drop " +
|
|
72
|
+
"the lock the store is holding");
|
|
73
|
+
});
|
|
74
|
+
it("starts a fresh count once the lock has expired", async () => {
|
|
75
|
+
await store.set(USER, { failures: 10, lockedUntil: now - 1 });
|
|
76
|
+
const result = await store.increment(USER, now);
|
|
77
|
+
assertEquals(result.failures, 1);
|
|
78
|
+
assertStrictEquals(result.lockedUntil, undefined, "an expired lock must be dropped, not carried into the next window");
|
|
79
|
+
});
|
|
80
|
+
it("gives every concurrent failure its own count", async () => {
|
|
81
|
+
const results = await Promise.all(Array.from({ length: 5 }, () => store.increment(USER, now)));
|
|
82
|
+
assertEquals(results.map((result) => result.failures).sort((a, b) => a - b), [1, 2, 3, 4, 5], "the lockout triggers on the returned count — concurrent failures " +
|
|
83
|
+
"sharing a count undershoot the threshold");
|
|
84
|
+
assertEquals((await store.get(USER))?.failures, 5);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
describe("set", () => {
|
|
88
|
+
it("persists the record as given", async () => {
|
|
89
|
+
const lockedUntil = now + 60_000;
|
|
90
|
+
await store.set(USER, { failures: 10, lockedUntil });
|
|
91
|
+
const record = await store.get(USER);
|
|
92
|
+
assertEquals(record?.failures, 10);
|
|
93
|
+
assertStrictEquals(record?.lockedUntil, lockedUntil);
|
|
94
|
+
});
|
|
95
|
+
it("replaces a record that was already there", async () => {
|
|
96
|
+
await store.increment(USER, now);
|
|
97
|
+
await store.set(USER, { failures: 4 });
|
|
98
|
+
const record = await store.get(USER);
|
|
99
|
+
assertEquals(record?.failures, 4);
|
|
100
|
+
assertStrictEquals(record?.lockedUntil, undefined);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
describe("clear", () => {
|
|
104
|
+
it("drops the account's failures and lock", async () => {
|
|
105
|
+
await store.set(USER, { failures: 10, lockedUntil: now + 60_000 });
|
|
106
|
+
await store.clear(USER);
|
|
107
|
+
assertStrictEquals(await store.get(USER), undefined);
|
|
108
|
+
assertEquals((await store.increment(USER, now)).failures, 1);
|
|
109
|
+
});
|
|
110
|
+
it("clears only the account it is given", async () => {
|
|
111
|
+
await store.increment(USER, now);
|
|
112
|
+
await store.increment(OTHER_USER, now);
|
|
113
|
+
await store.clear(USER);
|
|
114
|
+
assertEquals((await store.get(OTHER_USER))?.failures, 1);
|
|
115
|
+
});
|
|
116
|
+
it("is a no-op for an account with no record", async () => {
|
|
117
|
+
await store.clear(USER);
|
|
118
|
+
assertStrictEquals(await store.get(USER), undefined);
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Contract test suite for {@link MfaStore} implementations.
|
|
3
|
+
*
|
|
4
|
+
* `MfaService` leans on three store methods for its concurrency guarantees —
|
|
5
|
+
* `activateTotp` (compare-and-swap on the pending secret), `advanceLastStep`
|
|
6
|
+
* (monotonic replay guard), and `consumeRecoveryHash` (single-use recovery
|
|
7
|
+
* codes). This suite checks each of them with concurrent calls, so a
|
|
8
|
+
* read-modify-write implementation fails here instead of silently letting two
|
|
9
|
+
* requests spend one recovery code.
|
|
10
|
+
*
|
|
11
|
+
* @example Verify a database-backed store
|
|
12
|
+
* ```ts
|
|
13
|
+
* import { runMfaStoreContractTests } from "@udibo/oauth2/testing/contract";
|
|
14
|
+
* import type { MfaStore } from "@udibo/oauth2/identity/mfa";
|
|
15
|
+
*
|
|
16
|
+
* declare function freshMfaStore(): Promise<MfaStore>;
|
|
17
|
+
*
|
|
18
|
+
* runMfaStoreContractTests({
|
|
19
|
+
* describeName: "DrizzleMfaStore satisfies MfaStore contract",
|
|
20
|
+
* makeStore: freshMfaStore,
|
|
21
|
+
* });
|
|
22
|
+
* ```
|
|
23
|
+
*
|
|
24
|
+
* @module
|
|
25
|
+
*/
|
|
26
|
+
import type { MfaStore } from "../../identity/mfa/service.js";
|
|
27
|
+
/** Options for {@link runMfaStoreContractTests}. */
|
|
28
|
+
export interface MfaStoreContractOptions {
|
|
29
|
+
/**
|
|
30
|
+
* Returns a fresh, empty store for each test. Isolation is required — a
|
|
31
|
+
* recovery hash left behind by one test makes the single-use checks
|
|
32
|
+
* meaningless.
|
|
33
|
+
*/
|
|
34
|
+
makeStore(): Promise<MfaStore> | MfaStore;
|
|
35
|
+
/**
|
|
36
|
+
* Overrides the name passed to the outer `describe` block. Defaults to
|
|
37
|
+
* `"MfaStore contract"`.
|
|
38
|
+
*/
|
|
39
|
+
describeName?: string;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Runs the {@link MfaStore} contract suite. Call it from your own test file —
|
|
43
|
+
* it registers `describe` / `it` blocks the test runner picks up.
|
|
44
|
+
*
|
|
45
|
+
* The atomicity cases issue two or more overlapping calls and assert the
|
|
46
|
+
* outcome a conditional write produces. An implementation that reads, then
|
|
47
|
+
* awaits, then writes fails them, which is the point: the same store passes
|
|
48
|
+
* every sequential check.
|
|
49
|
+
*/
|
|
50
|
+
export declare function runMfaStoreContractTests(options: MfaStoreContractOptions): void;
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Contract test suite for {@link MfaStore} implementations.
|
|
3
|
+
*
|
|
4
|
+
* `MfaService` leans on three store methods for its concurrency guarantees —
|
|
5
|
+
* `activateTotp` (compare-and-swap on the pending secret), `advanceLastStep`
|
|
6
|
+
* (monotonic replay guard), and `consumeRecoveryHash` (single-use recovery
|
|
7
|
+
* codes). This suite checks each of them with concurrent calls, so a
|
|
8
|
+
* read-modify-write implementation fails here instead of silently letting two
|
|
9
|
+
* requests spend one recovery code.
|
|
10
|
+
*
|
|
11
|
+
* @example Verify a database-backed store
|
|
12
|
+
* ```ts
|
|
13
|
+
* import { runMfaStoreContractTests } from "@udibo/oauth2/testing/contract";
|
|
14
|
+
* import type { MfaStore } from "@udibo/oauth2/identity/mfa";
|
|
15
|
+
*
|
|
16
|
+
* declare function freshMfaStore(): Promise<MfaStore>;
|
|
17
|
+
*
|
|
18
|
+
* runMfaStoreContractTests({
|
|
19
|
+
* describeName: "DrizzleMfaStore satisfies MfaStore contract",
|
|
20
|
+
* makeStore: freshMfaStore,
|
|
21
|
+
* });
|
|
22
|
+
* ```
|
|
23
|
+
*
|
|
24
|
+
* @module
|
|
25
|
+
*/
|
|
26
|
+
import { assertEquals, assertStrictEquals } from "../../deps/jsr.io/@std/assert/1.0.19/mod.js";
|
|
27
|
+
import { beforeEach, describe, it } from "../../deps/jsr.io/@std/testing/1.0.20/bdd.js";
|
|
28
|
+
const USER = "user-1";
|
|
29
|
+
const OTHER_USER = "user-2";
|
|
30
|
+
const SECRET_A = "AAAAAAAAAAAAAAAA";
|
|
31
|
+
const SECRET_B = "BBBBBBBBBBBBBBBB";
|
|
32
|
+
function countTrue(results) {
|
|
33
|
+
return results.filter((result) => result).length;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Runs the {@link MfaStore} contract suite. Call it from your own test file —
|
|
37
|
+
* it registers `describe` / `it` blocks the test runner picks up.
|
|
38
|
+
*
|
|
39
|
+
* The atomicity cases issue two or more overlapping calls and assert the
|
|
40
|
+
* outcome a conditional write produces. An implementation that reads, then
|
|
41
|
+
* awaits, then writes fails them, which is the point: the same store passes
|
|
42
|
+
* every sequential check.
|
|
43
|
+
*/
|
|
44
|
+
export function runMfaStoreContractTests(options) {
|
|
45
|
+
describe(options.describeName ?? "MfaStore contract", () => {
|
|
46
|
+
let store;
|
|
47
|
+
beforeEach(async () => {
|
|
48
|
+
store = await options.makeStore();
|
|
49
|
+
});
|
|
50
|
+
describe("getTotp / setPendingTotp", () => {
|
|
51
|
+
it("returns undefined for a user with no record", async () => {
|
|
52
|
+
assertStrictEquals(await store.getTotp(USER), undefined);
|
|
53
|
+
});
|
|
54
|
+
it("stores a pending secret without activating it", async () => {
|
|
55
|
+
await store.setPendingTotp(USER, SECRET_A);
|
|
56
|
+
const record = await store.getTotp(USER);
|
|
57
|
+
assertEquals(record?.pendingSecretBase32, SECRET_A);
|
|
58
|
+
assertStrictEquals(record?.secretBase32, undefined);
|
|
59
|
+
});
|
|
60
|
+
it("replaces a previous pending secret", async () => {
|
|
61
|
+
await store.setPendingTotp(USER, SECRET_A);
|
|
62
|
+
await store.setPendingTotp(USER, SECRET_B);
|
|
63
|
+
const record = await store.getTotp(USER);
|
|
64
|
+
assertEquals(record?.pendingSecretBase32, SECRET_B);
|
|
65
|
+
});
|
|
66
|
+
it("keeps an active credential when a new enrollment starts", async () => {
|
|
67
|
+
await store.setPendingTotp(USER, SECRET_A);
|
|
68
|
+
await store.activateTotp(USER, SECRET_A, 100);
|
|
69
|
+
await store.setPendingTotp(USER, SECRET_B);
|
|
70
|
+
const record = await store.getTotp(USER);
|
|
71
|
+
assertEquals(record?.secretBase32, SECRET_A);
|
|
72
|
+
assertEquals(record?.lastStep, 100);
|
|
73
|
+
assertEquals(record?.pendingSecretBase32, SECRET_B);
|
|
74
|
+
});
|
|
75
|
+
it("keeps each user's record separate", async () => {
|
|
76
|
+
await store.setPendingTotp(USER, SECRET_A);
|
|
77
|
+
assertStrictEquals(await store.getTotp(OTHER_USER), undefined);
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
describe("activateTotp", () => {
|
|
81
|
+
it("promotes the matching pending secret and clears it", async () => {
|
|
82
|
+
await store.setPendingTotp(USER, SECRET_A);
|
|
83
|
+
assertStrictEquals(await store.activateTotp(USER, SECRET_A, 42), true);
|
|
84
|
+
const record = await store.getTotp(USER);
|
|
85
|
+
assertEquals(record?.secretBase32, SECRET_A);
|
|
86
|
+
assertEquals(record?.lastStep, 42);
|
|
87
|
+
assertStrictEquals(record?.pendingSecretBase32, undefined);
|
|
88
|
+
});
|
|
89
|
+
it("refuses when the user has no pending secret", async () => {
|
|
90
|
+
assertStrictEquals(await store.activateTotp(USER, SECRET_A, 42), false);
|
|
91
|
+
assertStrictEquals(await store.getTotp(USER), undefined);
|
|
92
|
+
});
|
|
93
|
+
it("refuses a secret a newer enrollment replaced, changing nothing", async () => {
|
|
94
|
+
await store.setPendingTotp(USER, SECRET_A);
|
|
95
|
+
await store.setPendingTotp(USER, SECRET_B);
|
|
96
|
+
assertStrictEquals(await store.activateTotp(USER, SECRET_A, 42), false);
|
|
97
|
+
const record = await store.getTotp(USER);
|
|
98
|
+
assertStrictEquals(record?.secretBase32, undefined);
|
|
99
|
+
assertEquals(record?.pendingSecretBase32, SECRET_B);
|
|
100
|
+
});
|
|
101
|
+
it("activates for exactly one of two concurrent confirmations", async () => {
|
|
102
|
+
await store.setPendingTotp(USER, SECRET_A);
|
|
103
|
+
const results = await Promise.all([
|
|
104
|
+
store.activateTotp(USER, SECRET_A, 10),
|
|
105
|
+
store.activateTotp(USER, SECRET_A, 20),
|
|
106
|
+
]);
|
|
107
|
+
assertStrictEquals(countTrue(results), 1, "the pending secret must be claimable once — a second concurrent " +
|
|
108
|
+
"confirmation finds it already spent");
|
|
109
|
+
const record = await store.getTotp(USER);
|
|
110
|
+
assertEquals(record?.secretBase32, SECRET_A);
|
|
111
|
+
assertStrictEquals(record?.pendingSecretBase32, undefined);
|
|
112
|
+
});
|
|
113
|
+
it("activates for exactly one of two concurrent enrollments", async () => {
|
|
114
|
+
await store.setPendingTotp(USER, SECRET_B);
|
|
115
|
+
const results = await Promise.all([
|
|
116
|
+
store.activateTotp(USER, SECRET_A, 10),
|
|
117
|
+
store.activateTotp(USER, SECRET_B, 20),
|
|
118
|
+
]);
|
|
119
|
+
assertStrictEquals(countTrue(results), 1);
|
|
120
|
+
const record = await store.getTotp(USER);
|
|
121
|
+
assertEquals(record?.secretBase32, SECRET_B, "only the secret that was actually pending may become active");
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
describe("clearTotp", () => {
|
|
125
|
+
it("removes the active credential and any pending secret", async () => {
|
|
126
|
+
await store.setPendingTotp(USER, SECRET_A);
|
|
127
|
+
await store.activateTotp(USER, SECRET_A, 5);
|
|
128
|
+
await store.setPendingTotp(USER, SECRET_B);
|
|
129
|
+
await store.clearTotp(USER);
|
|
130
|
+
assertStrictEquals(await store.getTotp(USER), undefined);
|
|
131
|
+
});
|
|
132
|
+
it("is a no-op for a user with no record", async () => {
|
|
133
|
+
await store.clearTotp(USER);
|
|
134
|
+
assertStrictEquals(await store.getTotp(USER), undefined);
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
describe("advanceLastStep", () => {
|
|
138
|
+
beforeEach(async () => {
|
|
139
|
+
await store.setPendingTotp(USER, SECRET_A);
|
|
140
|
+
await store.activateTotp(USER, SECRET_A, 100);
|
|
141
|
+
});
|
|
142
|
+
it("advances to a higher step", async () => {
|
|
143
|
+
assertStrictEquals(await store.advanceLastStep(USER, 101), true);
|
|
144
|
+
assertEquals((await store.getTotp(USER))?.lastStep, 101);
|
|
145
|
+
});
|
|
146
|
+
it("refuses the step already spent", async () => {
|
|
147
|
+
assertStrictEquals(await store.advanceLastStep(USER, 100), false);
|
|
148
|
+
assertEquals((await store.getTotp(USER))?.lastStep, 100);
|
|
149
|
+
});
|
|
150
|
+
it("refuses an earlier step, leaving the guard where it was", async () => {
|
|
151
|
+
assertStrictEquals(await store.advanceLastStep(USER, 99), false);
|
|
152
|
+
assertEquals((await store.getTotp(USER))?.lastStep, 100);
|
|
153
|
+
});
|
|
154
|
+
it("refuses for a user with no record", async () => {
|
|
155
|
+
assertStrictEquals(await store.advanceLastStep(OTHER_USER, 1), false);
|
|
156
|
+
});
|
|
157
|
+
it("admits exactly one of two concurrent claims on the same step", async () => {
|
|
158
|
+
const results = await Promise.all([
|
|
159
|
+
store.advanceLastStep(USER, 101),
|
|
160
|
+
store.advanceLastStep(USER, 101),
|
|
161
|
+
]);
|
|
162
|
+
assertStrictEquals(countTrue(results), 1, "a time step is spendable once — the loser must read as a replay");
|
|
163
|
+
assertEquals((await store.getTotp(USER))?.lastStep, 101);
|
|
164
|
+
});
|
|
165
|
+
it("never regresses under concurrent claims on different steps", async () => {
|
|
166
|
+
await Promise.all([
|
|
167
|
+
store.advanceLastStep(USER, 105),
|
|
168
|
+
store.advanceLastStep(USER, 101),
|
|
169
|
+
]);
|
|
170
|
+
assertEquals((await store.getTotp(USER))?.lastStep, 105, "the highest accepted step must survive a concurrent lower one");
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
describe("recovery hashes", () => {
|
|
174
|
+
it("returns an empty list for a user with none", async () => {
|
|
175
|
+
assertEquals(await store.getRecoveryHashes(USER), []);
|
|
176
|
+
});
|
|
177
|
+
it("replaces the stored set exactly", async () => {
|
|
178
|
+
await store.setRecoveryHashes(USER, ["h1", "h2", "h3"]);
|
|
179
|
+
assertEquals([...await store.getRecoveryHashes(USER)].sort(), ["h1", "h2", "h3"]);
|
|
180
|
+
await store.setRecoveryHashes(USER, ["h4"]);
|
|
181
|
+
assertEquals(await store.getRecoveryHashes(USER), ["h4"]);
|
|
182
|
+
});
|
|
183
|
+
it("clears the set when given an empty list", async () => {
|
|
184
|
+
await store.setRecoveryHashes(USER, ["h1"]);
|
|
185
|
+
await store.setRecoveryHashes(USER, []);
|
|
186
|
+
assertEquals(await store.getRecoveryHashes(USER), []);
|
|
187
|
+
});
|
|
188
|
+
it("keeps each user's codes separate", async () => {
|
|
189
|
+
await store.setRecoveryHashes(USER, ["h1"]);
|
|
190
|
+
assertEquals(await store.getRecoveryHashes(OTHER_USER), []);
|
|
191
|
+
assertStrictEquals(await store.consumeRecoveryHash(OTHER_USER, "h1"), false);
|
|
192
|
+
assertEquals(await store.getRecoveryHashes(USER), ["h1"]);
|
|
193
|
+
});
|
|
194
|
+
it("burns a consumed hash and refuses it afterwards", async () => {
|
|
195
|
+
await store.setRecoveryHashes(USER, ["h1", "h2"]);
|
|
196
|
+
assertStrictEquals(await store.consumeRecoveryHash(USER, "h1"), true);
|
|
197
|
+
assertStrictEquals(await store.consumeRecoveryHash(USER, "h1"), false);
|
|
198
|
+
assertEquals(await store.getRecoveryHashes(USER), ["h2"]);
|
|
199
|
+
});
|
|
200
|
+
it("refuses a hash that was never stored", async () => {
|
|
201
|
+
await store.setRecoveryHashes(USER, ["h1"]);
|
|
202
|
+
assertStrictEquals(await store.consumeRecoveryHash(USER, "nope"), false);
|
|
203
|
+
assertEquals(await store.getRecoveryHashes(USER), ["h1"]);
|
|
204
|
+
});
|
|
205
|
+
it("burns a code for exactly one of two concurrent redemptions", async () => {
|
|
206
|
+
await store.setRecoveryHashes(USER, ["h1", "h2"]);
|
|
207
|
+
const results = await Promise.all([
|
|
208
|
+
store.consumeRecoveryHash(USER, "h1"),
|
|
209
|
+
store.consumeRecoveryHash(USER, "h1"),
|
|
210
|
+
]);
|
|
211
|
+
assertStrictEquals(countTrue(results), 1, "a recovery code is single-use — two requests racing one code must " +
|
|
212
|
+
"not both be told they redeemed it");
|
|
213
|
+
assertEquals(await store.getRecoveryHashes(USER), ["h2"]);
|
|
214
|
+
});
|
|
215
|
+
it("burns both codes when two different ones are redeemed concurrently", async () => {
|
|
216
|
+
await store.setRecoveryHashes(USER, ["h1", "h2", "h3"]);
|
|
217
|
+
const results = await Promise.all([
|
|
218
|
+
store.consumeRecoveryHash(USER, "h1"),
|
|
219
|
+
store.consumeRecoveryHash(USER, "h2"),
|
|
220
|
+
]);
|
|
221
|
+
assertStrictEquals(countTrue(results), 2);
|
|
222
|
+
assertEquals(await store.getRecoveryHashes(USER), ["h3"], "neither deletion may be lost to the other's stale read");
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
}
|