@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,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Request logging that removes query values, including OAuth callback credentials.
|
|
3
|
+
*
|
|
4
|
+
* @module hono/log
|
|
5
|
+
*/
|
|
6
|
+
import "../../_dnt.polyfills.js";
|
|
7
|
+
/**
|
|
8
|
+
* Produces a request target with every query value replaced by `[redacted]`.
|
|
9
|
+
* Keeps the pathname and percent-encoded parameter names, including repeats,
|
|
10
|
+
* and drops the origin and fragment. Use it before recording a request URL.
|
|
11
|
+
* Paths and parameter names must themselves contain no secrets.
|
|
12
|
+
*
|
|
13
|
+
* @param url An absolute request URL.
|
|
14
|
+
* @returns The pathname followed by redacted query parameters, if any.
|
|
15
|
+
* @throws TypeError if `url` is not a valid absolute URL.
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```ts
|
|
19
|
+
* import { redactedRequestTarget } from "@udibo/oauth2/hono/log";
|
|
20
|
+
*
|
|
21
|
+
* const target = redactedRequestTarget(
|
|
22
|
+
* "https://app.example.com/auth/callback?code=secret&state=nonce",
|
|
23
|
+
* );
|
|
24
|
+
* // "/auth/callback?code=[redacted]&state=[redacted]"
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
export function redactedRequestTarget(url) {
|
|
28
|
+
const { pathname, searchParams } = new URL(url);
|
|
29
|
+
const names = [...searchParams.keys()];
|
|
30
|
+
if (names.length === 0)
|
|
31
|
+
return pathname;
|
|
32
|
+
const query = names
|
|
33
|
+
.map((name) => `${encodeURIComponent(name)}=[redacted]`)
|
|
34
|
+
.join("&");
|
|
35
|
+
return `${pathname}?${query}`;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Logs Hono requests and responses without any query values. Mount before
|
|
39
|
+
* authentication routes so OAuth callback codes and state stay out of logs.
|
|
40
|
+
* Logs method, redacted target, response status, and elapsed milliseconds;
|
|
41
|
+
* does not log headers or bodies. Paths and parameter names remain visible.
|
|
42
|
+
*
|
|
43
|
+
* @param print Receives each log line; defaults to `console.log`, resolved per line.
|
|
44
|
+
* @returns Middleware emitting a request line and, after downstream completion,
|
|
45
|
+
* a response line.
|
|
46
|
+
*
|
|
47
|
+
* @example
|
|
48
|
+
* ```ts
|
|
49
|
+
* import { Hono } from "hono";
|
|
50
|
+
* import { requestLogger } from "@udibo/oauth2/hono/log";
|
|
51
|
+
*
|
|
52
|
+
* const app = new Hono();
|
|
53
|
+
* app.use(requestLogger());
|
|
54
|
+
* app.get("/", (c) => c.text("Hello"));
|
|
55
|
+
* ```
|
|
56
|
+
*/
|
|
57
|
+
export function requestLogger(print) {
|
|
58
|
+
const emit = print ?? ((line) => console.log(line));
|
|
59
|
+
return async (c, next) => {
|
|
60
|
+
const target = redactedRequestTarget(c.req.url);
|
|
61
|
+
emit(`<-- ${c.req.method} ${target}`);
|
|
62
|
+
const start = Date.now();
|
|
63
|
+
await next();
|
|
64
|
+
emit(`--> ${c.req.method} ${target} ${c.res.status} ${Date.now() - start}ms`);
|
|
65
|
+
};
|
|
66
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hono adapter for the core {@link ResourceServer}.
|
|
3
|
+
*
|
|
4
|
+
* Provides middleware and accessors that let a Hono app authenticate
|
|
5
|
+
* requests against an OAuth2 authorization server. The core server
|
|
6
|
+
* implementation is framework-agnostic; this adapter is the only place that
|
|
7
|
+
* knows about `Context` and middleware wiring.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```ts
|
|
11
|
+
* import { Hono } from "hono";
|
|
12
|
+
* import {
|
|
13
|
+
* HonoResourceServer,
|
|
14
|
+
* type HonoResourceServerVariables,
|
|
15
|
+
* } from "@udibo/oauth2/hono/resource-server";
|
|
16
|
+
*
|
|
17
|
+
* const resourceServer = new HonoResourceServer({
|
|
18
|
+
* resolve: () => ({ services: { tokenService } }),
|
|
19
|
+
* });
|
|
20
|
+
* const app = new Hono<{
|
|
21
|
+
* Variables: HonoResourceServerVariables<Client, User>;
|
|
22
|
+
* }>();
|
|
23
|
+
*
|
|
24
|
+
* app.use("/api/*", resourceServer.protect());
|
|
25
|
+
* app.get("/api/me", (c) => c.json(resourceServer.getContext(c).user));
|
|
26
|
+
* ```
|
|
27
|
+
*
|
|
28
|
+
* @module
|
|
29
|
+
*/
|
|
30
|
+
import "../../_dnt.polyfills.js";
|
|
31
|
+
import type { Context, MiddlewareHandler } from "hono";
|
|
32
|
+
import type { ClientInterface } from "../../models/client.js";
|
|
33
|
+
import type { AbstractScope, BasicScope } from "../../models/scope.js";
|
|
34
|
+
import { type AuthenticatedContext, ResourceServer } from "../../server/resource-server.js";
|
|
35
|
+
import type { RequireConditions } from "../../models/authorization.js";
|
|
36
|
+
export { type HonoResourceServerVariables, OAUTH2_CONTEXT_KEY, } from "./_common.js";
|
|
37
|
+
/**
|
|
38
|
+
* Hono-specific wrapper around {@link ResourceServer}. Construction options
|
|
39
|
+
* are identical to the core server; the subclass only adds Hono middleware
|
|
40
|
+
* helpers.
|
|
41
|
+
*/
|
|
42
|
+
export declare class HonoResourceServer<Client extends ClientInterface, User, S extends AbstractScope = BasicScope> extends ResourceServer<Client, User, S> {
|
|
43
|
+
/**
|
|
44
|
+
* Returns a Hono middleware that validates the bearer token.
|
|
45
|
+
*
|
|
46
|
+
* On success, sets the authenticated context at `c.get("@udibo/oauth2")`
|
|
47
|
+
* (typed via {@link HonoResourceServerVariables}) and calls `next()`. On
|
|
48
|
+
* failure, returns an RFC 6749 error response with a `WWW-Authenticate`
|
|
49
|
+
* header per RFC 6750 Section 3.
|
|
50
|
+
*/
|
|
51
|
+
protect(requiredScope?: S | string): MiddlewareHandler;
|
|
52
|
+
/**
|
|
53
|
+
* Returns a Hono middleware that asserts the **already-authenticated**
|
|
54
|
+
* request carries `requiredScope`, **without** re-validating the token.
|
|
55
|
+
*
|
|
56
|
+
* Mount it downstream of {@link protect} (or a BFF `protect`, which sets the
|
|
57
|
+
* same context) when one authenticated mount exposes routes needing
|
|
58
|
+
* different scopes — e.g. `read` on GETs, `write` on mutations — that a
|
|
59
|
+
* single `protect(scope)` can't express. It reads the context `protect`
|
|
60
|
+
* already populated and checks the scope in memory, so unlike a second
|
|
61
|
+
* `protect(scope)` it performs no token lookup. On insufficient scope it
|
|
62
|
+
* returns the same RFC 6750 `insufficient_scope` response (403 with a
|
|
63
|
+
* `WWW-Authenticate` header) as `protect`.
|
|
64
|
+
*
|
|
65
|
+
* @example
|
|
66
|
+
* ```ts
|
|
67
|
+
* app.use("/api/*", resourceServer.protect()); // authenticate once
|
|
68
|
+
* app.get("/api/items", resourceServer.requireScope("read"), list);
|
|
69
|
+
* app.post("/api/items", resourceServer.requireScope("write"), create);
|
|
70
|
+
* ```
|
|
71
|
+
*/
|
|
72
|
+
requireScope(requiredScope: S | string): MiddlewareHandler;
|
|
73
|
+
/**
|
|
74
|
+
* Middleware asserting the already-authenticated request satisfies every
|
|
75
|
+
* condition — AND semantics across keys, arrays mean all-of (see
|
|
76
|
+
* {@linkcode RequireConditions}). Scope failures answer the RFC 6750
|
|
77
|
+
* `insufficient_scope` challenge exactly as {@linkcode requireScope} does;
|
|
78
|
+
* permission, role, and organization failures answer a plain 403. Mount a
|
|
79
|
+
* {@linkcode protect} first — this middleware does not validate the token.
|
|
80
|
+
*
|
|
81
|
+
* @example
|
|
82
|
+
* ```ts
|
|
83
|
+
* app.use("/api/*", resourceServer.protect());
|
|
84
|
+
* app.post(
|
|
85
|
+
* "/api/posts",
|
|
86
|
+
* resourceServer.require({ scope: "posts:write", permission: "posts:write" }),
|
|
87
|
+
* create,
|
|
88
|
+
* );
|
|
89
|
+
* ```
|
|
90
|
+
*/
|
|
91
|
+
require(conditions: RequireConditions): MiddlewareHandler;
|
|
92
|
+
/**
|
|
93
|
+
* Authenticates a Hono {@link Context} or a raw {@link Request} manually,
|
|
94
|
+
* for handlers that want to authenticate inline instead of using
|
|
95
|
+
* {@link protect} as middleware. Throws on failure — pair with
|
|
96
|
+
* {@link ResourceServer.handleAuthError} to convert to a response.
|
|
97
|
+
*/
|
|
98
|
+
authenticate(c: Context | Request, requiredScope?: S | string): Promise<AuthenticatedContext<Client, User, S>>;
|
|
99
|
+
/** Typed accessor for the authenticated OAuth2 context. */
|
|
100
|
+
getContext(c: Context): AuthenticatedContext<Client, User, S>;
|
|
101
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hono adapter for the core {@link ResourceServer}.
|
|
3
|
+
*
|
|
4
|
+
* Provides middleware and accessors that let a Hono app authenticate
|
|
5
|
+
* requests against an OAuth2 authorization server. The core server
|
|
6
|
+
* implementation is framework-agnostic; this adapter is the only place that
|
|
7
|
+
* knows about `Context` and middleware wiring.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```ts
|
|
11
|
+
* import { Hono } from "hono";
|
|
12
|
+
* import {
|
|
13
|
+
* HonoResourceServer,
|
|
14
|
+
* type HonoResourceServerVariables,
|
|
15
|
+
* } from "@udibo/oauth2/hono/resource-server";
|
|
16
|
+
*
|
|
17
|
+
* const resourceServer = new HonoResourceServer({
|
|
18
|
+
* resolve: () => ({ services: { tokenService } }),
|
|
19
|
+
* });
|
|
20
|
+
* const app = new Hono<{
|
|
21
|
+
* Variables: HonoResourceServerVariables<Client, User>;
|
|
22
|
+
* }>();
|
|
23
|
+
*
|
|
24
|
+
* app.use("/api/*", resourceServer.protect());
|
|
25
|
+
* app.get("/api/me", (c) => c.json(resourceServer.getContext(c).user));
|
|
26
|
+
* ```
|
|
27
|
+
*
|
|
28
|
+
* @module
|
|
29
|
+
*/
|
|
30
|
+
import "../../_dnt.polyfills.js";
|
|
31
|
+
import { ResourceServer, } from "../../server/resource-server.js";
|
|
32
|
+
import { authenticateHonoRequest, createProtectMiddleware, createRequireMiddleware, createRequireScopeMiddleware, readContext, } from "./_common.js";
|
|
33
|
+
export { OAUTH2_CONTEXT_KEY, } from "./_common.js";
|
|
34
|
+
/**
|
|
35
|
+
* Hono-specific wrapper around {@link ResourceServer}. Construction options
|
|
36
|
+
* are identical to the core server; the subclass only adds Hono middleware
|
|
37
|
+
* helpers.
|
|
38
|
+
*/
|
|
39
|
+
export class HonoResourceServer extends ResourceServer {
|
|
40
|
+
/**
|
|
41
|
+
* Returns a Hono middleware that validates the bearer token.
|
|
42
|
+
*
|
|
43
|
+
* On success, sets the authenticated context at `c.get("@udibo/oauth2")`
|
|
44
|
+
* (typed via {@link HonoResourceServerVariables}) and calls `next()`. On
|
|
45
|
+
* failure, returns an RFC 6749 error response with a `WWW-Authenticate`
|
|
46
|
+
* header per RFC 6750 Section 3.
|
|
47
|
+
*/
|
|
48
|
+
protect(requiredScope) {
|
|
49
|
+
return createProtectMiddleware(this, requiredScope);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Returns a Hono middleware that asserts the **already-authenticated**
|
|
53
|
+
* request carries `requiredScope`, **without** re-validating the token.
|
|
54
|
+
*
|
|
55
|
+
* Mount it downstream of {@link protect} (or a BFF `protect`, which sets the
|
|
56
|
+
* same context) when one authenticated mount exposes routes needing
|
|
57
|
+
* different scopes — e.g. `read` on GETs, `write` on mutations — that a
|
|
58
|
+
* single `protect(scope)` can't express. It reads the context `protect`
|
|
59
|
+
* already populated and checks the scope in memory, so unlike a second
|
|
60
|
+
* `protect(scope)` it performs no token lookup. On insufficient scope it
|
|
61
|
+
* returns the same RFC 6750 `insufficient_scope` response (403 with a
|
|
62
|
+
* `WWW-Authenticate` header) as `protect`.
|
|
63
|
+
*
|
|
64
|
+
* @example
|
|
65
|
+
* ```ts
|
|
66
|
+
* app.use("/api/*", resourceServer.protect()); // authenticate once
|
|
67
|
+
* app.get("/api/items", resourceServer.requireScope("read"), list);
|
|
68
|
+
* app.post("/api/items", resourceServer.requireScope("write"), create);
|
|
69
|
+
* ```
|
|
70
|
+
*/
|
|
71
|
+
requireScope(requiredScope) {
|
|
72
|
+
return createRequireScopeMiddleware(this, "HonoResourceServer", requiredScope);
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Middleware asserting the already-authenticated request satisfies every
|
|
76
|
+
* condition — AND semantics across keys, arrays mean all-of (see
|
|
77
|
+
* {@linkcode RequireConditions}). Scope failures answer the RFC 6750
|
|
78
|
+
* `insufficient_scope` challenge exactly as {@linkcode requireScope} does;
|
|
79
|
+
* permission, role, and organization failures answer a plain 403. Mount a
|
|
80
|
+
* {@linkcode protect} first — this middleware does not validate the token.
|
|
81
|
+
*
|
|
82
|
+
* @example
|
|
83
|
+
* ```ts
|
|
84
|
+
* app.use("/api/*", resourceServer.protect());
|
|
85
|
+
* app.post(
|
|
86
|
+
* "/api/posts",
|
|
87
|
+
* resourceServer.require({ scope: "posts:write", permission: "posts:write" }),
|
|
88
|
+
* create,
|
|
89
|
+
* );
|
|
90
|
+
* ```
|
|
91
|
+
*/
|
|
92
|
+
require(conditions) {
|
|
93
|
+
return createRequireMiddleware(this, "HonoResourceServer", conditions);
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Authenticates a Hono {@link Context} or a raw {@link Request} manually,
|
|
97
|
+
* for handlers that want to authenticate inline instead of using
|
|
98
|
+
* {@link protect} as middleware. Throws on failure — pair with
|
|
99
|
+
* {@link ResourceServer.handleAuthError} to convert to a response.
|
|
100
|
+
*/
|
|
101
|
+
authenticate(c, requiredScope) {
|
|
102
|
+
return authenticateHonoRequest(this, c, requiredScope);
|
|
103
|
+
}
|
|
104
|
+
/** Typed accessor for the authenticated OAuth2 context. */
|
|
105
|
+
getContext(c) {
|
|
106
|
+
return readContext(c, "HonoResourceServer");
|
|
107
|
+
}
|
|
108
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The response ladder every {@link DirectClient} network call climbs: bounded
|
|
3
|
+
* read, deadline, redirect refusal, JSON validation, RFC 6749 §5.2 error
|
|
4
|
+
* mapping.
|
|
5
|
+
*
|
|
6
|
+
* Not part of the public API. The authorization server's bytes become either a
|
|
7
|
+
* typed {@link OAuth2Error} or a validated JSON object here and nowhere else,
|
|
8
|
+
* so the guards sit next to the tests that pin them.
|
|
9
|
+
*
|
|
10
|
+
* @module
|
|
11
|
+
*/
|
|
12
|
+
import { type OAuth2Error } from "../errors.js";
|
|
13
|
+
/** Response bytes accepted from an endpoint before the call is refused. */
|
|
14
|
+
export declare const MAX_RESPONSE_BYTES: number;
|
|
15
|
+
/** Wall-clock budget for one call, covering the connect and the body read. */
|
|
16
|
+
export declare const REQUEST_TIMEOUT_MS = 10000;
|
|
17
|
+
/** How a call describes itself in an error message, e.g. `"token endpoint"`. */
|
|
18
|
+
export type Endpoint = string;
|
|
19
|
+
/** How {@link sendGuarded} treats a `3xx`. */
|
|
20
|
+
export type RedirectPolicy = "refuse" | "follow";
|
|
21
|
+
/**
|
|
22
|
+
* Sends one request with a deadline.
|
|
23
|
+
*
|
|
24
|
+
* With the default `"refuse"` policy a `3xx` throws rather than being
|
|
25
|
+
* followed: these requests carry client credentials, an authorization code, a
|
|
26
|
+
* refresh token, or a bearer token, and replaying any of them at whatever host
|
|
27
|
+
* the response names would hand the secret to that host. Discovery passes
|
|
28
|
+
* `"follow"` — it sends no credential, and the mix-up it could otherwise
|
|
29
|
+
* enable is closed by validating the `issuer` the document reports.
|
|
30
|
+
*
|
|
31
|
+
* @param fetchImpl The client's resolved `fetch`.
|
|
32
|
+
* @param url Where to send it.
|
|
33
|
+
* @param init Request options; `redirect` and `signal` are supplied here. A
|
|
34
|
+
* caller-supplied `signal` is honoured *in addition to* the deadline.
|
|
35
|
+
* @param what The endpoint's name, for error messages.
|
|
36
|
+
* @param options `redirect` policy and `timeoutMs` deadline.
|
|
37
|
+
* @returns The response, which may still be a non-OK status.
|
|
38
|
+
* @throws {ServerError} when the request cannot be made, times out, or (under
|
|
39
|
+
* `"refuse"`) answers with a redirect.
|
|
40
|
+
*/
|
|
41
|
+
export declare function sendGuarded(fetchImpl: typeof fetch, url: string, init: RequestInit, what: Endpoint, options?: {
|
|
42
|
+
redirect?: RedirectPolicy;
|
|
43
|
+
timeoutMs?: number;
|
|
44
|
+
}): Promise<Response>;
|
|
45
|
+
/**
|
|
46
|
+
* Reads a response as a JSON object, refusing anything else.
|
|
47
|
+
*
|
|
48
|
+
* A non-OK status becomes the typed {@link OAuth2Error} the body names, or a
|
|
49
|
+
* {@link ServerError} carrying the status when it names nothing.
|
|
50
|
+
*
|
|
51
|
+
* @param res The response from {@link sendGuarded}.
|
|
52
|
+
* @param what The endpoint's name, for error messages.
|
|
53
|
+
* @returns The parsed JSON object.
|
|
54
|
+
* @throws {OAuth2Error} on a non-OK status, an oversized body, a body that is
|
|
55
|
+
* not valid JSON, or JSON that is not an object.
|
|
56
|
+
*/
|
|
57
|
+
export declare function receiveJson(res: Response, what: Endpoint): Promise<Record<string, unknown>>;
|
|
58
|
+
/**
|
|
59
|
+
* Turns a non-OK response into the typed error its body names. Use for
|
|
60
|
+
* endpoints whose success case carries no JSON (RFC 7009 revocation).
|
|
61
|
+
*
|
|
62
|
+
* @param res A non-OK response from {@link sendGuarded}.
|
|
63
|
+
* @param what The endpoint's name, for error messages.
|
|
64
|
+
* @returns The error to throw.
|
|
65
|
+
*/
|
|
66
|
+
export declare function errorFromResponse(res: Response, what: Endpoint): Promise<OAuth2Error>;
|
|
67
|
+
/**
|
|
68
|
+
* Enforces RFC 6749 §5.2 on a token response that arrived with a `2xx`.
|
|
69
|
+
*
|
|
70
|
+
* A server may report a failure in the body of a `200`; and a body with no
|
|
71
|
+
* usable `access_token` is not a token response no matter what it says. Both
|
|
72
|
+
* must throw here, because the alternative is persisting a bundle whose
|
|
73
|
+
* `accessToken` is `undefined` and then sending every subsequent request with
|
|
74
|
+
* no `Authorization` header at all.
|
|
75
|
+
*
|
|
76
|
+
* @param body The parsed token-endpoint response.
|
|
77
|
+
* @param what The endpoint's name, for error messages.
|
|
78
|
+
* @throws {OAuth2Error} when the body reports an error or carries no non-empty
|
|
79
|
+
* string `access_token`.
|
|
80
|
+
*/
|
|
81
|
+
export declare function assertTokenResponse(body: Record<string, unknown>, what: Endpoint): void;
|
|
82
|
+
/**
|
|
83
|
+
* Requires a named non-empty string field on a `2xx` response body, and
|
|
84
|
+
* rejects a body that reports an OAuth2 error alongside its `200`.
|
|
85
|
+
*
|
|
86
|
+
* @param body The parsed response.
|
|
87
|
+
* @param field The field the caller cannot proceed without.
|
|
88
|
+
* @param what The endpoint's name, for error messages.
|
|
89
|
+
* @throws {OAuth2Error} when the body reports an error or lacks the field.
|
|
90
|
+
*/
|
|
91
|
+
export declare function assertField(body: Record<string, unknown>, field: string, what: Endpoint): void;
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The response ladder every {@link DirectClient} network call climbs: bounded
|
|
3
|
+
* read, deadline, redirect refusal, JSON validation, RFC 6749 §5.2 error
|
|
4
|
+
* mapping.
|
|
5
|
+
*
|
|
6
|
+
* Not part of the public API. The authorization server's bytes become either a
|
|
7
|
+
* typed {@link OAuth2Error} or a validated JSON object here and nowhere else,
|
|
8
|
+
* so the guards sit next to the tests that pin them.
|
|
9
|
+
*
|
|
10
|
+
* @module
|
|
11
|
+
*/
|
|
12
|
+
import { AccessDeniedError, AuthorizationPendingError, ExpiredTokenError, InsufficientScopeError, InvalidClientError, InvalidGrantError, InvalidRequestError, InvalidScopeError, InvalidTokenError, OAuth2Error as OAuth2ErrorClass, ServerError, SlowDownError, TemporarilyUnavailableError, UnauthorizedClientError, UnsupportedGrantTypeError, UnsupportedResponseTypeError, UnsupportedTokenTypeError, } from "../errors.js";
|
|
13
|
+
import { sanitizeProviderText } from "../utils/text.js";
|
|
14
|
+
/** Response bytes accepted from an endpoint before the call is refused. */
|
|
15
|
+
export const MAX_RESPONSE_BYTES = 64 * 1024;
|
|
16
|
+
/** Wall-clock budget for one call, covering the connect and the body read. */
|
|
17
|
+
export const REQUEST_TIMEOUT_MS = 10_000;
|
|
18
|
+
/**
|
|
19
|
+
* Maps an OAuth2 error code to the specific error class so callers can
|
|
20
|
+
* `catch (e) { if (e instanceof InvalidGrantError) … }` instead of
|
|
21
|
+
* inspecting `e.extensions.error` at every call site.
|
|
22
|
+
*/
|
|
23
|
+
const ERROR_CLASS_BY_CODE = {
|
|
24
|
+
access_denied: AccessDeniedError,
|
|
25
|
+
authorization_pending: AuthorizationPendingError,
|
|
26
|
+
expired_token: ExpiredTokenError,
|
|
27
|
+
insufficient_scope: InsufficientScopeError,
|
|
28
|
+
invalid_client: InvalidClientError,
|
|
29
|
+
invalid_grant: InvalidGrantError,
|
|
30
|
+
invalid_request: InvalidRequestError,
|
|
31
|
+
invalid_scope: InvalidScopeError,
|
|
32
|
+
invalid_token: InvalidTokenError,
|
|
33
|
+
server_error: ServerError,
|
|
34
|
+
slow_down: SlowDownError,
|
|
35
|
+
temporarily_unavailable: TemporarilyUnavailableError,
|
|
36
|
+
unauthorized_client: UnauthorizedClientError,
|
|
37
|
+
unsupported_grant_type: UnsupportedGrantTypeError,
|
|
38
|
+
unsupported_response_type: UnsupportedResponseTypeError,
|
|
39
|
+
unsupported_token_type: UnsupportedTokenTypeError,
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* Sends one request with a deadline.
|
|
43
|
+
*
|
|
44
|
+
* With the default `"refuse"` policy a `3xx` throws rather than being
|
|
45
|
+
* followed: these requests carry client credentials, an authorization code, a
|
|
46
|
+
* refresh token, or a bearer token, and replaying any of them at whatever host
|
|
47
|
+
* the response names would hand the secret to that host. Discovery passes
|
|
48
|
+
* `"follow"` — it sends no credential, and the mix-up it could otherwise
|
|
49
|
+
* enable is closed by validating the `issuer` the document reports.
|
|
50
|
+
*
|
|
51
|
+
* @param fetchImpl The client's resolved `fetch`.
|
|
52
|
+
* @param url Where to send it.
|
|
53
|
+
* @param init Request options; `redirect` and `signal` are supplied here. A
|
|
54
|
+
* caller-supplied `signal` is honoured *in addition to* the deadline.
|
|
55
|
+
* @param what The endpoint's name, for error messages.
|
|
56
|
+
* @param options `redirect` policy and `timeoutMs` deadline.
|
|
57
|
+
* @returns The response, which may still be a non-OK status.
|
|
58
|
+
* @throws {ServerError} when the request cannot be made, times out, or (under
|
|
59
|
+
* `"refuse"`) answers with a redirect.
|
|
60
|
+
*/
|
|
61
|
+
export async function sendGuarded(fetchImpl, url, init, what, options = {}) {
|
|
62
|
+
const redirect = options.redirect ?? "refuse";
|
|
63
|
+
const deadline = AbortSignal.timeout(options.timeoutMs ?? REQUEST_TIMEOUT_MS);
|
|
64
|
+
let res;
|
|
65
|
+
try {
|
|
66
|
+
res = await fetchImpl(url, {
|
|
67
|
+
...init,
|
|
68
|
+
redirect: redirect === "refuse" ? "manual" : "follow",
|
|
69
|
+
signal: init.signal ? AbortSignal.any([init.signal, deadline]) : deadline,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
throw new ServerError(`could not reach the ${what} (${describeError(error)})`, { cause: error });
|
|
74
|
+
}
|
|
75
|
+
if (redirect === "refuse" && res.status >= 300 && res.status < 400) {
|
|
76
|
+
await res.body?.cancel();
|
|
77
|
+
throw new ServerError(`the ${what} answered with a redirect (HTTP ${res.status}), which this ` +
|
|
78
|
+
`client refuses to follow — replaying the request to another host ` +
|
|
79
|
+
`would leak the credentials it carries`);
|
|
80
|
+
}
|
|
81
|
+
return res;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Reads a response as a JSON object, refusing anything else.
|
|
85
|
+
*
|
|
86
|
+
* A non-OK status becomes the typed {@link OAuth2Error} the body names, or a
|
|
87
|
+
* {@link ServerError} carrying the status when it names nothing.
|
|
88
|
+
*
|
|
89
|
+
* @param res The response from {@link sendGuarded}.
|
|
90
|
+
* @param what The endpoint's name, for error messages.
|
|
91
|
+
* @returns The parsed JSON object.
|
|
92
|
+
* @throws {OAuth2Error} on a non-OK status, an oversized body, a body that is
|
|
93
|
+
* not valid JSON, or JSON that is not an object.
|
|
94
|
+
*/
|
|
95
|
+
export async function receiveJson(res, what) {
|
|
96
|
+
const body = await readBounded(res);
|
|
97
|
+
if (!res.ok)
|
|
98
|
+
throw errorFromBody(res, safeParse(body.text), what, body.text);
|
|
99
|
+
if (body.truncated) {
|
|
100
|
+
throw new ServerError(`the ${what} response exceeded ${MAX_RESPONSE_BYTES} bytes — refusing ` +
|
|
101
|
+
`to parse a response this large`);
|
|
102
|
+
}
|
|
103
|
+
let parsed;
|
|
104
|
+
try {
|
|
105
|
+
parsed = JSON.parse(body.text);
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
throw new ServerError(`the ${what} returned ${describeMediaType(res)} that was not valid ` +
|
|
109
|
+
`JSON (${describeError(error)})`, { cause: error });
|
|
110
|
+
}
|
|
111
|
+
if (!isJsonObject(parsed)) {
|
|
112
|
+
throw new ServerError(`the ${what} returned ${describeJsonValue(parsed)} where a JSON ` +
|
|
113
|
+
`object was required`);
|
|
114
|
+
}
|
|
115
|
+
return parsed;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Turns a non-OK response into the typed error its body names. Use for
|
|
119
|
+
* endpoints whose success case carries no JSON (RFC 7009 revocation).
|
|
120
|
+
*
|
|
121
|
+
* @param res A non-OK response from {@link sendGuarded}.
|
|
122
|
+
* @param what The endpoint's name, for error messages.
|
|
123
|
+
* @returns The error to throw.
|
|
124
|
+
*/
|
|
125
|
+
export async function errorFromResponse(res, what) {
|
|
126
|
+
const body = await readBounded(res);
|
|
127
|
+
return errorFromBody(res, safeParse(body.text), what, body.text);
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Enforces RFC 6749 §5.2 on a token response that arrived with a `2xx`.
|
|
131
|
+
*
|
|
132
|
+
* A server may report a failure in the body of a `200`; and a body with no
|
|
133
|
+
* usable `access_token` is not a token response no matter what it says. Both
|
|
134
|
+
* must throw here, because the alternative is persisting a bundle whose
|
|
135
|
+
* `accessToken` is `undefined` and then sending every subsequent request with
|
|
136
|
+
* no `Authorization` header at all.
|
|
137
|
+
*
|
|
138
|
+
* @param body The parsed token-endpoint response.
|
|
139
|
+
* @param what The endpoint's name, for error messages.
|
|
140
|
+
* @throws {OAuth2Error} when the body reports an error or carries no non-empty
|
|
141
|
+
* string `access_token`.
|
|
142
|
+
*/
|
|
143
|
+
export function assertTokenResponse(body, what) {
|
|
144
|
+
const reported = reportedError(body);
|
|
145
|
+
if (reported)
|
|
146
|
+
throw buildError(reported);
|
|
147
|
+
const accessToken = body.access_token;
|
|
148
|
+
if (typeof accessToken !== "string" || accessToken.length === 0) {
|
|
149
|
+
throw new ServerError(`the ${what} returned a success response with no "access_token"`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Requires a named non-empty string field on a `2xx` response body, and
|
|
154
|
+
* rejects a body that reports an OAuth2 error alongside its `200`.
|
|
155
|
+
*
|
|
156
|
+
* @param body The parsed response.
|
|
157
|
+
* @param field The field the caller cannot proceed without.
|
|
158
|
+
* @param what The endpoint's name, for error messages.
|
|
159
|
+
* @throws {OAuth2Error} when the body reports an error or lacks the field.
|
|
160
|
+
*/
|
|
161
|
+
export function assertField(body, field, what) {
|
|
162
|
+
const reported = reportedError(body);
|
|
163
|
+
if (reported)
|
|
164
|
+
throw buildError(reported);
|
|
165
|
+
const value = body[field];
|
|
166
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
167
|
+
throw new ServerError(`the ${what} returned a success response with no "${field}"`);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Reads up to {@link MAX_RESPONSE_BYTES} and reports whether more was coming.
|
|
172
|
+
*
|
|
173
|
+
* Never throws for being too large: on a **failed** response the bytes are the
|
|
174
|
+
* only place the OAuth2 error code lives, and refusing to look would turn a
|
|
175
|
+
* `invalid_grant` that happens to arrive oversized into a generic transport
|
|
176
|
+
* error — which reads as "retry later" instead of "this grant is dead", so the
|
|
177
|
+
* client would keep presenting a revoked refresh token forever. Truncation is
|
|
178
|
+
* reported instead, and the success path refuses it.
|
|
179
|
+
*/
|
|
180
|
+
async function readBounded(res) {
|
|
181
|
+
if (!res.body)
|
|
182
|
+
return { text: "", truncated: false };
|
|
183
|
+
const reader = res.body.getReader();
|
|
184
|
+
const chunks = [];
|
|
185
|
+
let size = 0;
|
|
186
|
+
let truncated = false;
|
|
187
|
+
try {
|
|
188
|
+
while (true) {
|
|
189
|
+
const { done, value } = await reader.read();
|
|
190
|
+
if (done)
|
|
191
|
+
break;
|
|
192
|
+
if (size + value.byteLength > MAX_RESPONSE_BYTES) {
|
|
193
|
+
const room = MAX_RESPONSE_BYTES - size;
|
|
194
|
+
if (room > 0) {
|
|
195
|
+
chunks.push(value.subarray(0, room));
|
|
196
|
+
size += room;
|
|
197
|
+
}
|
|
198
|
+
truncated = true;
|
|
199
|
+
break;
|
|
200
|
+
}
|
|
201
|
+
chunks.push(value);
|
|
202
|
+
size += value.byteLength;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
catch {
|
|
206
|
+
return { text: decode(chunks, size), truncated: true };
|
|
207
|
+
}
|
|
208
|
+
finally {
|
|
209
|
+
await reader.cancel().catch(() => { });
|
|
210
|
+
}
|
|
211
|
+
return { text: decode(chunks, size), truncated };
|
|
212
|
+
}
|
|
213
|
+
function decode(chunks, size) {
|
|
214
|
+
const buffer = new Uint8Array(size);
|
|
215
|
+
let offset = 0;
|
|
216
|
+
for (const chunk of chunks) {
|
|
217
|
+
buffer.set(chunk, offset);
|
|
218
|
+
offset += chunk.byteLength;
|
|
219
|
+
}
|
|
220
|
+
return new TextDecoder().decode(buffer);
|
|
221
|
+
}
|
|
222
|
+
function reportedError(value) {
|
|
223
|
+
if (!isJsonObject(value))
|
|
224
|
+
return null;
|
|
225
|
+
const error = value.error;
|
|
226
|
+
if (typeof error !== "string" || error.length === 0)
|
|
227
|
+
return null;
|
|
228
|
+
return {
|
|
229
|
+
error: sanitizeProviderText(error),
|
|
230
|
+
description: typeof value.error_description === "string"
|
|
231
|
+
? sanitizeProviderText(value.error_description)
|
|
232
|
+
: undefined,
|
|
233
|
+
uri: typeof value.error_uri === "string"
|
|
234
|
+
? sanitizeProviderText(value.error_uri)
|
|
235
|
+
: undefined,
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* `status` is passed through only when it is a real error status: a server
|
|
240
|
+
* that reports `error` inside a `200` still needs a throwable, and `HttpError`
|
|
241
|
+
* refuses to carry a 2xx.
|
|
242
|
+
*/
|
|
243
|
+
function buildError(reported, status) {
|
|
244
|
+
const ErrorClass = ERROR_CLASS_BY_CODE[reported.error] ?? OAuth2ErrorClass;
|
|
245
|
+
return new ErrorClass(reported.description ?? reported.error, {
|
|
246
|
+
status: status !== undefined && status >= 400 ? status : undefined,
|
|
247
|
+
extensions: {
|
|
248
|
+
error: reported.error,
|
|
249
|
+
error_description: reported.description,
|
|
250
|
+
error_uri: reported.uri,
|
|
251
|
+
},
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Recovers the `error` code from a body that did not parse.
|
|
256
|
+
*
|
|
257
|
+
* The realistic reason a failed token response is unparseable here is that the
|
|
258
|
+
* cap cut it mid-string — a server padding `error_description`, or a proxy
|
|
259
|
+
* appending a page of HTML. The code itself is short and near the front, and
|
|
260
|
+
* losing it costs the caller the difference between "this grant is dead" and
|
|
261
|
+
* "something went wrong", so it is worth one bounded scan. Restricted to the
|
|
262
|
+
* lowercase-underscore shape RFC 6749 §5.2 defines, so nothing else matches.
|
|
263
|
+
*/
|
|
264
|
+
function scanErrorCode(text) {
|
|
265
|
+
const match = /"error"\s*:\s*"([a-z_]{1,64})"/.exec(text);
|
|
266
|
+
return match ? { error: match[1] } : null;
|
|
267
|
+
}
|
|
268
|
+
function errorFromBody(res, parsed, what, raw = "") {
|
|
269
|
+
const reported = reportedError(parsed) ?? scanErrorCode(raw);
|
|
270
|
+
if (reported)
|
|
271
|
+
return buildError(reported, res.status);
|
|
272
|
+
return new ServerError(`${what}: HTTP ${res.status}`, res.status >= 400 ? { status: res.status } : undefined);
|
|
273
|
+
}
|
|
274
|
+
function safeParse(text) {
|
|
275
|
+
try {
|
|
276
|
+
return JSON.parse(text);
|
|
277
|
+
}
|
|
278
|
+
catch {
|
|
279
|
+
return undefined;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
function isJsonObject(value) {
|
|
283
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
284
|
+
}
|
|
285
|
+
function describeJsonValue(value) {
|
|
286
|
+
if (value === null)
|
|
287
|
+
return "JSON null";
|
|
288
|
+
if (Array.isArray(value))
|
|
289
|
+
return "a JSON array";
|
|
290
|
+
return `a JSON ${typeof value}`;
|
|
291
|
+
}
|
|
292
|
+
function describeMediaType(res) {
|
|
293
|
+
const contentType = sanitizeProviderText(res.headers.get("content-type")?.split(";")[0] ?? "");
|
|
294
|
+
return contentType ? `a "${contentType}" response` : "a response";
|
|
295
|
+
}
|
|
296
|
+
function describeError(error) {
|
|
297
|
+
if (error instanceof Error) {
|
|
298
|
+
if (error.name === "TimeoutError")
|
|
299
|
+
return "timed out";
|
|
300
|
+
return sanitizeProviderText(error.message);
|
|
301
|
+
}
|
|
302
|
+
return sanitizeProviderText(String(error));
|
|
303
|
+
}
|