@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,254 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Testing helpers for the Hono BFF adapter.
|
|
3
|
+
*
|
|
4
|
+
* Lets a consumer's tests drive protected endpoints without walking
|
|
5
|
+
* through the full login → authorize → callback flow, and ages the
|
|
6
|
+
* access token in the underlying {@link SessionStore} so a follow-up
|
|
7
|
+
* request through `attachToken()` exercises the refresh path
|
|
8
|
+
* deterministically (no `setTimeout` required).
|
|
9
|
+
*
|
|
10
|
+
* The runner-style helper {@link runSessionStoreContractTests} is
|
|
11
|
+
* available for verifying custom {@link SessionStore} implementations
|
|
12
|
+
* (Postgres, Redis, DynamoDB, …) satisfy the create / read / update /
|
|
13
|
+
* destroy contract.
|
|
14
|
+
*
|
|
15
|
+
* @module
|
|
16
|
+
*/
|
|
17
|
+
import "../../../_dnt.polyfills.js";
|
|
18
|
+
import { assert, assertEquals, assertStrictEquals } from "../../../deps/jsr.io/@std/assert/1.0.19/mod.js";
|
|
19
|
+
import { delay } from "../../../deps/jsr.io/@std/async/1.5.0/delay.js";
|
|
20
|
+
import { beforeEach, describe, it } from "../../../deps/jsr.io/@std/testing/1.0.20/bdd.js";
|
|
21
|
+
import { sessionStoreMaxAgeMs, } from "./session-store.js";
|
|
22
|
+
/**
|
|
23
|
+
* Seeds a session in `bff.sessionStore` and returns a `Cookie` header
|
|
24
|
+
* value (`<name>=<value>`) ready to attach to subsequent
|
|
25
|
+
* `app.request(...)` calls. Skips the authorize / callback round-trip
|
|
26
|
+
* entirely — useful for tests that only care about authenticated
|
|
27
|
+
* endpoint behavior.
|
|
28
|
+
*
|
|
29
|
+
* **Important — token recognition.** The seeded access token is random by
|
|
30
|
+
* default and is NOT registered with any resource server. That is enough for
|
|
31
|
+
* `GET /auth/session` (which reads the cached `user` claim), but a route
|
|
32
|
+
* guarded by {@link HonoBff.protect} validates the session's access token
|
|
33
|
+
* against the resource server's token service — so an unrecognized token
|
|
34
|
+
* yields 401. For protected API routes, either pre-register the token
|
|
35
|
+
* (`tokenService.save({ accessToken, ... })` then pass the same
|
|
36
|
+
* `tokens.accessToken` here), or use {@link createAuthenticatedTestSession},
|
|
37
|
+
* which does both in one call.
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* ```ts
|
|
41
|
+
* // Probe-only (GET /auth/session) — no resource server involved:
|
|
42
|
+
* const cookie = await createTestSession(bff, {
|
|
43
|
+
* user: { sub: "u1", name: "Alice" },
|
|
44
|
+
* });
|
|
45
|
+
* const res = await app.request("/auth/session", { headers: { cookie } });
|
|
46
|
+
* ```
|
|
47
|
+
*/
|
|
48
|
+
export async function createTestSession(bff, options = {}) {
|
|
49
|
+
const now = Date.now();
|
|
50
|
+
const data = {
|
|
51
|
+
tokens: {
|
|
52
|
+
accessToken: options.tokens?.accessToken ?? crypto.randomUUID(),
|
|
53
|
+
accessTokenExpiresAt: options.tokens?.accessTokenExpiresAt ??
|
|
54
|
+
now + 60 * 60 * 1000,
|
|
55
|
+
tokenType: options.tokens?.tokenType ?? "Bearer",
|
|
56
|
+
scope: options.tokens?.scope,
|
|
57
|
+
idToken: options.tokens?.idToken,
|
|
58
|
+
},
|
|
59
|
+
refreshToken: options.refreshToken,
|
|
60
|
+
user: options.user ?? null,
|
|
61
|
+
createdAt: options.createdAt ?? now,
|
|
62
|
+
updatedAt: options.updatedAt ?? now,
|
|
63
|
+
};
|
|
64
|
+
const cookieValue = await bff.sessionStore.create(data);
|
|
65
|
+
return `${bff.cookieName}=${cookieValue}`;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Mints a real access token, registers it with the resource server's token
|
|
69
|
+
* service, AND seeds a matching BFF session — the one-call setup for testing a
|
|
70
|
+
* route guarded by {@link HonoBff.protect}. Returns a `Cookie` header ready to
|
|
71
|
+
* attach to `app.request(...)`.
|
|
72
|
+
*
|
|
73
|
+
* This is the combined form of the two-step pattern (save a token, then
|
|
74
|
+
* {@link createTestSession} with the same access token) that protected-route
|
|
75
|
+
* tests otherwise hand-roll. Works with any {@link SessionStore} the BFF can
|
|
76
|
+
* `create()` into (e.g. `MemorySessionStore`,
|
|
77
|
+
* `EncryptedCookieSessionStore`); apps with a DB-backed store that mints
|
|
78
|
+
* its own cookie/secret should seed via that store directly.
|
|
79
|
+
*
|
|
80
|
+
* @example
|
|
81
|
+
* ```ts
|
|
82
|
+
* const cookie = await createAuthenticatedTestSession(bff, {
|
|
83
|
+
* tokenService,
|
|
84
|
+
* client: { id: "spa" },
|
|
85
|
+
* user: { id: "u1", username: "alice" },
|
|
86
|
+
* scope: new BasicScope("read write"),
|
|
87
|
+
* claims: { sub: "u1", name: "Alice" },
|
|
88
|
+
* });
|
|
89
|
+
* const res = await app.request("/api/me", { headers: { cookie } });
|
|
90
|
+
* ```
|
|
91
|
+
*/
|
|
92
|
+
export async function createAuthenticatedTestSession(bff, options) {
|
|
93
|
+
const accessToken = crypto.randomUUID();
|
|
94
|
+
const accessTokenExpiresAt = new Date(Date.now() + (options.accessTokenLifetime ?? 60 * 60) * 1000);
|
|
95
|
+
await options.tokenService.save({
|
|
96
|
+
accessToken,
|
|
97
|
+
accessTokenExpiresAt,
|
|
98
|
+
client: options.client,
|
|
99
|
+
user: options.user,
|
|
100
|
+
scope: options.scope,
|
|
101
|
+
});
|
|
102
|
+
return createTestSession(bff, {
|
|
103
|
+
tokens: {
|
|
104
|
+
accessToken,
|
|
105
|
+
accessTokenExpiresAt: accessTokenExpiresAt.getTime(),
|
|
106
|
+
},
|
|
107
|
+
user: options.claims ?? null,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Ages the access token in the underlying session so the next request
|
|
112
|
+
* through {@link HonoBff.attachToken} sees it as expired and triggers
|
|
113
|
+
* the refresh path.
|
|
114
|
+
*
|
|
115
|
+
* Returns a fresh `Cookie` header. For stateful stores
|
|
116
|
+
* (`MemorySessionStore` and DB-backed variants) the cookie value
|
|
117
|
+
* is unchanged. For stateless stores
|
|
118
|
+
* (`EncryptedCookieSessionStore`) the value is re-encrypted and
|
|
119
|
+
* differs from the input — use the returned header for subsequent
|
|
120
|
+
* requests.
|
|
121
|
+
*
|
|
122
|
+
* @param expiredAt Optional time to set the access token's expiry to
|
|
123
|
+
* (epoch ms). Defaults to one second in the past.
|
|
124
|
+
*/
|
|
125
|
+
export async function forceTokenExpiry(bff, cookieHeader, expiredAt) {
|
|
126
|
+
const value = parseCookieHeader(cookieHeader, bff.cookieName);
|
|
127
|
+
const data = await bff.sessionStore.read(value);
|
|
128
|
+
if (!data) {
|
|
129
|
+
throw new Error(`forceTokenExpiry: no session found for cookie value "${value}"`);
|
|
130
|
+
}
|
|
131
|
+
const aged = {
|
|
132
|
+
...data,
|
|
133
|
+
tokens: {
|
|
134
|
+
...data.tokens,
|
|
135
|
+
accessTokenExpiresAt: expiredAt ?? Date.now() - 1000,
|
|
136
|
+
},
|
|
137
|
+
updatedAt: Date.now(),
|
|
138
|
+
};
|
|
139
|
+
const newValue = await bff.sessionStore.update(value, aged);
|
|
140
|
+
return `${bff.cookieName}=${newValue}`;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Returns the session data currently stored under the given cookie
|
|
144
|
+
* header, or `null` if there is none. Convenience wrapper around
|
|
145
|
+
* `bff.sessionStore.read` that handles cookie parsing.
|
|
146
|
+
*/
|
|
147
|
+
export function readTestSession(bff, cookieHeader) {
|
|
148
|
+
const value = parseCookieHeader(cookieHeader, bff.cookieName);
|
|
149
|
+
return Promise.resolve(bff.sessionStore.read(value));
|
|
150
|
+
}
|
|
151
|
+
function parseCookieHeader(header, name) {
|
|
152
|
+
if (!header.includes("="))
|
|
153
|
+
return header;
|
|
154
|
+
const cookies = header.split(/;\s*/);
|
|
155
|
+
for (const cookie of cookies) {
|
|
156
|
+
const idx = cookie.indexOf("=");
|
|
157
|
+
if (idx < 0)
|
|
158
|
+
continue;
|
|
159
|
+
const k = cookie.slice(0, idx).trim();
|
|
160
|
+
if (k === name)
|
|
161
|
+
return cookie.slice(idx + 1).trim();
|
|
162
|
+
}
|
|
163
|
+
throw new Error(`parseCookieHeader: cookie "${name}" not found in header "${header}"`);
|
|
164
|
+
}
|
|
165
|
+
const BOUND_MS = 1000;
|
|
166
|
+
/** Contract test suite for {@link SessionStore} implementations. */
|
|
167
|
+
export function runSessionStoreContractTests(options) {
|
|
168
|
+
const destroyClearsRead = options.destroyClearsRead ?? true;
|
|
169
|
+
const makeBoundedStore = options.makeBoundedStore;
|
|
170
|
+
describe(options.describeName ?? "SessionStore contract", () => {
|
|
171
|
+
let store;
|
|
172
|
+
beforeEach(async () => {
|
|
173
|
+
store = await options.makeStore();
|
|
174
|
+
});
|
|
175
|
+
function sampleSession() {
|
|
176
|
+
const now = Date.now();
|
|
177
|
+
return {
|
|
178
|
+
tokens: {
|
|
179
|
+
accessToken: "access-1",
|
|
180
|
+
accessTokenExpiresAt: now + 60_000,
|
|
181
|
+
tokenType: "Bearer",
|
|
182
|
+
},
|
|
183
|
+
refreshToken: "refresh-1",
|
|
184
|
+
user: { sub: "u1" },
|
|
185
|
+
createdAt: now,
|
|
186
|
+
updatedAt: now,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
it("create returns a non-empty cookie value", async () => {
|
|
190
|
+
const value = await store.create(sampleSession());
|
|
191
|
+
assertEquals(typeof value, "string");
|
|
192
|
+
assert(value.length > 0);
|
|
193
|
+
});
|
|
194
|
+
it("read returns null for an unknown cookie value", async () => {
|
|
195
|
+
assertStrictEquals(await store.read("no-such-session"), null);
|
|
196
|
+
});
|
|
197
|
+
it("create + read round-trips the session data", async () => {
|
|
198
|
+
const original = sampleSession();
|
|
199
|
+
const value = await store.create(original);
|
|
200
|
+
const fetched = await store.read(value);
|
|
201
|
+
assert(fetched !== null, "read should return data for a created session");
|
|
202
|
+
assertEquals(fetched.tokens.accessToken, original.tokens.accessToken);
|
|
203
|
+
assertEquals(fetched.refreshToken, original.refreshToken);
|
|
204
|
+
assertEquals(fetched.user, original.user);
|
|
205
|
+
});
|
|
206
|
+
it("preserves createdAt, which carries the session's absolute age", async () => {
|
|
207
|
+
const original = sampleSession();
|
|
208
|
+
const value = await store.create(original);
|
|
209
|
+
const fetched = await store.read(value);
|
|
210
|
+
assert(fetched !== null, "read should return data for a created session");
|
|
211
|
+
assertEquals(fetched.createdAt, original.createdAt, "a store that drops createdAt silently opts its sessions out of " +
|
|
212
|
+
"HonoBffOptions.sessionMaxAgeMs, so they never age out");
|
|
213
|
+
});
|
|
214
|
+
it("update reflects changes on subsequent read", async () => {
|
|
215
|
+
const value = await store.create(sampleSession());
|
|
216
|
+
const updated = {
|
|
217
|
+
...sampleSession(),
|
|
218
|
+
user: { sub: "u1", role: "admin" },
|
|
219
|
+
updatedAt: Date.now(),
|
|
220
|
+
};
|
|
221
|
+
const newValue = await store.update(value, updated);
|
|
222
|
+
const fetched = await store.read(newValue);
|
|
223
|
+
assertEquals(fetched?.user, { sub: "u1", role: "admin" });
|
|
224
|
+
});
|
|
225
|
+
if (destroyClearsRead) {
|
|
226
|
+
it("destroy removes the session", async () => {
|
|
227
|
+
const value = await store.create(sampleSession());
|
|
228
|
+
await store.destroy(value);
|
|
229
|
+
assertStrictEquals(await store.read(value), null);
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
it("never advertises a max age without proving it enforces one", () => {
|
|
233
|
+
const advertised = sessionStoreMaxAgeMs(store);
|
|
234
|
+
assertEquals(advertised !== undefined && makeBoundedStore === undefined, false, `this store advertises maxAgeMs=${advertised}, which HonoBff trusts ` +
|
|
235
|
+
"when it refuses a session cookie that would expire before the " +
|
|
236
|
+
"session does. Supply makeBoundedStore so the contract can prove " +
|
|
237
|
+
"the bound is real.");
|
|
238
|
+
});
|
|
239
|
+
if (makeBoundedStore) {
|
|
240
|
+
it("reports the max age it was built with, so a BFF can outlive it", async () => {
|
|
241
|
+
const bounded = await makeBoundedStore(BOUND_MS);
|
|
242
|
+
assertStrictEquals(sessionStoreMaxAgeMs(bounded), BOUND_MS, "a store that hides its bound gets no divergence check from HonoBff");
|
|
243
|
+
});
|
|
244
|
+
it("stops reading a session older than the max age it enforces", async () => {
|
|
245
|
+
const bounded = await makeBoundedStore(BOUND_MS);
|
|
246
|
+
const value = await bounded.create(sampleSession());
|
|
247
|
+
assert(await bounded.read(value) !== null, "a session created moments ago must still read");
|
|
248
|
+
await delay(BOUND_MS + 500);
|
|
249
|
+
assertStrictEquals(await bounded.read(value), null, "a session past the store's own bound must stop reading, or a " +
|
|
250
|
+
"captured cookie outlives the window the bound promises");
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Thin, optional Hono route factory over {@link IdentityService} for the
|
|
3
|
+
* **password-credential** flows.
|
|
4
|
+
*
|
|
5
|
+
* `honoIdentityRoutes(service, opts)` mounts exactly five POST endpoints —
|
|
6
|
+
* `/signup`, `/signin`, `/password/reset-request`, `/password/reset`, and
|
|
7
|
+
* `/email/verify` — by delegating to the service's methods. It is deliberately
|
|
8
|
+
* **not** an opinionated framework: the service methods are the real API, and
|
|
9
|
+
* this just wires the password path to routes for apps that want them mounted
|
|
10
|
+
* (the `IdentityService` stays usable directly from your own
|
|
11
|
+
* routes/loaders/actions).
|
|
12
|
+
*
|
|
13
|
+
* **Scope — password path only, by design.** The newer flows are intentionally
|
|
14
|
+
* *not* mounted here, because their safe wiring carries app-owned policy the
|
|
15
|
+
* factory can't guess. Drive them by calling the service methods from your own
|
|
16
|
+
* handlers, as each guide shows:
|
|
17
|
+
* - **MFA** (enroll / confirm / verify / disable / regenerate, via `MfaService`):
|
|
18
|
+
* *when* a challenge is required, where the pending challenge state lives, and
|
|
19
|
+
* the cookie shape are all yours — see `docs/guides/add-mfa.md`.
|
|
20
|
+
* - **Passwordless** (`requestSignInLink` / `consumeSignInLink`,
|
|
21
|
+
* `requestSignInCode` / `verifySignInCode`): the enumeration-safe response
|
|
22
|
+
* shape, whether `rate_limited` is swallowed, and IP throttling are yours —
|
|
23
|
+
* see `docs/guides/passwordless.md`.
|
|
24
|
+
* - **Social / OIDC** (start / callback, via `@udibo/oauth2/identity/external`):
|
|
25
|
+
* transient `state`/PKCE custody and the account-resolution/linking policy
|
|
26
|
+
* (where the account-takeover bugs live) are yours — see
|
|
27
|
+
* `docs/guides/social-sign-in.md`.
|
|
28
|
+
*
|
|
29
|
+
* Session creation stays in your app: the `signup`/`signin` routes call your
|
|
30
|
+
* `onAuthenticated(c, user)` hook on success, where you mint your session and
|
|
31
|
+
* return a `Response` (e.g. a redirect via `bff.loginContinuation`). Omit the
|
|
32
|
+
* hook to mount only the stateless reset/verify routes.
|
|
33
|
+
*
|
|
34
|
+
* Accepts JSON or form-encoded bodies. Responses are simple JSON
|
|
35
|
+
* (`{ ok: true }` / `{ error }`); render your own UI around them. Because a
|
|
36
|
+
* form-encoded POST is exactly what a cross-site page can forge, the routes
|
|
37
|
+
* refuse requests a browser reports as cross-site — see
|
|
38
|
+
* {@link HonoIdentityOptions.csrf}.
|
|
39
|
+
*
|
|
40
|
+
* @module
|
|
41
|
+
*/
|
|
42
|
+
import "../../_dnt.polyfills.js";
|
|
43
|
+
import { Hono } from "hono";
|
|
44
|
+
import type { Context } from "hono";
|
|
45
|
+
import type { IdentityService, IdentityUser } from "../../identity/service.js";
|
|
46
|
+
/** Paths for the routes mounted by {@link honoIdentityRoutes}. */
|
|
47
|
+
export interface HonoIdentityPaths {
|
|
48
|
+
/** `POST` sign-up. Default `/signup`. */
|
|
49
|
+
signUp?: string;
|
|
50
|
+
/** `POST` sign-in. Default `/signin`. */
|
|
51
|
+
signIn?: string;
|
|
52
|
+
/** `POST` password-reset request (enumeration-safe). Default `/password/reset-request`. */
|
|
53
|
+
requestPasswordReset?: string;
|
|
54
|
+
/** `POST` password reset (consume token). Default `/password/reset`. */
|
|
55
|
+
resetPassword?: string;
|
|
56
|
+
/** `POST` email verification (consume token). Default `/email/verify`. */
|
|
57
|
+
verifyEmail?: string;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Tunes the same-origin guard {@link honoIdentityRoutes} mounts by default.
|
|
61
|
+
* Pass `csrf: false` instead to turn the guard off entirely — only when
|
|
62
|
+
* something in front of these routes already stops cross-site requests.
|
|
63
|
+
*/
|
|
64
|
+
export interface HonoIdentityCsrfOptions {
|
|
65
|
+
/**
|
|
66
|
+
* Additional origins allowed to post to these routes, as exact
|
|
67
|
+
* `scheme://host[:port]` strings (e.g. `"https://app.example.com"`). Use it
|
|
68
|
+
* when the page holding your sign-in form is served from a different origin
|
|
69
|
+
* than the routes. The origin serving the routes is always allowed.
|
|
70
|
+
*/
|
|
71
|
+
allowedOrigins?: string[];
|
|
72
|
+
}
|
|
73
|
+
/** Options for {@link honoIdentityRoutes}. */
|
|
74
|
+
export interface HonoIdentityOptions<User extends IdentityUser> {
|
|
75
|
+
/**
|
|
76
|
+
* Cross-site request protection, **on by default**. These routes carry no
|
|
77
|
+
* token of their own, so the guard reads what the browser states about the
|
|
78
|
+
* request: a `Sec-Fetch-Site` of `same-origin`/`none` passes, any other value
|
|
79
|
+
* is refused, and browsers too old to send it fall back to matching the
|
|
80
|
+
* `Origin` header's host against the request's. A request with neither header
|
|
81
|
+
* — curl, a server-side call, a native app — passes, since cross-site request
|
|
82
|
+
* forgery needs a browser.
|
|
83
|
+
*
|
|
84
|
+
* Refusals get `403` `{ error: "forbidden_origin" }` before any handler runs.
|
|
85
|
+
* Set {@link HonoIdentityCsrfOptions.allowedOrigins} for a legitimate
|
|
86
|
+
* cross-origin caller, or `false` to disable the guard when a proxy or an
|
|
87
|
+
* outer middleware already terminates CSRF.
|
|
88
|
+
*/
|
|
89
|
+
csrf?: HonoIdentityCsrfOptions | false;
|
|
90
|
+
/**
|
|
91
|
+
* Called after `signup`/`signin` authenticates a user. Your app owns what
|
|
92
|
+
* happens next — create your session and return a `Response` (e.g. a redirect
|
|
93
|
+
* via `bff.loginContinuation`). **Required to mount the signup/signin routes**;
|
|
94
|
+
* omit it to mount only the stateless reset/verify routes.
|
|
95
|
+
*/
|
|
96
|
+
onAuthenticated?: (c: Context, user: User, action: "signUp" | "signIn") => Response | Promise<Response>;
|
|
97
|
+
/** Override the default paths. */
|
|
98
|
+
paths?: HonoIdentityPaths;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Build a Hono app mounting the password-credential endpoints
|
|
102
|
+
* (`/signup`, `/signin`, `/password/reset-request`, `/password/reset`,
|
|
103
|
+
* `/email/verify`) over `service`. Mount it wherever you like, e.g.
|
|
104
|
+
* `app.route("/auth", honoIdentityRoutes(service, …))`.
|
|
105
|
+
*
|
|
106
|
+
* A JSON body keeps each field's type except `password`, `identifier`, `email`,
|
|
107
|
+
* and `token`, which are coerced to strings when present; on the sign-up route
|
|
108
|
+
* every other field is passed to `service.signUp` as `profile`. A form-encoded
|
|
109
|
+
* body is all strings.
|
|
110
|
+
*
|
|
111
|
+
* Every mounted route sits behind the same-origin guard described in
|
|
112
|
+
* {@link HonoIdentityOptions.csrf}, which is on unless you pass `csrf: false`.
|
|
113
|
+
*
|
|
114
|
+
* Passwordless, MFA, and social are intentionally not mounted — see the module
|
|
115
|
+
* doc for why and where to hand-wire each.
|
|
116
|
+
*/
|
|
117
|
+
export declare function honoIdentityRoutes<User extends IdentityUser>(service: IdentityService<User>, options?: HonoIdentityOptions<User>): Hono;
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Thin, optional Hono route factory over {@link IdentityService} for the
|
|
3
|
+
* **password-credential** flows.
|
|
4
|
+
*
|
|
5
|
+
* `honoIdentityRoutes(service, opts)` mounts exactly five POST endpoints —
|
|
6
|
+
* `/signup`, `/signin`, `/password/reset-request`, `/password/reset`, and
|
|
7
|
+
* `/email/verify` — by delegating to the service's methods. It is deliberately
|
|
8
|
+
* **not** an opinionated framework: the service methods are the real API, and
|
|
9
|
+
* this just wires the password path to routes for apps that want them mounted
|
|
10
|
+
* (the `IdentityService` stays usable directly from your own
|
|
11
|
+
* routes/loaders/actions).
|
|
12
|
+
*
|
|
13
|
+
* **Scope — password path only, by design.** The newer flows are intentionally
|
|
14
|
+
* *not* mounted here, because their safe wiring carries app-owned policy the
|
|
15
|
+
* factory can't guess. Drive them by calling the service methods from your own
|
|
16
|
+
* handlers, as each guide shows:
|
|
17
|
+
* - **MFA** (enroll / confirm / verify / disable / regenerate, via `MfaService`):
|
|
18
|
+
* *when* a challenge is required, where the pending challenge state lives, and
|
|
19
|
+
* the cookie shape are all yours — see `docs/guides/add-mfa.md`.
|
|
20
|
+
* - **Passwordless** (`requestSignInLink` / `consumeSignInLink`,
|
|
21
|
+
* `requestSignInCode` / `verifySignInCode`): the enumeration-safe response
|
|
22
|
+
* shape, whether `rate_limited` is swallowed, and IP throttling are yours —
|
|
23
|
+
* see `docs/guides/passwordless.md`.
|
|
24
|
+
* - **Social / OIDC** (start / callback, via `@udibo/oauth2/identity/external`):
|
|
25
|
+
* transient `state`/PKCE custody and the account-resolution/linking policy
|
|
26
|
+
* (where the account-takeover bugs live) are yours — see
|
|
27
|
+
* `docs/guides/social-sign-in.md`.
|
|
28
|
+
*
|
|
29
|
+
* Session creation stays in your app: the `signup`/`signin` routes call your
|
|
30
|
+
* `onAuthenticated(c, user)` hook on success, where you mint your session and
|
|
31
|
+
* return a `Response` (e.g. a redirect via `bff.loginContinuation`). Omit the
|
|
32
|
+
* hook to mount only the stateless reset/verify routes.
|
|
33
|
+
*
|
|
34
|
+
* Accepts JSON or form-encoded bodies. Responses are simple JSON
|
|
35
|
+
* (`{ ok: true }` / `{ error }`); render your own UI around them. Because a
|
|
36
|
+
* form-encoded POST is exactly what a cross-site page can forge, the routes
|
|
37
|
+
* refuse requests a browser reports as cross-site — see
|
|
38
|
+
* {@link HonoIdentityOptions.csrf}.
|
|
39
|
+
*
|
|
40
|
+
* @module
|
|
41
|
+
*/
|
|
42
|
+
import "../../_dnt.polyfills.js";
|
|
43
|
+
import { Hono } from "hono";
|
|
44
|
+
import { identityErrorStatus, isIdentityError, } from "../../identity/errors.js";
|
|
45
|
+
/**
|
|
46
|
+
* Wrap a handler so a thrown {@link IdentityError} (e.g. `identifier_taken` from
|
|
47
|
+
* your user store, or `weak_password` from a policy check) becomes a consistent
|
|
48
|
+
* `{ error: code }` response with the conventional status; other errors
|
|
49
|
+
* propagate untouched.
|
|
50
|
+
*/
|
|
51
|
+
function handle(fn) {
|
|
52
|
+
return async (c) => {
|
|
53
|
+
try {
|
|
54
|
+
return await fn(c);
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
if (!isIdentityError(error))
|
|
58
|
+
throw error;
|
|
59
|
+
if (error.retryAfterMs !== undefined) {
|
|
60
|
+
c.header("Retry-After", String(Math.ceil(error.retryAfterMs / 1000)));
|
|
61
|
+
}
|
|
62
|
+
return c.json({ error: error.code }, identityErrorStatus(error.code));
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
const STRING_FIELDS = new Set(["password", "identifier", "email", "token"]);
|
|
67
|
+
/**
|
|
68
|
+
* Reads a JSON or form-encoded request body. A JSON body keeps each value's
|
|
69
|
+
* type except for {@link STRING_FIELDS}; a body that is not a JSON object
|
|
70
|
+
* (array, string, `null`) reads as empty. A form-encoded body is all strings by
|
|
71
|
+
* construction.
|
|
72
|
+
*/
|
|
73
|
+
async function readBody(c) {
|
|
74
|
+
const contentType = c.req.header("content-type") ?? "";
|
|
75
|
+
if (contentType.includes("application/json")) {
|
|
76
|
+
try {
|
|
77
|
+
const body = await c.req.json();
|
|
78
|
+
if (typeof body !== "object" || body === null || Array.isArray(body)) {
|
|
79
|
+
return {};
|
|
80
|
+
}
|
|
81
|
+
const out = {};
|
|
82
|
+
for (const [key, value] of Object.entries(body)) {
|
|
83
|
+
out[key] = STRING_FIELDS.has(key) && value !== null &&
|
|
84
|
+
value !== undefined
|
|
85
|
+
? String(value)
|
|
86
|
+
: value;
|
|
87
|
+
}
|
|
88
|
+
return out;
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
return {};
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
try {
|
|
95
|
+
const form = await c.req.formData();
|
|
96
|
+
const out = {};
|
|
97
|
+
for (const [key, value] of form)
|
|
98
|
+
out[key] = String(value);
|
|
99
|
+
return out;
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
return {};
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
const FORBIDDEN_ORIGIN = "forbidden_origin";
|
|
106
|
+
const SAFE_METHODS = new Set(["GET", "HEAD", "OPTIONS"]);
|
|
107
|
+
function hostOf(origin) {
|
|
108
|
+
try {
|
|
109
|
+
return new URL(origin).host;
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
return undefined;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
function isTrustedOrigin(c, allowedOrigins) {
|
|
116
|
+
const origin = c.req.header("origin");
|
|
117
|
+
const site = c.req.header("sec-fetch-site");
|
|
118
|
+
if (site) {
|
|
119
|
+
return site === "same-origin" || site === "none" ||
|
|
120
|
+
(!!origin && allowedOrigins.has(origin));
|
|
121
|
+
}
|
|
122
|
+
if (!origin)
|
|
123
|
+
return true;
|
|
124
|
+
return allowedOrigins.has(origin) ||
|
|
125
|
+
hostOf(origin) === new URL(c.req.url).host;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Refuses an unsafe-method request a browser reports as coming from another
|
|
129
|
+
* site, per {@link HonoIdentityOptions.csrf}.
|
|
130
|
+
*/
|
|
131
|
+
function sameOriginGuard(options) {
|
|
132
|
+
const allowedOrigins = new Set(options.allowedOrigins ?? []);
|
|
133
|
+
return async (c, next) => {
|
|
134
|
+
if (!SAFE_METHODS.has(c.req.method) && !isTrustedOrigin(c, allowedOrigins)) {
|
|
135
|
+
return c.json({ error: FORBIDDEN_ORIGIN }, identityErrorStatus(FORBIDDEN_ORIGIN));
|
|
136
|
+
}
|
|
137
|
+
await next();
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Build a Hono app mounting the password-credential endpoints
|
|
142
|
+
* (`/signup`, `/signin`, `/password/reset-request`, `/password/reset`,
|
|
143
|
+
* `/email/verify`) over `service`. Mount it wherever you like, e.g.
|
|
144
|
+
* `app.route("/auth", honoIdentityRoutes(service, …))`.
|
|
145
|
+
*
|
|
146
|
+
* A JSON body keeps each field's type except `password`, `identifier`, `email`,
|
|
147
|
+
* and `token`, which are coerced to strings when present; on the sign-up route
|
|
148
|
+
* every other field is passed to `service.signUp` as `profile`. A form-encoded
|
|
149
|
+
* body is all strings.
|
|
150
|
+
*
|
|
151
|
+
* Every mounted route sits behind the same-origin guard described in
|
|
152
|
+
* {@link HonoIdentityOptions.csrf}, which is on unless you pass `csrf: false`.
|
|
153
|
+
*
|
|
154
|
+
* Passwordless, MFA, and social are intentionally not mounted — see the module
|
|
155
|
+
* doc for why and where to hand-wire each.
|
|
156
|
+
*/
|
|
157
|
+
export function honoIdentityRoutes(service, options = {}) {
|
|
158
|
+
const app = new Hono();
|
|
159
|
+
const paths = {
|
|
160
|
+
signUp: "/signup",
|
|
161
|
+
signIn: "/signin",
|
|
162
|
+
requestPasswordReset: "/password/reset-request",
|
|
163
|
+
resetPassword: "/password/reset",
|
|
164
|
+
verifyEmail: "/email/verify",
|
|
165
|
+
...options.paths,
|
|
166
|
+
};
|
|
167
|
+
const onAuthenticated = options.onAuthenticated;
|
|
168
|
+
if (options.csrf !== false) {
|
|
169
|
+
app.use("*", sameOriginGuard(options.csrf ?? {}));
|
|
170
|
+
}
|
|
171
|
+
if (onAuthenticated) {
|
|
172
|
+
app.post(paths.signUp, handle(async (c) => {
|
|
173
|
+
const { password, ...profile } = await readBody(c);
|
|
174
|
+
if (typeof password !== "string" || !password) {
|
|
175
|
+
return c.json({ error: "invalid_request" }, 400);
|
|
176
|
+
}
|
|
177
|
+
const user = await service.signUp({ password, profile });
|
|
178
|
+
return await onAuthenticated(c, user, "signUp");
|
|
179
|
+
}));
|
|
180
|
+
app.post(paths.signIn, handle(async (c) => {
|
|
181
|
+
const body = await readBody(c);
|
|
182
|
+
const user = await service.signIn({
|
|
183
|
+
identifier: String(body.identifier ?? ""),
|
|
184
|
+
password: String(body.password ?? ""),
|
|
185
|
+
});
|
|
186
|
+
if (!user)
|
|
187
|
+
return c.json({ error: "invalid_credentials" }, 401);
|
|
188
|
+
return await onAuthenticated(c, user, "signIn");
|
|
189
|
+
}));
|
|
190
|
+
}
|
|
191
|
+
app.post(paths.requestPasswordReset, handle(async (c) => {
|
|
192
|
+
const body = await readBody(c);
|
|
193
|
+
await service.requestPasswordReset(String(body.email ?? ""));
|
|
194
|
+
return c.json({ ok: true });
|
|
195
|
+
}));
|
|
196
|
+
app.post(paths.resetPassword, handle(async (c) => {
|
|
197
|
+
const body = await readBody(c);
|
|
198
|
+
const result = await service.resetPassword({
|
|
199
|
+
token: String(body.token ?? ""),
|
|
200
|
+
password: String(body.password ?? ""),
|
|
201
|
+
});
|
|
202
|
+
return result
|
|
203
|
+
? c.json({ ok: true })
|
|
204
|
+
: c.json({ error: "invalid_token" }, 400);
|
|
205
|
+
}));
|
|
206
|
+
app.post(paths.verifyEmail, handle(async (c) => {
|
|
207
|
+
const body = await readBody(c);
|
|
208
|
+
const result = await service.verifyEmail(String(body.token ?? ""));
|
|
209
|
+
if (result.status === "success") {
|
|
210
|
+
return c.json({ ok: true, userId: result.userId });
|
|
211
|
+
}
|
|
212
|
+
return c.json({ error: `token_${result.status}` }, 400);
|
|
213
|
+
}));
|
|
214
|
+
return app;
|
|
215
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Request logging that removes query values, including OAuth callback credentials.
|
|
3
|
+
*
|
|
4
|
+
* @module hono/log
|
|
5
|
+
*/
|
|
6
|
+
import "../../_dnt.polyfills.js";
|
|
7
|
+
import type { MiddlewareHandler } from "hono";
|
|
8
|
+
/**
|
|
9
|
+
* Produces a request target with every query value replaced by `[redacted]`.
|
|
10
|
+
* Keeps the pathname and percent-encoded parameter names, including repeats,
|
|
11
|
+
* and drops the origin and fragment. Use it before recording a request URL.
|
|
12
|
+
* Paths and parameter names must themselves contain no secrets.
|
|
13
|
+
*
|
|
14
|
+
* @param url An absolute request URL.
|
|
15
|
+
* @returns The pathname followed by redacted query parameters, if any.
|
|
16
|
+
* @throws TypeError if `url` is not a valid absolute URL.
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```ts
|
|
20
|
+
* import { redactedRequestTarget } from "@udibo/oauth2/hono/log";
|
|
21
|
+
*
|
|
22
|
+
* const target = redactedRequestTarget(
|
|
23
|
+
* "https://app.example.com/auth/callback?code=secret&state=nonce",
|
|
24
|
+
* );
|
|
25
|
+
* // "/auth/callback?code=[redacted]&state=[redacted]"
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
export declare function redactedRequestTarget(url: string | URL): string;
|
|
29
|
+
/**
|
|
30
|
+
* Logs Hono requests and responses without any query values. Mount before
|
|
31
|
+
* authentication routes so OAuth callback codes and state stay out of logs.
|
|
32
|
+
* Logs method, redacted target, response status, and elapsed milliseconds;
|
|
33
|
+
* does not log headers or bodies. Paths and parameter names remain visible.
|
|
34
|
+
*
|
|
35
|
+
* @param print Receives each log line; defaults to `console.log`, resolved per line.
|
|
36
|
+
* @returns Middleware emitting a request line and, after downstream completion,
|
|
37
|
+
* a response line.
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* ```ts
|
|
41
|
+
* import { Hono } from "hono";
|
|
42
|
+
* import { requestLogger } from "@udibo/oauth2/hono/log";
|
|
43
|
+
*
|
|
44
|
+
* const app = new Hono();
|
|
45
|
+
* app.use(requestLogger());
|
|
46
|
+
* app.get("/", (c) => c.text("Hello"));
|
|
47
|
+
* ```
|
|
48
|
+
*/
|
|
49
|
+
export declare function requestLogger(print?: (line: string) => void): MiddlewareHandler;
|