@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,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generic plain-OAuth2 connector: fixed authorize/token endpoints, an optional
|
|
3
|
+
* UserInfo endpoint, and a caller-supplied profile mapper. The connector for
|
|
4
|
+
* providers that speak OAuth2 but publish no OpenID Connect discovery document
|
|
5
|
+
* and issue no `id_token` — where {@link oidcProvider} does not apply and
|
|
6
|
+
* hand-rolling a full {@link ExternalProvider} would be repetitive.
|
|
7
|
+
*
|
|
8
|
+
* @module
|
|
9
|
+
*/
|
|
10
|
+
import type { ExternalProvider } from "./provider.js";
|
|
11
|
+
/** The token-endpoint response fields the mapper may read. */
|
|
12
|
+
export interface OAuth2TokenResult {
|
|
13
|
+
/** The bearer access token issued by the token endpoint. */
|
|
14
|
+
accessToken: string;
|
|
15
|
+
/** The token endpoint's full JSON body, for provider-specific fields. */
|
|
16
|
+
raw: Record<string, unknown>;
|
|
17
|
+
}
|
|
18
|
+
/** The normalized fields a {@link OAuth2ProfileMapper} returns. */
|
|
19
|
+
export interface OAuth2ProfileMapping {
|
|
20
|
+
/** The provider's stable unique id for the user. Required and non-empty. */
|
|
21
|
+
subject: string;
|
|
22
|
+
/** The user's email, when the provider shared one. */
|
|
23
|
+
email?: string;
|
|
24
|
+
/**
|
|
25
|
+
* `true` only when the provider positively asserted the email is verified.
|
|
26
|
+
* Defaults to `false` — never auto-link on an unverified email.
|
|
27
|
+
*/
|
|
28
|
+
emailVerified?: boolean;
|
|
29
|
+
/** Display name, when shared. */
|
|
30
|
+
name?: string;
|
|
31
|
+
/** Given (first) name, when shared. */
|
|
32
|
+
givenName?: string;
|
|
33
|
+
/** Family (last) name, when shared. */
|
|
34
|
+
familyName?: string;
|
|
35
|
+
/** Avatar/profile picture URL, when shared. */
|
|
36
|
+
picture?: string;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Maps a provider's raw profile (the UserInfo body, or the token response when
|
|
40
|
+
* no UserInfo endpoint is configured) to the normalized {@link ExternalProfile}
|
|
41
|
+
* fields. Throw {@link ExternalAuthError} `provider_error` when a required field
|
|
42
|
+
* (typically the subject) is absent.
|
|
43
|
+
*/
|
|
44
|
+
export type OAuth2ProfileMapper = (context: {
|
|
45
|
+
/** The UserInfo JSON, or the token response body when no UserInfo endpoint. */
|
|
46
|
+
profile: Record<string, unknown>;
|
|
47
|
+
/** The token-endpoint result, for providers that carry claims in it. */
|
|
48
|
+
tokens: OAuth2TokenResult;
|
|
49
|
+
}) => OAuth2ProfileMapping;
|
|
50
|
+
/** Options for {@link oauth2Provider}. */
|
|
51
|
+
export interface OAuth2ProviderOptions {
|
|
52
|
+
/** Provider id used in {@link ExternalProfile.provider} and error messages. */
|
|
53
|
+
id: string;
|
|
54
|
+
/** Human-readable name for buttons and logs (e.g. `"Discord"`). */
|
|
55
|
+
displayName: string;
|
|
56
|
+
/** The provider's authorization endpoint (browser redirect target). */
|
|
57
|
+
authorizationEndpoint: string;
|
|
58
|
+
/** The provider's token endpoint (server-to-server code exchange). */
|
|
59
|
+
tokenEndpoint: string;
|
|
60
|
+
/**
|
|
61
|
+
* The provider's UserInfo endpoint, called with the bearer token to build the
|
|
62
|
+
* profile. Omit when the profile is carried in the token response itself.
|
|
63
|
+
*/
|
|
64
|
+
userInfoEndpoint?: string;
|
|
65
|
+
/** The client id registered with the provider. */
|
|
66
|
+
clientId: string;
|
|
67
|
+
/** The matching client secret (sent as `client_secret` in the token body). */
|
|
68
|
+
clientSecret: string;
|
|
69
|
+
/** Turns the raw profile into normalized fields. */
|
|
70
|
+
mapProfile: OAuth2ProfileMapper;
|
|
71
|
+
/** Scopes requested when the caller passes none. */
|
|
72
|
+
defaultScopes: string[];
|
|
73
|
+
/** Scopes override; falls back to `defaultScopes`. */
|
|
74
|
+
scopes?: string[];
|
|
75
|
+
/** Separator joining scopes in the authorize URL. Defaults to a space. */
|
|
76
|
+
scopeSeparator?: string;
|
|
77
|
+
/** Whether to run PKCE (S256). Defaults to `false`. */
|
|
78
|
+
usesPkce?: boolean;
|
|
79
|
+
/** Extra fixed query params to add to the authorize URL. */
|
|
80
|
+
authorizationParams?: Record<string, string>;
|
|
81
|
+
/** Fetch implementation for all provider traffic. Defaults to `globalThis.fetch`. */
|
|
82
|
+
fetch?: typeof fetch;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Creates a connector for a plain-OAuth2 provider from its fixed endpoints and
|
|
86
|
+
* a profile mapper.
|
|
87
|
+
*
|
|
88
|
+
* The exchange posts `authorization_code` to `tokenEndpoint` with the client
|
|
89
|
+
* credentials in the body (`client_secret_post`); the profile is read from
|
|
90
|
+
* `userInfoEndpoint` with the bearer token when set, else from the token
|
|
91
|
+
* response, and handed to `mapProfile`. Presets like {@link discordProvider}
|
|
92
|
+
* are thin wrappers over this.
|
|
93
|
+
*
|
|
94
|
+
* @example
|
|
95
|
+
* ```ts
|
|
96
|
+
* const provider = oauth2Provider({
|
|
97
|
+
* id: "discord",
|
|
98
|
+
* displayName: "Discord",
|
|
99
|
+
* authorizationEndpoint: "https://discord.com/oauth2/authorize",
|
|
100
|
+
* tokenEndpoint: "https://discord.com/api/oauth2/token",
|
|
101
|
+
* userInfoEndpoint: "https://discord.com/api/users/@me",
|
|
102
|
+
* clientId,
|
|
103
|
+
* clientSecret,
|
|
104
|
+
* defaultScopes: ["identify", "email"],
|
|
105
|
+
* mapProfile: ({ profile }) => ({ subject: String(profile.id) }),
|
|
106
|
+
* });
|
|
107
|
+
* const flow = new ExternalAuthFlow({ provider });
|
|
108
|
+
* ```
|
|
109
|
+
*/
|
|
110
|
+
export declare function oauth2Provider(options: OAuth2ProviderOptions): ExternalProvider;
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generic plain-OAuth2 connector: fixed authorize/token endpoints, an optional
|
|
3
|
+
* UserInfo endpoint, and a caller-supplied profile mapper. The connector for
|
|
4
|
+
* providers that speak OAuth2 but publish no OpenID Connect discovery document
|
|
5
|
+
* and issue no `id_token` — where {@link oidcProvider} does not apply and
|
|
6
|
+
* hand-rolling a full {@link ExternalProvider} would be repetitive.
|
|
7
|
+
*
|
|
8
|
+
* @module
|
|
9
|
+
*/
|
|
10
|
+
import { generateCodeChallenge } from "../../utils/pkce.js";
|
|
11
|
+
import { describeError, readErrorBody } from "./_shared.js";
|
|
12
|
+
import { runTokenExchange } from "./_token-exchange.js";
|
|
13
|
+
import { ExternalAuthError } from "./errors.js";
|
|
14
|
+
/**
|
|
15
|
+
* Creates a connector for a plain-OAuth2 provider from its fixed endpoints and
|
|
16
|
+
* a profile mapper.
|
|
17
|
+
*
|
|
18
|
+
* The exchange posts `authorization_code` to `tokenEndpoint` with the client
|
|
19
|
+
* credentials in the body (`client_secret_post`); the profile is read from
|
|
20
|
+
* `userInfoEndpoint` with the bearer token when set, else from the token
|
|
21
|
+
* response, and handed to `mapProfile`. Presets like {@link discordProvider}
|
|
22
|
+
* are thin wrappers over this.
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```ts
|
|
26
|
+
* const provider = oauth2Provider({
|
|
27
|
+
* id: "discord",
|
|
28
|
+
* displayName: "Discord",
|
|
29
|
+
* authorizationEndpoint: "https://discord.com/oauth2/authorize",
|
|
30
|
+
* tokenEndpoint: "https://discord.com/api/oauth2/token",
|
|
31
|
+
* userInfoEndpoint: "https://discord.com/api/users/@me",
|
|
32
|
+
* clientId,
|
|
33
|
+
* clientSecret,
|
|
34
|
+
* defaultScopes: ["identify", "email"],
|
|
35
|
+
* mapProfile: ({ profile }) => ({ subject: String(profile.id) }),
|
|
36
|
+
* });
|
|
37
|
+
* const flow = new ExternalAuthFlow({ provider });
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
export function oauth2Provider(options) {
|
|
41
|
+
const id = options.id;
|
|
42
|
+
const fetchImpl = (input, init) => (options.fetch ?? globalThis.fetch)(input, init);
|
|
43
|
+
const scopeSeparator = options.scopeSeparator ?? " ";
|
|
44
|
+
async function exchangeCode(input) {
|
|
45
|
+
const body = new URLSearchParams({
|
|
46
|
+
grant_type: "authorization_code",
|
|
47
|
+
code: input.code,
|
|
48
|
+
redirect_uri: input.redirectUri,
|
|
49
|
+
client_id: options.clientId,
|
|
50
|
+
client_secret: options.clientSecret,
|
|
51
|
+
});
|
|
52
|
+
if (input.codeVerifier)
|
|
53
|
+
body.set("code_verifier", input.codeVerifier);
|
|
54
|
+
const { value, raw } = await runTokenExchange({
|
|
55
|
+
provider: id,
|
|
56
|
+
displayName: options.displayName,
|
|
57
|
+
endpoint: options.tokenEndpoint,
|
|
58
|
+
body,
|
|
59
|
+
requiredField: "access_token",
|
|
60
|
+
fetch: fetchImpl,
|
|
61
|
+
httpErrorHint: `Check that clientId/clientSecret and the redirect URI ` +
|
|
62
|
+
`match the ${options.displayName} app registration, then try again.`,
|
|
63
|
+
tokenErrorHint: oauth2TokenErrorHint,
|
|
64
|
+
});
|
|
65
|
+
return { accessToken: value, raw };
|
|
66
|
+
}
|
|
67
|
+
async function fetchUserInfo(accessToken) {
|
|
68
|
+
let res;
|
|
69
|
+
try {
|
|
70
|
+
res = await fetchImpl(options.userInfoEndpoint, {
|
|
71
|
+
headers: {
|
|
72
|
+
Authorization: `Bearer ${accessToken}`,
|
|
73
|
+
Accept: "application/json",
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
throw new ExternalAuthError(id, "provider_error", `could not reach the ${options.displayName} profile endpoint ` +
|
|
79
|
+
`(${describeError(error)}). Try again.`, { cause: error });
|
|
80
|
+
}
|
|
81
|
+
if (!res.ok) {
|
|
82
|
+
const detail = await readErrorBody(res);
|
|
83
|
+
throw new ExternalAuthError(id, "provider_error", `profile endpoint responded with HTTP ${res.status}${detail}. The ` +
|
|
84
|
+
`access token was issued but lacks profile access — ensure the ` +
|
|
85
|
+
`identity scope is requested.`);
|
|
86
|
+
}
|
|
87
|
+
try {
|
|
88
|
+
return (await res.json());
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
throw new ExternalAuthError(id, "provider_error", `the ${options.displayName} profile endpoint returned a response ` +
|
|
92
|
+
`that was not valid JSON (${describeError(error)}).`, { cause: error });
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return {
|
|
96
|
+
id,
|
|
97
|
+
displayName: options.displayName,
|
|
98
|
+
defaultScopes: options.scopes ?? options.defaultScopes,
|
|
99
|
+
usesPkce: options.usesPkce ?? false,
|
|
100
|
+
usesNonce: false,
|
|
101
|
+
async buildAuthorizationUrl(input) {
|
|
102
|
+
const url = new URL(options.authorizationEndpoint);
|
|
103
|
+
url.searchParams.set("response_type", "code");
|
|
104
|
+
url.searchParams.set("client_id", options.clientId);
|
|
105
|
+
url.searchParams.set("redirect_uri", input.redirectUri);
|
|
106
|
+
url.searchParams.set("scope", input.scopes.join(scopeSeparator));
|
|
107
|
+
url.searchParams.set("state", input.state);
|
|
108
|
+
for (const [key, value] of Object.entries(options.authorizationParams ?? {})) {
|
|
109
|
+
url.searchParams.set(key, value);
|
|
110
|
+
}
|
|
111
|
+
if (input.codeVerifier) {
|
|
112
|
+
url.searchParams.set("code_challenge", await generateCodeChallenge(input.codeVerifier));
|
|
113
|
+
url.searchParams.set("code_challenge_method", "S256");
|
|
114
|
+
}
|
|
115
|
+
if (input.prompt)
|
|
116
|
+
url.searchParams.set("prompt", input.prompt);
|
|
117
|
+
return url.toString();
|
|
118
|
+
},
|
|
119
|
+
async fetchProfile(input) {
|
|
120
|
+
const tokens = await exchangeCode(input);
|
|
121
|
+
const profile = options.userInfoEndpoint
|
|
122
|
+
? await fetchUserInfo(tokens.accessToken)
|
|
123
|
+
: tokens.raw;
|
|
124
|
+
const mapped = options.mapProfile({ profile, tokens });
|
|
125
|
+
if (typeof mapped.subject !== "string" || mapped.subject.length === 0) {
|
|
126
|
+
throw new ExternalAuthError(id, "provider_error", `the ${options.displayName} profile is missing a stable subject id.`);
|
|
127
|
+
}
|
|
128
|
+
return {
|
|
129
|
+
provider: id,
|
|
130
|
+
subject: mapped.subject,
|
|
131
|
+
email: mapped.email,
|
|
132
|
+
emailVerified: mapped.emailVerified === true,
|
|
133
|
+
name: mapped.name,
|
|
134
|
+
givenName: mapped.givenName,
|
|
135
|
+
familyName: mapped.familyName,
|
|
136
|
+
picture: mapped.picture,
|
|
137
|
+
raw: profile,
|
|
138
|
+
};
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
function oauth2TokenErrorHint(error) {
|
|
143
|
+
if (error === "invalid_client" || error === "unauthorized_client") {
|
|
144
|
+
return `The provider rejected the client credentials — check clientId ` +
|
|
145
|
+
`and clientSecret against the app registration.`;
|
|
146
|
+
}
|
|
147
|
+
if (error === "invalid_grant") {
|
|
148
|
+
return `The provider rejected the code — it expired, was already used, ` +
|
|
149
|
+
`or the redirect URI does not match the registered one. Ask the user ` +
|
|
150
|
+
`to start again.`;
|
|
151
|
+
}
|
|
152
|
+
if (error === "invalid_scope") {
|
|
153
|
+
return `The provider rejected the requested scopes — check the ` +
|
|
154
|
+
`connector's scopes against what the app registration allows.`;
|
|
155
|
+
}
|
|
156
|
+
return `The code may have expired or already been used — ask the user to ` +
|
|
157
|
+
`start again.`;
|
|
158
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generic OpenID Connect connector: discovery-driven endpoints, PKCE + nonce,
|
|
3
|
+
* profile from id_token claims merged with the UserInfo response.
|
|
4
|
+
*
|
|
5
|
+
* @module
|
|
6
|
+
*/
|
|
7
|
+
import type { DiscoveryCache } from "../../client/discovery-cache.js";
|
|
8
|
+
import { type AzpPolicy } from "./_shared.js";
|
|
9
|
+
import type { ExternalProvider } from "./provider.js";
|
|
10
|
+
/** Options for {@link oidcProvider}. */
|
|
11
|
+
export interface OidcProviderOptions {
|
|
12
|
+
/** Provider id used in profiles and error messages. Defaults to `"oidc"`. */
|
|
13
|
+
id?: string;
|
|
14
|
+
/** Human-readable name for buttons and logs. Defaults to `"OpenID Connect"`. */
|
|
15
|
+
displayName?: string;
|
|
16
|
+
/**
|
|
17
|
+
* The provider's issuer URL. Endpoints are resolved from
|
|
18
|
+
* `${issuer}/.well-known/openid-configuration` (with an RFC 8414 fallback)
|
|
19
|
+
* on first use and cached.
|
|
20
|
+
*/
|
|
21
|
+
issuer: string;
|
|
22
|
+
/** The client id registered with the provider. */
|
|
23
|
+
clientId: string;
|
|
24
|
+
/**
|
|
25
|
+
* The client secret, for providers that registered this app as a
|
|
26
|
+
* confidential client (sent via HTTP Basic auth on the token exchange).
|
|
27
|
+
* Omit for public clients — PKCE is always used either way.
|
|
28
|
+
*/
|
|
29
|
+
clientSecret?: string;
|
|
30
|
+
/** Scopes requested by default. Defaults to `["openid", "email", "profile"]`. */
|
|
31
|
+
scopes?: string[];
|
|
32
|
+
/**
|
|
33
|
+
* How the id_token's `azp` claim is checked. Defaults to `"conditional"`
|
|
34
|
+
* (OpenID Connect Core §3.1.3.7). Set `"ignore"` only for a provider that
|
|
35
|
+
* reports a different authorized party than the client doing the exchange —
|
|
36
|
+
* see {@link AzpPolicy}.
|
|
37
|
+
*/
|
|
38
|
+
azp?: AzpPolicy;
|
|
39
|
+
/** Fetch implementation for all provider traffic. Defaults to `globalThis.fetch`. */
|
|
40
|
+
fetch?: typeof fetch;
|
|
41
|
+
/**
|
|
42
|
+
* Cache the discovery document is read from and written to, shared with any
|
|
43
|
+
* other connector given the same instance. Supply one when the connector is
|
|
44
|
+
* rebuilt per request (a server resolving provider config from a database),
|
|
45
|
+
* or discovery is re-fetched on every sign-in leg. `MemoryDiscoveryCache`
|
|
46
|
+
* from `@udibo/oauth2/client` is the ready-made implementation.
|
|
47
|
+
*/
|
|
48
|
+
discoveryCache?: DiscoveryCache;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Creates a connector for any spec-compliant OpenID Connect provider.
|
|
52
|
+
*
|
|
53
|
+
* The exchange reuses the package's `DirectClient` (discovery caching with
|
|
54
|
+
* both well-known paths, Basic client auth, RFC 6749 error mapping) with PKCE
|
|
55
|
+
* always on. The profile is built from the id_token claims, merged with the
|
|
56
|
+
* UserInfo response when the provider advertises a `userinfo_endpoint` (the
|
|
57
|
+
* UserInfo `sub` must match the id_token `sub` per OIDC Core §5.3.2, and a
|
|
58
|
+
* UserInfo failure is non-fatal — the id_token claims already suffice).
|
|
59
|
+
*
|
|
60
|
+
* **id_token validation:** the `iss`, `aud`, `azp` (when the token has multiple
|
|
61
|
+
* audiences or an `azp` claim, unless `azp: "ignore"` is configured), `exp`,
|
|
62
|
+
* and `nonce` claims are checked per OpenID Connect Core §3.1.3.7. The
|
|
63
|
+
* signature is deliberately **not** verified: the token arrives directly from
|
|
64
|
+
* the token endpoint over TLS, where §3.1.3.7 permits relying on TLS server
|
|
65
|
+
* validation instead of signature checks (rule 6). Every other rule —
|
|
66
|
+
* including expiry (rule 9) — still applies. Do not feed this connector
|
|
67
|
+
* id_tokens obtained out-of-band.
|
|
68
|
+
*
|
|
69
|
+
* @example
|
|
70
|
+
* ```ts
|
|
71
|
+
* const provider = oidcProvider({
|
|
72
|
+
* id: "acme",
|
|
73
|
+
* displayName: "Acme SSO",
|
|
74
|
+
* issuer: "https://sso.acme.example",
|
|
75
|
+
* clientId: "my-app",
|
|
76
|
+
* clientSecret: Deno.env.get("ACME_CLIENT_SECRET")!,
|
|
77
|
+
* });
|
|
78
|
+
* const flow = new ExternalAuthFlow({ provider });
|
|
79
|
+
* ```
|
|
80
|
+
*/
|
|
81
|
+
export declare function oidcProvider(options: OidcProviderOptions): ExternalProvider;
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generic OpenID Connect connector: discovery-driven endpoints, PKCE + nonce,
|
|
3
|
+
* profile from id_token claims merged with the UserInfo response.
|
|
4
|
+
*
|
|
5
|
+
* @module
|
|
6
|
+
*/
|
|
7
|
+
import { DirectClient } from "../../client/direct-client.js";
|
|
8
|
+
import { MemoryAuthRequestStorage } from "../../client/storage.js";
|
|
9
|
+
import { isOAuth2Error } from "../../errors.js";
|
|
10
|
+
import { generateCodeChallenge } from "../../utils/pkce.js";
|
|
11
|
+
import { assertIdTokenClaims, describeError, stripTrailingSlash, } from "./_shared.js";
|
|
12
|
+
import { ExternalAuthError } from "./errors.js";
|
|
13
|
+
/**
|
|
14
|
+
* Creates a connector for any spec-compliant OpenID Connect provider.
|
|
15
|
+
*
|
|
16
|
+
* The exchange reuses the package's `DirectClient` (discovery caching with
|
|
17
|
+
* both well-known paths, Basic client auth, RFC 6749 error mapping) with PKCE
|
|
18
|
+
* always on. The profile is built from the id_token claims, merged with the
|
|
19
|
+
* UserInfo response when the provider advertises a `userinfo_endpoint` (the
|
|
20
|
+
* UserInfo `sub` must match the id_token `sub` per OIDC Core §5.3.2, and a
|
|
21
|
+
* UserInfo failure is non-fatal — the id_token claims already suffice).
|
|
22
|
+
*
|
|
23
|
+
* **id_token validation:** the `iss`, `aud`, `azp` (when the token has multiple
|
|
24
|
+
* audiences or an `azp` claim, unless `azp: "ignore"` is configured), `exp`,
|
|
25
|
+
* and `nonce` claims are checked per OpenID Connect Core §3.1.3.7. The
|
|
26
|
+
* signature is deliberately **not** verified: the token arrives directly from
|
|
27
|
+
* the token endpoint over TLS, where §3.1.3.7 permits relying on TLS server
|
|
28
|
+
* validation instead of signature checks (rule 6). Every other rule —
|
|
29
|
+
* including expiry (rule 9) — still applies. Do not feed this connector
|
|
30
|
+
* id_tokens obtained out-of-band.
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* ```ts
|
|
34
|
+
* const provider = oidcProvider({
|
|
35
|
+
* id: "acme",
|
|
36
|
+
* displayName: "Acme SSO",
|
|
37
|
+
* issuer: "https://sso.acme.example",
|
|
38
|
+
* clientId: "my-app",
|
|
39
|
+
* clientSecret: Deno.env.get("ACME_CLIENT_SECRET")!,
|
|
40
|
+
* });
|
|
41
|
+
* const flow = new ExternalAuthFlow({ provider });
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
44
|
+
export function oidcProvider(options) {
|
|
45
|
+
const id = options.id ?? "oidc";
|
|
46
|
+
assertSecureIssuer(id, options.issuer);
|
|
47
|
+
const displayName = options.displayName ?? "OpenID Connect";
|
|
48
|
+
const defaultScopes = options.scopes ?? ["openid", "email", "profile"];
|
|
49
|
+
const fetchImpl = (input, init) => {
|
|
50
|
+
assertSecureIssuer(id, input instanceof Request ? input.url : String(input));
|
|
51
|
+
return (options.fetch ?? globalThis.fetch)(input, {
|
|
52
|
+
...init,
|
|
53
|
+
redirect: "error",
|
|
54
|
+
});
|
|
55
|
+
};
|
|
56
|
+
const authRequestStorage = new MemoryAuthRequestStorage();
|
|
57
|
+
const client = new DirectClient({
|
|
58
|
+
clientId: options.clientId,
|
|
59
|
+
...(options.clientSecret === undefined
|
|
60
|
+
? {}
|
|
61
|
+
: { clientSecret: options.clientSecret }),
|
|
62
|
+
issuer: options.issuer,
|
|
63
|
+
fetch: fetchImpl,
|
|
64
|
+
authRequestStorage,
|
|
65
|
+
discoveryCache: options.discoveryCache,
|
|
66
|
+
});
|
|
67
|
+
async function discover() {
|
|
68
|
+
let meta;
|
|
69
|
+
try {
|
|
70
|
+
meta = await client.discover();
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
throw new ExternalAuthError(id, "configuration", `OIDC discovery failed for issuer "${options.issuer}" ` +
|
|
74
|
+
`(${describeError(error)}). Check that the issuer URL is correct ` +
|
|
75
|
+
`and serves "${wellKnownUrl(options.issuer)}".`, { cause: error });
|
|
76
|
+
}
|
|
77
|
+
if (normalizeIssuer(meta.issuer) !== normalizeIssuer(options.issuer)) {
|
|
78
|
+
throw new ExternalAuthError(id, "configuration", `discovery document "issuer" is "${String(meta.issuer)}" but the configured issuer is "${options.issuer}" — OpenID Connect ` +
|
|
79
|
+
`Discovery §4.3 / RFC 8414 §3.3 require them to be identical. The ` +
|
|
80
|
+
`configured issuer likely points at a proxy or the wrong tenant.`);
|
|
81
|
+
}
|
|
82
|
+
for (const endpoint of [
|
|
83
|
+
meta.authorization_endpoint,
|
|
84
|
+
meta.token_endpoint,
|
|
85
|
+
meta.userinfo_endpoint,
|
|
86
|
+
]) {
|
|
87
|
+
if (endpoint !== undefined)
|
|
88
|
+
assertSecureIssuer(id, endpoint);
|
|
89
|
+
}
|
|
90
|
+
return meta;
|
|
91
|
+
}
|
|
92
|
+
async function exchangeCode(input) {
|
|
93
|
+
const exchangeKey = crypto.randomUUID();
|
|
94
|
+
await authRequestStorage.set(exchangeKey, {
|
|
95
|
+
codeVerifier: input.codeVerifier ?? "",
|
|
96
|
+
createdAt: Date.now(),
|
|
97
|
+
});
|
|
98
|
+
try {
|
|
99
|
+
const { raw } = await client.exchangeAuthorizationCode(input.code, exchangeKey, { redirectUri: input.redirectUri });
|
|
100
|
+
return raw;
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
throw new ExternalAuthError(id, "provider_error", `token exchange failed (${describeError(error)}). ` +
|
|
104
|
+
tokenExchangeHint(error), { cause: error });
|
|
105
|
+
}
|
|
106
|
+
finally {
|
|
107
|
+
await authRequestStorage.delete(exchangeKey);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
function decodeAndValidateIdToken(response, meta, expectedNonce) {
|
|
111
|
+
const idToken = response.id_token;
|
|
112
|
+
if (!idToken) {
|
|
113
|
+
throw new ExternalAuthError(id, "provider_error", `token response contained no id_token. Ensure the "openid" scope ` +
|
|
114
|
+
`is requested and that issuer "${options.issuer}" is an OpenID ` +
|
|
115
|
+
`Connect provider (for plain OAuth2 providers, implement ` +
|
|
116
|
+
`ExternalProvider directly).`);
|
|
117
|
+
}
|
|
118
|
+
let claims;
|
|
119
|
+
try {
|
|
120
|
+
claims = client.decodeIdToken(idToken);
|
|
121
|
+
}
|
|
122
|
+
catch (error) {
|
|
123
|
+
throw new ExternalAuthError(id, "provider_error", `token endpoint returned a malformed id_token ` +
|
|
124
|
+
`(${describeError(error)}).`, { cause: error });
|
|
125
|
+
}
|
|
126
|
+
assertIdTokenClaims({
|
|
127
|
+
provider: id,
|
|
128
|
+
claims,
|
|
129
|
+
issuer: meta.issuer,
|
|
130
|
+
clientId: options.clientId,
|
|
131
|
+
expectedNonce,
|
|
132
|
+
azp: options.azp,
|
|
133
|
+
});
|
|
134
|
+
return claims;
|
|
135
|
+
}
|
|
136
|
+
async function fetchUserInfoClaims(meta, accessToken, idTokenSub) {
|
|
137
|
+
if (!meta.userinfo_endpoint)
|
|
138
|
+
return null;
|
|
139
|
+
try {
|
|
140
|
+
const res = await fetchImpl(meta.userinfo_endpoint, {
|
|
141
|
+
headers: {
|
|
142
|
+
Authorization: `Bearer ${accessToken}`,
|
|
143
|
+
Accept: "application/json",
|
|
144
|
+
},
|
|
145
|
+
});
|
|
146
|
+
if (!res.ok) {
|
|
147
|
+
await res.body?.cancel();
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
const info = (await res.json());
|
|
151
|
+
return info.sub === idTokenSub ? info : null;
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return {
|
|
158
|
+
id,
|
|
159
|
+
displayName,
|
|
160
|
+
defaultScopes,
|
|
161
|
+
usesPkce: true,
|
|
162
|
+
usesNonce: true,
|
|
163
|
+
async buildAuthorizationUrl(input) {
|
|
164
|
+
const meta = await discover();
|
|
165
|
+
if (!meta.authorization_endpoint) {
|
|
166
|
+
throw new ExternalAuthError(id, "configuration", `issuer "${options.issuer}" metadata does not advertise an ` +
|
|
167
|
+
`authorization_endpoint — it cannot drive a browser sign-in.`);
|
|
168
|
+
}
|
|
169
|
+
const url = new URL(meta.authorization_endpoint);
|
|
170
|
+
url.searchParams.set("response_type", "code");
|
|
171
|
+
url.searchParams.set("client_id", options.clientId);
|
|
172
|
+
url.searchParams.set("redirect_uri", input.redirectUri);
|
|
173
|
+
url.searchParams.set("scope", input.scopes.join(" "));
|
|
174
|
+
url.searchParams.set("state", input.state);
|
|
175
|
+
if (input.nonce)
|
|
176
|
+
url.searchParams.set("nonce", input.nonce);
|
|
177
|
+
if (input.codeVerifier) {
|
|
178
|
+
url.searchParams.set("code_challenge", await generateCodeChallenge(input.codeVerifier));
|
|
179
|
+
url.searchParams.set("code_challenge_method", "S256");
|
|
180
|
+
}
|
|
181
|
+
if (input.prompt)
|
|
182
|
+
url.searchParams.set("prompt", input.prompt);
|
|
183
|
+
return url.toString();
|
|
184
|
+
},
|
|
185
|
+
async fetchProfile(input) {
|
|
186
|
+
const meta = await discover();
|
|
187
|
+
const response = await exchangeCode(input);
|
|
188
|
+
const claims = decodeAndValidateIdToken(response, meta, input.nonce);
|
|
189
|
+
const userInfo = await fetchUserInfoClaims(meta, response.access_token, claims.sub);
|
|
190
|
+
return profileFromOidcClaims(id, { ...claims, ...userInfo });
|
|
191
|
+
},
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
function profileFromOidcClaims(provider, claims) {
|
|
195
|
+
const subject = claims.sub;
|
|
196
|
+
if (typeof subject !== "string" || subject.length === 0) {
|
|
197
|
+
throw new ExternalAuthError(provider, "provider_error", `id_token is missing the required "sub" claim.`);
|
|
198
|
+
}
|
|
199
|
+
return {
|
|
200
|
+
provider,
|
|
201
|
+
subject,
|
|
202
|
+
email: asString(claims.email),
|
|
203
|
+
emailVerified: claims.email_verified === true,
|
|
204
|
+
name: asString(claims.name),
|
|
205
|
+
givenName: asString(claims.given_name),
|
|
206
|
+
familyName: asString(claims.family_name),
|
|
207
|
+
picture: asString(claims.picture),
|
|
208
|
+
raw: claims,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
function asString(value) {
|
|
212
|
+
return typeof value === "string" ? value : undefined;
|
|
213
|
+
}
|
|
214
|
+
function wellKnownUrl(issuer) {
|
|
215
|
+
return `${stripTrailingSlash(issuer)}/.well-known/openid-configuration`;
|
|
216
|
+
}
|
|
217
|
+
function normalizeIssuer(value) {
|
|
218
|
+
return stripTrailingSlash(typeof value === "string" ? value : "");
|
|
219
|
+
}
|
|
220
|
+
function assertSecureIssuer(id, issuer) {
|
|
221
|
+
let url;
|
|
222
|
+
try {
|
|
223
|
+
url = new URL(issuer);
|
|
224
|
+
}
|
|
225
|
+
catch {
|
|
226
|
+
throw new ExternalAuthError(id, "configuration", `issuer "${issuer}" is not a valid absolute URL.`);
|
|
227
|
+
}
|
|
228
|
+
const loopback = url.hostname === "localhost" ||
|
|
229
|
+
url.hostname === "127.0.0.1" || url.hostname === "[::1]";
|
|
230
|
+
if (url.protocol !== "https:" && !(url.protocol === "http:" && loopback)) {
|
|
231
|
+
throw new ExternalAuthError(id, "configuration", `issuer "${issuer}" must use https — this connector trusts the ` +
|
|
232
|
+
`id_token because it arrives directly from the token endpoint over ` +
|
|
233
|
+
`TLS, so a non-TLS issuer removes that guarantee. Only localhost may ` +
|
|
234
|
+
`use http, for development.`);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
function tokenExchangeHint(error) {
|
|
238
|
+
const code = isOAuth2Error(error) ? error.extensions.error : undefined;
|
|
239
|
+
if (code === "invalid_client") {
|
|
240
|
+
return `The provider rejected the client credentials — check clientId ` +
|
|
241
|
+
`and clientSecret against the provider's app registration.`;
|
|
242
|
+
}
|
|
243
|
+
if (code === "invalid_grant") {
|
|
244
|
+
return `The provider rejected the code — most often the redirectUri ` +
|
|
245
|
+
`does not exactly match the URI registered with the provider, or the ` +
|
|
246
|
+
`code expired or was already used.`;
|
|
247
|
+
}
|
|
248
|
+
return `Check the provider's app registration (client credentials and ` +
|
|
249
|
+
`redirect URI) if this persists.`;
|
|
250
|
+
}
|