@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,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The contract between {@link ExternalAuthFlow} and a concrete external
|
|
3
|
+
* provider connector ({@link oidcProvider}, {@link googleProvider},
|
|
4
|
+
* {@link githubProvider}, or your own).
|
|
5
|
+
*
|
|
6
|
+
* A provider's job ends at returning a verified, normalized
|
|
7
|
+
* {@link ExternalProfile}. What the app does with it — create a user, link an
|
|
8
|
+
* account, reject — is app code; the library deliberately stores nothing.
|
|
9
|
+
*
|
|
10
|
+
* @module
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* A verified, normalized profile returned by an external provider after a
|
|
14
|
+
* successful sign-in. This is the connector layer's entire output — account
|
|
15
|
+
* creation/linking is the app's job.
|
|
16
|
+
*/
|
|
17
|
+
export interface ExternalProfile {
|
|
18
|
+
/** The provider id the profile came from (e.g. `"google"`). */
|
|
19
|
+
provider: string;
|
|
20
|
+
/**
|
|
21
|
+
* The provider's stable unique id for the user (OIDC `sub`, GitHub user
|
|
22
|
+
* id). Key identities by `(provider, subject)` — never by email, which can
|
|
23
|
+
* change or be reassigned.
|
|
24
|
+
*/
|
|
25
|
+
subject: string;
|
|
26
|
+
/** The user's email address, when the provider shared one. */
|
|
27
|
+
email?: string;
|
|
28
|
+
/**
|
|
29
|
+
* `true` only when the provider positively asserted the email is verified
|
|
30
|
+
* (OIDC `email_verified: true`, GitHub verified primary email). Defaults to
|
|
31
|
+
* `false` — treat unverified emails as attacker-controllable input and do
|
|
32
|
+
* not auto-link accounts on them.
|
|
33
|
+
*/
|
|
34
|
+
emailVerified: boolean;
|
|
35
|
+
/** Display name, when shared. */
|
|
36
|
+
name?: string;
|
|
37
|
+
/** Given (first) name, when shared. */
|
|
38
|
+
givenName?: string;
|
|
39
|
+
/** Family (last) name, when shared. */
|
|
40
|
+
familyName?: string;
|
|
41
|
+
/** Avatar/profile picture URL, when shared. */
|
|
42
|
+
picture?: string;
|
|
43
|
+
/**
|
|
44
|
+
* The provider's raw claims/profile payload, for app-specific fields the
|
|
45
|
+
* normalized shape omits (e.g. Google's `hd`, GitHub's `login`).
|
|
46
|
+
*/
|
|
47
|
+
raw: Record<string, unknown>;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* The per-authorization transient state {@link ExternalAuthFlow.start}
|
|
51
|
+
* returns. JSON-serializable by construction — put it in a sealed cookie or
|
|
52
|
+
* your session store and hand it back to {@link ExternalAuthFlow.finish} on
|
|
53
|
+
* the callback. It is the only state the flow needs; the library stores
|
|
54
|
+
* nothing.
|
|
55
|
+
*/
|
|
56
|
+
export interface ExternalAuthTransient {
|
|
57
|
+
/** CSRF token echoed back by the provider as the callback `state` param. */
|
|
58
|
+
state: string;
|
|
59
|
+
/** PKCE code verifier, present when the provider uses PKCE. */
|
|
60
|
+
codeVerifier?: string;
|
|
61
|
+
/** OIDC replay-protection nonce, present when the provider uses one. */
|
|
62
|
+
nonce?: string;
|
|
63
|
+
/** The id of the provider the attempt was started with. */
|
|
64
|
+
provider: string;
|
|
65
|
+
/** The redirect URI the authorization request was built with. */
|
|
66
|
+
redirectUri: string;
|
|
67
|
+
/** Unix ms when the attempt started; bounds the callback window. */
|
|
68
|
+
createdAt: number;
|
|
69
|
+
}
|
|
70
|
+
/** Input to {@link ExternalProvider.buildAuthorizationUrl}. */
|
|
71
|
+
export interface ExternalAuthorizationUrlInput {
|
|
72
|
+
/** The callback URL registered with the provider for this app. */
|
|
73
|
+
redirectUri: string;
|
|
74
|
+
/** CSRF token to send as the `state` parameter. */
|
|
75
|
+
state: string;
|
|
76
|
+
/** PKCE code verifier to derive the S256 challenge from, when PKCE is on. */
|
|
77
|
+
codeVerifier?: string;
|
|
78
|
+
/** OIDC nonce to bind into the id_token, when the provider uses one. */
|
|
79
|
+
nonce?: string;
|
|
80
|
+
/** Scopes to request (already defaulted by the flow). */
|
|
81
|
+
scopes: string[];
|
|
82
|
+
/** OIDC `prompt` parameter (e.g. `"select_account"`), when requested. */
|
|
83
|
+
prompt?: string;
|
|
84
|
+
}
|
|
85
|
+
/** Input to {@link ExternalProvider.fetchProfile}. */
|
|
86
|
+
export interface ExternalProfileInput {
|
|
87
|
+
/** The authorization code from the callback. */
|
|
88
|
+
code: string;
|
|
89
|
+
/** The redirect URI the flow was started with (must match the exchange). */
|
|
90
|
+
redirectUri: string;
|
|
91
|
+
/** The PKCE code verifier from the transient, when PKCE was used. */
|
|
92
|
+
codeVerifier?: string;
|
|
93
|
+
/** The expected id_token `nonce` from the transient, when one was sent. */
|
|
94
|
+
nonce?: string;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* What a connector must provide for {@link ExternalAuthFlow} to drive it.
|
|
98
|
+
*
|
|
99
|
+
* The flow owns the cross-provider policy (state generation and timing-safe
|
|
100
|
+
* verification, transient expiry, callback error surfacing); the provider
|
|
101
|
+
* owns the wire protocol (authorize URL shape, code exchange, id_token nonce
|
|
102
|
+
* validation, profile normalization). Implement this only for providers the
|
|
103
|
+
* built-in {@link oidcProvider} / {@link githubProvider} cannot cover.
|
|
104
|
+
*/
|
|
105
|
+
export interface ExternalProvider {
|
|
106
|
+
/** Stable id used in {@link ExternalProfile.provider} and error messages. */
|
|
107
|
+
readonly id: string;
|
|
108
|
+
/** Human-readable name for buttons and logs (e.g. `"Google"`). */
|
|
109
|
+
readonly displayName: string;
|
|
110
|
+
/** Scopes requested when the caller passes none. */
|
|
111
|
+
readonly defaultScopes: string[];
|
|
112
|
+
/** Whether the flow should generate a PKCE code verifier for this provider. */
|
|
113
|
+
readonly usesPkce: boolean;
|
|
114
|
+
/** Whether the flow should generate an OIDC nonce for this provider. */
|
|
115
|
+
readonly usesNonce: boolean;
|
|
116
|
+
/**
|
|
117
|
+
* Builds the provider's authorization URL for the given per-attempt values.
|
|
118
|
+
*
|
|
119
|
+
* @throws {ExternalAuthError} `configuration` when the provider cannot
|
|
120
|
+
* resolve its authorization endpoint (e.g. OIDC discovery failed).
|
|
121
|
+
*/
|
|
122
|
+
buildAuthorizationUrl(input: ExternalAuthorizationUrlInput): Promise<string>;
|
|
123
|
+
/**
|
|
124
|
+
* Exchanges the authorization code and returns the verified, normalized
|
|
125
|
+
* profile. Implementations must validate the id_token `nonce` against
|
|
126
|
+
* `input.nonce` when one was sent.
|
|
127
|
+
*
|
|
128
|
+
* @throws {ExternalAuthError} `provider_error` when the exchange or profile
|
|
129
|
+
* fetch fails, `nonce_mismatch` when the id_token nonce does not match.
|
|
130
|
+
*/
|
|
131
|
+
fetchProfile(input: ExternalProfileInput): Promise<ExternalProfile>;
|
|
132
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The contract between {@link ExternalAuthFlow} and a concrete external
|
|
3
|
+
* provider connector ({@link oidcProvider}, {@link googleProvider},
|
|
4
|
+
* {@link githubProvider}, or your own).
|
|
5
|
+
*
|
|
6
|
+
* A provider's job ends at returning a verified, normalized
|
|
7
|
+
* {@link ExternalProfile}. What the app does with it — create a user, link an
|
|
8
|
+
* account, reject — is app code; the library deliberately stores nothing.
|
|
9
|
+
*
|
|
10
|
+
* @module
|
|
11
|
+
*/
|
|
12
|
+
export {};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Breached-password rejection via the Have I Been Pwned range API — the
|
|
3
|
+
* privacy-preserving k-anonymity model: only the first five characters of the
|
|
4
|
+
* password's SHA-1 hash ever leave the process; the full password and full
|
|
5
|
+
* hash never do.
|
|
6
|
+
*
|
|
7
|
+
* {@link breachedPasswordValidator} returns an async validator for
|
|
8
|
+
* {@link PasswordPolicy.validators}, so a policy rejects known-breached
|
|
9
|
+
* passwords at sign-up and password reset. **Fail-open by default**: if the
|
|
10
|
+
* range API is unreachable, the password is allowed —
|
|
11
|
+
* availability of your sign-up flow shouldn't hinge on a third party. Set
|
|
12
|
+
* `failOpen: false` to invert that.
|
|
13
|
+
*
|
|
14
|
+
* Pass `onEvent` so those fail-opens reach your audit store as
|
|
15
|
+
* `password_policy.check_unavailable` events; an egress rule that blocks
|
|
16
|
+
* `api.pwnedpasswords.com` disables this control silently otherwise.
|
|
17
|
+
*
|
|
18
|
+
* @module
|
|
19
|
+
*/
|
|
20
|
+
import { type IdentityEventHook } from "./events.js";
|
|
21
|
+
/** Options for {@link breachedPasswordValidator}. */
|
|
22
|
+
export interface BreachedPasswordOptions {
|
|
23
|
+
/**
|
|
24
|
+
* Reject when the password appears in at least this many breaches.
|
|
25
|
+
* Defaults to 1.
|
|
26
|
+
*/
|
|
27
|
+
threshold?: number;
|
|
28
|
+
/**
|
|
29
|
+
* Allow the password when the range API can't be reached (default `true`).
|
|
30
|
+
* `false` rejects with a retry message instead.
|
|
31
|
+
*/
|
|
32
|
+
failOpen?: boolean;
|
|
33
|
+
/** Abort the range request after this many ms. Defaults to 3000. */
|
|
34
|
+
timeoutMs?: number;
|
|
35
|
+
/** Fetch implementation — inject for tests. Defaults to `globalThis.fetch`. */
|
|
36
|
+
fetch?: typeof fetch;
|
|
37
|
+
/** Range API base URL. Defaults to `https://api.pwnedpasswords.com/range/`. */
|
|
38
|
+
baseUrl?: string;
|
|
39
|
+
/**
|
|
40
|
+
* Receives a `password_policy.check_unavailable` {@link IdentityEvent} every
|
|
41
|
+
* time the range lookup fails, so a fail-open is answerable from the same
|
|
42
|
+
* audit store as every other identity outcome ("was this check actually
|
|
43
|
+
* running last month?"). Pass the hook you gave `IdentityService`'s
|
|
44
|
+
* `onEvent`.
|
|
45
|
+
*
|
|
46
|
+
* Without it the failure is only a `console.warn` — greppable in container
|
|
47
|
+
* logs, invisible to an audit query. The hook follows the
|
|
48
|
+
* {@link IdentityEventHook} contract: awaited, and a rejection is logged
|
|
49
|
+
* rather than rethrown, so the sign-up it observes still completes.
|
|
50
|
+
*/
|
|
51
|
+
onEvent?: IdentityEventHook;
|
|
52
|
+
}
|
|
53
|
+
/** Uppercase hex SHA-1, the digest format the HIBP range API keys on. */
|
|
54
|
+
export declare function sha1Hex(input: string): Promise<string>;
|
|
55
|
+
/**
|
|
56
|
+
* Build a {@link PasswordPolicy} validator that rejects passwords found in the
|
|
57
|
+
* HIBP breach corpus.
|
|
58
|
+
*
|
|
59
|
+
* @example
|
|
60
|
+
* ```ts
|
|
61
|
+
* const policy: PasswordPolicy = {
|
|
62
|
+
* minLength: 8,
|
|
63
|
+
* validators: [breachedPasswordValidator({ onEvent })],
|
|
64
|
+
* };
|
|
65
|
+
* ```
|
|
66
|
+
*/
|
|
67
|
+
export declare function breachedPasswordValidator(options?: BreachedPasswordOptions): (password: string) => Promise<string | undefined>;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Breached-password rejection via the Have I Been Pwned range API — the
|
|
3
|
+
* privacy-preserving k-anonymity model: only the first five characters of the
|
|
4
|
+
* password's SHA-1 hash ever leave the process; the full password and full
|
|
5
|
+
* hash never do.
|
|
6
|
+
*
|
|
7
|
+
* {@link breachedPasswordValidator} returns an async validator for
|
|
8
|
+
* {@link PasswordPolicy.validators}, so a policy rejects known-breached
|
|
9
|
+
* passwords at sign-up and password reset. **Fail-open by default**: if the
|
|
10
|
+
* range API is unreachable, the password is allowed —
|
|
11
|
+
* availability of your sign-up flow shouldn't hinge on a third party. Set
|
|
12
|
+
* `failOpen: false` to invert that.
|
|
13
|
+
*
|
|
14
|
+
* Pass `onEvent` so those fail-opens reach your audit store as
|
|
15
|
+
* `password_policy.check_unavailable` events; an egress rule that blocks
|
|
16
|
+
* `api.pwnedpasswords.com` disables this control silently otherwise.
|
|
17
|
+
*
|
|
18
|
+
* @module
|
|
19
|
+
*/
|
|
20
|
+
import { encodeHex } from "../deps/jsr.io/@std/encoding/1.0.11/hex.js";
|
|
21
|
+
import { dispatchIdentityEvent } from "./events.js";
|
|
22
|
+
const DEFAULT_BASE_URL = "https://api.pwnedpasswords.com/range/";
|
|
23
|
+
const VALIDATOR_NAME = "breached_password";
|
|
24
|
+
const BREACHED_MESSAGE = "This password has appeared in a data breach; choose a different one.";
|
|
25
|
+
const UNAVAILABLE_MESSAGE = "The password couldn't be checked against known breaches; try again.";
|
|
26
|
+
/** Uppercase hex SHA-1, the digest format the HIBP range API keys on. */
|
|
27
|
+
export async function sha1Hex(input) {
|
|
28
|
+
const digest = await crypto.subtle.digest("SHA-1", new TextEncoder().encode(input));
|
|
29
|
+
return encodeHex(digest).toUpperCase();
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Build a {@link PasswordPolicy} validator that rejects passwords found in the
|
|
33
|
+
* HIBP breach corpus.
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* ```ts
|
|
37
|
+
* const policy: PasswordPolicy = {
|
|
38
|
+
* minLength: 8,
|
|
39
|
+
* validators: [breachedPasswordValidator({ onEvent })],
|
|
40
|
+
* };
|
|
41
|
+
* ```
|
|
42
|
+
*/
|
|
43
|
+
export function breachedPasswordValidator(options = {}) {
|
|
44
|
+
const threshold = options.threshold ?? 1;
|
|
45
|
+
const failOpen = options.failOpen ?? true;
|
|
46
|
+
const timeoutMs = options.timeoutMs ?? 3000;
|
|
47
|
+
const fetchImpl = options.fetch ?? fetch;
|
|
48
|
+
const baseUrl = options.baseUrl ?? DEFAULT_BASE_URL;
|
|
49
|
+
const onEvent = options.onEvent;
|
|
50
|
+
return async (password) => {
|
|
51
|
+
try {
|
|
52
|
+
const hash = await sha1Hex(password);
|
|
53
|
+
const prefix = hash.slice(0, 5);
|
|
54
|
+
const suffix = hash.slice(5);
|
|
55
|
+
const response = await fetchImpl(`${baseUrl}${prefix}`, {
|
|
56
|
+
headers: { "Add-Padding": "true" },
|
|
57
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
58
|
+
});
|
|
59
|
+
if (!response.ok) {
|
|
60
|
+
await response.body?.cancel();
|
|
61
|
+
throw new Error(`range API responded ${response.status}`);
|
|
62
|
+
}
|
|
63
|
+
const body = await response.text();
|
|
64
|
+
for (const line of body.split("\n")) {
|
|
65
|
+
const [lineSuffix, countText] = line.trim().split(":");
|
|
66
|
+
if (lineSuffix === suffix && Number(countText) >= threshold) {
|
|
67
|
+
return BREACHED_MESSAGE;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
if (onEvent) {
|
|
74
|
+
await dispatchIdentityEvent(onEvent, "breachedPasswordValidator", {
|
|
75
|
+
type: "password_policy.check_unavailable",
|
|
76
|
+
validator: VALIDATOR_NAME,
|
|
77
|
+
failedOpen: failOpen,
|
|
78
|
+
error: error instanceof Error ? error.message : String(error),
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
console.warn("[@udibo/oauth2] breached-password check failed:", error);
|
|
83
|
+
}
|
|
84
|
+
return failOpen ? undefined : UNAVAILABLE_MESSAGE;
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sign-in identifier classification + resolution.
|
|
3
|
+
*
|
|
4
|
+
* A sign-in form usually accepts "email or username" (sometimes phone), and
|
|
5
|
+
* every app hand-rolls the `identifier.includes("@")` branch to decide which
|
|
6
|
+
* lookup to run. This makes that a configured concern instead.
|
|
7
|
+
*
|
|
8
|
+
* @module
|
|
9
|
+
*/
|
|
10
|
+
/** How a sign-in identifier was classified. */
|
|
11
|
+
export type IdentifierKind = "email" | "username" | "phone";
|
|
12
|
+
/**
|
|
13
|
+
* Classify a sign-in identifier as an email, phone number, or username.
|
|
14
|
+
*
|
|
15
|
+
* Heuristic: an `@` → `email`; otherwise a value made only of phone characters
|
|
16
|
+
* (`+`, digits, spaces, `-`, `()`, `.`) with at least 7 digits → `phone`;
|
|
17
|
+
* everything else → `username`. The value is trimmed first.
|
|
18
|
+
*/
|
|
19
|
+
export declare function classifyIdentifier(identifier: string): IdentifierKind;
|
|
20
|
+
/** Per-kind user lookups supplied by the app. Omit a kind to disallow it. */
|
|
21
|
+
export interface IdentifierLookups<User> {
|
|
22
|
+
/** Resolve a user by email; omit to disallow signing in with an email. */
|
|
23
|
+
email?: (email: string) => Promise<User | undefined> | User | undefined;
|
|
24
|
+
/** Resolve a user by username; omit to disallow signing in with a username. */
|
|
25
|
+
username?: (username: string) => Promise<User | undefined> | User | undefined;
|
|
26
|
+
/** Resolve a user by phone number; omit to disallow signing in with a phone. */
|
|
27
|
+
phone?: (phone: string) => Promise<User | undefined> | User | undefined;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Build a resolver that classifies an identifier ({@link classifyIdentifier})
|
|
31
|
+
* and dispatches to the matching app-supplied lookup. Returns `undefined` when
|
|
32
|
+
* no lookup is configured for the classified kind, or the user isn't found —
|
|
33
|
+
* callers should treat both as "invalid credentials" without revealing which.
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* ```ts
|
|
37
|
+
* const resolve = createIdentifierResolver({
|
|
38
|
+
* email: (e) => userService.getByEmail(e),
|
|
39
|
+
* username: (u) => userService.getByUsername(u),
|
|
40
|
+
* });
|
|
41
|
+
* const user = await resolve(form.get("identifier"));
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
44
|
+
export declare function createIdentifierResolver<User>(lookups: IdentifierLookups<User>): (identifier: string) => Promise<User | undefined>;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sign-in identifier classification + resolution.
|
|
3
|
+
*
|
|
4
|
+
* A sign-in form usually accepts "email or username" (sometimes phone), and
|
|
5
|
+
* every app hand-rolls the `identifier.includes("@")` branch to decide which
|
|
6
|
+
* lookup to run. This makes that a configured concern instead.
|
|
7
|
+
*
|
|
8
|
+
* @module
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Classify a sign-in identifier as an email, phone number, or username.
|
|
12
|
+
*
|
|
13
|
+
* Heuristic: an `@` → `email`; otherwise a value made only of phone characters
|
|
14
|
+
* (`+`, digits, spaces, `-`, `()`, `.`) with at least 7 digits → `phone`;
|
|
15
|
+
* everything else → `username`. The value is trimmed first.
|
|
16
|
+
*/
|
|
17
|
+
export function classifyIdentifier(identifier) {
|
|
18
|
+
const value = identifier.trim();
|
|
19
|
+
if (value.includes("@"))
|
|
20
|
+
return "email";
|
|
21
|
+
const digitCount = (value.match(/\d/g) ?? []).length;
|
|
22
|
+
if (digitCount >= 7 && /^\+?[\d\s\-().]+$/.test(value))
|
|
23
|
+
return "phone";
|
|
24
|
+
return "username";
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Build a resolver that classifies an identifier ({@link classifyIdentifier})
|
|
28
|
+
* and dispatches to the matching app-supplied lookup. Returns `undefined` when
|
|
29
|
+
* no lookup is configured for the classified kind, or the user isn't found —
|
|
30
|
+
* callers should treat both as "invalid credentials" without revealing which.
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* ```ts
|
|
34
|
+
* const resolve = createIdentifierResolver({
|
|
35
|
+
* email: (e) => userService.getByEmail(e),
|
|
36
|
+
* username: (u) => userService.getByUsername(u),
|
|
37
|
+
* });
|
|
38
|
+
* const user = await resolve(form.get("identifier"));
|
|
39
|
+
* ```
|
|
40
|
+
*/
|
|
41
|
+
export function createIdentifierResolver(lookups) {
|
|
42
|
+
return async (identifier) => {
|
|
43
|
+
const value = identifier.trim();
|
|
44
|
+
const lookup = lookups[classifyIdentifier(value)];
|
|
45
|
+
return lookup ? await lookup(value) : undefined;
|
|
46
|
+
};
|
|
47
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Failed-attempt account lockout — the brute-force floor for sign-in.
|
|
3
|
+
*
|
|
4
|
+
* Distinct from rate limiting: the {@link RateLimiter} throttles *attempts per
|
|
5
|
+
* window* keyed however you like (identifier, IP), while lockout tracks
|
|
6
|
+
* *consecutive failures per account* and locks the account for a cooldown once
|
|
7
|
+
* a threshold is crossed. Storage is a pluggable {@link LockoutStore}
|
|
8
|
+
* (a {@link MemoryLockoutStore} ships for dev/single-process; back it with your
|
|
9
|
+
* database to share state across instances — e.g. columns on your users table,
|
|
10
|
+
* which also gives admins visibility).
|
|
11
|
+
*
|
|
12
|
+
* `AccountLockout` is a pure state machine: it never decides whether to block.
|
|
13
|
+
* `IdentityService` consults it and applies its `protectionMode`
|
|
14
|
+
* (enforce vs log-only), so adopters can observe lockout behavior before
|
|
15
|
+
* turning it on.
|
|
16
|
+
*
|
|
17
|
+
* @module
|
|
18
|
+
*/
|
|
19
|
+
/** Per-account lockout state. */
|
|
20
|
+
export interface LockoutRecord {
|
|
21
|
+
/** Consecutive failed sign-in attempts since the last success/unlock. */
|
|
22
|
+
failures: number;
|
|
23
|
+
/** When the active lock expires (epoch ms); absent when not locked. */
|
|
24
|
+
lockedUntil?: number;
|
|
25
|
+
}
|
|
26
|
+
/** Pluggable per-account state store for {@link AccountLockout}. */
|
|
27
|
+
export interface LockoutStore {
|
|
28
|
+
/** The account's current record, or `undefined` when it has none. */
|
|
29
|
+
get(userId: string): Promise<LockoutRecord | undefined>;
|
|
30
|
+
/**
|
|
31
|
+
* Add one failure and return the new record: an expired lock resets the
|
|
32
|
+
* count to 1 and drops the lock; otherwise `failures + 1` with any active
|
|
33
|
+
* lock kept. Must be atomic (a single statement / compare-and-set in shared
|
|
34
|
+
* stores) — a read-modify-write here lets concurrent failed attempts lose
|
|
35
|
+
* counts and undershoot the lockout threshold.
|
|
36
|
+
*/
|
|
37
|
+
increment(userId: string, now: number): Promise<LockoutRecord>;
|
|
38
|
+
/**
|
|
39
|
+
* Persist the account's record, replacing any existing one rather than
|
|
40
|
+
* merging: a record with no `lockedUntil` leaves the account unlocked.
|
|
41
|
+
*/
|
|
42
|
+
set(userId: string, record: LockoutRecord): Promise<void>;
|
|
43
|
+
/** Clear the account's record (successful sign-in or unlock). */
|
|
44
|
+
clear(userId: string): Promise<void>;
|
|
45
|
+
}
|
|
46
|
+
/** Options for {@link AccountLockout}. */
|
|
47
|
+
export interface AccountLockoutOptions {
|
|
48
|
+
/** Consecutive failures that trigger a lock. Defaults to 10. */
|
|
49
|
+
maxAttempts?: number;
|
|
50
|
+
/** How long a lock lasts, in ms. Defaults to 15 minutes. */
|
|
51
|
+
lockDurationMs?: number;
|
|
52
|
+
/** State storage. Defaults to {@link MemoryLockoutStore}. */
|
|
53
|
+
store?: LockoutStore;
|
|
54
|
+
}
|
|
55
|
+
/** The outcome of {@link AccountLockout.recordFailure}. */
|
|
56
|
+
export interface LockoutFailureResult {
|
|
57
|
+
/** Consecutive failures after this one. */
|
|
58
|
+
failures: number;
|
|
59
|
+
/** Whether the account is now locked. */
|
|
60
|
+
locked: boolean;
|
|
61
|
+
/** Whether THIS failure crossed the threshold and created the lock. */
|
|
62
|
+
justLocked: boolean;
|
|
63
|
+
/** When the lock expires (epoch ms); absent when not locked. */
|
|
64
|
+
lockedUntil?: number;
|
|
65
|
+
}
|
|
66
|
+
/** The outcome of {@link AccountLockout.status}. */
|
|
67
|
+
export interface LockoutStatus {
|
|
68
|
+
/** Whether an unexpired lock is active. */
|
|
69
|
+
locked: boolean;
|
|
70
|
+
/** Consecutive failures recorded so far. */
|
|
71
|
+
failures: number;
|
|
72
|
+
/** When the active lock expires (epoch ms); absent when not locked. */
|
|
73
|
+
lockedUntil?: number;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* The lockout contract {@link IdentityService} accepts — the
|
|
77
|
+
* bring-your-own-policy seam, symmetric with `RateLimiterLike`.
|
|
78
|
+
* {@link AccountLockout} is the shipped consecutive-failure implementation and
|
|
79
|
+
* satisfies it; implement this directly for a different policy (exponential
|
|
80
|
+
* backoff, per-IP lockout, a lockout your admin tooling already owns).
|
|
81
|
+
*
|
|
82
|
+
* Prefer {@link LockoutStore} when you only need the built-in policy to be
|
|
83
|
+
* shared across instances — that seam swaps storage, not the state machine.
|
|
84
|
+
* Like the class, an implementation decides state only: whether a locked
|
|
85
|
+
* account is refused is `IdentityService`'s `protectionMode`, never yours.
|
|
86
|
+
*
|
|
87
|
+
* @example
|
|
88
|
+
* ```ts
|
|
89
|
+
* import type { AccountLockoutLike } from "@udibo/oauth2/identity";
|
|
90
|
+
*
|
|
91
|
+
* declare const backoff: {
|
|
92
|
+
* read(userId: string): Promise<{ failures: number; until?: number }>;
|
|
93
|
+
* bump(userId: string): Promise<{ failures: number; until?: number }>;
|
|
94
|
+
* clear(userId: string): Promise<void>;
|
|
95
|
+
* };
|
|
96
|
+
*
|
|
97
|
+
* const lockout: AccountLockoutLike = {
|
|
98
|
+
* async status(userId, now = Date.now()) {
|
|
99
|
+
* const { failures, until } = await backoff.read(userId);
|
|
100
|
+
* const locked = until !== undefined && until > now;
|
|
101
|
+
* return { locked, failures, lockedUntil: locked ? until : undefined };
|
|
102
|
+
* },
|
|
103
|
+
* async recordFailure(userId, now = Date.now()) {
|
|
104
|
+
* const { failures, until } = await backoff.bump(userId);
|
|
105
|
+
* const locked = until !== undefined && until > now;
|
|
106
|
+
* return { failures, locked, justLocked: locked, lockedUntil: until };
|
|
107
|
+
* },
|
|
108
|
+
* reset: (userId) => backoff.clear(userId),
|
|
109
|
+
* };
|
|
110
|
+
* ```
|
|
111
|
+
*/
|
|
112
|
+
export interface AccountLockoutLike {
|
|
113
|
+
/** The account's current lock state. An expired lock must read as unlocked. */
|
|
114
|
+
status(userId: string, now?: number): Promise<LockoutStatus>;
|
|
115
|
+
/**
|
|
116
|
+
* Record one failed attempt and report the resulting state. `justLocked` must
|
|
117
|
+
* be true for exactly one attempt per lock — `IdentityService` fires the
|
|
118
|
+
* lockout event and any unlock email off it.
|
|
119
|
+
*/
|
|
120
|
+
recordFailure(userId: string, now?: number): Promise<LockoutFailureResult>;
|
|
121
|
+
/** Clear the account's failures and any lock (successful sign-in, unlock). */
|
|
122
|
+
reset(userId: string): Promise<void>;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Consecutive-failure lockout with safe defaults (10 failures → 15-minute
|
|
126
|
+
* lock) — the default {@link AccountLockoutLike}. Wire into
|
|
127
|
+
* {@link IdentityService} via its `lockout` option; call {@link reset} from
|
|
128
|
+
* your unlock flow.
|
|
129
|
+
*/
|
|
130
|
+
export declare class AccountLockout implements AccountLockoutLike {
|
|
131
|
+
#private;
|
|
132
|
+
/** Fixes thresholds and the backing store (defaults to {@linkcode MemoryLockoutStore}). */
|
|
133
|
+
constructor(options?: AccountLockoutOptions);
|
|
134
|
+
/** The account's current lock state. An expired lock reads as not locked. */
|
|
135
|
+
status(userId: string, now?: number): Promise<LockoutStatus>;
|
|
136
|
+
/**
|
|
137
|
+
* Record one failed attempt, locking the account when the consecutive-failure
|
|
138
|
+
* threshold is crossed. An attempt after a lock expired starts a fresh count.
|
|
139
|
+
*/
|
|
140
|
+
recordFailure(userId: string, now?: number): Promise<LockoutFailureResult>;
|
|
141
|
+
/** Clear the account's failures and any lock (successful sign-in, unlock). */
|
|
142
|
+
reset(userId: string): Promise<void>;
|
|
143
|
+
}
|
|
144
|
+
/** In-memory {@link LockoutStore} for dev / single-process use. */
|
|
145
|
+
export declare class MemoryLockoutStore implements LockoutStore {
|
|
146
|
+
#private;
|
|
147
|
+
/** The stored record for `userId`, if any. */
|
|
148
|
+
get(userId: string): Promise<LockoutRecord | undefined>;
|
|
149
|
+
/** Add one failure synchronously (atomic in a single-process store). */
|
|
150
|
+
increment(userId: string, now: number): Promise<LockoutRecord>;
|
|
151
|
+
/** Store `record` for `userId`. */
|
|
152
|
+
set(userId: string, record: LockoutRecord): Promise<void>;
|
|
153
|
+
/** Drop `userId`'s record. */
|
|
154
|
+
clear(userId: string): Promise<void>;
|
|
155
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Failed-attempt account lockout — the brute-force floor for sign-in.
|
|
3
|
+
*
|
|
4
|
+
* Distinct from rate limiting: the {@link RateLimiter} throttles *attempts per
|
|
5
|
+
* window* keyed however you like (identifier, IP), while lockout tracks
|
|
6
|
+
* *consecutive failures per account* and locks the account for a cooldown once
|
|
7
|
+
* a threshold is crossed. Storage is a pluggable {@link LockoutStore}
|
|
8
|
+
* (a {@link MemoryLockoutStore} ships for dev/single-process; back it with your
|
|
9
|
+
* database to share state across instances — e.g. columns on your users table,
|
|
10
|
+
* which also gives admins visibility).
|
|
11
|
+
*
|
|
12
|
+
* `AccountLockout` is a pure state machine: it never decides whether to block.
|
|
13
|
+
* `IdentityService` consults it and applies its `protectionMode`
|
|
14
|
+
* (enforce vs log-only), so adopters can observe lockout behavior before
|
|
15
|
+
* turning it on.
|
|
16
|
+
*
|
|
17
|
+
* @module
|
|
18
|
+
*/
|
|
19
|
+
const DEFAULT_MAX_ATTEMPTS = 10;
|
|
20
|
+
const DEFAULT_LOCK_DURATION_MS = 15 * 60 * 1000;
|
|
21
|
+
/**
|
|
22
|
+
* Consecutive-failure lockout with safe defaults (10 failures → 15-minute
|
|
23
|
+
* lock) — the default {@link AccountLockoutLike}. Wire into
|
|
24
|
+
* {@link IdentityService} via its `lockout` option; call {@link reset} from
|
|
25
|
+
* your unlock flow.
|
|
26
|
+
*/
|
|
27
|
+
export class AccountLockout {
|
|
28
|
+
#maxAttempts;
|
|
29
|
+
#lockDurationMs;
|
|
30
|
+
#store;
|
|
31
|
+
/** Fixes thresholds and the backing store (defaults to {@linkcode MemoryLockoutStore}). */
|
|
32
|
+
constructor(options = {}) {
|
|
33
|
+
this.#maxAttempts = options.maxAttempts ?? DEFAULT_MAX_ATTEMPTS;
|
|
34
|
+
this.#lockDurationMs = options.lockDurationMs ?? DEFAULT_LOCK_DURATION_MS;
|
|
35
|
+
this.#store = options.store ?? new MemoryLockoutStore();
|
|
36
|
+
}
|
|
37
|
+
/** The account's current lock state. An expired lock reads as not locked. */
|
|
38
|
+
async status(userId, now = Date.now()) {
|
|
39
|
+
const record = await this.#store.get(userId);
|
|
40
|
+
if (!record)
|
|
41
|
+
return { locked: false, failures: 0 };
|
|
42
|
+
const locked = record.lockedUntil !== undefined && record.lockedUntil > now;
|
|
43
|
+
return {
|
|
44
|
+
locked,
|
|
45
|
+
failures: record.failures,
|
|
46
|
+
lockedUntil: locked ? record.lockedUntil : undefined,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Record one failed attempt, locking the account when the consecutive-failure
|
|
51
|
+
* threshold is crossed. An attempt after a lock expired starts a fresh count.
|
|
52
|
+
*/
|
|
53
|
+
async recordFailure(userId, now = Date.now()) {
|
|
54
|
+
const record = await this.#store.increment(userId, now);
|
|
55
|
+
const { failures } = record;
|
|
56
|
+
if (record.lockedUntil !== undefined && record.lockedUntil > now) {
|
|
57
|
+
return {
|
|
58
|
+
failures,
|
|
59
|
+
locked: true,
|
|
60
|
+
justLocked: false,
|
|
61
|
+
lockedUntil: record.lockedUntil,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
if (failures >= this.#maxAttempts) {
|
|
65
|
+
const lockedUntil = now + this.#lockDurationMs;
|
|
66
|
+
await this.#store.set(userId, { failures, lockedUntil });
|
|
67
|
+
// Concurrent attempts can overshoot the threshold before the lock lands;
|
|
68
|
+
// only the attempt that hits it exactly reports justLocked, so the
|
|
69
|
+
// lockout event (and any unlock email) fires once.
|
|
70
|
+
return {
|
|
71
|
+
failures,
|
|
72
|
+
locked: true,
|
|
73
|
+
justLocked: failures === this.#maxAttempts,
|
|
74
|
+
lockedUntil,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
return { failures, locked: false, justLocked: false };
|
|
78
|
+
}
|
|
79
|
+
/** Clear the account's failures and any lock (successful sign-in, unlock). */
|
|
80
|
+
reset(userId) {
|
|
81
|
+
return this.#store.clear(userId);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
/** In-memory {@link LockoutStore} for dev / single-process use. */
|
|
85
|
+
export class MemoryLockoutStore {
|
|
86
|
+
#records = new Map();
|
|
87
|
+
/** The stored record for `userId`, if any. */
|
|
88
|
+
get(userId) {
|
|
89
|
+
return Promise.resolve(this.#records.get(userId));
|
|
90
|
+
}
|
|
91
|
+
/** Add one failure synchronously (atomic in a single-process store). */
|
|
92
|
+
increment(userId, now) {
|
|
93
|
+
const record = this.#records.get(userId);
|
|
94
|
+
const lockExpired = record?.lockedUntil !== undefined &&
|
|
95
|
+
record.lockedUntil <= now;
|
|
96
|
+
const next = record && !lockExpired
|
|
97
|
+
? {
|
|
98
|
+
failures: record.failures + 1,
|
|
99
|
+
...(record.lockedUntil !== undefined
|
|
100
|
+
? { lockedUntil: record.lockedUntil }
|
|
101
|
+
: {}),
|
|
102
|
+
}
|
|
103
|
+
: { failures: 1 };
|
|
104
|
+
this.#records.set(userId, next);
|
|
105
|
+
return Promise.resolve(next);
|
|
106
|
+
}
|
|
107
|
+
/** Store `record` for `userId`. */
|
|
108
|
+
set(userId, record) {
|
|
109
|
+
this.#records.set(userId, record);
|
|
110
|
+
return Promise.resolve();
|
|
111
|
+
}
|
|
112
|
+
/** Drop `userId`'s record. */
|
|
113
|
+
clear(userId) {
|
|
114
|
+
this.#records.delete(userId);
|
|
115
|
+
return Promise.resolve();
|
|
116
|
+
}
|
|
117
|
+
}
|