@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,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MFA primitives for apps that own their login: TOTP (RFC 6238) and single-use
|
|
3
|
+
* recovery codes, plus a small orchestrator over an app-owned store — the same
|
|
4
|
+
* thin-seam philosophy as the rest of the identity layer. Built entirely on
|
|
5
|
+
* Web Crypto and `@std`; no other dependencies.
|
|
6
|
+
*
|
|
7
|
+
* - {@link generateTotpSecret} / {@link buildOtpauthUri} /
|
|
8
|
+
* {@link generateTotpCode} / {@link verifyTotpCode} — the TOTP math, with a
|
|
9
|
+
* constant-time verify that reports the matched time step for replay
|
|
10
|
+
* rejection.
|
|
11
|
+
* - {@link generateRecoveryCodes} / {@link hashRecoveryCode} /
|
|
12
|
+
* {@link verifyRecoveryCode} — readable single-use fallback codes, hashed at
|
|
13
|
+
* rest.
|
|
14
|
+
* - {@link MfaService} over an app-implemented {@link MfaStore} — enrollment
|
|
15
|
+
* (pending → confirmed), verification with atomic replay and burn handling,
|
|
16
|
+
* regeneration, disable, plus the layer's usual seams (`rateLimiter`,
|
|
17
|
+
* `onEvent` for `mfa.*` audit events). {@link MemoryMfaStore} ships for
|
|
18
|
+
* dev/tests.
|
|
19
|
+
*
|
|
20
|
+
* The library stops at the primitives: whether MFA is optional or required,
|
|
21
|
+
* where challenges happen, and sessions are the app's policy and belong in
|
|
22
|
+
* its routes.
|
|
23
|
+
*
|
|
24
|
+
* @example Wire MFA into an app
|
|
25
|
+
* ```ts
|
|
26
|
+
* import { MemoryMfaStore, MfaService } from "@udibo/oauth2/identity/mfa";
|
|
27
|
+
*
|
|
28
|
+
* const mfa = new MfaService({ store: new MemoryMfaStore() });
|
|
29
|
+
* const { otpauthUri } = await mfa.startEnrollment(user.id, {
|
|
30
|
+
* issuer: "Example",
|
|
31
|
+
* accountName: user.email,
|
|
32
|
+
* });
|
|
33
|
+
* // render otpauthUri as a QR, then:
|
|
34
|
+
* const confirmation = await mfa.confirmEnrollment(user.id, submittedCode);
|
|
35
|
+
* ```
|
|
36
|
+
*
|
|
37
|
+
* @module
|
|
38
|
+
*/
|
|
39
|
+
import "../../_dnt.polyfills.js";
|
|
40
|
+
export { buildOtpauthUri, decodeTotpSecret, type GeneratedTotpSecret, generateTotpCode, type GenerateTotpCodeOptions, generateTotpSecret, type OtpauthUriOptions, type TotpAlgorithm, type TotpVerification, verifyTotpCode, type VerifyTotpCodeOptions, } from "./totp.js";
|
|
41
|
+
export { type GeneratedRecoveryCodes, generateRecoveryCodes, type GenerateRecoveryCodesOptions, hashRecoveryCode, normalizeRecoveryCode, RECOVERY_CODE_LENGTH, type RecoveryCodeVerification, verifyRecoveryCode, type VerifyRecoveryCodeOptions, } from "./recovery-codes.js";
|
|
42
|
+
export { MemoryMfaStore, type MfaEnrollmentConfirmation, type MfaEnrollmentStart, type MfaEnrollmentStatus, MfaService, type MfaServiceOptions, type MfaStore, type MfaTotpOptions, type MfaTotpRecord, type MfaVerification, type MfaVerifyFailureReason, type MfaVerifyOptions, type StartEnrollmentOptions, } from "./service.js";
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MFA primitives for apps that own their login: TOTP (RFC 6238) and single-use
|
|
3
|
+
* recovery codes, plus a small orchestrator over an app-owned store — the same
|
|
4
|
+
* thin-seam philosophy as the rest of the identity layer. Built entirely on
|
|
5
|
+
* Web Crypto and `@std`; no other dependencies.
|
|
6
|
+
*
|
|
7
|
+
* - {@link generateTotpSecret} / {@link buildOtpauthUri} /
|
|
8
|
+
* {@link generateTotpCode} / {@link verifyTotpCode} — the TOTP math, with a
|
|
9
|
+
* constant-time verify that reports the matched time step for replay
|
|
10
|
+
* rejection.
|
|
11
|
+
* - {@link generateRecoveryCodes} / {@link hashRecoveryCode} /
|
|
12
|
+
* {@link verifyRecoveryCode} — readable single-use fallback codes, hashed at
|
|
13
|
+
* rest.
|
|
14
|
+
* - {@link MfaService} over an app-implemented {@link MfaStore} — enrollment
|
|
15
|
+
* (pending → confirmed), verification with atomic replay and burn handling,
|
|
16
|
+
* regeneration, disable, plus the layer's usual seams (`rateLimiter`,
|
|
17
|
+
* `onEvent` for `mfa.*` audit events). {@link MemoryMfaStore} ships for
|
|
18
|
+
* dev/tests.
|
|
19
|
+
*
|
|
20
|
+
* The library stops at the primitives: whether MFA is optional or required,
|
|
21
|
+
* where challenges happen, and sessions are the app's policy and belong in
|
|
22
|
+
* its routes.
|
|
23
|
+
*
|
|
24
|
+
* @example Wire MFA into an app
|
|
25
|
+
* ```ts
|
|
26
|
+
* import { MemoryMfaStore, MfaService } from "@udibo/oauth2/identity/mfa";
|
|
27
|
+
*
|
|
28
|
+
* const mfa = new MfaService({ store: new MemoryMfaStore() });
|
|
29
|
+
* const { otpauthUri } = await mfa.startEnrollment(user.id, {
|
|
30
|
+
* issuer: "Example",
|
|
31
|
+
* accountName: user.email,
|
|
32
|
+
* });
|
|
33
|
+
* // render otpauthUri as a QR, then:
|
|
34
|
+
* const confirmation = await mfa.confirmEnrollment(user.id, submittedCode);
|
|
35
|
+
* ```
|
|
36
|
+
*
|
|
37
|
+
* @module
|
|
38
|
+
*/
|
|
39
|
+
import "../../_dnt.polyfills.js";
|
|
40
|
+
export { buildOtpauthUri, decodeTotpSecret, generateTotpCode, generateTotpSecret, verifyTotpCode, } from "./totp.js";
|
|
41
|
+
export { generateRecoveryCodes, hashRecoveryCode, normalizeRecoveryCode, RECOVERY_CODE_LENGTH, verifyRecoveryCode, } from "./recovery-codes.js";
|
|
42
|
+
export { MemoryMfaStore, MfaService, } from "./service.js";
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Recovery-code primitives for MFA — the single-use fallback a user redeems
|
|
3
|
+
* when their authenticator is unavailable.
|
|
4
|
+
*
|
|
5
|
+
* Codes are generated from a Crockford-style alphabet (no `I`, `L`, `O`, or
|
|
6
|
+
* `U`, so nothing reads ambiguously off paper) as two 8-character groups —
|
|
7
|
+
* 80 bits of entropy per code. Only SHA-256 hashes are meant to be stored:
|
|
8
|
+
* at 80 bits an unsalted fast hash is sufficient — even a full offline
|
|
9
|
+
* keyspace sweep is infeasible, so a leaked hash table stays unbroken and the
|
|
10
|
+
* deterministic hash doubles as an exact-match storage key for atomic
|
|
11
|
+
* burn-on-use. Online guessing should be rate-limited like any credential.
|
|
12
|
+
* Verification is constant-time against every stored hash and reports which
|
|
13
|
+
* one matched so the app can delete ("burn") it — **single use is the app's
|
|
14
|
+
* storage responsibility**.
|
|
15
|
+
*
|
|
16
|
+
* @module
|
|
17
|
+
*/
|
|
18
|
+
/**
|
|
19
|
+
* The length of a normalized recovery code (separators stripped). A submitted
|
|
20
|
+
* value of any other length cannot be a recovery code, so callers can skip
|
|
21
|
+
* hashing and storage lookups for it.
|
|
22
|
+
*/
|
|
23
|
+
export declare const RECOVERY_CODE_LENGTH = 16;
|
|
24
|
+
/**
|
|
25
|
+
* Normalize a recovery code to its canonical stored form: uppercase with
|
|
26
|
+
* separators (anything non-alphanumeric) stripped. Hashing and verification
|
|
27
|
+
* apply this automatically, so users can submit codes in any case and with or
|
|
28
|
+
* without the display hyphens.
|
|
29
|
+
*/
|
|
30
|
+
export declare function normalizeRecoveryCode(code: string): string;
|
|
31
|
+
/**
|
|
32
|
+
* Hash a recovery code for storage: hex-encoded SHA-256 of the normalized
|
|
33
|
+
* code, the same stored-hash format the package's other high-entropy
|
|
34
|
+
* credentials use. Fast on purpose — see the module doc for why the code's
|
|
35
|
+
* entropy makes a slow hash unnecessary. Store only the hash; the plaintext
|
|
36
|
+
* code is shown to the user once at generation time.
|
|
37
|
+
*/
|
|
38
|
+
export declare function hashRecoveryCode(code: string): Promise<string>;
|
|
39
|
+
/** Options for {@link generateRecoveryCodes}. */
|
|
40
|
+
export interface GenerateRecoveryCodesOptions {
|
|
41
|
+
/** How many codes to generate. Defaults to `10`. */
|
|
42
|
+
count?: number;
|
|
43
|
+
}
|
|
44
|
+
/** A freshly generated recovery-code set (see {@link generateRecoveryCodes}). */
|
|
45
|
+
export interface GeneratedRecoveryCodes {
|
|
46
|
+
/**
|
|
47
|
+
* The plaintext codes, e.g. `"4Q7MB2XW-9ZHT3KVR"`. Show them to the user
|
|
48
|
+
* exactly once; they are not recoverable afterwards.
|
|
49
|
+
*/
|
|
50
|
+
codes: string[];
|
|
51
|
+
/**
|
|
52
|
+
* The hash of each code, index-aligned with `codes` — persist these (see
|
|
53
|
+
* {@link hashRecoveryCode}).
|
|
54
|
+
*/
|
|
55
|
+
hashes: string[];
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Generate a set of single-use recovery codes plus the hashes to store.
|
|
59
|
+
* Persist `hashes`, display `codes` once, and delete a hash when
|
|
60
|
+
* {@link verifyRecoveryCode} reports its index matched.
|
|
61
|
+
*
|
|
62
|
+
* @example Issue codes on MFA enrollment
|
|
63
|
+
* ```ts
|
|
64
|
+
* const { codes, hashes } = await generateRecoveryCodes();
|
|
65
|
+
* await store.setRecoveryHashes(user.id, hashes);
|
|
66
|
+
* // display `codes` to the user exactly once
|
|
67
|
+
* ```
|
|
68
|
+
*/
|
|
69
|
+
export declare function generateRecoveryCodes(options?: GenerateRecoveryCodesOptions): Promise<GeneratedRecoveryCodes>;
|
|
70
|
+
/** Options for {@link verifyRecoveryCode}. */
|
|
71
|
+
export interface VerifyRecoveryCodeOptions {
|
|
72
|
+
/** The code the user submitted (any case, separators optional). */
|
|
73
|
+
code: string;
|
|
74
|
+
/** The stored hashes for the user's unused codes. */
|
|
75
|
+
hashes: string[];
|
|
76
|
+
}
|
|
77
|
+
/** The outcome of {@link verifyRecoveryCode}. */
|
|
78
|
+
export type RecoveryCodeVerification = {
|
|
79
|
+
valid: false;
|
|
80
|
+
} | {
|
|
81
|
+
valid: true;
|
|
82
|
+
/**
|
|
83
|
+
* The index of the matching hash. Burn it with an atomic delete-by-value
|
|
84
|
+
* (a conditional `DELETE` on the hash, never read-filter-write) so the
|
|
85
|
+
* code cannot be redeemed again — the library does not do this for you at
|
|
86
|
+
* this level ({@link MfaService} does, atomically).
|
|
87
|
+
*/
|
|
88
|
+
index: number;
|
|
89
|
+
};
|
|
90
|
+
/**
|
|
91
|
+
* Check a submitted recovery code against the stored hashes. Comparison is
|
|
92
|
+
* constant-time and every hash is checked even after a match, so timing does
|
|
93
|
+
* not reveal whether or where a match occurred. On success the caller must
|
|
94
|
+
* burn the matched hash (see {@link RecoveryCodeVerification}).
|
|
95
|
+
*/
|
|
96
|
+
export declare function verifyRecoveryCode(options: VerifyRecoveryCodeOptions): Promise<RecoveryCodeVerification>;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Recovery-code primitives for MFA — the single-use fallback a user redeems
|
|
3
|
+
* when their authenticator is unavailable.
|
|
4
|
+
*
|
|
5
|
+
* Codes are generated from a Crockford-style alphabet (no `I`, `L`, `O`, or
|
|
6
|
+
* `U`, so nothing reads ambiguously off paper) as two 8-character groups —
|
|
7
|
+
* 80 bits of entropy per code. Only SHA-256 hashes are meant to be stored:
|
|
8
|
+
* at 80 bits an unsalted fast hash is sufficient — even a full offline
|
|
9
|
+
* keyspace sweep is infeasible, so a leaked hash table stays unbroken and the
|
|
10
|
+
* deterministic hash doubles as an exact-match storage key for atomic
|
|
11
|
+
* burn-on-use. Online guessing should be rate-limited like any credential.
|
|
12
|
+
* Verification is constant-time against every stored hash and reports which
|
|
13
|
+
* one matched so the app can delete ("burn") it — **single use is the app's
|
|
14
|
+
* storage responsibility**.
|
|
15
|
+
*
|
|
16
|
+
* @module
|
|
17
|
+
*/
|
|
18
|
+
import { sha256Hash } from "../../server/utils/hash.js";
|
|
19
|
+
import { timingSafeMatchIndex } from "../../utils/_timing-safe.js";
|
|
20
|
+
const RECOVERY_ALPHABET = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
|
|
21
|
+
const DEFAULT_COUNT = 10;
|
|
22
|
+
/**
|
|
23
|
+
* The length of a normalized recovery code (separators stripped). A submitted
|
|
24
|
+
* value of any other length cannot be a recovery code, so callers can skip
|
|
25
|
+
* hashing and storage lookups for it.
|
|
26
|
+
*/
|
|
27
|
+
export const RECOVERY_CODE_LENGTH = 16;
|
|
28
|
+
/**
|
|
29
|
+
* Normalize a recovery code to its canonical stored form: uppercase with
|
|
30
|
+
* separators (anything non-alphanumeric) stripped. Hashing and verification
|
|
31
|
+
* apply this automatically, so users can submit codes in any case and with or
|
|
32
|
+
* without the display hyphens.
|
|
33
|
+
*/
|
|
34
|
+
export function normalizeRecoveryCode(code) {
|
|
35
|
+
return code.toUpperCase().replaceAll(/[^0-9A-Z]/g, "");
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Hash a recovery code for storage: hex-encoded SHA-256 of the normalized
|
|
39
|
+
* code, the same stored-hash format the package's other high-entropy
|
|
40
|
+
* credentials use. Fast on purpose — see the module doc for why the code's
|
|
41
|
+
* entropy makes a slow hash unnecessary. Store only the hash; the plaintext
|
|
42
|
+
* code is shown to the user once at generation time.
|
|
43
|
+
*/
|
|
44
|
+
export function hashRecoveryCode(code) {
|
|
45
|
+
return sha256Hash(normalizeRecoveryCode(code));
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Generate a set of single-use recovery codes plus the hashes to store.
|
|
49
|
+
* Persist `hashes`, display `codes` once, and delete a hash when
|
|
50
|
+
* {@link verifyRecoveryCode} reports its index matched.
|
|
51
|
+
*
|
|
52
|
+
* @example Issue codes on MFA enrollment
|
|
53
|
+
* ```ts
|
|
54
|
+
* const { codes, hashes } = await generateRecoveryCodes();
|
|
55
|
+
* await store.setRecoveryHashes(user.id, hashes);
|
|
56
|
+
* // display `codes` to the user exactly once
|
|
57
|
+
* ```
|
|
58
|
+
*/
|
|
59
|
+
export async function generateRecoveryCodes(options) {
|
|
60
|
+
const count = options?.count ?? DEFAULT_COUNT;
|
|
61
|
+
const bytes = crypto.getRandomValues(new Uint8Array(count * RECOVERY_CODE_LENGTH));
|
|
62
|
+
const codes = [];
|
|
63
|
+
for (let i = 0; i < count; i++) {
|
|
64
|
+
const raw = Array.from(bytes.subarray(i * RECOVERY_CODE_LENGTH, (i + 1) * RECOVERY_CODE_LENGTH), (byte) => RECOVERY_ALPHABET[byte % RECOVERY_ALPHABET.length]).join("");
|
|
65
|
+
codes.push(`${raw.slice(0, 8)}-${raw.slice(8)}`);
|
|
66
|
+
}
|
|
67
|
+
const hashes = await Promise.all(codes.map(hashRecoveryCode));
|
|
68
|
+
return { codes, hashes };
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Check a submitted recovery code against the stored hashes. Comparison is
|
|
72
|
+
* constant-time and every hash is checked even after a match, so timing does
|
|
73
|
+
* not reveal whether or where a match occurred. On success the caller must
|
|
74
|
+
* burn the matched hash (see {@link RecoveryCodeVerification}).
|
|
75
|
+
*/
|
|
76
|
+
export async function verifyRecoveryCode(options) {
|
|
77
|
+
const submitted = await hashRecoveryCode(options.code);
|
|
78
|
+
const index = timingSafeMatchIndex(options.hashes, submitted);
|
|
79
|
+
return index === undefined ? { valid: false } : { valid: true, index };
|
|
80
|
+
}
|
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The MFA orchestrator: a small service that wires the TOTP and recovery-code
|
|
3
|
+
* primitives to an app-owned {@link MfaStore} — the same thin storage seam as
|
|
4
|
+
* every other layer here. It owns the lifecycle rules (pending vs. active
|
|
5
|
+
* secrets, TOTP replay rejection, recovery-code burning); the app owns the
|
|
6
|
+
* rows, the routes, and the policy for *when* MFA is required.
|
|
7
|
+
*
|
|
8
|
+
* @module
|
|
9
|
+
*/
|
|
10
|
+
import { type IdentityEventHook } from "../events.js";
|
|
11
|
+
import { type RateLimiterLike } from "../rate-limit.js";
|
|
12
|
+
import { type TotpAlgorithm } from "./totp.js";
|
|
13
|
+
/**
|
|
14
|
+
* A user's TOTP state as the store persists it. `secretBase32` is a symmetric
|
|
15
|
+
* key — encrypt it at rest. A record may hold only a pending secret (enrollment
|
|
16
|
+
* started but unconfirmed) or only an active one.
|
|
17
|
+
*/
|
|
18
|
+
export interface MfaTotpRecord {
|
|
19
|
+
/** The active, confirmed secret. Absent until enrollment is confirmed. */
|
|
20
|
+
secretBase32?: string;
|
|
21
|
+
/**
|
|
22
|
+
* The highest time-step counter a code has been accepted for — the replay
|
|
23
|
+
* guard. Present whenever `secretBase32` is.
|
|
24
|
+
*/
|
|
25
|
+
lastStep?: number;
|
|
26
|
+
/** A generated-but-unconfirmed secret awaiting {@link MfaService.confirmEnrollment}. */
|
|
27
|
+
pendingSecretBase32?: string;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* App-owned persistence for {@link MfaService}, keyed by user id. Implement it
|
|
31
|
+
* over your database; {@link MemoryMfaStore} ships for dev and tests. The
|
|
32
|
+
* service never reads or writes anything else — encrypt `secretBase32` /
|
|
33
|
+
* `pendingSecretBase32` at rest however your app seals secrets (they are
|
|
34
|
+
* symmetric keys, not hashes).
|
|
35
|
+
*
|
|
36
|
+
* Three methods are the concurrency guards of the whole design and **must be
|
|
37
|
+
* atomic** in a real implementation (a conditional `UPDATE`/`DELETE`, not a
|
|
38
|
+
* read-then-write): {@link activateTotp}, {@link advanceLastStep}, and
|
|
39
|
+
* {@link consumeRecoveryHash}.
|
|
40
|
+
*/
|
|
41
|
+
export interface MfaStore {
|
|
42
|
+
/** Return the user's TOTP record, or `undefined` when none exists. */
|
|
43
|
+
getTotp(userId: string): Promise<MfaTotpRecord | undefined>;
|
|
44
|
+
/**
|
|
45
|
+
* Save a pending (unconfirmed) secret, preserving any active credential.
|
|
46
|
+
* A repeat call overwrites the previous pending secret.
|
|
47
|
+
*/
|
|
48
|
+
setPendingTotp(userId: string, secretBase32: string): Promise<void>;
|
|
49
|
+
/**
|
|
50
|
+
* Atomically promote the pending secret to active — but only when the
|
|
51
|
+
* stored pending secret still equals `secretBase32` (the secret the
|
|
52
|
+
* confirmation code was verified against): set `secretBase32`, set
|
|
53
|
+
* `lastStep`, clear the pending value, and return `true`. Return `false`
|
|
54
|
+
* without changing anything when the pending secret is missing or has been
|
|
55
|
+
* replaced by a newer enrollment.
|
|
56
|
+
*/
|
|
57
|
+
activateTotp(userId: string, secretBase32: string, lastStep: number): Promise<boolean>;
|
|
58
|
+
/** Delete the user's TOTP record entirely (active and pending). */
|
|
59
|
+
clearTotp(userId: string): Promise<void>;
|
|
60
|
+
/**
|
|
61
|
+
* Atomically advance the replay guard: persist `step` and return `true`
|
|
62
|
+
* only when it is greater than the stored `lastStep` (e.g.
|
|
63
|
+
* `UPDATE … SET last_step = $step WHERE last_step < $step`). Return `false`
|
|
64
|
+
* otherwise — a concurrent request already spent this step, so the caller
|
|
65
|
+
* treats the code as replayed.
|
|
66
|
+
*/
|
|
67
|
+
advanceLastStep(userId: string, step: number): Promise<boolean>;
|
|
68
|
+
/** Return the user's unused recovery-code hashes (empty when none). */
|
|
69
|
+
getRecoveryHashes(userId: string): Promise<string[]>;
|
|
70
|
+
/**
|
|
71
|
+
* Replace the user's recovery-code hashes with exactly this set — used for
|
|
72
|
+
* initial issue, regeneration, and disable (`[]`).
|
|
73
|
+
*/
|
|
74
|
+
setRecoveryHashes(userId: string, hashes: string[]): Promise<void>;
|
|
75
|
+
/**
|
|
76
|
+
* Atomically delete one stored hash and return whether it existed (e.g.
|
|
77
|
+
* `DELETE … WHERE user_id = $user AND hash = $hash` checking the row
|
|
78
|
+
* count). This is what makes a recovery code single-use under concurrent
|
|
79
|
+
* redemption — never implement it as read-filter-write.
|
|
80
|
+
*/
|
|
81
|
+
consumeRecoveryHash(userId: string, hash: string): Promise<boolean>;
|
|
82
|
+
}
|
|
83
|
+
/** TOTP parameters an {@link MfaService} applies to every credential it manages. */
|
|
84
|
+
export interface MfaTotpOptions {
|
|
85
|
+
/** Code length. Defaults to `6`. */
|
|
86
|
+
digits?: number;
|
|
87
|
+
/** Time-step length in seconds. Defaults to `30`. */
|
|
88
|
+
periodSeconds?: number;
|
|
89
|
+
/** HMAC hash. Defaults to `"SHA-1"` (see {@link TotpAlgorithm}). */
|
|
90
|
+
algorithm?: TotpAlgorithm;
|
|
91
|
+
/**
|
|
92
|
+
* Clock-skew windows accepted on each side of the current step during
|
|
93
|
+
* {@link MfaService.verify} and {@link MfaService.confirmEnrollment}.
|
|
94
|
+
* Defaults to `1`.
|
|
95
|
+
*/
|
|
96
|
+
windows?: number;
|
|
97
|
+
}
|
|
98
|
+
/** Options for {@link MfaService}. */
|
|
99
|
+
export interface MfaServiceOptions {
|
|
100
|
+
/** The app-owned persistence seam. */
|
|
101
|
+
store: MfaStore;
|
|
102
|
+
/**
|
|
103
|
+
* TOTP parameters. Set them once at construction — changing them later
|
|
104
|
+
* invalidates codes for already-enrolled authenticators.
|
|
105
|
+
*/
|
|
106
|
+
totp?: MfaTotpOptions;
|
|
107
|
+
/** How many recovery codes to issue on confirm/regenerate. Defaults to `10`. */
|
|
108
|
+
recoveryCodeCount?: number;
|
|
109
|
+
/**
|
|
110
|
+
* Throttles {@link verify} per user id (a 6-digit code is brute-forceable
|
|
111
|
+
* without one). When the limit is hit the service emits
|
|
112
|
+
* `mfa.verify.rate_limited` and — under `protectionMode: "enforce"` —
|
|
113
|
+
* throws an `IdentityError` with code `rate_limited`. A successful
|
|
114
|
+
* verification resets the user's window, so legitimate sign-ins never
|
|
115
|
+
* accumulate toward a lockout.
|
|
116
|
+
*/
|
|
117
|
+
rateLimiter?: RateLimiterLike;
|
|
118
|
+
/**
|
|
119
|
+
* `"enforce"` (default) applies the rate limit; `"log-only"` emits the
|
|
120
|
+
* event but lets the attempt through, for observing limits before turning
|
|
121
|
+
* them on.
|
|
122
|
+
*/
|
|
123
|
+
protectionMode?: "enforce" | "log-only";
|
|
124
|
+
/** Receives `mfa.*` {@link IdentityEvent}s for audit capture. */
|
|
125
|
+
onEvent?: IdentityEventHook;
|
|
126
|
+
}
|
|
127
|
+
/** Options for {@link MfaService.startEnrollment}. */
|
|
128
|
+
export interface StartEnrollmentOptions {
|
|
129
|
+
/** The service name shown in the authenticator app (e.g. `"Udibo"`). */
|
|
130
|
+
issuer: string;
|
|
131
|
+
/** The account label shown under the issuer (typically the user's email). */
|
|
132
|
+
accountName: string;
|
|
133
|
+
}
|
|
134
|
+
/** The result of {@link MfaService.startEnrollment}. */
|
|
135
|
+
export interface MfaEnrollmentStart {
|
|
136
|
+
/** The pending secret in base32, for manual entry into an authenticator. */
|
|
137
|
+
base32: string;
|
|
138
|
+
/** The `otpauth://` URI to render as a QR code. */
|
|
139
|
+
otpauthUri: string;
|
|
140
|
+
}
|
|
141
|
+
/** The result of {@link MfaService.confirmEnrollment}. */
|
|
142
|
+
export type MfaEnrollmentConfirmation = {
|
|
143
|
+
confirmed: false;
|
|
144
|
+
} | {
|
|
145
|
+
confirmed: true;
|
|
146
|
+
/**
|
|
147
|
+
* The plaintext recovery codes. Display them to the user exactly once;
|
|
148
|
+
* only their hashes are stored.
|
|
149
|
+
*/
|
|
150
|
+
recoveryCodes: string[];
|
|
151
|
+
};
|
|
152
|
+
/** Per-call options for {@link MfaService.verify} and {@link MfaService.confirmEnrollment}. */
|
|
153
|
+
export interface MfaVerifyOptions {
|
|
154
|
+
/**
|
|
155
|
+
* Restrict which factor the code is checked against. Defaults to trying
|
|
156
|
+
* TOTP first, then recovery — pass `"totp"` when your UI has a dedicated
|
|
157
|
+
* recovery-code screen and the authenticator field must never silently burn
|
|
158
|
+
* a recovery code.
|
|
159
|
+
*/
|
|
160
|
+
method?: "totp" | "recovery";
|
|
161
|
+
/** Epoch milliseconds to verify against. Defaults to `Date.now()`. */
|
|
162
|
+
timestamp?: number;
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Why {@link MfaService.verify} rejected a code, on the invalid branch of
|
|
166
|
+
* {@link MfaVerification}. Mirrors the `reason` on the `mfa.verify.failed`
|
|
167
|
+
* event — same source of truth — so a caller need not subscribe to `onEvent`
|
|
168
|
+
* to tell the two apart:
|
|
169
|
+
*
|
|
170
|
+
* - `"replayed"` — a *valid* TOTP code whose time step was already spent. A
|
|
171
|
+
* UI can say "that code was already used, wait for the next one"; it is also
|
|
172
|
+
* the classic signal of an intercepted or reused code.
|
|
173
|
+
* - `"invalid"` — a wrong code, a non-recovery string, or a code submitted by
|
|
174
|
+
* a user with no active credential (indistinguishable on purpose).
|
|
175
|
+
*/
|
|
176
|
+
export type MfaVerifyFailureReason = "invalid" | "replayed";
|
|
177
|
+
/** The result of {@link MfaService.verify}. */
|
|
178
|
+
export type MfaVerification = {
|
|
179
|
+
valid: false;
|
|
180
|
+
/** Why the code was rejected — see {@link MfaVerifyFailureReason}. */
|
|
181
|
+
reason: MfaVerifyFailureReason;
|
|
182
|
+
} | {
|
|
183
|
+
valid: true;
|
|
184
|
+
method: "totp";
|
|
185
|
+
} | {
|
|
186
|
+
valid: true;
|
|
187
|
+
method: "recovery";
|
|
188
|
+
/**
|
|
189
|
+
* How many unused recovery codes remain — surface it so the UI can
|
|
190
|
+
* prompt regeneration when low.
|
|
191
|
+
*/
|
|
192
|
+
remainingRecoveryCodes: number;
|
|
193
|
+
};
|
|
194
|
+
/**
|
|
195
|
+
* A user's TOTP enrollment stage, as {@link MfaService.enrollmentStatus}
|
|
196
|
+
* reports it:
|
|
197
|
+
*
|
|
198
|
+
* - `"none"` — no credential and no enrollment in progress.
|
|
199
|
+
* - `"pending"` — {@link MfaService.startEnrollment} ran but
|
|
200
|
+
* {@link MfaService.confirmEnrollment} has not; a secret is stored but does
|
|
201
|
+
* not yet authenticate. An enrollment screen can resume instead of minting a
|
|
202
|
+
* fresh secret (which would invalidate the one already in the authenticator).
|
|
203
|
+
* - `"active"` — a confirmed credential that {@link MfaService.verify} accepts.
|
|
204
|
+
*/
|
|
205
|
+
export type MfaEnrollmentStatus = "none" | "pending" | "active";
|
|
206
|
+
/**
|
|
207
|
+
* Orchestrates TOTP enrollment, verification, and recovery codes over an
|
|
208
|
+
* app-owned {@link MfaStore}.
|
|
209
|
+
*
|
|
210
|
+
* Lifecycle: {@link startEnrollment} stores a *pending* secret (ignored by
|
|
211
|
+
* {@link verify}, so an unconfirmed enrollment can never lock a user out);
|
|
212
|
+
* {@link confirmEnrollment} proves the user's authenticator works, activates
|
|
213
|
+
* the secret, and issues recovery codes; {@link verify} accepts either a TOTP
|
|
214
|
+
* code (rejecting replays via the stored last step) or a recovery code
|
|
215
|
+
* (burning it on use). Replacing an active credential requires
|
|
216
|
+
* {@link disable} first — gate that route behind a fresh re-authentication,
|
|
217
|
+
* or a hijacked session could silently swap the user's authenticator.
|
|
218
|
+
* Deciding *when* to demand a code — sign-in policy, step-up, admin reset —
|
|
219
|
+
* is the app's job.
|
|
220
|
+
*
|
|
221
|
+
* @example Enroll, confirm, and verify
|
|
222
|
+
* ```ts
|
|
223
|
+
* const mfa = new MfaService({ store, rateLimiter });
|
|
224
|
+
* const { otpauthUri } = await mfa.startEnrollment(user.id, {
|
|
225
|
+
* issuer: "Udibo", accountName: user.email,
|
|
226
|
+
* });
|
|
227
|
+
* // …render QR, user scans and submits a code…
|
|
228
|
+
* const confirmation = await mfa.confirmEnrollment(user.id, code);
|
|
229
|
+
* // …later, at sign-in…
|
|
230
|
+
* const result = await mfa.verify(user.id, submittedCode);
|
|
231
|
+
* ```
|
|
232
|
+
*/
|
|
233
|
+
export declare class MfaService {
|
|
234
|
+
#private;
|
|
235
|
+
/**
|
|
236
|
+
* Builds the service around an app-owned {@link MfaStore}.
|
|
237
|
+
*
|
|
238
|
+
* @throws {TypeError} on unusable configuration — non-positive-integer
|
|
239
|
+
* `digits`/`periodSeconds`/`recoveryCodeCount` or negative `windows` — so a
|
|
240
|
+
* miswired deploy fails at construction instead of silently rejecting every
|
|
241
|
+
* code an enrolled user submits.
|
|
242
|
+
*/
|
|
243
|
+
constructor(options: MfaServiceOptions);
|
|
244
|
+
/**
|
|
245
|
+
* Begin TOTP enrollment: generates a fresh secret, stores it as *pending*,
|
|
246
|
+
* and returns what the enrollment screen needs. Repeating the call replaces
|
|
247
|
+
* the pending secret.
|
|
248
|
+
*
|
|
249
|
+
* @throws {IdentityError} `mfa_already_enrolled` when an active credential
|
|
250
|
+
* exists — replacing an authenticator requires {@link disable} first (behind
|
|
251
|
+
* re-authentication), so a stolen session cannot silently swap it.
|
|
252
|
+
*/
|
|
253
|
+
startEnrollment(userId: string, options: StartEnrollmentOptions): Promise<MfaEnrollmentStart>;
|
|
254
|
+
/**
|
|
255
|
+
* Confirm enrollment by verifying a code against the *pending* secret. On
|
|
256
|
+
* success the secret becomes active, a fresh recovery-code set is generated
|
|
257
|
+
* and stored, and the plaintext codes are returned for one-time display.
|
|
258
|
+
* Returns `{ confirmed: false }` when the code is wrong, no enrollment is
|
|
259
|
+
* pending, or the pending secret changed under a concurrent enrollment. If
|
|
260
|
+
* storing the recovery codes fails after activation, the just-activated
|
|
261
|
+
* credential is rolled back and the error rethrown — a failure leaves the
|
|
262
|
+
* user *not* enrolled (free to retry), never enrolled without the recovery
|
|
263
|
+
* codes they were promised.
|
|
264
|
+
*
|
|
265
|
+
* @throws {IdentityError} `mfa_already_enrolled` when an active credential
|
|
266
|
+
* exists (see {@link startEnrollment}).
|
|
267
|
+
*/
|
|
268
|
+
confirmEnrollment(userId: string, code: string, options?: Pick<MfaVerifyOptions, "timestamp">): Promise<MfaEnrollmentConfirmation>;
|
|
269
|
+
/**
|
|
270
|
+
* Verify a second-factor code: first as TOTP against the *active* secret
|
|
271
|
+
* (pending secrets are ignored — they need {@link confirmEnrollment}), then
|
|
272
|
+
* as a recovery code (restrict with {@link MfaVerifyOptions.method}). An
|
|
273
|
+
* accepted TOTP code atomically advances the stored last step, so the same
|
|
274
|
+
* code is rejected on replay even under concurrent submission; an accepted
|
|
275
|
+
* recovery code is atomically burned, so it is single-use. Without an
|
|
276
|
+
* active credential every code is rejected — recovery codes are a fallback
|
|
277
|
+
* *for* the enrolled factor, so stale hashes can never authenticate a user
|
|
278
|
+
* whose MFA was disabled. A successful verification resets the per-user
|
|
279
|
+
* rate-limit window. On failure the result carries a
|
|
280
|
+
* {@link MfaVerifyFailureReason} — `"replayed"` for a valid-but-spent TOTP
|
|
281
|
+
* code, `"invalid"` otherwise — so a caller can distinguish a reused code
|
|
282
|
+
* from a wrong one without subscribing to `onEvent`.
|
|
283
|
+
*
|
|
284
|
+
* @throws {IdentityError} `rate_limited` when a `rateLimiter` is configured,
|
|
285
|
+
* the per-user limit is hit, and `protectionMode` is `"enforce"`.
|
|
286
|
+
*/
|
|
287
|
+
verify(userId: string, code: string, options?: MfaVerifyOptions): Promise<MfaVerification>;
|
|
288
|
+
/** Whether the user has an *active* (confirmed) TOTP credential. */
|
|
289
|
+
isEnrolled(userId: string): Promise<boolean>;
|
|
290
|
+
/**
|
|
291
|
+
* The user's TOTP enrollment stage — `"none"`, `"pending"`, or `"active"`
|
|
292
|
+
* (see {@link MfaEnrollmentStatus}). Unlike {@link isEnrolled}, this also
|
|
293
|
+
* reveals a started-but-unconfirmed enrollment, so an enrollment screen can
|
|
294
|
+
* resume a pending secret the user already scanned rather than calling
|
|
295
|
+
* {@link startEnrollment} again and invalidating it.
|
|
296
|
+
*/
|
|
297
|
+
enrollmentStatus(userId: string): Promise<MfaEnrollmentStatus>;
|
|
298
|
+
/**
|
|
299
|
+
* Remove the user's MFA entirely: recovery codes first, then active and
|
|
300
|
+
* pending secrets — so a partial failure can only leave MFA still *on*,
|
|
301
|
+
* never a "disabled" account whose old recovery codes still authenticate.
|
|
302
|
+
* Gate the route that calls this behind a fresh re-authentication —
|
|
303
|
+
* disabling MFA is what an account takeover wants most.
|
|
304
|
+
*/
|
|
305
|
+
disable(userId: string): Promise<void>;
|
|
306
|
+
/**
|
|
307
|
+
* Replace the user's recovery codes with a fresh set and return the
|
|
308
|
+
* plaintext for one-time display. All previously issued codes stop working.
|
|
309
|
+
*
|
|
310
|
+
* @throws {IdentityError} `mfa_not_enrolled` when the user has no active
|
|
311
|
+
* TOTP enrollment.
|
|
312
|
+
*/
|
|
313
|
+
regenerateRecoveryCodes(userId: string): Promise<string[]>;
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* In-memory {@link MfaStore} for development and tests. State is lost on
|
|
317
|
+
* restart; back the contract with a database (secrets encrypted at rest,
|
|
318
|
+
* conditional writes for the atomic methods) for production.
|
|
319
|
+
*/
|
|
320
|
+
export declare class MemoryMfaStore implements MfaStore {
|
|
321
|
+
#private;
|
|
322
|
+
/** Return a copy of the user's TOTP record, or `undefined`. */
|
|
323
|
+
getTotp(userId: string): Promise<MfaTotpRecord | undefined>;
|
|
324
|
+
/** Store a pending secret, keeping any active credential. */
|
|
325
|
+
setPendingTotp(userId: string, secretBase32: string): Promise<void>;
|
|
326
|
+
/** Promote the matching pending secret to active; `false` when it changed. */
|
|
327
|
+
activateTotp(userId: string, secretBase32: string, lastStep: number): Promise<boolean>;
|
|
328
|
+
/** Delete the user's TOTP record. */
|
|
329
|
+
clearTotp(userId: string): Promise<void>;
|
|
330
|
+
/** Advance the replay guard only forward; `false` when the step was spent. */
|
|
331
|
+
advanceLastStep(userId: string, step: number): Promise<boolean>;
|
|
332
|
+
/** Return the user's stored recovery hashes (empty when none). */
|
|
333
|
+
getRecoveryHashes(userId: string): Promise<string[]>;
|
|
334
|
+
/** Replace the user's recovery hashes with exactly this set. */
|
|
335
|
+
setRecoveryHashes(userId: string, hashes: string[]): Promise<void>;
|
|
336
|
+
/** Delete one hash, reporting whether it was present. */
|
|
337
|
+
consumeRecoveryHash(userId: string, hash: string): Promise<boolean>;
|
|
338
|
+
}
|