@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,1304 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Authorization Server implementation for issuing OAuth2 tokens.
|
|
3
|
+
*
|
|
4
|
+
* Implements the OAuth 2.0 Authorization Framework (RFC 6749) with support for:
|
|
5
|
+
* - Token endpoint (RFC 6749 Section 3.2)
|
|
6
|
+
* - Authorization endpoint (RFC 6749 Section 3.1)
|
|
7
|
+
* - Token revocation (RFC 7009)
|
|
8
|
+
* - Token introspection (RFC 7662)
|
|
9
|
+
* - Server metadata (RFC 8414)
|
|
10
|
+
* - Device authorization (RFC 8628)
|
|
11
|
+
*
|
|
12
|
+
* **Configuration.** A single instance serves every tenant/origin. The only
|
|
13
|
+
* configuration seam is `resolve(request)`, which returns the services the
|
|
14
|
+
* server's own endpoints need (`{ clientService, tokenService }`) plus the
|
|
15
|
+
* issuer/endpoints. Each grant resolves its own services independently. For a
|
|
16
|
+
* single-tenant server, `resolve` is a constant function (create the services
|
|
17
|
+
* once, outside it, and return them every time).
|
|
18
|
+
*
|
|
19
|
+
* @see https://datatracker.ietf.org/doc/html/rfc6749
|
|
20
|
+
* @module
|
|
21
|
+
*/
|
|
22
|
+
import "../_dnt.polyfills.js";
|
|
23
|
+
import { snapshotAuthenticationContext } from "../utils/authentication-context.js";
|
|
24
|
+
import { AccessDeniedError, InvalidRequestError, InvalidTokenError, isOAuth2Error, OAuth2Error, ServerError, UnauthorizedClientError, UnsupportedGrantTypeError, UnsupportedResponseTypeError, } from "../errors.js";
|
|
25
|
+
import { authenticateClientCredentials, extractClientCredentials, } from "./client-authentication.js";
|
|
26
|
+
import { isRedirectUriPattern, matchRedirectUri } from "./redirect-uri.js";
|
|
27
|
+
import { AuthorizationCodeGrant } from "./grants/authorization-code.js";
|
|
28
|
+
import { DeviceAuthorizationGrant } from "./grants/device-authorization.js";
|
|
29
|
+
import { ResourceServer, } from "./resource-server.js";
|
|
30
|
+
import { defaultSubjectOf, signJwt, verifyJwt, } from "./signing-keys.js";
|
|
31
|
+
import { validateCodeChallenge } from "../utils/pkce.js";
|
|
32
|
+
const ENDPOINTS = {
|
|
33
|
+
authorization: {
|
|
34
|
+
option: "authorizationEndpoint",
|
|
35
|
+
path: "/authorize",
|
|
36
|
+
metadata: "authorization_endpoint",
|
|
37
|
+
methods: ["GET"],
|
|
38
|
+
oidc: false,
|
|
39
|
+
},
|
|
40
|
+
token: {
|
|
41
|
+
option: "tokenEndpoint",
|
|
42
|
+
path: "/token",
|
|
43
|
+
metadata: "token_endpoint",
|
|
44
|
+
methods: ["POST"],
|
|
45
|
+
oidc: false,
|
|
46
|
+
},
|
|
47
|
+
revocation: {
|
|
48
|
+
option: "revocationEndpoint",
|
|
49
|
+
path: "/revoke",
|
|
50
|
+
metadata: "revocation_endpoint",
|
|
51
|
+
methods: ["POST"],
|
|
52
|
+
oidc: false,
|
|
53
|
+
},
|
|
54
|
+
introspection: {
|
|
55
|
+
option: "introspectionEndpoint",
|
|
56
|
+
path: "/introspect",
|
|
57
|
+
metadata: "introspection_endpoint",
|
|
58
|
+
methods: ["POST"],
|
|
59
|
+
oidc: false,
|
|
60
|
+
},
|
|
61
|
+
deviceAuthorization: {
|
|
62
|
+
option: "deviceAuthorizationEndpoint",
|
|
63
|
+
path: "/device_authorization",
|
|
64
|
+
metadata: "device_authorization_endpoint",
|
|
65
|
+
methods: ["POST"],
|
|
66
|
+
oidc: false,
|
|
67
|
+
},
|
|
68
|
+
jwks: {
|
|
69
|
+
option: "jwksEndpoint",
|
|
70
|
+
path: "/jwks",
|
|
71
|
+
metadata: "jwks_uri",
|
|
72
|
+
methods: ["GET"],
|
|
73
|
+
oidc: true,
|
|
74
|
+
},
|
|
75
|
+
userinfo: {
|
|
76
|
+
option: "userinfoEndpoint",
|
|
77
|
+
path: "/userinfo",
|
|
78
|
+
metadata: "userinfo_endpoint",
|
|
79
|
+
methods: ["GET", "POST"],
|
|
80
|
+
oidc: true,
|
|
81
|
+
},
|
|
82
|
+
endSession: {
|
|
83
|
+
option: "endSessionEndpoint",
|
|
84
|
+
path: "/end_session",
|
|
85
|
+
metadata: "end_session_endpoint",
|
|
86
|
+
methods: ["GET", "POST"],
|
|
87
|
+
oidc: true,
|
|
88
|
+
},
|
|
89
|
+
metadata: {
|
|
90
|
+
path: "/.well-known/oauth-authorization-server",
|
|
91
|
+
methods: ["GET"],
|
|
92
|
+
},
|
|
93
|
+
oidcMetadata: {
|
|
94
|
+
path: "/.well-known/openid-configuration",
|
|
95
|
+
methods: ["GET"],
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
const _endpointKeysInSync = true;
|
|
99
|
+
const ENDPOINT_ENTRIES = Object.entries(ENDPOINTS);
|
|
100
|
+
const ADVERTISED_ENTRIES = ENDPOINT_ENTRIES.filter((entry) => "metadata" in entry[1]);
|
|
101
|
+
const DEVICE_AUTHORIZATION_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:device_code";
|
|
102
|
+
const AUTHORIZATION_CODE_GRANT_TYPE = "authorization_code";
|
|
103
|
+
/**
|
|
104
|
+
* The path each endpoint is served from when the request context supplies only
|
|
105
|
+
* an `issuer` (so the endpoint URL is `${issuer}${path}`). Mount handlers at
|
|
106
|
+
* these paths — as the Hono adapter's `routes()` does — to match what the
|
|
107
|
+
* server advertises in its RFC 8414 metadata.
|
|
108
|
+
*
|
|
109
|
+
* Covers the whole served surface: the seven endpoints the server advertises,
|
|
110
|
+
* plus the two discovery documents (`metadata` for RFC 8414,
|
|
111
|
+
* `oidcMetadata` for OIDC Discovery) that are served but never advertised.
|
|
112
|
+
* Pair it with {@link ENDPOINT_METHODS} to enumerate path *and* verbs.
|
|
113
|
+
*
|
|
114
|
+
* @example Mount every endpoint yourself
|
|
115
|
+
* ```ts
|
|
116
|
+
* for (const key of Object.keys(ENDPOINT_PATHS) as (keyof typeof ENDPOINT_PATHS)[]) {
|
|
117
|
+
* app.on([...ENDPOINT_METHODS[key]], ENDPOINT_PATHS[key], handlerFor(key));
|
|
118
|
+
* }
|
|
119
|
+
* ```
|
|
120
|
+
*/
|
|
121
|
+
export const ENDPOINT_PATHS = Object
|
|
122
|
+
.fromEntries(ENDPOINT_ENTRIES.map(([key, { path }]) => [key, path]));
|
|
123
|
+
/**
|
|
124
|
+
* The HTTP methods each endpoint in {@link ENDPOINT_PATHS} answers — `POST` for
|
|
125
|
+
* the token-style endpoints, `GET` for authorize/JWKS/discovery, both for
|
|
126
|
+
* UserInfo. Read it instead of re-deriving verbs per endpoint, so a hand-rolled
|
|
127
|
+
* mount answers exactly what the built-in `routes()` does.
|
|
128
|
+
*
|
|
129
|
+
* @example
|
|
130
|
+
* ```ts
|
|
131
|
+
* app.on([...ENDPOINT_METHODS.userinfo], ENDPOINT_PATHS.userinfo, handler);
|
|
132
|
+
* ```
|
|
133
|
+
*/
|
|
134
|
+
export const ENDPOINT_METHODS = Object.fromEntries(ENDPOINT_ENTRIES.map(([key, { methods }]) => [key, methods]));
|
|
135
|
+
function isAuthorizationCodeGrant(grant) {
|
|
136
|
+
return grant instanceof AuthorizationCodeGrant;
|
|
137
|
+
}
|
|
138
|
+
function isDeviceAuthorizationGrant(grant) {
|
|
139
|
+
return grant instanceof DeviceAuthorizationGrant;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Authorization Server for issuing OAuth2 tokens.
|
|
143
|
+
*
|
|
144
|
+
* Handles OAuth2 endpoints including:
|
|
145
|
+
* - Token endpoint (POST /token)
|
|
146
|
+
* - Authorization endpoint (GET /authorize)
|
|
147
|
+
* - Revocation endpoint (POST /revoke) - RFC 7009
|
|
148
|
+
* - Introspection endpoint (POST /introspect) - RFC 7662
|
|
149
|
+
* - Device authorization endpoint (POST /device_authorization) - RFC 8628
|
|
150
|
+
* - Metadata endpoint (GET /.well-known/oauth-authorization-server) - RFC 8414
|
|
151
|
+
*
|
|
152
|
+
* Framework-agnostic: each `handle*Request` method takes a web `Request` and
|
|
153
|
+
* returns a `Response`, so an adapter routes its endpoints to them. Extends
|
|
154
|
+
* {@linkcode ResourceServer}, so the same instance also validates bearer
|
|
155
|
+
* tokens. A single instance serves every tenant/origin via the `resolve`
|
|
156
|
+
* callback in {@linkcode AuthorizationServerOptions}.
|
|
157
|
+
*
|
|
158
|
+
* The handlers throw {@linkcode HttpError} subclasses on failure (e.g.
|
|
159
|
+
* {@linkcode InvalidRequestError}, {@linkcode InvalidClientError},
|
|
160
|
+
* {@linkcode UnsupportedGrantTypeError}) and convert them to RFC 6749 §5.2
|
|
161
|
+
* error responses unless `throwOnError` is set.
|
|
162
|
+
*
|
|
163
|
+
* @template Client The client entity type.
|
|
164
|
+
* @template User The resource-owner entity type.
|
|
165
|
+
* @template S The scope type used across grants and metadata.
|
|
166
|
+
*
|
|
167
|
+
* @see https://datatracker.ietf.org/doc/html/rfc6749
|
|
168
|
+
*
|
|
169
|
+
* @example
|
|
170
|
+
* ```ts
|
|
171
|
+
* const server = new AuthorizationServer({
|
|
172
|
+
* resolve: () => ({ services, issuer: "https://auth.example.com" }),
|
|
173
|
+
* grants: { authorization_code: authCodeGrant, refresh_token: refreshGrant },
|
|
174
|
+
* });
|
|
175
|
+
*
|
|
176
|
+
* app.post("/token", (c) => server.handleTokenRequest(c.req.raw));
|
|
177
|
+
* ```
|
|
178
|
+
*/
|
|
179
|
+
export class AuthorizationServer extends ResourceServer {
|
|
180
|
+
/** Grants the server supports, keyed by `grant_type`. */
|
|
181
|
+
grants;
|
|
182
|
+
/** Scopes advertised in RFC 8414 metadata, if configured. */
|
|
183
|
+
scopesSupported;
|
|
184
|
+
/** OIDC signing keys; the OIDC surface is active when set. */
|
|
185
|
+
signingKeys;
|
|
186
|
+
#endSession;
|
|
187
|
+
#isPublicSuffix;
|
|
188
|
+
#subjectOf;
|
|
189
|
+
#userClaims;
|
|
190
|
+
#introspectionClaims;
|
|
191
|
+
#canIntrospectToken;
|
|
192
|
+
/** Resolves the per-request {@link AuthorizationServerContext}. */
|
|
193
|
+
#resolve;
|
|
194
|
+
/**
|
|
195
|
+
* Creates an authorization server from the given
|
|
196
|
+
* {@linkcode AuthorizationServerOptions}.
|
|
197
|
+
*
|
|
198
|
+
* @throws {Error} If any grant's map key does not equal its `grantType`.
|
|
199
|
+
* @throws {Error} If `requireOidc` is set without `signingKeys`.
|
|
200
|
+
*/
|
|
201
|
+
constructor(options) {
|
|
202
|
+
super({
|
|
203
|
+
resolve: options.resolve,
|
|
204
|
+
Scope: options.Scope,
|
|
205
|
+
realm: options.realm,
|
|
206
|
+
errorFormat: options.errorFormat,
|
|
207
|
+
throwOnError: options.throwOnError,
|
|
208
|
+
});
|
|
209
|
+
this.#resolve = options.resolve;
|
|
210
|
+
this.grants = { ...options.grants };
|
|
211
|
+
for (const [grantType, grant] of Object.entries(this.grants)) {
|
|
212
|
+
if (grant.grantType !== grantType) {
|
|
213
|
+
throw new Error(`grant registered under key "${grantType}" has grantType ` +
|
|
214
|
+
`"${grant.grantType}"; the map key must equal grant.grantType`);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
this.scopesSupported = options.scopesSupported;
|
|
218
|
+
this.signingKeys = options.signingKeys;
|
|
219
|
+
this.#endSession = options.endSession;
|
|
220
|
+
if (options.requireOidc && !this.signingKeys) {
|
|
221
|
+
throw new Error("requireOidc is set but no signingKeys provider was configured, so " +
|
|
222
|
+
"OIDC issuance is off: this instance would mint no id_token and " +
|
|
223
|
+
"answer 404 from /.well-known/openid-configuration, /jwks, and " +
|
|
224
|
+
"/userinfo. Load the signing key (importSigningKeyJwk) and pass it " +
|
|
225
|
+
"as the signingKeys option, or unset requireOidc if this server is " +
|
|
226
|
+
"plain OAuth2.");
|
|
227
|
+
}
|
|
228
|
+
this.#isPublicSuffix = options.isPublicSuffix;
|
|
229
|
+
this.#subjectOf = options.subjectOf ?? defaultSubjectOf;
|
|
230
|
+
this.#userClaims = options.userClaims;
|
|
231
|
+
this.#introspectionClaims = options.introspectionClaims;
|
|
232
|
+
this.#canIntrospectToken = options.canIntrospectToken;
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Whether this instance will actually serve the OIDC surface: `true` once
|
|
236
|
+
* `signingKeys` is set, which is what gates `id_token` issuance and the
|
|
237
|
+
* `/jwks`, `/userinfo`, and `openid-configuration` endpoints (they answer
|
|
238
|
+
* 404 while it is `false`).
|
|
239
|
+
*
|
|
240
|
+
* Read it in a readiness probe to keep an instance that booted without its
|
|
241
|
+
* signing key out of the load balancer instead of letting it 404 relying
|
|
242
|
+
* parties. It reports the provider this server configured — not that the
|
|
243
|
+
* provider can produce a key, which surfaces on first use — and it tracks
|
|
244
|
+
* the `signingKeys` field, so it is meaningful from construction onward and
|
|
245
|
+
* follows any later reassignment.
|
|
246
|
+
*/
|
|
247
|
+
get oidcEnabled() {
|
|
248
|
+
return this.signingKeys !== undefined;
|
|
249
|
+
}
|
|
250
|
+
/** Resolves the per-request {@link AuthorizationServerContext}. */
|
|
251
|
+
async authorizationContext(request) {
|
|
252
|
+
return await this.#resolve(request);
|
|
253
|
+
}
|
|
254
|
+
/** Resolves the endpoint URLs for a request (issuer-derived unless set). */
|
|
255
|
+
#resolvedEndpoints(context) {
|
|
256
|
+
const { issuer } = context;
|
|
257
|
+
const endpoints = {};
|
|
258
|
+
for (const [key, { option, path }] of ADVERTISED_ENTRIES) {
|
|
259
|
+
endpoints[key] = context[option] ??
|
|
260
|
+
(issuer ? `${issuer}${path}` : undefined);
|
|
261
|
+
}
|
|
262
|
+
return endpoints;
|
|
263
|
+
}
|
|
264
|
+
/** The resolved endpoint URLs for a request. Used by {@link localAuthServerFetch}. */
|
|
265
|
+
async resolveEndpoints(request) {
|
|
266
|
+
return this.#resolvedEndpoints(await this.authorizationContext(request));
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Converts a token to a bearer token response body.
|
|
270
|
+
*/
|
|
271
|
+
bearerToken(token, services) {
|
|
272
|
+
const bearerToken = {
|
|
273
|
+
token_type: "Bearer",
|
|
274
|
+
access_token: token.accessToken,
|
|
275
|
+
};
|
|
276
|
+
const { tokenService } = services;
|
|
277
|
+
if (token.accessTokenExpiresAt) {
|
|
278
|
+
bearerToken.expires_in = Math.round((token.accessTokenExpiresAt.getTime() - Date.now()) / 1000);
|
|
279
|
+
}
|
|
280
|
+
else if (tokenService) {
|
|
281
|
+
bearerToken.expires_in = tokenService.accessTokenLifetime;
|
|
282
|
+
}
|
|
283
|
+
if ("refreshToken" in token && token.refreshToken) {
|
|
284
|
+
bearerToken.refresh_token = token.refreshToken;
|
|
285
|
+
}
|
|
286
|
+
if (token.scope) {
|
|
287
|
+
bearerToken.scope = token.scope.toString();
|
|
288
|
+
}
|
|
289
|
+
return bearerToken;
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* Creates a token response. When the issuing `grant` is passed, an id_token
|
|
293
|
+
* is only minted for grants that represent an end-user authentication
|
|
294
|
+
* (`grant.issuesIdToken !== false`).
|
|
295
|
+
*/
|
|
296
|
+
async createTokenResponse(token, context, grant) {
|
|
297
|
+
const body = this.bearerToken(token, context.services);
|
|
298
|
+
if (grant?.issuesIdToken !== false) {
|
|
299
|
+
const idToken = await this.mintIdToken(token, context);
|
|
300
|
+
if (idToken)
|
|
301
|
+
body.id_token = idToken;
|
|
302
|
+
}
|
|
303
|
+
return new Response(JSON.stringify(body), {
|
|
304
|
+
status: 200,
|
|
305
|
+
headers: {
|
|
306
|
+
"Content-Type": "application/json;charset=UTF-8",
|
|
307
|
+
"Cache-Control": "no-store",
|
|
308
|
+
"Pragma": "no-cache",
|
|
309
|
+
},
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* Applies `Cache-Control: no-store` and `Pragma: no-cache` headers to an
|
|
314
|
+
* error per RFC 6749 Section 5.2. The headers land on both the rethrown
|
|
315
|
+
* error (when {@link throwOnError} is true) and the generated response.
|
|
316
|
+
*/
|
|
317
|
+
addCacheHeaders(error) {
|
|
318
|
+
error.headers.set("Cache-Control", "no-store");
|
|
319
|
+
error.headers.set("Pragma", "no-cache");
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* Error-prepare callback for the endpoints that authenticate the client
|
|
323
|
+
* (token, revocation, introspection, device authorization). Attaches the
|
|
324
|
+
* RFC 6749 Section 5.2 cache headers, and — when client authentication was
|
|
325
|
+
* attempted via the `Authorization` header and failed with a 401 — the
|
|
326
|
+
* matching `WWW-Authenticate: Basic` challenge that RFC 6749 Section 5.2
|
|
327
|
+
* requires.
|
|
328
|
+
*/
|
|
329
|
+
prepareClientAuthError(request) {
|
|
330
|
+
return (error) => {
|
|
331
|
+
this.addCacheHeaders(error);
|
|
332
|
+
if (error.status === 401 && request.headers.get("authorization")) {
|
|
333
|
+
error.headers.set("WWW-Authenticate", `Basic realm="${this.realm}"`);
|
|
334
|
+
}
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* Rejects a client that is not registered for a grant type
|
|
339
|
+
* (RFC 6749 Section 5.2 `unauthorized_client`).
|
|
340
|
+
*/
|
|
341
|
+
#assertGrantAuthorized(client, grantType) {
|
|
342
|
+
if (!client.grants?.includes(grantType)) {
|
|
343
|
+
throw new UnauthorizedClientError(`client is not authorized to use the ${grantType} grant type`);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
/** The grant a token request's `grant_type` dispatches to. */
|
|
347
|
+
#grantFromBody(body) {
|
|
348
|
+
const grantType = body.get("grant_type");
|
|
349
|
+
if (typeof grantType !== "string") {
|
|
350
|
+
throw new InvalidRequestError("grant_type parameter required");
|
|
351
|
+
}
|
|
352
|
+
const grant = this.grants[grantType];
|
|
353
|
+
if (!grant) {
|
|
354
|
+
throw new UnsupportedGrantTypeError("invalid grant_type");
|
|
355
|
+
}
|
|
356
|
+
return grant;
|
|
357
|
+
}
|
|
358
|
+
async #beginClientAuthenticatedRequest(request, options = {}) {
|
|
359
|
+
if (request.method !== "POST") {
|
|
360
|
+
throw new InvalidRequestError("method must be POST");
|
|
361
|
+
}
|
|
362
|
+
const contentType = request.headers.get("content-type");
|
|
363
|
+
if (!contentType?.includes("application/x-www-form-urlencoded")) {
|
|
364
|
+
throw new InvalidRequestError("content-type header must be application/x-www-form-urlencoded");
|
|
365
|
+
}
|
|
366
|
+
const context = await this.authorizationContext(request);
|
|
367
|
+
const body = await request.clone().formData().catch(() => {
|
|
368
|
+
throw new InvalidRequestError("body must be application/x-www-form-urlencoded");
|
|
369
|
+
});
|
|
370
|
+
const grant = options.grantFromBody?.(body);
|
|
371
|
+
const client = grant
|
|
372
|
+
? await grant.getAuthenticatedClient(request, body)
|
|
373
|
+
: await this.authenticateClient(request, body, context.services);
|
|
374
|
+
const grantType = grant?.grantType ?? options.requiredGrantType;
|
|
375
|
+
if (grantType)
|
|
376
|
+
this.#assertGrantAuthorized(client, grantType);
|
|
377
|
+
return { context, body, client, grant };
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* Handles a token request (POST /token).
|
|
381
|
+
* Supports multiple grant types based on the configured grants.
|
|
382
|
+
*
|
|
383
|
+
* A malformed request — wrong method, a content-type other than
|
|
384
|
+
* `application/x-www-form-urlencoded`, or an unsupported grant — is a 400
|
|
385
|
+
* `invalid_request`.
|
|
386
|
+
*/
|
|
387
|
+
async handleTokenRequest(request) {
|
|
388
|
+
try {
|
|
389
|
+
const { context, body, client, grant } = await this
|
|
390
|
+
.#beginClientAuthenticatedRequest(request, {
|
|
391
|
+
grantFromBody: (body) => this.#grantFromBody(body),
|
|
392
|
+
});
|
|
393
|
+
const token = await grant.token(request, client, body);
|
|
394
|
+
return await this.createTokenResponse(token, context, grant);
|
|
395
|
+
}
|
|
396
|
+
catch (error) {
|
|
397
|
+
return this.handleError(error, this.prepareClientAuthError(request));
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
/**
|
|
401
|
+
* Mint an OIDC id_token for a user-bound token whose scope includes
|
|
402
|
+
* `openid`, or `undefined` when the OIDC surface is off (no signing keys),
|
|
403
|
+
* the token has no user, the scope lacks `openid`, or no issuer resolved.
|
|
404
|
+
*/
|
|
405
|
+
async mintIdToken(token, context) {
|
|
406
|
+
if (!this.signingKeys || !token.user || !context.issuer)
|
|
407
|
+
return undefined;
|
|
408
|
+
if (!token.scope?.has("openid"))
|
|
409
|
+
return undefined;
|
|
410
|
+
const key = await this.signingKeys.getSigningKey();
|
|
411
|
+
const now = Math.floor(Date.now() / 1000);
|
|
412
|
+
const claims = {
|
|
413
|
+
...(await this.#oidcClaims(token.user, token.scope, token.authenticationContext)),
|
|
414
|
+
iss: context.issuer,
|
|
415
|
+
aud: token.client.id,
|
|
416
|
+
iat: now,
|
|
417
|
+
exp: now + 3600,
|
|
418
|
+
};
|
|
419
|
+
if (token.nonce)
|
|
420
|
+
claims.nonce = token.nonce;
|
|
421
|
+
return await signJwt(key, claims);
|
|
422
|
+
}
|
|
423
|
+
/** OIDC claims released for a user: `userClaims` under the winning `sub`. */
|
|
424
|
+
async #oidcClaims(user, scope, authenticationContext) {
|
|
425
|
+
return {
|
|
426
|
+
...(await this.#userClaims?.(user, scope, snapshotAuthenticationContext(authenticationContext)) ?? {}),
|
|
427
|
+
sub: this.#subjectOf(user),
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
/**
|
|
431
|
+
* Handles an OIDC discovery request (GET /.well-known/openid-configuration):
|
|
432
|
+
* the RFC 8414 metadata document including the OIDC fields. 404 when OIDC
|
|
433
|
+
* issuance is off (no signing keys), so relying parties fail fast instead of
|
|
434
|
+
* reading a document missing its REQUIRED OIDC members.
|
|
435
|
+
*/
|
|
436
|
+
async handleOidcMetadataRequest(request) {
|
|
437
|
+
if (!this.signingKeys) {
|
|
438
|
+
return this.handleError(new OAuth2Error(404, "OIDC issuance is not enabled", {
|
|
439
|
+
extensions: { error: "not_found" },
|
|
440
|
+
}));
|
|
441
|
+
}
|
|
442
|
+
return await this.handleMetadataRequest(request);
|
|
443
|
+
}
|
|
444
|
+
/**
|
|
445
|
+
* Handles a JWKS request (GET /jwks): the public signing keys id_tokens and
|
|
446
|
+
* JWT access tokens verify against. 404 when OIDC issuance is off.
|
|
447
|
+
*/
|
|
448
|
+
async handleJwksRequest(_request) {
|
|
449
|
+
if (!this.signingKeys) {
|
|
450
|
+
return this.handleError(new OAuth2Error(404, "OIDC issuance is not enabled", {
|
|
451
|
+
extensions: { error: "not_found" },
|
|
452
|
+
}));
|
|
453
|
+
}
|
|
454
|
+
try {
|
|
455
|
+
const jwks = await this.signingKeys.getPublicJwks();
|
|
456
|
+
return new Response(JSON.stringify(jwks), {
|
|
457
|
+
status: 200,
|
|
458
|
+
headers: {
|
|
459
|
+
"Content-Type": "application/json;charset=UTF-8",
|
|
460
|
+
"Cache-Control": "public, max-age=300",
|
|
461
|
+
},
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
catch (error) {
|
|
465
|
+
return this.handleError(error);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
/**
|
|
469
|
+
* Handles an OIDC UserInfo request (GET/POST /userinfo): validates the
|
|
470
|
+
* bearer token (which must carry the `openid` scope and a user) and returns
|
|
471
|
+
* `sub` plus any configured `userClaims`.
|
|
472
|
+
*
|
|
473
|
+
* Failures follow RFC 6750 Section 3.1, which OIDC Core Section 5.3.3 defers
|
|
474
|
+
* to: a missing, malformed, or expired token is a 401 (`invalid_token`,
|
|
475
|
+
* omitted from the challenge when no credentials were presented at all),
|
|
476
|
+
* while a **valid** token that simply lacks the `openid` scope is a 403
|
|
477
|
+
* `insufficient_scope` whose challenge names `scope="openid"`. The
|
|
478
|
+
* distinction matters to relying parties — a 401 tells an RP its credential
|
|
479
|
+
* is bad, so answering one for a scope problem sends it to refresh a token
|
|
480
|
+
* that will fail again the same way.
|
|
481
|
+
*/
|
|
482
|
+
async handleUserInfoRequest(request) {
|
|
483
|
+
try {
|
|
484
|
+
const { user, scope, token } = await this.authenticate(request);
|
|
485
|
+
this.assertScope(scope, "openid");
|
|
486
|
+
if (!user) {
|
|
487
|
+
throw new InvalidTokenError("access token has no resource owner");
|
|
488
|
+
}
|
|
489
|
+
const claims = await this.#oidcClaims(user, scope, token.authenticationContext);
|
|
490
|
+
return new Response(JSON.stringify(claims), {
|
|
491
|
+
status: 200,
|
|
492
|
+
headers: {
|
|
493
|
+
"Content-Type": "application/json;charset=UTF-8",
|
|
494
|
+
"Cache-Control": "no-store",
|
|
495
|
+
},
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
catch (error) {
|
|
499
|
+
return this.handleAuthError(error);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
/**
|
|
503
|
+
* Handles an OIDC RP-Initiated Logout request (GET/POST /end_session).
|
|
504
|
+
*
|
|
505
|
+
* The server does not know what a session **is** — that is the app's — so
|
|
506
|
+
* `endSession` does the ending and this method does the protocol around it:
|
|
507
|
+
* identifying the relying party, authorizing the return trip, and shaping
|
|
508
|
+
* the response. `endSession` is always called, even when nothing else about
|
|
509
|
+
* the request checks out, because a logout that refuses to log anyone out is
|
|
510
|
+
* the worst possible failure mode for this endpoint.
|
|
511
|
+
*
|
|
512
|
+
* **Authorizing `post_logout_redirect_uri` is the whole security surface.**
|
|
513
|
+
* It is matched by **exact string** against the resolved client's
|
|
514
|
+
* {@link ClientInterface.postLogoutRedirectUris} — no patterns, no prefixes,
|
|
515
|
+
* unlike the authorization endpoint's redirect matching. A logout redirect
|
|
516
|
+
* carries no code to steal, but an unvalidated one is an open redirect on the
|
|
517
|
+
* IDP's own domain, which is a phishing primitive: the victim really did just
|
|
518
|
+
* come from your login page. When no client resolves, or the URI is not
|
|
519
|
+
* registered, the browser is **not** sent there — it goes to `fallback` if
|
|
520
|
+
* the app supplied one, or gets a bare 204.
|
|
521
|
+
*
|
|
522
|
+
* `id_token_hint` is verified against the signing keys but its expiry is
|
|
523
|
+
* ignored: by the time anyone logs out the id_token naming their session has
|
|
524
|
+
* usually expired, and it is a hint, not authority. `client_id` is accepted
|
|
525
|
+
* as the fallback identifier the spec allows. `state` rides along only to a
|
|
526
|
+
* URI that passed authorization.
|
|
527
|
+
*
|
|
528
|
+
* Wire the app's session teardown through the `endSession` **constructor
|
|
529
|
+
* option**; this method answers 404 without it, and the discovery document
|
|
530
|
+
* omits `end_session_endpoint` to match.
|
|
531
|
+
*
|
|
532
|
+
* @example
|
|
533
|
+
* ```ts
|
|
534
|
+
* const server = new AuthorizationServer({
|
|
535
|
+
* resolve,
|
|
536
|
+
* grants,
|
|
537
|
+
* signingKeys,
|
|
538
|
+
* endSession: async ({ subject }) => {
|
|
539
|
+
* if (subject) await sessions.deleteByUserId(subject);
|
|
540
|
+
* return { headers: { "Set-Cookie": clearSessionCookie() }, fallback: "/" };
|
|
541
|
+
* },
|
|
542
|
+
* });
|
|
543
|
+
* app.all("/end_session", (c) => server.handleEndSessionRequest(c.req.raw));
|
|
544
|
+
* ```
|
|
545
|
+
*/
|
|
546
|
+
async handleEndSessionRequest(request) {
|
|
547
|
+
const endSession = this.#endSession;
|
|
548
|
+
if (!endSession)
|
|
549
|
+
return new Response(null, { status: 404 });
|
|
550
|
+
try {
|
|
551
|
+
const params = await this.#endSessionParameters(request);
|
|
552
|
+
const client = await this.#resolveEndSessionClient(request, params);
|
|
553
|
+
const result = await endSession({
|
|
554
|
+
request,
|
|
555
|
+
client,
|
|
556
|
+
subject: params.subject,
|
|
557
|
+
logoutHint: params.logoutHint,
|
|
558
|
+
}) ?? {};
|
|
559
|
+
const headers = new Headers(result.headers);
|
|
560
|
+
const authorized = this.#postLogoutRedirect(client, params);
|
|
561
|
+
const target = authorized ?? result.fallback;
|
|
562
|
+
if (!target)
|
|
563
|
+
return new Response(null, { status: 204, headers });
|
|
564
|
+
headers.set("Location", target);
|
|
565
|
+
return new Response(null, { status: 302, headers });
|
|
566
|
+
}
|
|
567
|
+
catch (error) {
|
|
568
|
+
return this.handleError(error);
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
/**
|
|
572
|
+
* The logout parameters, from the query string on GET and the form body on
|
|
573
|
+
* POST — RP-Initiated Logout §2 permits both. A verified `id_token_hint`
|
|
574
|
+
* contributes the subject and the audience; an unverifiable one contributes
|
|
575
|
+
* nothing rather than refusing the logout.
|
|
576
|
+
*/
|
|
577
|
+
async #endSessionParameters(request) {
|
|
578
|
+
const url = new URL(request.url);
|
|
579
|
+
const params = request.method === "POST"
|
|
580
|
+
? new URLSearchParams(await request.text())
|
|
581
|
+
: url.searchParams;
|
|
582
|
+
const idTokenHint = params.get("id_token_hint");
|
|
583
|
+
const hint = idTokenHint ? await this.#readIdTokenHint(idTokenHint) : null;
|
|
584
|
+
return {
|
|
585
|
+
clientId: params.get("client_id") ?? hint?.audience ?? null,
|
|
586
|
+
subject: hint?.subject ?? null,
|
|
587
|
+
postLogoutRedirectUri: params.get("post_logout_redirect_uri") ?? null,
|
|
588
|
+
state: params.get("state") ?? null,
|
|
589
|
+
logoutHint: params.get("logout_hint") ?? null,
|
|
590
|
+
};
|
|
591
|
+
}
|
|
592
|
+
/** The subject and audience of a signature-valid `id_token_hint`, if any. */
|
|
593
|
+
async #readIdTokenHint(idTokenHint) {
|
|
594
|
+
const key = await this.signingKeys?.getSigningKey();
|
|
595
|
+
if (!key)
|
|
596
|
+
return null;
|
|
597
|
+
const payload = await verifyJwt(idTokenHint, key.publicJwk, {
|
|
598
|
+
ignoreExpiration: true,
|
|
599
|
+
});
|
|
600
|
+
if (!payload)
|
|
601
|
+
return null;
|
|
602
|
+
const aud = payload.aud;
|
|
603
|
+
const audience = typeof aud === "string"
|
|
604
|
+
? aud
|
|
605
|
+
: Array.isArray(aud) && typeof aud[0] === "string"
|
|
606
|
+
? aud[0]
|
|
607
|
+
: null;
|
|
608
|
+
return {
|
|
609
|
+
subject: typeof payload.sub === "string" ? payload.sub : null,
|
|
610
|
+
audience,
|
|
611
|
+
};
|
|
612
|
+
}
|
|
613
|
+
/** The client the logout names, or null when it named none we can find. */
|
|
614
|
+
async #resolveEndSessionClient(request, params) {
|
|
615
|
+
if (!params.clientId)
|
|
616
|
+
return null;
|
|
617
|
+
const { services } = await this.authorizationContext(request);
|
|
618
|
+
try {
|
|
619
|
+
return await services.clientService.get(params.clientId) ?? null;
|
|
620
|
+
}
|
|
621
|
+
catch {
|
|
622
|
+
return null;
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
/**
|
|
626
|
+
* The `post_logout_redirect_uri` to honor, with `state` appended — or
|
|
627
|
+
* undefined when the request named none, named no resolvable client, or
|
|
628
|
+
* named a URI that client did not register.
|
|
629
|
+
*/
|
|
630
|
+
#postLogoutRedirect(client, params) {
|
|
631
|
+
const requested = params.postLogoutRedirectUri;
|
|
632
|
+
if (!requested || !client)
|
|
633
|
+
return undefined;
|
|
634
|
+
const registered = client.postLogoutRedirectUris ?? [];
|
|
635
|
+
if (!registered.includes(requested))
|
|
636
|
+
return undefined;
|
|
637
|
+
if (!params.state)
|
|
638
|
+
return requested;
|
|
639
|
+
const url = new URL(requested);
|
|
640
|
+
url.searchParams.set("state", params.state);
|
|
641
|
+
return url.toString();
|
|
642
|
+
}
|
|
643
|
+
/**
|
|
644
|
+
* Parses authorization request parameters from the query string.
|
|
645
|
+
*
|
|
646
|
+
* The authorization endpoint is GET-only (RFC 6749 Section 3.1 requires GET
|
|
647
|
+
* and only permits POST), so parameters are read from `url.searchParams`.
|
|
648
|
+
*/
|
|
649
|
+
parseAuthorizeParameters(request) {
|
|
650
|
+
const url = new URL(request.url);
|
|
651
|
+
const params = url.searchParams;
|
|
652
|
+
return {
|
|
653
|
+
responseType: params.get("response_type") ?? undefined,
|
|
654
|
+
clientId: params.get("client_id") ?? undefined,
|
|
655
|
+
redirectUri: params.get("redirect_uri") ?? undefined,
|
|
656
|
+
state: params.get("state") ?? undefined,
|
|
657
|
+
scope: params.get("scope") ?? undefined,
|
|
658
|
+
challenge: params.get("code_challenge") ?? undefined,
|
|
659
|
+
challengeMethod: params.get("code_challenge_method") ?? undefined,
|
|
660
|
+
nonce: params.get("nonce") ?? undefined,
|
|
661
|
+
acrValues: params.get("acr_values") ?? undefined,
|
|
662
|
+
maxAge: params.get("max_age") ?? undefined,
|
|
663
|
+
prompt: params.get("prompt") ?? undefined,
|
|
664
|
+
};
|
|
665
|
+
}
|
|
666
|
+
/**
|
|
667
|
+
* Handles an authorization request (GET /authorize).
|
|
668
|
+
* Used for the authorization code flow.
|
|
669
|
+
*
|
|
670
|
+
* Once the request has named a redirect URI the client registered, failures
|
|
671
|
+
* are reported to that URI as an RFC 6749 Section 4.1.2.1 error redirect;
|
|
672
|
+
* before that they are error responses from this endpoint, since there is no
|
|
673
|
+
* verified place to send the user back to.
|
|
674
|
+
*
|
|
675
|
+
* Requires a grant extending {@linkcode AuthorizationCodeGrant} registered
|
|
676
|
+
* under `authorization_code`; without one the endpoint answers 500
|
|
677
|
+
* `server_error` (or rethrows, with `throwOnError`).
|
|
678
|
+
*
|
|
679
|
+
* @param request The incoming request
|
|
680
|
+
* @param authenticateUser Function to authenticate the user
|
|
681
|
+
* @param handleConsent Optional function to handle user consent
|
|
682
|
+
* @returns A Response (redirect to client or error page)
|
|
683
|
+
*/
|
|
684
|
+
async handleAuthorizeRequest(request, authenticateUser, handleConsent) {
|
|
685
|
+
const target = { url: null };
|
|
686
|
+
try {
|
|
687
|
+
return await this.#authorize(request, target, authenticateUser, handleConsent);
|
|
688
|
+
}
|
|
689
|
+
catch (error) {
|
|
690
|
+
return this.#authorizeErrorResponse(error, target.url);
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
/**
|
|
694
|
+
* The authorize flow proper, recording the redirect target on `target` as
|
|
695
|
+
* soon as one is verified so {@link handleAuthorizeRequest} can report later
|
|
696
|
+
* failures to the client.
|
|
697
|
+
*/
|
|
698
|
+
async #authorize(request, target, authenticateUser, handleConsent) {
|
|
699
|
+
const params = this.parseAuthorizeParameters(request);
|
|
700
|
+
const grant = this.#authorizationCodeGrant();
|
|
701
|
+
const { services } = await this.authorizationContext(request);
|
|
702
|
+
const client = await this.#resolveAuthorizeClient(grant, params.clientId, services);
|
|
703
|
+
const redirectUrl = this.#resolveRedirectTarget(client, params.redirectUri);
|
|
704
|
+
target.url = redirectUrl;
|
|
705
|
+
if (!params.state) {
|
|
706
|
+
throw new InvalidRequestError("state required");
|
|
707
|
+
}
|
|
708
|
+
redirectUrl.searchParams.set("state", params.state);
|
|
709
|
+
this.#validateResponseType(params.responseType);
|
|
710
|
+
const requestedScope = grant.parseScope(params.scope);
|
|
711
|
+
this.#validateChallenge(grant, params);
|
|
712
|
+
const authResult = await authenticateUser(request);
|
|
713
|
+
if (authResult instanceof Response)
|
|
714
|
+
return authResult;
|
|
715
|
+
if (!authResult) {
|
|
716
|
+
throw new AccessDeniedError("authentication required");
|
|
717
|
+
}
|
|
718
|
+
const { user, authorizedScope } = authResult;
|
|
719
|
+
const authenticationContext = snapshotAuthenticationContext(authResult.authenticationContext);
|
|
720
|
+
if (grant.requirePKCE && !params.challenge) {
|
|
721
|
+
throw new InvalidRequestError("code_challenge required (PKCE is required for this grant)");
|
|
722
|
+
}
|
|
723
|
+
const consented = await this.#resolveScopeAndConsent({
|
|
724
|
+
grant,
|
|
725
|
+
client,
|
|
726
|
+
user,
|
|
727
|
+
requestedScope,
|
|
728
|
+
authorizedScope,
|
|
729
|
+
services,
|
|
730
|
+
handleConsent,
|
|
731
|
+
});
|
|
732
|
+
if (consented instanceof Response)
|
|
733
|
+
return consented;
|
|
734
|
+
const authorizationCode = await grant.generateAuthorizationCode({
|
|
735
|
+
client,
|
|
736
|
+
user,
|
|
737
|
+
scope: consented.scope,
|
|
738
|
+
redirectUri: params.redirectUri ?? null,
|
|
739
|
+
challenge: params.challenge ?? null,
|
|
740
|
+
challengeMethod: params.challengeMethod ?? null,
|
|
741
|
+
nonce: params.nonce ?? null,
|
|
742
|
+
authenticationContext,
|
|
743
|
+
}, request);
|
|
744
|
+
redirectUrl.searchParams.set("code", authorizationCode.code);
|
|
745
|
+
return Response.redirect(redirectUrl.toString(), 302);
|
|
746
|
+
}
|
|
747
|
+
/**
|
|
748
|
+
* Reports an authorize failure: an RFC 6749 Section 4.1.2.1 error redirect
|
|
749
|
+
* once a verified `redirectUrl` exists, otherwise a plain error response.
|
|
750
|
+
*/
|
|
751
|
+
#authorizeErrorResponse(error, redirectUrl) {
|
|
752
|
+
if (!redirectUrl || !(error instanceof Error) || !isOAuth2Error(error)) {
|
|
753
|
+
return this.handleError(error);
|
|
754
|
+
}
|
|
755
|
+
const { searchParams } = redirectUrl;
|
|
756
|
+
searchParams.set("error", error.extensions.error ?? "server_error");
|
|
757
|
+
// Use exposedMessage, never raw error.message, so 5xx server_error detail isn't leaked into the redirect.
|
|
758
|
+
const description = error.extensions.error_description ??
|
|
759
|
+
error.exposedMessage;
|
|
760
|
+
if (description) {
|
|
761
|
+
searchParams.set("error_description", description);
|
|
762
|
+
}
|
|
763
|
+
const errorUri = error.extensions.error_uri ?? error.type;
|
|
764
|
+
if (errorUri) {
|
|
765
|
+
searchParams.set("error_uri", errorUri);
|
|
766
|
+
}
|
|
767
|
+
return Response.redirect(redirectUrl.toString(), 302);
|
|
768
|
+
}
|
|
769
|
+
#authorizationCodeGrant() {
|
|
770
|
+
const grant = this.grants[AUTHORIZATION_CODE_GRANT_TYPE];
|
|
771
|
+
if (!grant) {
|
|
772
|
+
throw new ServerError("missing authorization code grant");
|
|
773
|
+
}
|
|
774
|
+
if (!isAuthorizationCodeGrant(grant)) {
|
|
775
|
+
throw new ServerError(`the grant registered as "${AUTHORIZATION_CODE_GRANT_TYPE}" ` +
|
|
776
|
+
`(${grant.constructor.name}) cannot serve the authorization ` +
|
|
777
|
+
`endpoint: it must extend AuthorizationCodeGrant`);
|
|
778
|
+
}
|
|
779
|
+
return grant;
|
|
780
|
+
}
|
|
781
|
+
#deviceAuthorizationGrant() {
|
|
782
|
+
const grant = this.grants[DEVICE_AUTHORIZATION_GRANT_TYPE];
|
|
783
|
+
if (!grant) {
|
|
784
|
+
throw new ServerError("device authorization grant not configured");
|
|
785
|
+
}
|
|
786
|
+
if (!isDeviceAuthorizationGrant(grant)) {
|
|
787
|
+
throw new ServerError(`the grant registered as "${DEVICE_AUTHORIZATION_GRANT_TYPE}" ` +
|
|
788
|
+
`(${grant.constructor.name}) cannot serve the device authorization ` +
|
|
789
|
+
`endpoint: it must extend DeviceAuthorizationGrant`);
|
|
790
|
+
}
|
|
791
|
+
return grant;
|
|
792
|
+
}
|
|
793
|
+
/**
|
|
794
|
+
* The client the authorize request names, once it is registered for the
|
|
795
|
+
* authorization code grant.
|
|
796
|
+
*/
|
|
797
|
+
async #resolveAuthorizeClient(grant, clientId, services) {
|
|
798
|
+
if (!clientId) {
|
|
799
|
+
throw new InvalidRequestError("client_id parameter required");
|
|
800
|
+
}
|
|
801
|
+
const client = await grant.getClient(clientId, services.clientService);
|
|
802
|
+
this.#assertGrantAuthorized(client, AUTHORIZATION_CODE_GRANT_TYPE);
|
|
803
|
+
return client;
|
|
804
|
+
}
|
|
805
|
+
/**
|
|
806
|
+
* The URL the browser is sent back to: the requested `redirect_uri` once it
|
|
807
|
+
* matches a registration, or the client's first literal registration when the
|
|
808
|
+
* request omitted one.
|
|
809
|
+
*/
|
|
810
|
+
#resolveRedirectTarget(client, redirectUri) {
|
|
811
|
+
if (!client.redirectUris?.length) {
|
|
812
|
+
throw new UnauthorizedClientError("no authorized redirect_uri");
|
|
813
|
+
}
|
|
814
|
+
if (redirectUri &&
|
|
815
|
+
!matchRedirectUri(client.redirectUris, redirectUri, this.#isPublicSuffix)) {
|
|
816
|
+
throw new UnauthorizedClientError("redirect_uri not authorized");
|
|
817
|
+
}
|
|
818
|
+
const defaultRedirectUri = client.redirectUris.find((uri) => !isRedirectUriPattern(uri));
|
|
819
|
+
if (!redirectUri && !defaultRedirectUri) {
|
|
820
|
+
throw new InvalidRequestError("redirect_uri required when every registered redirect_uri is a pattern");
|
|
821
|
+
}
|
|
822
|
+
return new URL(redirectUri ?? defaultRedirectUri);
|
|
823
|
+
}
|
|
824
|
+
/** Rejects any `response_type` other than the `code` this endpoint issues. */
|
|
825
|
+
#validateResponseType(responseType) {
|
|
826
|
+
if (!responseType) {
|
|
827
|
+
throw new InvalidRequestError("response_type required");
|
|
828
|
+
}
|
|
829
|
+
// RFC 6749 §4.1.2.1: unsupported response_type is unsupported_response_type, not invalid_request.
|
|
830
|
+
if (responseType !== "code") {
|
|
831
|
+
throw new UnsupportedResponseTypeError("response_type not supported");
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
/**
|
|
835
|
+
* Rejects PKCE parameters the grant cannot honor (RFC 7636 Section 4.3), and
|
|
836
|
+
* a `code_challenge` malformed for a method whose shape Section 4.2 fixes.
|
|
837
|
+
*/
|
|
838
|
+
#validateChallenge(grant, params) {
|
|
839
|
+
const { challenge, challengeMethod } = params;
|
|
840
|
+
if (challengeMethod && !challenge) {
|
|
841
|
+
throw new InvalidRequestError("code_challenge required when code_challenge_method is set");
|
|
842
|
+
}
|
|
843
|
+
if (challenge && !grant.validateChallengeMethod(challengeMethod)) {
|
|
844
|
+
throw new InvalidRequestError("unsupported code_challenge_method");
|
|
845
|
+
}
|
|
846
|
+
if (challenge && !validateCodeChallenge(challenge, challengeMethod)) {
|
|
847
|
+
throw new InvalidRequestError("code_challenge must be 43-128 characters using [A-Z] / [a-z] / [0-9] / - / . / _ / ~");
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
/**
|
|
851
|
+
* The scope the authorization code will carry: the requested scope narrowed
|
|
852
|
+
* by the token service, then by consent when the user has not already
|
|
853
|
+
* pre-authorized it. A `Response` short-circuits the flow (a consent page).
|
|
854
|
+
*/
|
|
855
|
+
async #resolveScopeAndConsent(options) {
|
|
856
|
+
const { grant, client, user, requestedScope, authorizedScope, services, handleConsent, } = options;
|
|
857
|
+
const acceptedScope = await grant.acceptedScope(client, user, requestedScope, services.tokenService);
|
|
858
|
+
let scope = acceptedScope ?? undefined;
|
|
859
|
+
const needsConsent = scope !== undefined &&
|
|
860
|
+
!(authorizedScope && authorizedScope.has(scope));
|
|
861
|
+
if (needsConsent && handleConsent) {
|
|
862
|
+
const consent = await handleConsent(client, scope, user);
|
|
863
|
+
if (consent instanceof Response)
|
|
864
|
+
return consent;
|
|
865
|
+
if (!consent.approved) {
|
|
866
|
+
throw new AccessDeniedError("user denied consent");
|
|
867
|
+
}
|
|
868
|
+
if (consent.scope)
|
|
869
|
+
scope = consent.scope;
|
|
870
|
+
}
|
|
871
|
+
return { scope };
|
|
872
|
+
}
|
|
873
|
+
/**
|
|
874
|
+
* Authenticates a client from the credentials the request presents: Basic
|
|
875
|
+
* authentication (RFC 6749 Section 2.3.1) or the `client_id`/`client_secret`
|
|
876
|
+
* of `body`, the form body the endpoint already parsed. The grants
|
|
877
|
+
* authenticate through the same implementation, so a request that fails here
|
|
878
|
+
* fails identically at the token endpoint.
|
|
879
|
+
*
|
|
880
|
+
* @throws {InvalidClientError} If client authentication fails
|
|
881
|
+
*/
|
|
882
|
+
async authenticateClient(request, body, services) {
|
|
883
|
+
return await authenticateClientCredentials(extractClientCredentials(request, body), services.clientService);
|
|
884
|
+
}
|
|
885
|
+
/**
|
|
886
|
+
* Handles a token revocation request (POST /revoke).
|
|
887
|
+
*
|
|
888
|
+
* Per RFC 7009, requires client authentication and accepts:
|
|
889
|
+
* - token: The token to revoke (required)
|
|
890
|
+
* - token_type_hint: Hint about token type (optional)
|
|
891
|
+
*
|
|
892
|
+
* **A client can only revoke its own tokens.** RFC 7009 Section 2.1 requires
|
|
893
|
+
* the server to verify that the presented token was issued to the
|
|
894
|
+
* authenticated client; without that check any client that has observed a
|
|
895
|
+
* token value — a resource server it called, a shared browser, a log — could
|
|
896
|
+
* end another application's live sessions at will. A token belonging to a
|
|
897
|
+
* different client is left alone.
|
|
898
|
+
*
|
|
899
|
+
* Always returns 200 OK, even if the token was invalid, already revoked, or
|
|
900
|
+
* issued to another client: RFC 7009 Section 2.2 requires that answer so the
|
|
901
|
+
* endpoint cannot be used as an oracle for whether a token value is live or
|
|
902
|
+
* whose it is. That covers the token only: a malformed request — wrong
|
|
903
|
+
* method, a content-type other than `application/x-www-form-urlencoded`, or
|
|
904
|
+
* no `token` parameter — is a 400 `invalid_request`, as on the token
|
|
905
|
+
* endpoint.
|
|
906
|
+
*
|
|
907
|
+
* `token_type_hint` orders the lookup rather than restricting it, so a client
|
|
908
|
+
* that mislabels its token still gets it revoked.
|
|
909
|
+
*
|
|
910
|
+
* @see https://datatracker.ietf.org/doc/html/rfc7009
|
|
911
|
+
*/
|
|
912
|
+
async handleRevocationRequest(request) {
|
|
913
|
+
try {
|
|
914
|
+
const { context, body, client } = await this
|
|
915
|
+
.#beginClientAuthenticatedRequest(request);
|
|
916
|
+
const token = body.get("token");
|
|
917
|
+
if (typeof token !== "string") {
|
|
918
|
+
throw new InvalidRequestError("token parameter required");
|
|
919
|
+
}
|
|
920
|
+
const tokenTypeHint = body.get("token_type_hint");
|
|
921
|
+
const hint = typeof tokenTypeHint === "string" ? tokenTypeHint : null;
|
|
922
|
+
const { tokenService } = context.services;
|
|
923
|
+
const resolved = await this.#resolveToken(tokenService, token, hint);
|
|
924
|
+
if (resolved &&
|
|
925
|
+
resolved.token.client.id.toString() === client.id.toString()) {
|
|
926
|
+
await tokenService.revoke(token, resolved.tokenType);
|
|
927
|
+
}
|
|
928
|
+
return new Response(null, { status: 200 });
|
|
929
|
+
}
|
|
930
|
+
catch (error) {
|
|
931
|
+
return this.handleError(error, this.prepareClientAuthError(request));
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
async #resolveToken(tokenService, value, hint) {
|
|
935
|
+
const refreshFirst = hint === "refresh_token";
|
|
936
|
+
if (refreshFirst) {
|
|
937
|
+
const refreshToken = await tokenService.getRefreshToken(value);
|
|
938
|
+
if (refreshToken) {
|
|
939
|
+
return { tokenType: "refresh_token", token: refreshToken };
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
const token = await tokenService.getToken(value);
|
|
943
|
+
if (token)
|
|
944
|
+
return { tokenType: "access_token", token };
|
|
945
|
+
if (refreshFirst)
|
|
946
|
+
return undefined;
|
|
947
|
+
const refreshToken = await tokenService.getRefreshToken(value);
|
|
948
|
+
return refreshToken
|
|
949
|
+
? { tokenType: "refresh_token", token: refreshToken }
|
|
950
|
+
: undefined;
|
|
951
|
+
}
|
|
952
|
+
#introspectedExpiry(resolved) {
|
|
953
|
+
return resolved.tokenType === "refresh_token"
|
|
954
|
+
? resolved.token.refreshTokenExpiresAt
|
|
955
|
+
: resolved.token.accessTokenExpiresAt;
|
|
956
|
+
}
|
|
957
|
+
/**
|
|
958
|
+
* Handles a token introspection request (POST /introspect).
|
|
959
|
+
*
|
|
960
|
+
* Per RFC 7662, requires client authentication and returns information
|
|
961
|
+
* about the token including whether it is active.
|
|
962
|
+
* {@linkcode AuthorizationServerOptions.canIntrospectToken} controls which
|
|
963
|
+
* tokens an authenticated caller may inspect. A denial returns only
|
|
964
|
+
* `{ active: false }`; without a policy, all admitted clients may inspect
|
|
965
|
+
* any token, including separately authorized resource servers.
|
|
966
|
+
*
|
|
967
|
+
* Both access tokens and refresh tokens introspect. `token_type_hint` orders
|
|
968
|
+
* the lookup rather than restricting it (RFC 7662 Section 2.1 requires the
|
|
969
|
+
* other kind to be tried when the hinted lookup misses), so a mislabelled
|
|
970
|
+
* token still resolves. An active access token carries
|
|
971
|
+
* `token_type: "Bearer"`; a refresh token carries no `token_type`, since
|
|
972
|
+
* that field describes how an access token is presented, and its `exp` is
|
|
973
|
+
* the refresh token's own expiry.
|
|
974
|
+
*
|
|
975
|
+
* A token with no user — the client acting as its own resource owner, as
|
|
976
|
+
* the client credentials grant issues — carries no `sub` and no `username`;
|
|
977
|
+
* `client_id` identifies the machine. `sub` is optional in RFC 7662, and
|
|
978
|
+
* leaving it absent keeps `sub` an unambiguous "there is a resource owner"
|
|
979
|
+
* signal rather than overloading `client_id` into the subject namespace
|
|
980
|
+
* (RFC 9700 §4.15.1).
|
|
981
|
+
*
|
|
982
|
+
* A wrong method, or a content type other than
|
|
983
|
+
* `application/x-www-form-urlencoded`, is a 400 `invalid_request`, as on the
|
|
984
|
+
* token endpoint.
|
|
985
|
+
*
|
|
986
|
+
* @see https://datatracker.ietf.org/doc/html/rfc7662
|
|
987
|
+
*/
|
|
988
|
+
async handleIntrospectionRequest(request) {
|
|
989
|
+
try {
|
|
990
|
+
const { context, body, client } = await this
|
|
991
|
+
.#beginClientAuthenticatedRequest(request);
|
|
992
|
+
const tokenValue = body.get("token");
|
|
993
|
+
if (typeof tokenValue !== "string") {
|
|
994
|
+
throw new InvalidRequestError("token parameter required");
|
|
995
|
+
}
|
|
996
|
+
const tokenTypeHint = body.get("token_type_hint");
|
|
997
|
+
const hint = typeof tokenTypeHint === "string" ? tokenTypeHint : null;
|
|
998
|
+
const { tokenService } = context.services;
|
|
999
|
+
let response;
|
|
1000
|
+
const resolved = await this.#resolveToken(tokenService, tokenValue, hint);
|
|
1001
|
+
const expiresAt = resolved && this.#introspectedExpiry(resolved);
|
|
1002
|
+
if (!resolved || (expiresAt && expiresAt < new Date()) ||
|
|
1003
|
+
(this.#canIntrospectToken &&
|
|
1004
|
+
await this.#canIntrospectToken(client, resolved.token, resolved.tokenType) !== true)) {
|
|
1005
|
+
response = { active: false };
|
|
1006
|
+
}
|
|
1007
|
+
else {
|
|
1008
|
+
const { token, tokenType } = resolved;
|
|
1009
|
+
response = {
|
|
1010
|
+
active: true,
|
|
1011
|
+
client_id: token.client.id,
|
|
1012
|
+
};
|
|
1013
|
+
if (tokenType === "access_token") {
|
|
1014
|
+
response.token_type = "Bearer";
|
|
1015
|
+
}
|
|
1016
|
+
if (token.scope) {
|
|
1017
|
+
response.scope = token.scope.toString();
|
|
1018
|
+
}
|
|
1019
|
+
if (expiresAt) {
|
|
1020
|
+
response.exp = Math.floor(expiresAt.getTime() / 1000);
|
|
1021
|
+
}
|
|
1022
|
+
if (context.issuer) {
|
|
1023
|
+
response.iss = context.issuer;
|
|
1024
|
+
}
|
|
1025
|
+
const user = token.user;
|
|
1026
|
+
if (user?.id) {
|
|
1027
|
+
response.sub = user.id;
|
|
1028
|
+
}
|
|
1029
|
+
if (user?.username) {
|
|
1030
|
+
response.username = user.username;
|
|
1031
|
+
}
|
|
1032
|
+
if (this.#introspectionClaims) {
|
|
1033
|
+
response = {
|
|
1034
|
+
...await this.#introspectionClaims(token),
|
|
1035
|
+
...response,
|
|
1036
|
+
};
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
return new Response(JSON.stringify(response), {
|
|
1040
|
+
status: 200,
|
|
1041
|
+
headers: {
|
|
1042
|
+
"Content-Type": "application/json;charset=UTF-8",
|
|
1043
|
+
"Cache-Control": "no-store",
|
|
1044
|
+
"Pragma": "no-cache",
|
|
1045
|
+
},
|
|
1046
|
+
});
|
|
1047
|
+
}
|
|
1048
|
+
catch (error) {
|
|
1049
|
+
return this.handleError(error, this.prepareClientAuthError(request));
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
/**
|
|
1053
|
+
* Gets OAuth2 server metadata (RFC 8414).
|
|
1054
|
+
*
|
|
1055
|
+
* Returns metadata about this authorization server including supported
|
|
1056
|
+
* grant types, endpoints, and capabilities.
|
|
1057
|
+
*
|
|
1058
|
+
* `code_challenge_methods_supported` is derived from the registered
|
|
1059
|
+
* authorization code grant's own `challengeMethods`, so a server that
|
|
1060
|
+
* registers an extra method (or drops `S256`) advertises what it will
|
|
1061
|
+
* actually accept. It is omitted entirely when no authorization code grant
|
|
1062
|
+
* is registered, since PKCE has nothing to apply to.
|
|
1063
|
+
*
|
|
1064
|
+
* `token_endpoint_auth_methods_supported` always includes `none`, the
|
|
1065
|
+
* RFC 7591 method for a client with no secret. The token endpoint accepts it
|
|
1066
|
+
* on every deployment and for every grant — a request carrying only
|
|
1067
|
+
* `client_id` reaches `ClientServiceInterface.getAuthenticated` with no
|
|
1068
|
+
* secret, which that interface's contract requires to resolve a public
|
|
1069
|
+
* client (an authorization-code exchange carrying a `code_verifier` looks
|
|
1070
|
+
* the client up with `get` instead when `requireClientAuthentication` is
|
|
1071
|
+
* explicitly `false`) — and there is no option that turns it off, so it is not
|
|
1072
|
+
* conditional on anything a caller configures. Which clients may use it is
|
|
1073
|
+
* decided per client, by whether the client has a secret; discovery
|
|
1074
|
+
* describes the endpoint, not the client population.
|
|
1075
|
+
*
|
|
1076
|
+
* @throws {ServerError} If the resolved context has no `issuer` — RFC 8414
|
|
1077
|
+
* makes it REQUIRED and it is the field clients validate against.
|
|
1078
|
+
* @see https://datatracker.ietf.org/doc/html/rfc8414#section-2
|
|
1079
|
+
* @see https://datatracker.ietf.org/doc/html/rfc7591#section-2
|
|
1080
|
+
*/
|
|
1081
|
+
getMetadata(context) {
|
|
1082
|
+
const grants = Object.keys(this.grants);
|
|
1083
|
+
const hasAuthCodeGrant = grants.includes("authorization_code");
|
|
1084
|
+
if (!context.issuer) {
|
|
1085
|
+
throw new ServerError("issuer must be configured to serve authorization server metadata " +
|
|
1086
|
+
"(RFC 8414)");
|
|
1087
|
+
}
|
|
1088
|
+
const endpoints = this.#resolvedEndpoints(context);
|
|
1089
|
+
const metadata = {
|
|
1090
|
+
issuer: context.issuer,
|
|
1091
|
+
};
|
|
1092
|
+
this.#advertiseEndpoints(metadata, endpoints, false);
|
|
1093
|
+
metadata.grant_types_supported = grants;
|
|
1094
|
+
metadata.token_endpoint_auth_methods_supported = [
|
|
1095
|
+
"client_secret_basic",
|
|
1096
|
+
"client_secret_post",
|
|
1097
|
+
"none",
|
|
1098
|
+
];
|
|
1099
|
+
const challengeMethods = this.#supportedChallengeMethods();
|
|
1100
|
+
if (challengeMethods.length) {
|
|
1101
|
+
metadata.code_challenge_methods_supported = challengeMethods;
|
|
1102
|
+
}
|
|
1103
|
+
metadata.response_types_supported = hasAuthCodeGrant ? ["code"] : [];
|
|
1104
|
+
if (this.scopesSupported) {
|
|
1105
|
+
metadata.scopes_supported = this.scopesSupported;
|
|
1106
|
+
}
|
|
1107
|
+
if (this.signingKeys) {
|
|
1108
|
+
this.#advertiseEndpoints(metadata, endpoints, true);
|
|
1109
|
+
metadata.id_token_signing_alg_values_supported = ["ES256"];
|
|
1110
|
+
metadata.subject_types_supported = ["public"];
|
|
1111
|
+
}
|
|
1112
|
+
return metadata;
|
|
1113
|
+
}
|
|
1114
|
+
#supportedChallengeMethods() {
|
|
1115
|
+
const grant = this.grants[AUTHORIZATION_CODE_GRANT_TYPE];
|
|
1116
|
+
if (!grant || !isAuthorizationCodeGrant(grant))
|
|
1117
|
+
return [];
|
|
1118
|
+
const { challengeMethods } = grant;
|
|
1119
|
+
return Object.keys(challengeMethods).filter((method) => typeof challengeMethods[method] === "function");
|
|
1120
|
+
}
|
|
1121
|
+
/** Publishes the resolved URLs of the OIDC (or plain OAuth2) endpoints. */
|
|
1122
|
+
#advertiseEndpoints(metadata, endpoints, oidc) {
|
|
1123
|
+
for (const [key, endpoint] of ADVERTISED_ENTRIES) {
|
|
1124
|
+
if (endpoint.oidc !== oidc)
|
|
1125
|
+
continue;
|
|
1126
|
+
if (key === "endSession" && !this.#endSession)
|
|
1127
|
+
continue;
|
|
1128
|
+
const url = endpoints[key];
|
|
1129
|
+
if (url)
|
|
1130
|
+
metadata[endpoint.metadata] = url;
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
/**
|
|
1134
|
+
* Handles a metadata request (GET /.well-known/oauth-authorization-server).
|
|
1135
|
+
*
|
|
1136
|
+
* Returns the authorization server metadata as JSON per RFC 8414.
|
|
1137
|
+
*
|
|
1138
|
+
* @see https://datatracker.ietf.org/doc/html/rfc8414
|
|
1139
|
+
*/
|
|
1140
|
+
async handleMetadataRequest(request) {
|
|
1141
|
+
try {
|
|
1142
|
+
const context = await this.authorizationContext(request);
|
|
1143
|
+
const metadata = this.getMetadata(context);
|
|
1144
|
+
return new Response(JSON.stringify(metadata), {
|
|
1145
|
+
status: 200,
|
|
1146
|
+
headers: {
|
|
1147
|
+
"Content-Type": "application/json;charset=UTF-8",
|
|
1148
|
+
},
|
|
1149
|
+
});
|
|
1150
|
+
}
|
|
1151
|
+
catch (error) {
|
|
1152
|
+
return this.handleError(error);
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
/**
|
|
1156
|
+
* Handles a device authorization request (POST /device_authorization).
|
|
1157
|
+
*
|
|
1158
|
+
* Initiates the device authorization flow by generating a device_code
|
|
1159
|
+
* and user_code that the user can use to authorize the request on
|
|
1160
|
+
* a secondary device.
|
|
1161
|
+
*
|
|
1162
|
+
* Requires a grant extending {@linkcode DeviceAuthorizationGrant} registered
|
|
1163
|
+
* under `urn:ietf:params:oauth:grant-type:device_code`, and a
|
|
1164
|
+
* `verificationUri` on the resolved context; without either the endpoint
|
|
1165
|
+
* answers 500 `server_error`. A wrong method, or a content type other than
|
|
1166
|
+
* `application/x-www-form-urlencoded`, is a 400 `invalid_request`, as on the
|
|
1167
|
+
* token endpoint.
|
|
1168
|
+
*
|
|
1169
|
+
* @see https://datatracker.ietf.org/doc/html/rfc8628#section-3.1
|
|
1170
|
+
* @see https://datatracker.ietf.org/doc/html/rfc8628#section-3.2
|
|
1171
|
+
*/
|
|
1172
|
+
async handleDeviceAuthorizationRequest(request) {
|
|
1173
|
+
try {
|
|
1174
|
+
const { context, body, client } = await this
|
|
1175
|
+
.#beginClientAuthenticatedRequest(request, {
|
|
1176
|
+
requiredGrantType: DEVICE_AUTHORIZATION_GRANT_TYPE,
|
|
1177
|
+
});
|
|
1178
|
+
const grant = this.#deviceAuthorizationGrant();
|
|
1179
|
+
const scopeText = body.get("scope");
|
|
1180
|
+
const scope = typeof scopeText === "string"
|
|
1181
|
+
? grant.parseScope(scopeText)
|
|
1182
|
+
: undefined;
|
|
1183
|
+
// RFC 8628 §3.2 requires a verification_uri the user can visit.
|
|
1184
|
+
if (!context.verificationUri) {
|
|
1185
|
+
throw new ServerError("verificationUri must be configured for the device authorization " +
|
|
1186
|
+
"endpoint (RFC 8628)");
|
|
1187
|
+
}
|
|
1188
|
+
const authorization = await grant.initiateDeviceAuthorization(client, request, scope);
|
|
1189
|
+
const response = {
|
|
1190
|
+
device_code: authorization.deviceCode,
|
|
1191
|
+
user_code: authorization.userCode,
|
|
1192
|
+
verification_uri: context.verificationUri,
|
|
1193
|
+
expires_in: Math.floor((authorization.expiresAt.getTime() - Date.now()) / 1000),
|
|
1194
|
+
interval: authorization.interval,
|
|
1195
|
+
};
|
|
1196
|
+
return new Response(JSON.stringify(response), {
|
|
1197
|
+
status: 200,
|
|
1198
|
+
headers: {
|
|
1199
|
+
"Content-Type": "application/json;charset=UTF-8",
|
|
1200
|
+
"Cache-Control": "no-store",
|
|
1201
|
+
"Pragma": "no-cache",
|
|
1202
|
+
},
|
|
1203
|
+
});
|
|
1204
|
+
}
|
|
1205
|
+
catch (error) {
|
|
1206
|
+
return this.handleError(error, this.prepareClientAuthError(request));
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
/**
|
|
1211
|
+
* Returns a `fetch`-compatible function that dispatches requests directly
|
|
1212
|
+
* to the {@link AuthorizationServer}'s handlers in-process, bypassing the
|
|
1213
|
+
* network.
|
|
1214
|
+
*
|
|
1215
|
+
* Use this to point a `DirectClient` at a co-located authorization
|
|
1216
|
+
* server — the request flow becomes a plain method call, so token refresh
|
|
1217
|
+
* (and every other client → server interaction) stays in-process.
|
|
1218
|
+
*
|
|
1219
|
+
* Routing is by pathname against the endpoints the server resolves for the
|
|
1220
|
+
* request (so it follows per-request issuer/origin):
|
|
1221
|
+
* - `POST {tokenEndpoint}` → `handleTokenRequest`
|
|
1222
|
+
* - `POST {revocationEndpoint}` → `handleRevocationRequest`
|
|
1223
|
+
* - `POST {introspectionEndpoint}` → `handleIntrospectionRequest`
|
|
1224
|
+
* - `POST {deviceAuthorizationEndpoint}` → `handleDeviceAuthorizationRequest`
|
|
1225
|
+
* - `GET /.well-known/oauth-authorization-server` → `handleMetadataRequest`
|
|
1226
|
+
*
|
|
1227
|
+
* The authorization endpoint is deliberately **not** routed: `/authorize`
|
|
1228
|
+
* needs an `authenticateUser` (and optional `handleConsent`) callback this
|
|
1229
|
+
* transport has no access to, and in real flows it's a browser navigation,
|
|
1230
|
+
* never a call through the client's `fetch`. A request to it throws an
|
|
1231
|
+
* actionable error pointing at `handleAuthorizeRequest`. Any other path also
|
|
1232
|
+
* throws — the helper intentionally does not proxy arbitrary paths; if a call
|
|
1233
|
+
* lands here unexpectedly, that's a wiring bug worth surfacing rather than
|
|
1234
|
+
* silently forwarding.
|
|
1235
|
+
*/
|
|
1236
|
+
export function localAuthServerFetch(authServer) {
|
|
1237
|
+
return async (input, init) => {
|
|
1238
|
+
const request = input instanceof Request
|
|
1239
|
+
? (init ? new Request(input, init) : input)
|
|
1240
|
+
: new Request(String(input), init);
|
|
1241
|
+
const url = new URL(request.url);
|
|
1242
|
+
const path = url.pathname;
|
|
1243
|
+
const method = request.method.toUpperCase();
|
|
1244
|
+
const endpoints = await authServer.resolveEndpoints(request);
|
|
1245
|
+
const pathOf = (endpoint) => endpoint ? new URL(endpoint).pathname : undefined;
|
|
1246
|
+
const handlers = {
|
|
1247
|
+
authorization: null,
|
|
1248
|
+
token: (request) => authServer.handleTokenRequest(request),
|
|
1249
|
+
revocation: (request) => authServer.handleRevocationRequest(request),
|
|
1250
|
+
introspection: (request) => authServer.handleIntrospectionRequest(request),
|
|
1251
|
+
deviceAuthorization: (request) => authServer.handleDeviceAuthorizationRequest(request),
|
|
1252
|
+
jwks: (request) => authServer.handleJwksRequest(request),
|
|
1253
|
+
userinfo: (request) => authServer.handleUserInfoRequest(request),
|
|
1254
|
+
endSession: null,
|
|
1255
|
+
};
|
|
1256
|
+
for (const [key, { methods }] of ADVERTISED_ENTRIES) {
|
|
1257
|
+
const handler = handlers[key];
|
|
1258
|
+
if (!handler || !methods.includes(method))
|
|
1259
|
+
continue;
|
|
1260
|
+
if (path === pathOf(endpoints[key]))
|
|
1261
|
+
return await handler(request);
|
|
1262
|
+
}
|
|
1263
|
+
if (method === "GET" && path === ENDPOINT_PATHS.metadata) {
|
|
1264
|
+
return await authServer.handleMetadataRequest(request);
|
|
1265
|
+
}
|
|
1266
|
+
if (method === "GET" && path === ENDPOINT_PATHS.oidcMetadata) {
|
|
1267
|
+
return await authServer.handleOidcMetadataRequest(request);
|
|
1268
|
+
}
|
|
1269
|
+
const endSessionPath = pathOf(endpoints.endSession);
|
|
1270
|
+
if (endSessionPath && path === endSessionPath) {
|
|
1271
|
+
throw new Error(`localAuthServerFetch cannot dispatch the end-session endpoint ` +
|
|
1272
|
+
`(${method} ${path}). RP-initiated logout requires an endSession ` +
|
|
1273
|
+
`callback this transport has no access to — only the app knows how ` +
|
|
1274
|
+
`to end its own session — and in real flows it is a browser ` +
|
|
1275
|
+
`navigation, not a client fetch. Drive it directly via ` +
|
|
1276
|
+
`server.handleEndSessionRequest(request, endSession).`);
|
|
1277
|
+
}
|
|
1278
|
+
const authorizePath = pathOf(endpoints.authorization);
|
|
1279
|
+
if (authorizePath && path === authorizePath) {
|
|
1280
|
+
throw new Error(`localAuthServerFetch cannot dispatch the authorization endpoint ` +
|
|
1281
|
+
`(${method} ${path}). /authorize requires an authenticateUser ` +
|
|
1282
|
+
`(and optional handleConsent) callback this transport has no ` +
|
|
1283
|
+
`access to, and in real flows it is a browser navigation, not a ` +
|
|
1284
|
+
`client fetch. Drive it directly via ` +
|
|
1285
|
+
`server.handleAuthorizeRequest(request, authenticateUser) or the ` +
|
|
1286
|
+
`createMemoryAuthorizationServer() harness's authorize() helper.`);
|
|
1287
|
+
}
|
|
1288
|
+
throw new Error(`localAuthServerFetch: no handler for ${method} ${path}. ` +
|
|
1289
|
+
`Configure the authorization server with explicit endpoint URLs ` +
|
|
1290
|
+
`and point your client at the matching paths.`);
|
|
1291
|
+
};
|
|
1292
|
+
}
|
|
1293
|
+
export { AbstractGrant } from "./grants/grant.js";
|
|
1294
|
+
export { AuthorizationCodeGrant } from "./grants/authorization-code.js";
|
|
1295
|
+
export { ClientCredentialsGrant } from "./grants/client-credentials.js";
|
|
1296
|
+
export { createJwtAccessTokenGenerator, exportSigningKeyJwk, generateSigningKey, importSigningKeyJwk, RotatingSigningKeyProvider, signJwt, StaticSigningKeyProvider, verifyJwt, } from "./signing-keys.js";
|
|
1297
|
+
export { RefreshTokenGrant } from "./grants/refresh-token.js";
|
|
1298
|
+
export { PasswordGrant } from "./grants/password.js";
|
|
1299
|
+
export { DeviceAuthorizationGrant } from "./grants/device-authorization.js";
|
|
1300
|
+
export { AbstractTokenService, } from "./services/token.js";
|
|
1301
|
+
export { AbstractAuthorizationCodeService, } from "./services/authorization-code.js";
|
|
1302
|
+
export { challengeMethods, CODE_VERIFIER_MAX_LENGTH, CODE_VERIFIER_MIN_LENGTH, CODE_VERIFIER_PATTERN, generateCodeChallenge, generateCodeVerifier, generateState, getChallengeMethod, validateCodeChallenge, validateCodeVerifier, verifyCodeChallenge, } from "../utils/pkce.js";
|
|
1303
|
+
export { encodeBasicAuth, parseBasicAuth, tryParseBasicAuth, } from "../utils/basic-auth.js";
|
|
1304
|
+
export { checkRedirectUriPattern, isRedirectUriPattern, matchRedirectUri, } from "./redirect-uri.js";
|