@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,351 @@
|
|
|
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 { IdentityError } from "../errors.js";
|
|
11
|
+
import { dispatchIdentityEvent, } from "../events.js";
|
|
12
|
+
import { enforceRateLimit } from "../rate-limit.js";
|
|
13
|
+
import { generateRecoveryCodes, hashRecoveryCode, normalizeRecoveryCode, RECOVERY_CODE_LENGTH, } from "./recovery-codes.js";
|
|
14
|
+
import { buildOtpauthUri, generateTotpSecret, validateTotpParams, verifyTotpCode, } from "./totp.js";
|
|
15
|
+
/**
|
|
16
|
+
* Orchestrates TOTP enrollment, verification, and recovery codes over an
|
|
17
|
+
* app-owned {@link MfaStore}.
|
|
18
|
+
*
|
|
19
|
+
* Lifecycle: {@link startEnrollment} stores a *pending* secret (ignored by
|
|
20
|
+
* {@link verify}, so an unconfirmed enrollment can never lock a user out);
|
|
21
|
+
* {@link confirmEnrollment} proves the user's authenticator works, activates
|
|
22
|
+
* the secret, and issues recovery codes; {@link verify} accepts either a TOTP
|
|
23
|
+
* code (rejecting replays via the stored last step) or a recovery code
|
|
24
|
+
* (burning it on use). Replacing an active credential requires
|
|
25
|
+
* {@link disable} first — gate that route behind a fresh re-authentication,
|
|
26
|
+
* or a hijacked session could silently swap the user's authenticator.
|
|
27
|
+
* Deciding *when* to demand a code — sign-in policy, step-up, admin reset —
|
|
28
|
+
* is the app's job.
|
|
29
|
+
*
|
|
30
|
+
* @example Enroll, confirm, and verify
|
|
31
|
+
* ```ts
|
|
32
|
+
* const mfa = new MfaService({ store, rateLimiter });
|
|
33
|
+
* const { otpauthUri } = await mfa.startEnrollment(user.id, {
|
|
34
|
+
* issuer: "Udibo", accountName: user.email,
|
|
35
|
+
* });
|
|
36
|
+
* // …render QR, user scans and submits a code…
|
|
37
|
+
* const confirmation = await mfa.confirmEnrollment(user.id, code);
|
|
38
|
+
* // …later, at sign-in…
|
|
39
|
+
* const result = await mfa.verify(user.id, submittedCode);
|
|
40
|
+
* ```
|
|
41
|
+
*/
|
|
42
|
+
export class MfaService {
|
|
43
|
+
#store;
|
|
44
|
+
#totp;
|
|
45
|
+
#recoveryCodeCount;
|
|
46
|
+
#rateLimiter;
|
|
47
|
+
#enforce;
|
|
48
|
+
#onEvent;
|
|
49
|
+
/**
|
|
50
|
+
* Builds the service around an app-owned {@link MfaStore}.
|
|
51
|
+
*
|
|
52
|
+
* @throws {TypeError} on unusable configuration — non-positive-integer
|
|
53
|
+
* `digits`/`periodSeconds`/`recoveryCodeCount` or negative `windows` — so a
|
|
54
|
+
* miswired deploy fails at construction instead of silently rejecting every
|
|
55
|
+
* code an enrolled user submits.
|
|
56
|
+
*/
|
|
57
|
+
constructor(options) {
|
|
58
|
+
validateTotpParams(options.totp ?? {});
|
|
59
|
+
const count = options.recoveryCodeCount;
|
|
60
|
+
if (count !== undefined && (!Number.isInteger(count) || count <= 0)) {
|
|
61
|
+
throw new TypeError("recoveryCodeCount must be a positive integer");
|
|
62
|
+
}
|
|
63
|
+
this.#store = options.store;
|
|
64
|
+
this.#totp = options.totp ?? {};
|
|
65
|
+
this.#recoveryCodeCount = count;
|
|
66
|
+
this.#rateLimiter = options.rateLimiter;
|
|
67
|
+
this.#enforce = (options.protectionMode ?? "enforce") === "enforce";
|
|
68
|
+
this.#onEvent = options.onEvent;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Begin TOTP enrollment: generates a fresh secret, stores it as *pending*,
|
|
72
|
+
* and returns what the enrollment screen needs. Repeating the call replaces
|
|
73
|
+
* the pending secret.
|
|
74
|
+
*
|
|
75
|
+
* @throws {IdentityError} `mfa_already_enrolled` when an active credential
|
|
76
|
+
* exists — replacing an authenticator requires {@link disable} first (behind
|
|
77
|
+
* re-authentication), so a stolen session cannot silently swap it.
|
|
78
|
+
*/
|
|
79
|
+
async startEnrollment(userId, options) {
|
|
80
|
+
this.#assertNotEnrolled(await this.#store.getTotp(userId));
|
|
81
|
+
const { base32 } = generateTotpSecret();
|
|
82
|
+
await this.#store.setPendingTotp(userId, base32);
|
|
83
|
+
return {
|
|
84
|
+
base32,
|
|
85
|
+
otpauthUri: buildOtpauthUri({
|
|
86
|
+
secret: base32,
|
|
87
|
+
issuer: options.issuer,
|
|
88
|
+
accountName: options.accountName,
|
|
89
|
+
digits: this.#totp.digits,
|
|
90
|
+
periodSeconds: this.#totp.periodSeconds,
|
|
91
|
+
algorithm: this.#totp.algorithm,
|
|
92
|
+
}),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Confirm enrollment by verifying a code against the *pending* secret. On
|
|
97
|
+
* success the secret becomes active, a fresh recovery-code set is generated
|
|
98
|
+
* and stored, and the plaintext codes are returned for one-time display.
|
|
99
|
+
* Returns `{ confirmed: false }` when the code is wrong, no enrollment is
|
|
100
|
+
* pending, or the pending secret changed under a concurrent enrollment. If
|
|
101
|
+
* storing the recovery codes fails after activation, the just-activated
|
|
102
|
+
* credential is rolled back and the error rethrown — a failure leaves the
|
|
103
|
+
* user *not* enrolled (free to retry), never enrolled without the recovery
|
|
104
|
+
* codes they were promised.
|
|
105
|
+
*
|
|
106
|
+
* @throws {IdentityError} `mfa_already_enrolled` when an active credential
|
|
107
|
+
* exists (see {@link startEnrollment}).
|
|
108
|
+
*/
|
|
109
|
+
async confirmEnrollment(userId, code, options) {
|
|
110
|
+
const record = await this.#store.getTotp(userId);
|
|
111
|
+
this.#assertNotEnrolled(record);
|
|
112
|
+
const pending = record?.pendingSecretBase32;
|
|
113
|
+
if (!pending)
|
|
114
|
+
return { confirmed: false };
|
|
115
|
+
const result = await verifyTotpCode({
|
|
116
|
+
secret: pending,
|
|
117
|
+
code,
|
|
118
|
+
timestamp: options?.timestamp,
|
|
119
|
+
...this.#totp,
|
|
120
|
+
});
|
|
121
|
+
if (!result.valid)
|
|
122
|
+
return { confirmed: false };
|
|
123
|
+
const activated = await this.#store.activateTotp(userId, pending, result.matchedStep);
|
|
124
|
+
if (!activated)
|
|
125
|
+
return { confirmed: false };
|
|
126
|
+
let recoveryCodes;
|
|
127
|
+
try {
|
|
128
|
+
recoveryCodes = await this.#issueRecoveryCodes(userId);
|
|
129
|
+
}
|
|
130
|
+
catch (error) {
|
|
131
|
+
await this.#store.clearTotp(userId);
|
|
132
|
+
throw error;
|
|
133
|
+
}
|
|
134
|
+
await this.#emit({ type: "mfa.enrollment.confirmed", userId });
|
|
135
|
+
return { confirmed: true, recoveryCodes };
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Verify a second-factor code: first as TOTP against the *active* secret
|
|
139
|
+
* (pending secrets are ignored — they need {@link confirmEnrollment}), then
|
|
140
|
+
* as a recovery code (restrict with {@link MfaVerifyOptions.method}). An
|
|
141
|
+
* accepted TOTP code atomically advances the stored last step, so the same
|
|
142
|
+
* code is rejected on replay even under concurrent submission; an accepted
|
|
143
|
+
* recovery code is atomically burned, so it is single-use. Without an
|
|
144
|
+
* active credential every code is rejected — recovery codes are a fallback
|
|
145
|
+
* *for* the enrolled factor, so stale hashes can never authenticate a user
|
|
146
|
+
* whose MFA was disabled. A successful verification resets the per-user
|
|
147
|
+
* rate-limit window. On failure the result carries a
|
|
148
|
+
* {@link MfaVerifyFailureReason} — `"replayed"` for a valid-but-spent TOTP
|
|
149
|
+
* code, `"invalid"` otherwise — so a caller can distinguish a reused code
|
|
150
|
+
* from a wrong one without subscribing to `onEvent`.
|
|
151
|
+
*
|
|
152
|
+
* @throws {IdentityError} `rate_limited` when a `rateLimiter` is configured,
|
|
153
|
+
* the per-user limit is hit, and `protectionMode` is `"enforce"`.
|
|
154
|
+
*/
|
|
155
|
+
async verify(userId, code, options) {
|
|
156
|
+
await this.#throttle(userId);
|
|
157
|
+
const secret = (await this.#store.getTotp(userId))?.secretBase32;
|
|
158
|
+
if (!secret)
|
|
159
|
+
return await this.#verifyFailed(userId, "invalid");
|
|
160
|
+
const method = options?.method;
|
|
161
|
+
if (method !== "recovery") {
|
|
162
|
+
const outcome = await this.#verifyTotp(userId, secret, code, options?.timestamp);
|
|
163
|
+
if (outcome)
|
|
164
|
+
return outcome;
|
|
165
|
+
}
|
|
166
|
+
if (method !== "totp") {
|
|
167
|
+
const outcome = await this.#verifyRecovery(userId, code);
|
|
168
|
+
if (outcome)
|
|
169
|
+
return outcome;
|
|
170
|
+
}
|
|
171
|
+
return await this.#verifyFailed(userId, "invalid");
|
|
172
|
+
}
|
|
173
|
+
/** Whether the user has an *active* (confirmed) TOTP credential. */
|
|
174
|
+
async isEnrolled(userId) {
|
|
175
|
+
const record = await this.#store.getTotp(userId);
|
|
176
|
+
return record?.secretBase32 !== undefined;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* The user's TOTP enrollment stage — `"none"`, `"pending"`, or `"active"`
|
|
180
|
+
* (see {@link MfaEnrollmentStatus}). Unlike {@link isEnrolled}, this also
|
|
181
|
+
* reveals a started-but-unconfirmed enrollment, so an enrollment screen can
|
|
182
|
+
* resume a pending secret the user already scanned rather than calling
|
|
183
|
+
* {@link startEnrollment} again and invalidating it.
|
|
184
|
+
*/
|
|
185
|
+
async enrollmentStatus(userId) {
|
|
186
|
+
const record = await this.#store.getTotp(userId);
|
|
187
|
+
if (record?.secretBase32 !== undefined)
|
|
188
|
+
return "active";
|
|
189
|
+
if (record?.pendingSecretBase32 !== undefined)
|
|
190
|
+
return "pending";
|
|
191
|
+
return "none";
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Remove the user's MFA entirely: recovery codes first, then active and
|
|
195
|
+
* pending secrets — so a partial failure can only leave MFA still *on*,
|
|
196
|
+
* never a "disabled" account whose old recovery codes still authenticate.
|
|
197
|
+
* Gate the route that calls this behind a fresh re-authentication —
|
|
198
|
+
* disabling MFA is what an account takeover wants most.
|
|
199
|
+
*/
|
|
200
|
+
async disable(userId) {
|
|
201
|
+
await this.#store.setRecoveryHashes(userId, []);
|
|
202
|
+
await this.#store.clearTotp(userId);
|
|
203
|
+
await this.#emit({ type: "mfa.disabled", userId });
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Replace the user's recovery codes with a fresh set and return the
|
|
207
|
+
* plaintext for one-time display. All previously issued codes stop working.
|
|
208
|
+
*
|
|
209
|
+
* @throws {IdentityError} `mfa_not_enrolled` when the user has no active
|
|
210
|
+
* TOTP enrollment.
|
|
211
|
+
*/
|
|
212
|
+
async regenerateRecoveryCodes(userId) {
|
|
213
|
+
if (!await this.isEnrolled(userId)) {
|
|
214
|
+
throw new IdentityError("mfa_not_enrolled", "cannot regenerate recovery codes: MFA is not enrolled");
|
|
215
|
+
}
|
|
216
|
+
const codes = await this.#issueRecoveryCodes(userId);
|
|
217
|
+
await this.#emit({ type: "mfa.recovery_codes.regenerated", userId });
|
|
218
|
+
return codes;
|
|
219
|
+
}
|
|
220
|
+
async #verifyTotp(userId, secret, code, timestamp) {
|
|
221
|
+
const result = await verifyTotpCode({
|
|
222
|
+
secret,
|
|
223
|
+
code,
|
|
224
|
+
timestamp,
|
|
225
|
+
...this.#totp,
|
|
226
|
+
});
|
|
227
|
+
if (!result.valid)
|
|
228
|
+
return undefined;
|
|
229
|
+
if (!await this.#store.advanceLastStep(userId, result.matchedStep)) {
|
|
230
|
+
return await this.#verifyFailed(userId, "replayed");
|
|
231
|
+
}
|
|
232
|
+
await this.#resetThrottle(userId);
|
|
233
|
+
await this.#emit({ type: "mfa.verify.succeeded", userId, method: "totp" });
|
|
234
|
+
return { valid: true, method: "totp" };
|
|
235
|
+
}
|
|
236
|
+
async #verifyRecovery(userId, code) {
|
|
237
|
+
if (normalizeRecoveryCode(code).length !== RECOVERY_CODE_LENGTH) {
|
|
238
|
+
return undefined;
|
|
239
|
+
}
|
|
240
|
+
const consumed = await this.#store.consumeRecoveryHash(userId, await hashRecoveryCode(code));
|
|
241
|
+
if (!consumed)
|
|
242
|
+
return undefined;
|
|
243
|
+
const remainingRecoveryCodes = (await this.#store.getRecoveryHashes(userId)).length;
|
|
244
|
+
await this.#resetThrottle(userId);
|
|
245
|
+
await this.#emit({
|
|
246
|
+
type: "mfa.verify.succeeded",
|
|
247
|
+
userId,
|
|
248
|
+
method: "recovery",
|
|
249
|
+
remainingRecoveryCodes,
|
|
250
|
+
});
|
|
251
|
+
return { valid: true, method: "recovery", remainingRecoveryCodes };
|
|
252
|
+
}
|
|
253
|
+
#assertNotEnrolled(record) {
|
|
254
|
+
if (record?.secretBase32) {
|
|
255
|
+
throw new IdentityError("mfa_already_enrolled", "disable MFA before enrolling a new authenticator");
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
async #issueRecoveryCodes(userId) {
|
|
259
|
+
const { codes, hashes } = await generateRecoveryCodes({
|
|
260
|
+
count: this.#recoveryCodeCount,
|
|
261
|
+
});
|
|
262
|
+
await this.#store.setRecoveryHashes(userId, hashes);
|
|
263
|
+
return codes;
|
|
264
|
+
}
|
|
265
|
+
#rateKey(userId) {
|
|
266
|
+
return `mfa:verify:${userId}`;
|
|
267
|
+
}
|
|
268
|
+
#throttle(userId) {
|
|
269
|
+
return enforceRateLimit({
|
|
270
|
+
rateLimiter: this.#rateLimiter,
|
|
271
|
+
key: this.#rateKey(userId),
|
|
272
|
+
message: "too many MFA attempts",
|
|
273
|
+
enforce: this.#enforce,
|
|
274
|
+
event: { type: "mfa.verify.rate_limited", userId },
|
|
275
|
+
emit: (e) => this.#emit(e),
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
async #resetThrottle(userId) {
|
|
279
|
+
await this.#rateLimiter?.reset(this.#rateKey(userId));
|
|
280
|
+
}
|
|
281
|
+
async #verifyFailed(userId, reason) {
|
|
282
|
+
await this.#emit({ type: "mfa.verify.failed", userId, reason });
|
|
283
|
+
return { valid: false, reason };
|
|
284
|
+
}
|
|
285
|
+
#emit(event) {
|
|
286
|
+
return dispatchIdentityEvent(this.#onEvent, "mfa", event);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* In-memory {@link MfaStore} for development and tests. State is lost on
|
|
291
|
+
* restart; back the contract with a database (secrets encrypted at rest,
|
|
292
|
+
* conditional writes for the atomic methods) for production.
|
|
293
|
+
*/
|
|
294
|
+
export class MemoryMfaStore {
|
|
295
|
+
#totp = new Map();
|
|
296
|
+
#recovery = new Map();
|
|
297
|
+
/** Return a copy of the user's TOTP record, or `undefined`. */
|
|
298
|
+
getTotp(userId) {
|
|
299
|
+
const record = this.#totp.get(userId);
|
|
300
|
+
return Promise.resolve(record ? { ...record } : undefined);
|
|
301
|
+
}
|
|
302
|
+
/** Store a pending secret, keeping any active credential. */
|
|
303
|
+
setPendingTotp(userId, secretBase32) {
|
|
304
|
+
const record = this.#totp.get(userId) ?? {};
|
|
305
|
+
record.pendingSecretBase32 = secretBase32;
|
|
306
|
+
this.#totp.set(userId, record);
|
|
307
|
+
return Promise.resolve();
|
|
308
|
+
}
|
|
309
|
+
/** Promote the matching pending secret to active; `false` when it changed. */
|
|
310
|
+
activateTotp(userId, secretBase32, lastStep) {
|
|
311
|
+
const record = this.#totp.get(userId);
|
|
312
|
+
if (record?.pendingSecretBase32 !== secretBase32) {
|
|
313
|
+
return Promise.resolve(false);
|
|
314
|
+
}
|
|
315
|
+
this.#totp.set(userId, { secretBase32, lastStep });
|
|
316
|
+
return Promise.resolve(true);
|
|
317
|
+
}
|
|
318
|
+
/** Delete the user's TOTP record. */
|
|
319
|
+
clearTotp(userId) {
|
|
320
|
+
this.#totp.delete(userId);
|
|
321
|
+
return Promise.resolve();
|
|
322
|
+
}
|
|
323
|
+
/** Advance the replay guard only forward; `false` when the step was spent. */
|
|
324
|
+
advanceLastStep(userId, step) {
|
|
325
|
+
const record = this.#totp.get(userId);
|
|
326
|
+
if (!record || step <= (record.lastStep ?? -1)) {
|
|
327
|
+
return Promise.resolve(false);
|
|
328
|
+
}
|
|
329
|
+
record.lastStep = step;
|
|
330
|
+
return Promise.resolve(true);
|
|
331
|
+
}
|
|
332
|
+
/** Return the user's stored recovery hashes (empty when none). */
|
|
333
|
+
getRecoveryHashes(userId) {
|
|
334
|
+
return Promise.resolve([...(this.#recovery.get(userId) ?? [])]);
|
|
335
|
+
}
|
|
336
|
+
/** Replace the user's recovery hashes with exactly this set. */
|
|
337
|
+
setRecoveryHashes(userId, hashes) {
|
|
338
|
+
this.#recovery.set(userId, [...hashes]);
|
|
339
|
+
return Promise.resolve();
|
|
340
|
+
}
|
|
341
|
+
/** Delete one hash, reporting whether it was present. */
|
|
342
|
+
consumeRecoveryHash(userId, hash) {
|
|
343
|
+
const hashes = this.#recovery.get(userId) ?? [];
|
|
344
|
+
const index = hashes.indexOf(hash);
|
|
345
|
+
if (index === -1)
|
|
346
|
+
return Promise.resolve(false);
|
|
347
|
+
hashes.splice(index, 1);
|
|
348
|
+
this.#recovery.set(userId, hashes);
|
|
349
|
+
return Promise.resolve(true);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
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
|
+
/**
|
|
15
|
+
* The HMAC hash a TOTP credential uses. `"SHA-1"` is the RFC default and what
|
|
16
|
+
* authenticator apps universally support; prefer it unless you control the
|
|
17
|
+
* client. Some authenticators silently ignore the `algorithm` URI parameter and
|
|
18
|
+
* always use SHA-1, so a non-default choice can produce codes that never match.
|
|
19
|
+
*/
|
|
20
|
+
export type TotpAlgorithm = "SHA-1" | "SHA-256" | "SHA-512";
|
|
21
|
+
/**
|
|
22
|
+
* Assert that TOTP parameters are usable: `digits` and `periodSeconds` must be
|
|
23
|
+
* positive integers and `windows` a non-negative integer. Every TOTP entry
|
|
24
|
+
* point applies this, so a corrupted or miswired configuration fails loudly
|
|
25
|
+
* instead of silently rejecting every code.
|
|
26
|
+
*
|
|
27
|
+
* @throws {TypeError} on the first invalid value.
|
|
28
|
+
*/
|
|
29
|
+
export declare function validateTotpParams(params: {
|
|
30
|
+
digits?: number;
|
|
31
|
+
periodSeconds?: number;
|
|
32
|
+
windows?: number;
|
|
33
|
+
}): void;
|
|
34
|
+
/**
|
|
35
|
+
* Decode a base32 TOTP secret into raw key bytes. Tolerates lowercase,
|
|
36
|
+
* whitespace, and missing `=` padding (authenticator conventions vary).
|
|
37
|
+
*
|
|
38
|
+
* @throws {TypeError} if the value is not valid RFC 4648 base32.
|
|
39
|
+
*/
|
|
40
|
+
export declare function decodeTotpSecret(base32: string): Uint8Array;
|
|
41
|
+
/** A freshly generated TOTP secret, in both raw and shareable forms. */
|
|
42
|
+
export interface GeneratedTotpSecret {
|
|
43
|
+
/** The raw 20-byte (160-bit) secret key. */
|
|
44
|
+
secret: Uint8Array;
|
|
45
|
+
/**
|
|
46
|
+
* The secret encoded as unpadded RFC 4648 base32 — the form authenticator
|
|
47
|
+
* apps accept for manual entry and the `otpauth://` URI carries.
|
|
48
|
+
*/
|
|
49
|
+
base32: string;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Generate a random 160-bit TOTP secret (the RFC 4226 recommended size and
|
|
53
|
+
* what authenticator apps expect). Store it encrypted at rest — it is a
|
|
54
|
+
* symmetric key, not a hash: anyone who reads it can mint valid codes.
|
|
55
|
+
*
|
|
56
|
+
* @example Enroll a user
|
|
57
|
+
* ```ts
|
|
58
|
+
* const { base32 } = generateTotpSecret();
|
|
59
|
+
* const uri = buildOtpauthUri({
|
|
60
|
+
* secret: base32,
|
|
61
|
+
* issuer: "Example",
|
|
62
|
+
* accountName: "user@example.com",
|
|
63
|
+
* });
|
|
64
|
+
* // render `uri` as a QR code; store `base32` encrypted
|
|
65
|
+
* ```
|
|
66
|
+
*/
|
|
67
|
+
export declare function generateTotpSecret(): GeneratedTotpSecret;
|
|
68
|
+
/** Options for {@link buildOtpauthUri}. */
|
|
69
|
+
export interface OtpauthUriOptions {
|
|
70
|
+
/** The base32-encoded TOTP secret (from {@link generateTotpSecret}). */
|
|
71
|
+
secret: string;
|
|
72
|
+
/** The service name shown in the authenticator app (e.g. `"Udibo"`). */
|
|
73
|
+
issuer: string;
|
|
74
|
+
/** The account label shown under the issuer (typically the user's email). */
|
|
75
|
+
accountName: string;
|
|
76
|
+
/** Code length. Defaults to `6`. */
|
|
77
|
+
digits?: number;
|
|
78
|
+
/** Time-step length in seconds. Defaults to `30`. */
|
|
79
|
+
periodSeconds?: number;
|
|
80
|
+
/** HMAC hash. Defaults to `"SHA-1"` (see {@link TotpAlgorithm}). */
|
|
81
|
+
algorithm?: TotpAlgorithm;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Build the `otpauth://totp/...` provisioning URI an enrollment screen renders
|
|
85
|
+
* as a QR code. Follows the de-facto Google Authenticator format: the issuer
|
|
86
|
+
* appears both as the URI-encoded label prefix (`Issuer:account`) and as the
|
|
87
|
+
* `issuer` query parameter, so apps that read either convention group the
|
|
88
|
+
* entry correctly.
|
|
89
|
+
*
|
|
90
|
+
* @throws {TypeError} if the secret is not base32 or `digits`/`periodSeconds`
|
|
91
|
+
* are not positive integers, so a corrupted value cannot inject URI parameters.
|
|
92
|
+
*/
|
|
93
|
+
export declare function buildOtpauthUri(options: OtpauthUriOptions): string;
|
|
94
|
+
/** Options for {@link generateTotpCode}. */
|
|
95
|
+
export interface GenerateTotpCodeOptions {
|
|
96
|
+
/** The TOTP secret, as base32 or raw key bytes. */
|
|
97
|
+
secret: string | Uint8Array;
|
|
98
|
+
/** Epoch milliseconds to generate the code for. Defaults to `Date.now()`. */
|
|
99
|
+
timestamp?: number;
|
|
100
|
+
/** Code length. Defaults to `6`. */
|
|
101
|
+
digits?: number;
|
|
102
|
+
/** Time-step length in seconds. Defaults to `30`. */
|
|
103
|
+
periodSeconds?: number;
|
|
104
|
+
/** HMAC hash. Defaults to `"SHA-1"` (see {@link TotpAlgorithm}). */
|
|
105
|
+
algorithm?: TotpAlgorithm;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Compute the TOTP code for a moment in time — what an authenticator app
|
|
109
|
+
* displays. Exported for tests and for tooling that needs to mint codes (e.g.
|
|
110
|
+
* an enrollment simulator); server-side verification should go through
|
|
111
|
+
* {@link verifyTotpCode}, which also handles the clock-skew window and
|
|
112
|
+
* constant-time comparison.
|
|
113
|
+
*
|
|
114
|
+
* @example Mint the current code in a test
|
|
115
|
+
* ```ts
|
|
116
|
+
* const code = await generateTotpCode({ secret: base32 });
|
|
117
|
+
* ```
|
|
118
|
+
*
|
|
119
|
+
* @throws {TypeError} if `digits` or `periodSeconds` is not a positive
|
|
120
|
+
* integer, or the secret is not valid base32.
|
|
121
|
+
*/
|
|
122
|
+
export declare function generateTotpCode(options: GenerateTotpCodeOptions): Promise<string>;
|
|
123
|
+
/** Options for {@link verifyTotpCode}. */
|
|
124
|
+
export interface VerifyTotpCodeOptions {
|
|
125
|
+
/** The TOTP secret, as base32 or raw key bytes. */
|
|
126
|
+
secret: string | Uint8Array;
|
|
127
|
+
/**
|
|
128
|
+
* The code the user submitted. Whitespace is stripped before checking, so
|
|
129
|
+
* a code pasted in the spaced groups authenticator apps display verifies.
|
|
130
|
+
*/
|
|
131
|
+
code: string;
|
|
132
|
+
/** Epoch milliseconds to verify against. Defaults to `Date.now()`. */
|
|
133
|
+
timestamp?: number;
|
|
134
|
+
/**
|
|
135
|
+
* How many time steps of clock skew to accept on each side of the current
|
|
136
|
+
* step. Defaults to `1` (accepts the previous, current, and next step —
|
|
137
|
+
* ±30 seconds at the default period). Keep it small: every extra window
|
|
138
|
+
* widens the guessing surface.
|
|
139
|
+
*/
|
|
140
|
+
windows?: number;
|
|
141
|
+
/** Code length. Defaults to `6`. */
|
|
142
|
+
digits?: number;
|
|
143
|
+
/** Time-step length in seconds. Defaults to `30`. */
|
|
144
|
+
periodSeconds?: number;
|
|
145
|
+
/** HMAC hash. Defaults to `"SHA-1"` (see {@link TotpAlgorithm}). */
|
|
146
|
+
algorithm?: TotpAlgorithm;
|
|
147
|
+
}
|
|
148
|
+
/** The outcome of {@link verifyTotpCode}. */
|
|
149
|
+
export type TotpVerification = {
|
|
150
|
+
valid: false;
|
|
151
|
+
} | {
|
|
152
|
+
valid: true;
|
|
153
|
+
/**
|
|
154
|
+
* The time-step counter the code matched. Persist the highest accepted
|
|
155
|
+
* step per user and reject codes with `matchedStep <= lastStep` — that is
|
|
156
|
+
* what makes each code single-use.
|
|
157
|
+
*/
|
|
158
|
+
matchedStep: number;
|
|
159
|
+
};
|
|
160
|
+
/**
|
|
161
|
+
* Verify a submitted TOTP code against the secret, accepting `windows` steps
|
|
162
|
+
* of clock skew on either side. Code comparison is constant-time, and every
|
|
163
|
+
* candidate step is checked even after a match, so timing does not reveal
|
|
164
|
+
* which step (if any) matched. When adjacent steps happen to produce the same
|
|
165
|
+
* code, the newest step wins, so the replay guard below never rejects a fresh
|
|
166
|
+
* code over a stale tie.
|
|
167
|
+
*
|
|
168
|
+
* **Replay protection is the caller's half of the contract:** a valid result
|
|
169
|
+
* includes `matchedStep`; store the last accepted step and reject any code
|
|
170
|
+
* whose `matchedStep` is `<=` that value ({@link MfaService} in this module
|
|
171
|
+
* does this for you). Without it, a code stays valid for the whole skew
|
|
172
|
+
* window and can be replayed.
|
|
173
|
+
*
|
|
174
|
+
* @example Verify with replay rejection
|
|
175
|
+
* ```ts
|
|
176
|
+
* const result = await verifyTotpCode({ secret, code });
|
|
177
|
+
* if (result.valid && result.matchedStep > lastAcceptedStep) {
|
|
178
|
+
* lastAcceptedStep = result.matchedStep; // persist atomically
|
|
179
|
+
* // accepted
|
|
180
|
+
* }
|
|
181
|
+
* ```
|
|
182
|
+
*
|
|
183
|
+
* @throws {TypeError} if `digits`/`periodSeconds` is not a positive integer,
|
|
184
|
+
* `windows` is not a non-negative integer, or the secret is not valid base32.
|
|
185
|
+
*/
|
|
186
|
+
export declare function verifyTotpCode(options: VerifyTotpCodeOptions): Promise<TotpVerification>;
|