@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,194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TOTP primitives (RFC 6238 over RFC 4226 HOTP) built on Web Crypto — no
|
|
3
|
+
* dependencies beyond `@std`. Generate a secret, render the `otpauth://` URI an
|
|
4
|
+
* authenticator app scans, and verify submitted codes with a constant-time
|
|
5
|
+
* comparison.
|
|
6
|
+
*
|
|
7
|
+
* Verification returns the matched time-step counter so the caller can enforce
|
|
8
|
+
* single use: persist the last accepted step per user and reject any code whose
|
|
9
|
+
* `matchedStep` is `<=` the stored value (see {@link verifyTotpCode}). The
|
|
10
|
+
* package owns the math; replay state, like all storage, stays app-owned.
|
|
11
|
+
*
|
|
12
|
+
* @module
|
|
13
|
+
*/
|
|
14
|
+
import { decodeBase32, encodeBase32 } from "../../deps/jsr.io/@std/encoding/1.0.11/base32.js";
|
|
15
|
+
import { toArrayBuffer } from "../../utils/_buffer.js";
|
|
16
|
+
import { timingSafeMatchIndex } from "../../utils/_timing-safe.js";
|
|
17
|
+
const DEFAULT_DIGITS = 6;
|
|
18
|
+
const DEFAULT_PERIOD_SECONDS = 30;
|
|
19
|
+
const DEFAULT_ALGORITHM = "SHA-1";
|
|
20
|
+
const DEFAULT_WINDOWS = 1;
|
|
21
|
+
function normalizeTotpSecret(base32) {
|
|
22
|
+
return base32.toUpperCase().replaceAll(/[\s=]/g, "");
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Assert that TOTP parameters are usable: `digits` and `periodSeconds` must be
|
|
26
|
+
* positive integers and `windows` a non-negative integer. Every TOTP entry
|
|
27
|
+
* point applies this, so a corrupted or miswired configuration fails loudly
|
|
28
|
+
* instead of silently rejecting every code.
|
|
29
|
+
*
|
|
30
|
+
* @throws {TypeError} on the first invalid value.
|
|
31
|
+
*/
|
|
32
|
+
export function validateTotpParams(params) {
|
|
33
|
+
const { digits, periodSeconds, windows } = params;
|
|
34
|
+
if (digits !== undefined && (!Number.isInteger(digits) || digits <= 0)) {
|
|
35
|
+
throw new TypeError("totp digits must be a positive integer");
|
|
36
|
+
}
|
|
37
|
+
if (periodSeconds !== undefined &&
|
|
38
|
+
(!Number.isInteger(periodSeconds) || periodSeconds <= 0)) {
|
|
39
|
+
throw new TypeError("totp period must be a positive integer");
|
|
40
|
+
}
|
|
41
|
+
if (windows !== undefined && (!Number.isInteger(windows) || windows < 0)) {
|
|
42
|
+
throw new TypeError("totp windows must be a non-negative integer");
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Decode a base32 TOTP secret into raw key bytes. Tolerates lowercase,
|
|
47
|
+
* whitespace, and missing `=` padding (authenticator conventions vary).
|
|
48
|
+
*
|
|
49
|
+
* @throws {TypeError} if the value is not valid RFC 4648 base32.
|
|
50
|
+
*/
|
|
51
|
+
export function decodeTotpSecret(base32) {
|
|
52
|
+
const normalized = normalizeTotpSecret(base32);
|
|
53
|
+
const padded = normalized.padEnd(normalized.length + ((8 - (normalized.length % 8)) % 8), "=");
|
|
54
|
+
return decodeBase32(padded);
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Generate a random 160-bit TOTP secret (the RFC 4226 recommended size and
|
|
58
|
+
* what authenticator apps expect). Store it encrypted at rest — it is a
|
|
59
|
+
* symmetric key, not a hash: anyone who reads it can mint valid codes.
|
|
60
|
+
*
|
|
61
|
+
* @example Enroll a user
|
|
62
|
+
* ```ts
|
|
63
|
+
* const { base32 } = generateTotpSecret();
|
|
64
|
+
* const uri = buildOtpauthUri({
|
|
65
|
+
* secret: base32,
|
|
66
|
+
* issuer: "Example",
|
|
67
|
+
* accountName: "user@example.com",
|
|
68
|
+
* });
|
|
69
|
+
* // render `uri` as a QR code; store `base32` encrypted
|
|
70
|
+
* ```
|
|
71
|
+
*/
|
|
72
|
+
export function generateTotpSecret() {
|
|
73
|
+
const secret = crypto.getRandomValues(new Uint8Array(20));
|
|
74
|
+
return { secret, base32: encodeBase32(secret).replaceAll("=", "") };
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Build the `otpauth://totp/...` provisioning URI an enrollment screen renders
|
|
78
|
+
* as a QR code. Follows the de-facto Google Authenticator format: the issuer
|
|
79
|
+
* appears both as the URI-encoded label prefix (`Issuer:account`) and as the
|
|
80
|
+
* `issuer` query parameter, so apps that read either convention group the
|
|
81
|
+
* entry correctly.
|
|
82
|
+
*
|
|
83
|
+
* @throws {TypeError} if the secret is not base32 or `digits`/`periodSeconds`
|
|
84
|
+
* are not positive integers, so a corrupted value cannot inject URI parameters.
|
|
85
|
+
*/
|
|
86
|
+
export function buildOtpauthUri(options) {
|
|
87
|
+
validateTotpParams(options);
|
|
88
|
+
const digits = options.digits ?? DEFAULT_DIGITS;
|
|
89
|
+
const periodSeconds = options.periodSeconds ?? DEFAULT_PERIOD_SECONDS;
|
|
90
|
+
const algorithm = options.algorithm ?? DEFAULT_ALGORITHM;
|
|
91
|
+
const secret = normalizeTotpSecret(options.secret);
|
|
92
|
+
if (!/^[A-Z2-7]+$/.test(secret)) {
|
|
93
|
+
throw new TypeError("otpauth secret must be base32");
|
|
94
|
+
}
|
|
95
|
+
const label = `${encodeURIComponent(options.issuer)}:${encodeURIComponent(options.accountName)}`;
|
|
96
|
+
const query = [
|
|
97
|
+
`secret=${secret}`,
|
|
98
|
+
`issuer=${encodeURIComponent(options.issuer)}`,
|
|
99
|
+
`algorithm=${algorithm.replaceAll("-", "")}`,
|
|
100
|
+
`digits=${digits}`,
|
|
101
|
+
`period=${periodSeconds}`,
|
|
102
|
+
].join("&");
|
|
103
|
+
return `otpauth://totp/${label}?${query}`;
|
|
104
|
+
}
|
|
105
|
+
function importHotpKey(key, algorithm) {
|
|
106
|
+
return crypto.subtle.importKey("raw", toArrayBuffer(key), { name: "HMAC", hash: algorithm }, false, ["sign"]);
|
|
107
|
+
}
|
|
108
|
+
async function hotp(hmacKey, counter, digits) {
|
|
109
|
+
const message = new Uint8Array(8);
|
|
110
|
+
new DataView(message.buffer).setBigUint64(0, BigInt(counter));
|
|
111
|
+
const mac = new Uint8Array(await crypto.subtle.sign("HMAC", hmacKey, message));
|
|
112
|
+
const offset = mac[mac.length - 1] & 0x0f;
|
|
113
|
+
const binary = ((mac[offset] & 0x7f) << 24) |
|
|
114
|
+
(mac[offset + 1] << 16) |
|
|
115
|
+
(mac[offset + 2] << 8) |
|
|
116
|
+
mac[offset + 3];
|
|
117
|
+
return (binary % 10 ** digits).toString().padStart(digits, "0");
|
|
118
|
+
}
|
|
119
|
+
function toKeyBytes(secret) {
|
|
120
|
+
return typeof secret === "string" ? decodeTotpSecret(secret) : secret;
|
|
121
|
+
}
|
|
122
|
+
function timeStep(timestamp, periodSeconds) {
|
|
123
|
+
return Math.floor(timestamp / 1000 / periodSeconds);
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Compute the TOTP code for a moment in time — what an authenticator app
|
|
127
|
+
* displays. Exported for tests and for tooling that needs to mint codes (e.g.
|
|
128
|
+
* an enrollment simulator); server-side verification should go through
|
|
129
|
+
* {@link verifyTotpCode}, which also handles the clock-skew window and
|
|
130
|
+
* constant-time comparison.
|
|
131
|
+
*
|
|
132
|
+
* @example Mint the current code in a test
|
|
133
|
+
* ```ts
|
|
134
|
+
* const code = await generateTotpCode({ secret: base32 });
|
|
135
|
+
* ```
|
|
136
|
+
*
|
|
137
|
+
* @throws {TypeError} if `digits` or `periodSeconds` is not a positive
|
|
138
|
+
* integer, or the secret is not valid base32.
|
|
139
|
+
*/
|
|
140
|
+
export async function generateTotpCode(options) {
|
|
141
|
+
validateTotpParams(options);
|
|
142
|
+
const algorithm = options.algorithm ?? DEFAULT_ALGORITHM;
|
|
143
|
+
const hmacKey = await importHotpKey(toKeyBytes(options.secret), algorithm);
|
|
144
|
+
return await hotp(hmacKey, timeStep(options.timestamp ?? Date.now(), options.periodSeconds ?? DEFAULT_PERIOD_SECONDS), options.digits ?? DEFAULT_DIGITS);
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Verify a submitted TOTP code against the secret, accepting `windows` steps
|
|
148
|
+
* of clock skew on either side. Code comparison is constant-time, and every
|
|
149
|
+
* candidate step is checked even after a match, so timing does not reveal
|
|
150
|
+
* which step (if any) matched. When adjacent steps happen to produce the same
|
|
151
|
+
* code, the newest step wins, so the replay guard below never rejects a fresh
|
|
152
|
+
* code over a stale tie.
|
|
153
|
+
*
|
|
154
|
+
* **Replay protection is the caller's half of the contract:** a valid result
|
|
155
|
+
* includes `matchedStep`; store the last accepted step and reject any code
|
|
156
|
+
* whose `matchedStep` is `<=` that value ({@link MfaService} in this module
|
|
157
|
+
* does this for you). Without it, a code stays valid for the whole skew
|
|
158
|
+
* window and can be replayed.
|
|
159
|
+
*
|
|
160
|
+
* @example Verify with replay rejection
|
|
161
|
+
* ```ts
|
|
162
|
+
* const result = await verifyTotpCode({ secret, code });
|
|
163
|
+
* if (result.valid && result.matchedStep > lastAcceptedStep) {
|
|
164
|
+
* lastAcceptedStep = result.matchedStep; // persist atomically
|
|
165
|
+
* // accepted
|
|
166
|
+
* }
|
|
167
|
+
* ```
|
|
168
|
+
*
|
|
169
|
+
* @throws {TypeError} if `digits`/`periodSeconds` is not a positive integer,
|
|
170
|
+
* `windows` is not a non-negative integer, or the secret is not valid base32.
|
|
171
|
+
*/
|
|
172
|
+
export async function verifyTotpCode(options) {
|
|
173
|
+
validateTotpParams(options);
|
|
174
|
+
const digits = options.digits ?? DEFAULT_DIGITS;
|
|
175
|
+
const periodSeconds = options.periodSeconds ?? DEFAULT_PERIOD_SECONDS;
|
|
176
|
+
const algorithm = options.algorithm ?? DEFAULT_ALGORITHM;
|
|
177
|
+
const windows = options.windows ?? DEFAULT_WINDOWS;
|
|
178
|
+
const code = options.code.replaceAll(/\s/g, "");
|
|
179
|
+
if (!/^\d+$/.test(code) || code.length !== digits) {
|
|
180
|
+
return { valid: false };
|
|
181
|
+
}
|
|
182
|
+
const hmacKey = await importHotpKey(toKeyBytes(options.secret), algorithm);
|
|
183
|
+
const step = timeStep(options.timestamp ?? Date.now(), periodSeconds);
|
|
184
|
+
const candidates = [];
|
|
185
|
+
for (let offset = -windows; offset <= windows; offset++) {
|
|
186
|
+
if (step + offset >= 0)
|
|
187
|
+
candidates.push(step + offset);
|
|
188
|
+
}
|
|
189
|
+
const expected = await Promise.all(candidates.map((candidate) => hotp(hmacKey, candidate, digits)));
|
|
190
|
+
const index = timingSafeMatchIndex(expected, code);
|
|
191
|
+
return index === undefined
|
|
192
|
+
? { valid: false }
|
|
193
|
+
: { valid: true, matchedStep: candidates[index] };
|
|
194
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Password-hash import primitives — the migration on-ramp for apps moving off
|
|
3
|
+
* another auth system without forcing a password reset.
|
|
4
|
+
*
|
|
5
|
+
* The model is **upgrade-on-login**: keep each imported user's existing
|
|
6
|
+
* (foreign-format) password hash, verify it on their first sign-in, and — on a
|
|
7
|
+
* match — transparently rehash the password into this package's native format
|
|
8
|
+
* ({@link PasswordIdentityService}, PBKDF2-SHA-256) and store that. The foreign
|
|
9
|
+
* hash is then cleared. Users never see a reset; the migration completes one
|
|
10
|
+
* login at a time. Wire it via {@link IdentityService}'s `legacyVerifiers`
|
|
11
|
+
* option plus the optional `getLegacyCredential`/`clearLegacyCredential` store
|
|
12
|
+
* hooks, plus atomic `replaceCredential` for upgrade writes.
|
|
13
|
+
*
|
|
14
|
+
* **Which algorithms ship built-in vs. bring-your-own (BYO).** The package's
|
|
15
|
+
* hard constraint is zero new runtime dependencies — Web Crypto only. So the
|
|
16
|
+
* only built-in verifier is {@link pbkdf2Verifier} (PBKDF2 is a Web Crypto
|
|
17
|
+
* primitive). **bcrypt, argon2, and scrypt cannot be implemented dep-free** —
|
|
18
|
+
* bcrypt and argon2 have no Web Crypto primitive, and scrypt is not exposed by
|
|
19
|
+
* Web Crypto or `@std/crypto`. Those are **BYO**: you implement the
|
|
20
|
+
* {@link LegacyPasswordVerifier} seam with your app's own bcrypt/argon2/scrypt
|
|
21
|
+
* dependency and pass it in. {@link parsePhc} is provided to parse the PHC /
|
|
22
|
+
* modular-crypt strings those algorithms store, so a BYO verifier is a few
|
|
23
|
+
* lines. See the migration guide for worked bcrypt/argon2 examples.
|
|
24
|
+
*
|
|
25
|
+
* @module
|
|
26
|
+
*/
|
|
27
|
+
import "../_dnt.polyfills.js";
|
|
28
|
+
/**
|
|
29
|
+
* A verifier for one foreign password-hash format, used during
|
|
30
|
+
* upgrade-on-login. Connectors identify the formats they handle by sniffing the
|
|
31
|
+
* stored hash string in {@link LegacyPasswordVerifier.canVerify}; the first
|
|
32
|
+
* verifier in {@link IdentityService}'s `legacyVerifiers` list whose `canVerify`
|
|
33
|
+
* returns `true` is asked to {@link LegacyPasswordVerifier.verify} the password.
|
|
34
|
+
*
|
|
35
|
+
* Implement this to bring your own bcrypt/argon2/scrypt verifier (using your
|
|
36
|
+
* app's dependency) — the package only ships {@link pbkdf2Verifier} built-in.
|
|
37
|
+
*
|
|
38
|
+
* @example Bring-your-own bcrypt (app adds an `npm:bcryptjs` dependency)
|
|
39
|
+
* ```ts
|
|
40
|
+
* import { compare } from "bcryptjs";
|
|
41
|
+
* import type { LegacyPasswordVerifier } from "@udibo/oauth2/identity/migration";
|
|
42
|
+
*
|
|
43
|
+
* const bcryptVerifier: LegacyPasswordVerifier = {
|
|
44
|
+
* id: "bcrypt",
|
|
45
|
+
* canVerify: (phc) => /^\$2[aby]?\$/.test(phc),
|
|
46
|
+
* verify: (password, phc) => compare(password, phc),
|
|
47
|
+
* };
|
|
48
|
+
* ```
|
|
49
|
+
*/
|
|
50
|
+
export interface LegacyPasswordVerifier {
|
|
51
|
+
/**
|
|
52
|
+
* Stable identifier for this format (e.g. `"bcrypt"`, `"argon2id"`,
|
|
53
|
+
* `"pbkdf2"`). Surfaced on the `password.upgraded` audit event so you can see
|
|
54
|
+
* which format a migrated user came from.
|
|
55
|
+
*/
|
|
56
|
+
readonly id: string;
|
|
57
|
+
/**
|
|
58
|
+
* Cheap, synchronous sniff of whether this verifier recognizes `phc` (its
|
|
59
|
+
* prefix/shape). Must not throw — return `false` for anything unrecognized or
|
|
60
|
+
* malformed. Selection stops at the first verifier that returns `true`.
|
|
61
|
+
*/
|
|
62
|
+
canVerify(phc: string): boolean;
|
|
63
|
+
/**
|
|
64
|
+
* Verify `password` against the stored foreign hash `phc`. Called only when
|
|
65
|
+
* {@link LegacyPasswordVerifier.canVerify} returned `true`. Resolve `false`
|
|
66
|
+
* (never throw) on a mismatch or a malformed hash. Use a constant-time
|
|
67
|
+
* comparison.
|
|
68
|
+
*/
|
|
69
|
+
verify(password: string, phc: string): Promise<boolean>;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* The first verifier in `verifiers` (in order) whose `canVerify` accepts `phc`,
|
|
73
|
+
* or `undefined` if none do. A verifier that throws from `canVerify` is skipped
|
|
74
|
+
* rather than propagated, so one misbehaving connector can't break selection.
|
|
75
|
+
*/
|
|
76
|
+
export declare function findLegacyVerifier(verifiers: readonly LegacyPasswordVerifier[], phc: string): LegacyPasswordVerifier | undefined;
|
|
77
|
+
/**
|
|
78
|
+
* Verify `password` against a stored foreign hash `phc` using the first matching
|
|
79
|
+
* verifier in `verifiers`. Resolves `false` when no verifier recognizes `phc` or
|
|
80
|
+
* the matched verifier rejects the password. Useful for a bulk-import
|
|
81
|
+
* dry-run/validation pass; {@link IdentityService.signIn} uses the same
|
|
82
|
+
* selection internally for upgrade-on-login.
|
|
83
|
+
*/
|
|
84
|
+
export declare function verifyLegacyPassword(verifiers: readonly LegacyPasswordVerifier[], password: string, phc: string): Promise<boolean>;
|
|
85
|
+
/**
|
|
86
|
+
* A parsed PHC / modular-crypt string ({@link parsePhc}). The salt and hash
|
|
87
|
+
* segments are decoded from base64 (the PHC "B64" alphabet, padding optional).
|
|
88
|
+
*/
|
|
89
|
+
export interface PhcHash {
|
|
90
|
+
/** Algorithm identifier, e.g. `"argon2id"`, `"scrypt"`, `"pbkdf2-sha256"`. */
|
|
91
|
+
id: string;
|
|
92
|
+
/** The `v=` version field, if present (e.g. argon2's `19`). */
|
|
93
|
+
version?: number;
|
|
94
|
+
/** The comma-separated `k=v` parameter field, e.g. argon2's `{ m, t, p }`. */
|
|
95
|
+
params: Record<string, string>;
|
|
96
|
+
/** Decoded salt bytes, if a salt segment was present. */
|
|
97
|
+
salt?: Uint8Array;
|
|
98
|
+
/** Decoded hash/checksum bytes, if a hash segment was present. */
|
|
99
|
+
hash?: Uint8Array;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Parse a PHC / modular-crypt string (`$id[$v=n][$k=v,...]$salt$hash`) into its
|
|
103
|
+
* parts. Returns `null` for anything that isn't a `$`-led PHC string. Salt and
|
|
104
|
+
* hash are decoded as base64 (padding optional). This is the parsing helper for
|
|
105
|
+
* writing a bring-your-own argon2/scrypt {@link LegacyPasswordVerifier}: parse
|
|
106
|
+
* the stored string, feed `params`/`salt`/`hash` to your KDF dependency, and
|
|
107
|
+
* constant-time compare.
|
|
108
|
+
*
|
|
109
|
+
* @example
|
|
110
|
+
* ```ts
|
|
111
|
+
* const parsed = parsePhc("$argon2id$v=19$m=65536,t=3,p=4$c29tZXNhbHQ$aGFzaA");
|
|
112
|
+
* parsed?.id; // "argon2id"
|
|
113
|
+
* parsed?.version; // 19
|
|
114
|
+
* parsed?.params.m; // "65536"
|
|
115
|
+
* ```
|
|
116
|
+
*/
|
|
117
|
+
export declare function parsePhc(phc: string): PhcHash | null;
|
|
118
|
+
/** Options for {@link pbkdf2Verifier}. */
|
|
119
|
+
export interface Pbkdf2VerifierOptions {
|
|
120
|
+
/** Verifier id surfaced on the `password.upgraded` event. Defaults to `"pbkdf2"`. */
|
|
121
|
+
id?: string;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Built-in {@link LegacyPasswordVerifier} for PBKDF2 hashes — the only family
|
|
125
|
+
* this package can verify dep-free (PBKDF2 is a Web Crypto primitive). Handles
|
|
126
|
+
* two common encodings, auto-detected:
|
|
127
|
+
*
|
|
128
|
+
* - **Django** — `pbkdf2_<digest>$<iterations>$<salt>$<base64 hash>`, where the
|
|
129
|
+
* salt is used as raw UTF-8 bytes and the hash is standard base64. This is
|
|
130
|
+
* Django's `PBKDF2PasswordHasher` output.
|
|
131
|
+
* - **PHC** — `$pbkdf2-<digest>$i=<iterations>$<base64 salt>$<base64 hash>`,
|
|
132
|
+
* with base64 (padding optional) salt and hash.
|
|
133
|
+
*
|
|
134
|
+
* `<digest>` is one of `sha1`, `sha256`, `sha512`. The derived-key length is
|
|
135
|
+
* taken from the stored hash. Comparison is constant-time. On a match,
|
|
136
|
+
* {@link IdentityService} rehashes into its native PBKDF2 format and clears the
|
|
137
|
+
* imported hash.
|
|
138
|
+
*
|
|
139
|
+
* A hash outside the accepted bounds is declined rather than verified —
|
|
140
|
+
* `canVerify` returns `false` and `verify` resolves `false`, which reads to your
|
|
141
|
+
* app as a wrong password. The bounds are more than 1,000,000 iterations and a
|
|
142
|
+
* stored checksum shorter than 16 bytes.
|
|
143
|
+
*
|
|
144
|
+
* @example
|
|
145
|
+
* ```ts
|
|
146
|
+
* import { IdentityService } from "@udibo/oauth2/identity";
|
|
147
|
+
* import { pbkdf2Verifier } from "@udibo/oauth2/identity/migration";
|
|
148
|
+
*
|
|
149
|
+
* const identity = new IdentityService({
|
|
150
|
+
* users: store, // implements getLegacyCredential/clearLegacyCredential
|
|
151
|
+
* legacyVerifiers: [pbkdf2Verifier()],
|
|
152
|
+
* });
|
|
153
|
+
* ```
|
|
154
|
+
*/
|
|
155
|
+
export declare function pbkdf2Verifier(options?: Pbkdf2VerifierOptions): LegacyPasswordVerifier;
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Password-hash import primitives — the migration on-ramp for apps moving off
|
|
3
|
+
* another auth system without forcing a password reset.
|
|
4
|
+
*
|
|
5
|
+
* The model is **upgrade-on-login**: keep each imported user's existing
|
|
6
|
+
* (foreign-format) password hash, verify it on their first sign-in, and — on a
|
|
7
|
+
* match — transparently rehash the password into this package's native format
|
|
8
|
+
* ({@link PasswordIdentityService}, PBKDF2-SHA-256) and store that. The foreign
|
|
9
|
+
* hash is then cleared. Users never see a reset; the migration completes one
|
|
10
|
+
* login at a time. Wire it via {@link IdentityService}'s `legacyVerifiers`
|
|
11
|
+
* option plus the optional `getLegacyCredential`/`clearLegacyCredential` store
|
|
12
|
+
* hooks, plus atomic `replaceCredential` for upgrade writes.
|
|
13
|
+
*
|
|
14
|
+
* **Which algorithms ship built-in vs. bring-your-own (BYO).** The package's
|
|
15
|
+
* hard constraint is zero new runtime dependencies — Web Crypto only. So the
|
|
16
|
+
* only built-in verifier is {@link pbkdf2Verifier} (PBKDF2 is a Web Crypto
|
|
17
|
+
* primitive). **bcrypt, argon2, and scrypt cannot be implemented dep-free** —
|
|
18
|
+
* bcrypt and argon2 have no Web Crypto primitive, and scrypt is not exposed by
|
|
19
|
+
* Web Crypto or `@std/crypto`. Those are **BYO**: you implement the
|
|
20
|
+
* {@link LegacyPasswordVerifier} seam with your app's own bcrypt/argon2/scrypt
|
|
21
|
+
* dependency and pass it in. {@link parsePhc} is provided to parse the PHC /
|
|
22
|
+
* modular-crypt strings those algorithms store, so a BYO verifier is a few
|
|
23
|
+
* lines. See the migration guide for worked bcrypt/argon2 examples.
|
|
24
|
+
*
|
|
25
|
+
* @module
|
|
26
|
+
*/
|
|
27
|
+
import "../_dnt.polyfills.js";
|
|
28
|
+
import { decodeBase64 } from "../deps/jsr.io/@std/encoding/1.0.11/mod.js";
|
|
29
|
+
import { timingSafeEqual } from "../deps/jsr.io/@std/crypto/1.1.0/timing_safe_equal.js";
|
|
30
|
+
/**
|
|
31
|
+
* The first verifier in `verifiers` (in order) whose `canVerify` accepts `phc`,
|
|
32
|
+
* or `undefined` if none do. A verifier that throws from `canVerify` is skipped
|
|
33
|
+
* rather than propagated, so one misbehaving connector can't break selection.
|
|
34
|
+
*/
|
|
35
|
+
export function findLegacyVerifier(verifiers, phc) {
|
|
36
|
+
return verifiers.find((verifier) => {
|
|
37
|
+
try {
|
|
38
|
+
return verifier.canVerify(phc);
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Verify `password` against a stored foreign hash `phc` using the first matching
|
|
47
|
+
* verifier in `verifiers`. Resolves `false` when no verifier recognizes `phc` or
|
|
48
|
+
* the matched verifier rejects the password. Useful for a bulk-import
|
|
49
|
+
* dry-run/validation pass; {@link IdentityService.signIn} uses the same
|
|
50
|
+
* selection internally for upgrade-on-login.
|
|
51
|
+
*/
|
|
52
|
+
export async function verifyLegacyPassword(verifiers, password, phc) {
|
|
53
|
+
const verifier = findLegacyVerifier(verifiers, phc);
|
|
54
|
+
if (!verifier)
|
|
55
|
+
return false;
|
|
56
|
+
try {
|
|
57
|
+
return await verifier.verify(password, phc);
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
function decodeB64(segment) {
|
|
64
|
+
const padded = segment + "=".repeat((4 - (segment.length % 4)) % 4);
|
|
65
|
+
return decodeBase64(padded);
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Parse a PHC / modular-crypt string (`$id[$v=n][$k=v,...]$salt$hash`) into its
|
|
69
|
+
* parts. Returns `null` for anything that isn't a `$`-led PHC string. Salt and
|
|
70
|
+
* hash are decoded as base64 (padding optional). This is the parsing helper for
|
|
71
|
+
* writing a bring-your-own argon2/scrypt {@link LegacyPasswordVerifier}: parse
|
|
72
|
+
* the stored string, feed `params`/`salt`/`hash` to your KDF dependency, and
|
|
73
|
+
* constant-time compare.
|
|
74
|
+
*
|
|
75
|
+
* @example
|
|
76
|
+
* ```ts
|
|
77
|
+
* const parsed = parsePhc("$argon2id$v=19$m=65536,t=3,p=4$c29tZXNhbHQ$aGFzaA");
|
|
78
|
+
* parsed?.id; // "argon2id"
|
|
79
|
+
* parsed?.version; // 19
|
|
80
|
+
* parsed?.params.m; // "65536"
|
|
81
|
+
* ```
|
|
82
|
+
*/
|
|
83
|
+
export function parsePhc(phc) {
|
|
84
|
+
if (typeof phc !== "string" || !phc.startsWith("$"))
|
|
85
|
+
return null;
|
|
86
|
+
const fields = phc.split("$").slice(1);
|
|
87
|
+
if (fields.length === 0 || fields[0] === "")
|
|
88
|
+
return null;
|
|
89
|
+
const id = fields[0];
|
|
90
|
+
let version;
|
|
91
|
+
const params = {};
|
|
92
|
+
const rest = fields.slice(1);
|
|
93
|
+
let index = 0;
|
|
94
|
+
if (rest[index]?.startsWith("v=")) {
|
|
95
|
+
version = Number(rest[index].slice(2));
|
|
96
|
+
index++;
|
|
97
|
+
}
|
|
98
|
+
if (rest[index] && rest[index].includes("=")) {
|
|
99
|
+
for (const pair of rest[index].split(",")) {
|
|
100
|
+
const eq = pair.indexOf("=");
|
|
101
|
+
if (eq === -1)
|
|
102
|
+
continue;
|
|
103
|
+
params[pair.slice(0, eq)] = pair.slice(eq + 1);
|
|
104
|
+
}
|
|
105
|
+
index++;
|
|
106
|
+
}
|
|
107
|
+
const saltSegment = rest[index++];
|
|
108
|
+
const hashSegment = rest[index];
|
|
109
|
+
const result = { id, params };
|
|
110
|
+
if (version !== undefined)
|
|
111
|
+
result.version = version;
|
|
112
|
+
try {
|
|
113
|
+
if (saltSegment)
|
|
114
|
+
result.salt = decodeB64(saltSegment);
|
|
115
|
+
if (hashSegment)
|
|
116
|
+
result.hash = decodeB64(hashSegment);
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
return result;
|
|
122
|
+
}
|
|
123
|
+
const PBKDF2_DIGEST = {
|
|
124
|
+
sha1: "SHA-1",
|
|
125
|
+
sha256: "SHA-256",
|
|
126
|
+
sha512: "SHA-512",
|
|
127
|
+
};
|
|
128
|
+
const encoder = new TextEncoder();
|
|
129
|
+
/**
|
|
130
|
+
* Reject an imported hash whose derived-key segment is implausibly short: a
|
|
131
|
+
* 0-byte checksum would make `deriveBits(0)` compare empty-to-empty and turn
|
|
132
|
+
* any password into a match on a runtime where that returns an empty buffer.
|
|
133
|
+
*/
|
|
134
|
+
const MIN_HASH_BYTES = 16;
|
|
135
|
+
const MAX_PBKDF2_ITERATIONS = 1_000_000;
|
|
136
|
+
function parseDjango(phc) {
|
|
137
|
+
const parts = phc.split("$");
|
|
138
|
+
if (parts.length !== 4)
|
|
139
|
+
return null;
|
|
140
|
+
const [algorithm, iterationsRaw, salt, hashB64] = parts;
|
|
141
|
+
const digestName = algorithm.slice("pbkdf2_".length);
|
|
142
|
+
const digest = PBKDF2_DIGEST[digestName];
|
|
143
|
+
if (!digest)
|
|
144
|
+
return null;
|
|
145
|
+
const iterations = Number(iterationsRaw);
|
|
146
|
+
if (!Number.isInteger(iterations) || iterations < 1)
|
|
147
|
+
return null;
|
|
148
|
+
try {
|
|
149
|
+
return {
|
|
150
|
+
digest,
|
|
151
|
+
iterations,
|
|
152
|
+
salt: encoder.encode(salt),
|
|
153
|
+
expected: decodeBase64(hashB64),
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
function parsePhcPbkdf2(phc) {
|
|
161
|
+
const parsed = parsePhc(phc);
|
|
162
|
+
if (!parsed || !parsed.salt || !parsed.hash)
|
|
163
|
+
return null;
|
|
164
|
+
const digestName = parsed.id.slice("pbkdf2-".length);
|
|
165
|
+
const digest = PBKDF2_DIGEST[digestName];
|
|
166
|
+
if (!digest)
|
|
167
|
+
return null;
|
|
168
|
+
const iterations = Number(parsed.params.i ?? parsed.params.rounds);
|
|
169
|
+
if (!Number.isInteger(iterations) || iterations < 1)
|
|
170
|
+
return null;
|
|
171
|
+
return { digest, iterations, salt: parsed.salt, expected: parsed.hash };
|
|
172
|
+
}
|
|
173
|
+
function parsePbkdf2(phc) {
|
|
174
|
+
const parsed = phc.startsWith("pbkdf2_")
|
|
175
|
+
? parseDjango(phc)
|
|
176
|
+
: phc.startsWith("$pbkdf2-")
|
|
177
|
+
? parsePhcPbkdf2(phc)
|
|
178
|
+
: null;
|
|
179
|
+
if (!parsed)
|
|
180
|
+
return null;
|
|
181
|
+
if (parsed.iterations > MAX_PBKDF2_ITERATIONS)
|
|
182
|
+
return null;
|
|
183
|
+
if (parsed.expected.length < MIN_HASH_BYTES)
|
|
184
|
+
return null;
|
|
185
|
+
return parsed;
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Built-in {@link LegacyPasswordVerifier} for PBKDF2 hashes — the only family
|
|
189
|
+
* this package can verify dep-free (PBKDF2 is a Web Crypto primitive). Handles
|
|
190
|
+
* two common encodings, auto-detected:
|
|
191
|
+
*
|
|
192
|
+
* - **Django** — `pbkdf2_<digest>$<iterations>$<salt>$<base64 hash>`, where the
|
|
193
|
+
* salt is used as raw UTF-8 bytes and the hash is standard base64. This is
|
|
194
|
+
* Django's `PBKDF2PasswordHasher` output.
|
|
195
|
+
* - **PHC** — `$pbkdf2-<digest>$i=<iterations>$<base64 salt>$<base64 hash>`,
|
|
196
|
+
* with base64 (padding optional) salt and hash.
|
|
197
|
+
*
|
|
198
|
+
* `<digest>` is one of `sha1`, `sha256`, `sha512`. The derived-key length is
|
|
199
|
+
* taken from the stored hash. Comparison is constant-time. On a match,
|
|
200
|
+
* {@link IdentityService} rehashes into its native PBKDF2 format and clears the
|
|
201
|
+
* imported hash.
|
|
202
|
+
*
|
|
203
|
+
* A hash outside the accepted bounds is declined rather than verified —
|
|
204
|
+
* `canVerify` returns `false` and `verify` resolves `false`, which reads to your
|
|
205
|
+
* app as a wrong password. The bounds are more than 1,000,000 iterations and a
|
|
206
|
+
* stored checksum shorter than 16 bytes.
|
|
207
|
+
*
|
|
208
|
+
* @example
|
|
209
|
+
* ```ts
|
|
210
|
+
* import { IdentityService } from "@udibo/oauth2/identity";
|
|
211
|
+
* import { pbkdf2Verifier } from "@udibo/oauth2/identity/migration";
|
|
212
|
+
*
|
|
213
|
+
* const identity = new IdentityService({
|
|
214
|
+
* users: store, // implements getLegacyCredential/clearLegacyCredential
|
|
215
|
+
* legacyVerifiers: [pbkdf2Verifier()],
|
|
216
|
+
* });
|
|
217
|
+
* ```
|
|
218
|
+
*/
|
|
219
|
+
export function pbkdf2Verifier(options = {}) {
|
|
220
|
+
const id = options.id ?? "pbkdf2";
|
|
221
|
+
return {
|
|
222
|
+
id,
|
|
223
|
+
canVerify(phc) {
|
|
224
|
+
return parsePbkdf2(phc) !== null;
|
|
225
|
+
},
|
|
226
|
+
async verify(password, phc) {
|
|
227
|
+
const parsed = parsePbkdf2(phc);
|
|
228
|
+
if (!parsed)
|
|
229
|
+
return false;
|
|
230
|
+
const key = await crypto.subtle.importKey("raw", encoder.encode(password), "PBKDF2", false, ["deriveBits"]);
|
|
231
|
+
const bits = await crypto.subtle.deriveBits({
|
|
232
|
+
name: "PBKDF2",
|
|
233
|
+
salt: new Uint8Array(parsed.salt),
|
|
234
|
+
iterations: parsed.iterations,
|
|
235
|
+
hash: parsed.digest,
|
|
236
|
+
}, key, parsed.expected.length * 8);
|
|
237
|
+
const derived = new Uint8Array(bits);
|
|
238
|
+
if (derived.length !== parsed.expected.length)
|
|
239
|
+
return false;
|
|
240
|
+
return timingSafeEqual(derived, parsed.expected);
|
|
241
|
+
},
|
|
242
|
+
};
|
|
243
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Identity self-service primitives for apps that own their login.
|
|
3
|
+
*
|
|
4
|
+
* The OAuth2/BFF machinery handles tokens and sessions; this layer covers the
|
|
5
|
+
* "own-auth" flows an app otherwise hand-rolls. It is **optional** and
|
|
6
|
+
* composable — own the flows and the token lifecycle, keep storage as thin,
|
|
7
|
+
* app-owned seams (the `SessionStore` philosophy), never a DB-adapter zoo.
|
|
8
|
+
*
|
|
9
|
+
* - {@link IdentityService} — orchestrates the primitives below into sign-up,
|
|
10
|
+
* sign-in, password-reset, and email-verification flows you call from your
|
|
11
|
+
* own routes (storage stays an app-owned {@link IdentityUserStore}).
|
|
12
|
+
* - {@link PasswordIdentityService} — PBKDF2 password hashing + constant-time
|
|
13
|
+
* verify (no more hand-rolled crypto); implement {@link PasswordHasherLike}
|
|
14
|
+
* instead to plug in argon2 or scrypt.
|
|
15
|
+
* - {@link TokenFlowService} — single-use, hashed-at-rest verify/reset/sign-in
|
|
16
|
+
* tokens over an app-owned {@link TokenFlowStore} (magic links reuse it via
|
|
17
|
+
* {@link IdentityService.requestSignInLink}).
|
|
18
|
+
* - {@link EmailOtpService} — short numeric one-time codes over an app-owned
|
|
19
|
+
* {@link OtpStore}: hashed at rest, attempt-capped (guessable-online threat
|
|
20
|
+
* model), purpose-generic so it doubles as a step-up factor. Wire it into
|
|
21
|
+
* {@link IdentityService} via the `otp` option for "email me a code"
|
|
22
|
+
* sign-in.
|
|
23
|
+
* - {@link createIdentifierResolver} / {@link classifyIdentifier} — accept
|
|
24
|
+
* "email or username (or phone)" on a sign-in form without the bespoke branch.
|
|
25
|
+
* - {@link DeliveryHooks} + URL builders — the seam your mailer/SMS plugs into.
|
|
26
|
+
* - {@link RevocableSessionService} / {@link ListableSessionService} /
|
|
27
|
+
* {@link isRecentlyAuthenticated} — session revocation on credential change,
|
|
28
|
+
* the optional listing half ({@link SessionSummary} rows for a "where you're
|
|
29
|
+
* signed in" screen), and step-up checks.
|
|
30
|
+
* - {@link RateLimiter} — opt-in throttling (wire it into
|
|
31
|
+
* {@link IdentityService}); {@link checkPasswordPolicy} — the password rules
|
|
32
|
+
* {@link IdentityService} already applies to `signUp`/`resetPassword`, at
|
|
33
|
+
* their 8–256 character defaults unless you pass a tighter `passwordPolicy`.
|
|
34
|
+
* - {@link CaptchaProvider} + {@link verifyCaptcha} — the bring-your-own
|
|
35
|
+
* bot-challenge seam (Turnstile/hCaptcha/reCAPTCHA), with fail-open/closed
|
|
36
|
+
* handling; no provider means no challenge.
|
|
37
|
+
* - {@link AccountLockout} — failed-attempt lockout with a self-service unlock
|
|
38
|
+
* flow ({@link IdentityService.unlockAccount}) and a log-only rollout mode
|
|
39
|
+
* via `protectionMode`.
|
|
40
|
+
* - {@link IdentityEvent} + {@link IdentityEventHook} — auth-outcome events for
|
|
41
|
+
* audit capture (wire via {@link IdentityService}'s `onEvent`).
|
|
42
|
+
* - {@link IdentityError} — a stable error contract a front-end can switch on.
|
|
43
|
+
* - Importing users from another auth system without a password reset
|
|
44
|
+
* (upgrade-on-login) lives on its own subpath,
|
|
45
|
+
* `@udibo/oauth2/identity/migration`: the `LegacyPasswordVerifier` seam, the
|
|
46
|
+
* built-in `pbkdf2Verifier`, and `parsePhc`. Wire them through this module's
|
|
47
|
+
* {@link IdentityServiceOptions.legacyVerifiers}.
|
|
48
|
+
*
|
|
49
|
+
* @module
|
|
50
|
+
*/
|
|
51
|
+
import "../_dnt.polyfills.js";
|
|
52
|
+
export { DEFAULT_PBKDF2_ITERATIONS, generateSalt, hashPassword, LEGACY_PBKDF2_ITERATIONS, type PasswordCredential, type PasswordHasherLike, type PasswordHashParams, PasswordIdentityService, type PasswordIdentityServiceOptions, PBKDF2_SHA256, verifyPassword, } from "./password.js";
|
|
53
|
+
export { classifyIdentifier, createIdentifierResolver, type IdentifierKind, type IdentifierLookups, } from "./identifier.js";
|
|
54
|
+
export { type CreatedToken, type CreateTokenOptions, MemoryTokenFlowStore, type ResolvedToken, type TokenFlowRecord, TokenFlowService, type TokenFlowStore, TokenPurpose, type TokenStatus, } from "./token-flow.js";
|
|
55
|
+
export { buildResetUrl, buildSignInUrl, buildTokenUrl, buildUnlockUrl, buildVerificationUrl, type CodeDeliveryHook, type CodeDeliveryMessage, type DeliveryHook, type DeliveryHooks, type DeliveryMessage, } from "./delivery.js";
|
|
56
|
+
export { isRecentlyAuthenticated, type ListableSessionService, type RevocableSessionService, type SessionSummary, supportsSessionListing, } from "./session.js";
|
|
57
|
+
export { type ConsumeSignInLinkResult, type IdentityRateLimiters, IdentityService, type IdentityServiceOptions, type IdentityUser, type IdentityUserStore, type UnlockAccountResult, type VerifyEmailResult, type VerifySignInCodeResult, } from "./service.js";
|
|
58
|
+
export { EmailOtpService, type EmailOtpServiceOptions, MemoryOtpStore, type OtpRecord, type OtpStore, type RequestedOtp, type RequestOtpOptions, type VerifyOtpOptions, type VerifyOtpResult, } from "./otp.js";
|
|
59
|
+
export { AccountLockout, type AccountLockoutLike, type AccountLockoutOptions, type LockoutFailureResult, type LockoutRecord, type LockoutStatus, type LockoutStore, MemoryLockoutStore, } from "./lockout.js";
|
|
60
|
+
export type { IdentityEvent, IdentityEventHook, SignInCodeFailureReason, SignInFailureReason, } from "./events.js";
|
|
61
|
+
export { IdentityError, type IdentityErrorCode, type IdentityErrorOptions, type IdentityErrorStatus, identityErrorStatus, isIdentityError, } from "./errors.js";
|
|
62
|
+
export { MemoryRateLimitStore, RateLimiter, type RateLimiterLike, type RateLimiterOptions, type RateLimitResult, type RateLimitStore, } from "./rate-limit.js";
|
|
63
|
+
export { type CaptchaOutcome, type CaptchaProvider, type CaptchaVerifyContext, type CaptchaVerifyResult, verifyCaptcha, type VerifyCaptchaOptions, } from "./captcha.js";
|
|
64
|
+
export { assertPasswordPolicy, checkPasswordPolicy, type PasswordPolicy, type PasswordPolicyResult, } from "./password-policy.js";
|
|
65
|
+
export { type BreachedPasswordOptions, breachedPasswordValidator, } from "./hibp.js";
|