@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,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Internal helpers shared by the external-provider connectors.
|
|
3
|
+
*
|
|
4
|
+
* Not part of the public API — reach for a connector from
|
|
5
|
+
* `@udibo/oauth2/identity/external` instead, which re-exports the one type an
|
|
6
|
+
* adopter configures ({@link AzpPolicy}). These live here so the same failure
|
|
7
|
+
* reads the same way whichever connector hit it: one error renderer, one
|
|
8
|
+
* error-body reader, one provider-text sanitizer, and one id_token claim gate
|
|
9
|
+
* rather than a copy per provider. The token exchange itself is
|
|
10
|
+
* `_token-exchange.ts`.
|
|
11
|
+
*
|
|
12
|
+
* @module
|
|
13
|
+
*/
|
|
14
|
+
import { isOAuth2Error } from "../../errors.js";
|
|
15
|
+
import { timingSafeEqualString } from "../../utils/crypto.js";
|
|
16
|
+
import { PROVIDER_TEXT_MAX_LENGTH, sanitizeProviderText, } from "../../utils/text.js";
|
|
17
|
+
import { ExternalAuthError } from "./errors.js";
|
|
18
|
+
export { PROVIDER_TEXT_MAX_LENGTH, sanitizeProviderText };
|
|
19
|
+
/**
|
|
20
|
+
* Clock-skew allowance, in seconds, when checking an id_token's `exp` claim.
|
|
21
|
+
* The default for {@link assertIdTokenClaims}.
|
|
22
|
+
*/
|
|
23
|
+
export const ID_TOKEN_EXPIRY_LEEWAY_SECONDS = 120;
|
|
24
|
+
/**
|
|
25
|
+
* Renders a thrown value for an error message, unwrapping an OAuth2 error into
|
|
26
|
+
* its `error` / `error_description` extensions so a provider-reported failure
|
|
27
|
+
* is quoted rather than buried in a generic message. Every branch is
|
|
28
|
+
* {@link sanitizeProviderText}-bounded: the thrown value routinely carries
|
|
29
|
+
* provider bytes — a JSON parse error quotes the body it choked on, and an
|
|
30
|
+
* OAuth2 error carries a description the provider wrote.
|
|
31
|
+
*/
|
|
32
|
+
export function describeError(error) {
|
|
33
|
+
if (isOAuth2Error(error)) {
|
|
34
|
+
const code = sanitizeProviderText(error.extensions.error ?? "server_error");
|
|
35
|
+
const description = sanitizeProviderText(error.extensions.error_description ?? error.message);
|
|
36
|
+
return description && description !== code
|
|
37
|
+
? `${code}: ${description}`
|
|
38
|
+
: code;
|
|
39
|
+
}
|
|
40
|
+
return sanitizeProviderText(error instanceof Error ? error.message : String(error));
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Reads a failed response's body into a bounded, single-line `": <text>"`
|
|
44
|
+
* suffix to append to an error message, or `""` when the body is empty or
|
|
45
|
+
* unreadable. Consumes the body, so the caller must not read it again.
|
|
46
|
+
*/
|
|
47
|
+
export async function readErrorBody(res) {
|
|
48
|
+
try {
|
|
49
|
+
const snippet = sanitizeProviderText(await res.text());
|
|
50
|
+
return snippet ? `: ${snippet}` : "";
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return "";
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Drops one trailing `"/"` so issuer URLs compare and concatenate consistently
|
|
58
|
+
* (`"https://x/"` and `"https://x"` identify the same issuer).
|
|
59
|
+
*/
|
|
60
|
+
export function stripTrailingSlash(value) {
|
|
61
|
+
return value.endsWith("/") ? value.slice(0, -1) : value;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Enforces the id_token claim rules of OpenID Connect Core §3.1.3.7 that do not
|
|
65
|
+
* depend on the signature: `iss`, `aud`, `azp`, `exp`, and `nonce`, in that
|
|
66
|
+
* order. Callers verify the signature (or establish the token's provenance)
|
|
67
|
+
* themselves before calling this.
|
|
68
|
+
*
|
|
69
|
+
* Throws {@link ExternalAuthError} with code `nonce_mismatch` when the `nonce`
|
|
70
|
+
* claim does not match, and `provider_error` for every other rule.
|
|
71
|
+
*/
|
|
72
|
+
export function assertIdTokenClaims(input) {
|
|
73
|
+
const { provider, claims, issuer, clientId, expectedNonce } = input;
|
|
74
|
+
if (claims.iss !== issuer) {
|
|
75
|
+
throw new ExternalAuthError(provider, "provider_error", `id_token "iss" is "${String(claims.iss)}" but the expected issuer is ` +
|
|
76
|
+
`"${issuer}" — the token was issued by a different provider, or the ` +
|
|
77
|
+
`configured issuer points at a proxy or the wrong tenant.`);
|
|
78
|
+
}
|
|
79
|
+
const aud = claims.aud;
|
|
80
|
+
const audienceOk = aud === clientId ||
|
|
81
|
+
(Array.isArray(aud) && aud.includes(clientId));
|
|
82
|
+
if (!audienceOk) {
|
|
83
|
+
throw new ExternalAuthError(provider, "provider_error", `id_token "aud" (${JSON.stringify(aud)}) does not include clientId ` +
|
|
84
|
+
`"${clientId}" — the token was issued to a different client ` +
|
|
85
|
+
`registration.`);
|
|
86
|
+
}
|
|
87
|
+
const audienceCount = Array.isArray(aud) ? aud.length : 1;
|
|
88
|
+
if ((input.azp ?? "conditional") === "conditional" &&
|
|
89
|
+
(audienceCount > 1 || claims.azp !== undefined) &&
|
|
90
|
+
claims.azp !== clientId) {
|
|
91
|
+
throw new ExternalAuthError(provider, "provider_error", `id_token "azp" (${JSON.stringify(claims.azp)}) is not this clientId ` +
|
|
92
|
+
`"${clientId}" — the token's authorized party is a different client ` +
|
|
93
|
+
`(OpenID Connect Core §3.1.3.7).`);
|
|
94
|
+
}
|
|
95
|
+
const exp = claims.exp;
|
|
96
|
+
const leewaySeconds = input.leewaySeconds ?? ID_TOKEN_EXPIRY_LEEWAY_SECONDS;
|
|
97
|
+
if (typeof exp !== "number" || Date.now() / 1000 > exp + leewaySeconds) {
|
|
98
|
+
throw new ExternalAuthError(provider, "provider_error", `id_token is expired or has no "exp" claim (exp=${JSON.stringify(exp)}) — reject per OpenID Connect Core §3.1.3.7. Ask the user to start ` +
|
|
99
|
+
`again.`);
|
|
100
|
+
}
|
|
101
|
+
if (expectedNonce !== undefined &&
|
|
102
|
+
!(typeof claims.nonce === "string" &&
|
|
103
|
+
timingSafeEqualString(claims.nonce, expectedNonce))) {
|
|
104
|
+
throw new ExternalAuthError(provider, "nonce_mismatch", `id_token "nonce" does not match this sign-in attempt — possible token ` +
|
|
105
|
+
`replay or a mixed-up callback. Ask the user to start again.`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `authorization_code` token exchange the plain-OAuth2 connectors post
|
|
3
|
+
* through — {@link githubProvider}, {@link oauth2Provider} (and its
|
|
4
|
+
* {@link discordProvider} preset), and {@link appleProvider}.
|
|
5
|
+
* {@link oidcProvider}, and {@link googleProvider} on top of it, exchange via
|
|
6
|
+
* `OAuth2Client` instead and do not reach this module.
|
|
7
|
+
*
|
|
8
|
+
* Not part of the public API. Split out of `_shared.ts` so the ladder — the one
|
|
9
|
+
* place a provider's bytes become an error message or a token — sits next to
|
|
10
|
+
* the tests that pin it.
|
|
11
|
+
*
|
|
12
|
+
* @module
|
|
13
|
+
*/
|
|
14
|
+
/** Response bytes accepted from a token endpoint before the exchange is refused. */
|
|
15
|
+
export declare const MAX_TOKEN_RESPONSE_BYTES: number;
|
|
16
|
+
/** Wall-clock budget for the exchange, covering the connect and the body read. */
|
|
17
|
+
export declare const TOKEN_EXCHANGE_TIMEOUT_MS = 10000;
|
|
18
|
+
/** Input for {@link runTokenExchange}. */
|
|
19
|
+
export interface TokenExchangeInput {
|
|
20
|
+
/** Provider id carried on every thrown {@link ExternalAuthError}. */
|
|
21
|
+
provider: string;
|
|
22
|
+
/** Human-readable provider name used in the error messages. */
|
|
23
|
+
displayName: string;
|
|
24
|
+
/** The provider's token endpoint. */
|
|
25
|
+
endpoint: string;
|
|
26
|
+
/** The form body to POST; client authentication must already be in it. */
|
|
27
|
+
body: URLSearchParams;
|
|
28
|
+
/**
|
|
29
|
+
* The token-response field the connector cannot proceed without — usually
|
|
30
|
+
* `"access_token"`, or `"id_token"` for a connector that only reads claims.
|
|
31
|
+
*/
|
|
32
|
+
requiredField: string;
|
|
33
|
+
/**
|
|
34
|
+
* Fetch implementation for the exchange. A wrapper **must forward the whole
|
|
35
|
+
* `RequestInit`** — `redirect` and `signal` in particular. They are the only
|
|
36
|
+
* channel the redirect refusal and the deadline reach the transport, so a
|
|
37
|
+
* wrapper that rebuilds `init` from scratch silently reopens the credential
|
|
38
|
+
* replay hole. `createGuardedFetch` forwards both.
|
|
39
|
+
*/
|
|
40
|
+
fetch: typeof fetch;
|
|
41
|
+
/** Guidance appended when a failure carries no OAuth2 error code. */
|
|
42
|
+
httpErrorHint: string;
|
|
43
|
+
/** Guidance appended for the OAuth2 error code the provider reported. */
|
|
44
|
+
tokenErrorHint: (code: string | undefined) => string;
|
|
45
|
+
/** Exchange deadline. Defaults to {@link TOKEN_EXCHANGE_TIMEOUT_MS}. */
|
|
46
|
+
timeoutMs?: number;
|
|
47
|
+
}
|
|
48
|
+
/** A token response that cleared every step of {@link runTokenExchange}. */
|
|
49
|
+
export interface TokenExchangeResult {
|
|
50
|
+
/** The value of {@link TokenExchangeInput.requiredField}, non-empty. */
|
|
51
|
+
value: string;
|
|
52
|
+
/** The full token-response body, for provider-specific fields. */
|
|
53
|
+
raw: Record<string, unknown>;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Posts the authorization code and returns a token response known to be a JSON
|
|
57
|
+
* object carrying `requiredField`. Every failure throws
|
|
58
|
+
* {@link ExternalAuthError} `provider_error`; a redirect is refused rather than
|
|
59
|
+
* followed, so the client credentials are never replayed to another host.
|
|
60
|
+
*/
|
|
61
|
+
export declare function runTokenExchange(input: TokenExchangeInput): Promise<TokenExchangeResult>;
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `authorization_code` token exchange the plain-OAuth2 connectors post
|
|
3
|
+
* through — {@link githubProvider}, {@link oauth2Provider} (and its
|
|
4
|
+
* {@link discordProvider} preset), and {@link appleProvider}.
|
|
5
|
+
* {@link oidcProvider}, and {@link googleProvider} on top of it, exchange via
|
|
6
|
+
* `OAuth2Client` instead and do not reach this module.
|
|
7
|
+
*
|
|
8
|
+
* Not part of the public API. Split out of `_shared.ts` so the ladder — the one
|
|
9
|
+
* place a provider's bytes become an error message or a token — sits next to
|
|
10
|
+
* the tests that pin it.
|
|
11
|
+
*
|
|
12
|
+
* @module
|
|
13
|
+
*/
|
|
14
|
+
import { describeError, sanitizeProviderText } from "./_shared.js";
|
|
15
|
+
import { ExternalAuthError } from "./errors.js";
|
|
16
|
+
/** Response bytes accepted from a token endpoint before the exchange is refused. */
|
|
17
|
+
export const MAX_TOKEN_RESPONSE_BYTES = 64 * 1024;
|
|
18
|
+
/** Wall-clock budget for the exchange, covering the connect and the body read. */
|
|
19
|
+
export const TOKEN_EXCHANGE_TIMEOUT_MS = 10_000;
|
|
20
|
+
/**
|
|
21
|
+
* Posts the authorization code and returns a token response known to be a JSON
|
|
22
|
+
* object carrying `requiredField`. Every failure throws
|
|
23
|
+
* {@link ExternalAuthError} `provider_error`; a redirect is refused rather than
|
|
24
|
+
* followed, so the client credentials are never replayed to another host.
|
|
25
|
+
*/
|
|
26
|
+
export async function runTokenExchange(input) {
|
|
27
|
+
const { provider, displayName, requiredField } = input;
|
|
28
|
+
const fail = (message, options) => new ExternalAuthError(provider, "provider_error", message, options);
|
|
29
|
+
let res;
|
|
30
|
+
try {
|
|
31
|
+
res = await input.fetch(input.endpoint, {
|
|
32
|
+
method: "POST",
|
|
33
|
+
headers: {
|
|
34
|
+
Accept: "application/json",
|
|
35
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
36
|
+
},
|
|
37
|
+
body: input.body,
|
|
38
|
+
redirect: "manual",
|
|
39
|
+
signal: AbortSignal.timeout(input.timeoutMs ?? TOKEN_EXCHANGE_TIMEOUT_MS),
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
throw fail(`could not reach the ${displayName} token endpoint ` +
|
|
44
|
+
`(${describeError(error)}). Try again.`, { cause: error });
|
|
45
|
+
}
|
|
46
|
+
if (isRedirect(res)) {
|
|
47
|
+
await res.body?.cancel();
|
|
48
|
+
throw fail(`token endpoint answered with a redirect (HTTP ${res.status}), which ` +
|
|
49
|
+
`this exchange refuses to follow — replaying the client credentials ` +
|
|
50
|
+
`to another host would leak them. Point the connector at the ` +
|
|
51
|
+
`${displayName} token endpoint directly.`);
|
|
52
|
+
}
|
|
53
|
+
let body = { text: "", oversized: false };
|
|
54
|
+
let readError;
|
|
55
|
+
try {
|
|
56
|
+
body = await readBoundedBody(res);
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
readError = error;
|
|
60
|
+
}
|
|
61
|
+
if (!res.ok) {
|
|
62
|
+
const reported = reportedOAuth2Error(safeParseJson(body.text));
|
|
63
|
+
if (reported) {
|
|
64
|
+
throw fail(`token endpoint responded with HTTP ${res.status}: ` +
|
|
65
|
+
`"${reported.code}"${reported.description}${reported.uri}. ` +
|
|
66
|
+
`${input.tokenErrorHint(reported.code)} ${input.httpErrorHint}`);
|
|
67
|
+
}
|
|
68
|
+
throw fail(`token endpoint responded with HTTP ${res.status}` +
|
|
69
|
+
`${errorBodySuffix(body.text)}${body.oversized ? " (truncated)" : ""}` +
|
|
70
|
+
`. ${input.httpErrorHint}`);
|
|
71
|
+
}
|
|
72
|
+
if (readError) {
|
|
73
|
+
throw fail(`the ${displayName} token endpoint response could not be read ` +
|
|
74
|
+
`(${describeError(readError)}).`, { cause: readError });
|
|
75
|
+
}
|
|
76
|
+
if (body.oversized) {
|
|
77
|
+
throw fail(`the ${displayName} token endpoint response exceeded ` +
|
|
78
|
+
`${MAX_TOKEN_RESPONSE_BYTES} bytes — refusing to parse a token ` +
|
|
79
|
+
`response this large.`);
|
|
80
|
+
}
|
|
81
|
+
let parsed;
|
|
82
|
+
try {
|
|
83
|
+
parsed = JSON.parse(body.text);
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
throw fail(`the ${displayName} token endpoint returned ` +
|
|
87
|
+
`${describeMediaType(res)} that was not valid JSON ` +
|
|
88
|
+
`(${describeError(error)}).`, { cause: error });
|
|
89
|
+
}
|
|
90
|
+
if (!isJsonObject(parsed)) {
|
|
91
|
+
throw fail(`the ${displayName} token endpoint returned ` +
|
|
92
|
+
`${describeJsonValue(parsed)} where a JSON object was required.`);
|
|
93
|
+
}
|
|
94
|
+
const reported = reportedOAuth2Error(parsed);
|
|
95
|
+
const value = parsed[requiredField];
|
|
96
|
+
if (reported || typeof value !== "string" || value.length === 0) {
|
|
97
|
+
const code = reported?.code ?? `missing ${requiredField}`;
|
|
98
|
+
throw fail(`token exchange failed: "${code}"${reported?.description ?? ""}` +
|
|
99
|
+
`${reported?.uri ?? ""}. ${input.tokenErrorHint(reported?.code)}`);
|
|
100
|
+
}
|
|
101
|
+
return { value, raw: parsed };
|
|
102
|
+
}
|
|
103
|
+
async function readBoundedBody(res) {
|
|
104
|
+
if (!res.body)
|
|
105
|
+
return { text: "", oversized: false };
|
|
106
|
+
const reader = res.body.getReader();
|
|
107
|
+
const chunks = [];
|
|
108
|
+
let size = 0;
|
|
109
|
+
let oversized = false;
|
|
110
|
+
try {
|
|
111
|
+
while (true) {
|
|
112
|
+
const { done, value } = await reader.read();
|
|
113
|
+
if (done)
|
|
114
|
+
break;
|
|
115
|
+
if (size + value.byteLength > MAX_TOKEN_RESPONSE_BYTES) {
|
|
116
|
+
oversized = true;
|
|
117
|
+
break;
|
|
118
|
+
}
|
|
119
|
+
chunks.push(value);
|
|
120
|
+
size += value.byteLength;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
finally {
|
|
124
|
+
await reader.cancel().catch(() => { });
|
|
125
|
+
}
|
|
126
|
+
const buffer = new Uint8Array(size);
|
|
127
|
+
let offset = 0;
|
|
128
|
+
for (const chunk of chunks) {
|
|
129
|
+
buffer.set(chunk, offset);
|
|
130
|
+
offset += chunk.byteLength;
|
|
131
|
+
}
|
|
132
|
+
return { text: new TextDecoder().decode(buffer), oversized };
|
|
133
|
+
}
|
|
134
|
+
function reportedOAuth2Error(value) {
|
|
135
|
+
if (!isJsonObject(value))
|
|
136
|
+
return null;
|
|
137
|
+
const code = sanitizeProviderText(asString(value.error) ?? "");
|
|
138
|
+
if (!code)
|
|
139
|
+
return null;
|
|
140
|
+
const description = sanitizeProviderText(asString(value.error_description) ?? "");
|
|
141
|
+
const uri = sanitizeProviderText(asString(value.error_uri) ?? "");
|
|
142
|
+
return {
|
|
143
|
+
code,
|
|
144
|
+
description: description ? ` (${description})` : "",
|
|
145
|
+
uri: uri ? ` [${uri}]` : "",
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
function asString(value) {
|
|
149
|
+
return typeof value === "string" ? value : undefined;
|
|
150
|
+
}
|
|
151
|
+
function errorBodySuffix(text) {
|
|
152
|
+
const snippet = sanitizeProviderText(text);
|
|
153
|
+
return snippet ? `: ${snippet}` : "";
|
|
154
|
+
}
|
|
155
|
+
function safeParseJson(text) {
|
|
156
|
+
try {
|
|
157
|
+
return JSON.parse(text);
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
return undefined;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
function isRedirect(res) {
|
|
164
|
+
return res.status >= 300 && res.status < 400;
|
|
165
|
+
}
|
|
166
|
+
function isJsonObject(value) {
|
|
167
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
168
|
+
}
|
|
169
|
+
function describeJsonValue(value) {
|
|
170
|
+
if (value === null)
|
|
171
|
+
return "JSON null";
|
|
172
|
+
if (Array.isArray(value))
|
|
173
|
+
return "a JSON array";
|
|
174
|
+
return `a JSON ${typeof value}`;
|
|
175
|
+
}
|
|
176
|
+
function describeMediaType(res) {
|
|
177
|
+
const contentType = sanitizeProviderText(res.headers.get("content-type")?.split(";")[0] ?? "");
|
|
178
|
+
return contentType ? `a "${contentType}" response` : "a response";
|
|
179
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sign in with Apple client-secret generation. Apple's "client secret" is not a
|
|
3
|
+
* static string but a short-lived ES256 JWT the app signs from its `.p8`
|
|
4
|
+
* private key — {@link generateAppleClientSecret} builds one,
|
|
5
|
+
* {@link createAppleClientSecretFactory} caches and renews it before expiry.
|
|
6
|
+
*
|
|
7
|
+
* @module
|
|
8
|
+
*/
|
|
9
|
+
/** Apple's audience for the client-secret JWT. */
|
|
10
|
+
export declare const APPLE_AUDIENCE = "https://appleid.apple.com";
|
|
11
|
+
/** Apple's hard cap on the client secret's lifetime: 6 months, in seconds. */
|
|
12
|
+
export declare const APPLE_CLIENT_SECRET_MAX_TTL_SECONDS = 15777000;
|
|
13
|
+
/** Default client-secret lifetime: ~180 days, comfortably under Apple's cap. */
|
|
14
|
+
export declare const DEFAULT_APPLE_CLIENT_SECRET_TTL_SECONDS = 15552000;
|
|
15
|
+
/** The Apple developer key material a client secret is signed from. */
|
|
16
|
+
export interface AppleClientSecretOptions {
|
|
17
|
+
/**
|
|
18
|
+
* The 10-character Apple Team ID (`iss`). Found top-right in the Apple
|
|
19
|
+
* Developer account membership page — not the App ID.
|
|
20
|
+
*/
|
|
21
|
+
teamId: string;
|
|
22
|
+
/**
|
|
23
|
+
* The Key ID (`kid`) of the `.p8` signing key, from Certificates,
|
|
24
|
+
* Identifiers & Profiles → Keys.
|
|
25
|
+
*/
|
|
26
|
+
keyId: string;
|
|
27
|
+
/**
|
|
28
|
+
* The Services ID (`sub`), i.e. the client id the token is for — the
|
|
29
|
+
* identifier configured for "Sign in with Apple" on the web, not the app's
|
|
30
|
+
* bundle id.
|
|
31
|
+
*/
|
|
32
|
+
clientId: string;
|
|
33
|
+
/**
|
|
34
|
+
* The PKCS#8 PEM contents of the `.p8` private key downloaded from Apple
|
|
35
|
+
* (downloadable once). Both the full `-----BEGIN PRIVATE KEY-----` block and
|
|
36
|
+
* the bare base64 body are accepted.
|
|
37
|
+
*/
|
|
38
|
+
privateKey: string;
|
|
39
|
+
/**
|
|
40
|
+
* Lifetime of the generated secret in seconds. Defaults to
|
|
41
|
+
* {@link DEFAULT_APPLE_CLIENT_SECRET_TTL_SECONDS}; must not exceed
|
|
42
|
+
* {@link APPLE_CLIENT_SECRET_MAX_TTL_SECONDS}.
|
|
43
|
+
*/
|
|
44
|
+
expiresInSeconds?: number;
|
|
45
|
+
/** Provider id used in error messages. Defaults to `"apple"`. */
|
|
46
|
+
providerId?: string;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Generate a single Sign in with Apple client secret: an ES256 JWT with
|
|
50
|
+
* `iss` = team id, `sub` = client id (Services ID), `aud` = Apple, and an `exp`
|
|
51
|
+
* bounded by {@link APPLE_CLIENT_SECRET_MAX_TTL_SECONDS}. Prefer
|
|
52
|
+
* {@link createAppleClientSecretFactory} in a long-lived process so the secret
|
|
53
|
+
* is cached and renewed rather than re-signed on every sign-in.
|
|
54
|
+
*
|
|
55
|
+
* @throws {ExternalAuthError} `configuration` naming the offending input when
|
|
56
|
+
* the team id / key id / client id is empty, the `.p8` is not a valid P-256
|
|
57
|
+
* key, or the requested TTL exceeds Apple's cap.
|
|
58
|
+
*
|
|
59
|
+
* @example
|
|
60
|
+
* ```ts
|
|
61
|
+
* const secret = await generateAppleClientSecret({
|
|
62
|
+
* teamId: "ABCDE12345",
|
|
63
|
+
* keyId: "KEY1234567",
|
|
64
|
+
* clientId: "com.example.web",
|
|
65
|
+
* privateKey: Deno.env.get("APPLE_PRIVATE_KEY")!,
|
|
66
|
+
* });
|
|
67
|
+
* ```
|
|
68
|
+
*/
|
|
69
|
+
export declare function generateAppleClientSecret(options: AppleClientSecretOptions): Promise<string>;
|
|
70
|
+
/** A cached client-secret source: returns a valid secret, re-signing lazily. */
|
|
71
|
+
export type AppleClientSecretFactory = () => Promise<string>;
|
|
72
|
+
/**
|
|
73
|
+
* Build a self-renewing Apple client-secret source. It signs a secret with
|
|
74
|
+
* {@link generateAppleClientSecret} on first call, caches it, and re-signs
|
|
75
|
+
* automatically once the cached secret is within `renewBeforeSeconds` of expiry
|
|
76
|
+
* — so a long-lived {@link appleProvider} never presents an expired assertion.
|
|
77
|
+
* The key material is validated on the first call, surfacing a misconfiguration
|
|
78
|
+
* as a `configuration` {@link ExternalAuthError}.
|
|
79
|
+
*/
|
|
80
|
+
export declare function createAppleClientSecretFactory(options: AppleClientSecretOptions & {
|
|
81
|
+
renewBeforeSeconds?: number;
|
|
82
|
+
}): AppleClientSecretFactory;
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sign in with Apple client-secret generation. Apple's "client secret" is not a
|
|
3
|
+
* static string but a short-lived ES256 JWT the app signs from its `.p8`
|
|
4
|
+
* private key — {@link generateAppleClientSecret} builds one,
|
|
5
|
+
* {@link createAppleClientSecretFactory} caches and renews it before expiry.
|
|
6
|
+
*
|
|
7
|
+
* @module
|
|
8
|
+
*/
|
|
9
|
+
import { decodeBase64 } from "../../deps/jsr.io/@std/encoding/1.0.11/base64.js";
|
|
10
|
+
import { toArrayBuffer } from "../../utils/_buffer.js";
|
|
11
|
+
import { base64urlEncode } from "../../utils/crypto.js";
|
|
12
|
+
import { ExternalAuthError } from "./errors.js";
|
|
13
|
+
/** Apple's audience for the client-secret JWT. */
|
|
14
|
+
export const APPLE_AUDIENCE = "https://appleid.apple.com";
|
|
15
|
+
/** Apple's hard cap on the client secret's lifetime: 6 months, in seconds. */
|
|
16
|
+
export const APPLE_CLIENT_SECRET_MAX_TTL_SECONDS = 15777000;
|
|
17
|
+
/** Default client-secret lifetime: ~180 days, comfortably under Apple's cap. */
|
|
18
|
+
export const DEFAULT_APPLE_CLIENT_SECRET_TTL_SECONDS = 15552000;
|
|
19
|
+
const encoder = new TextEncoder();
|
|
20
|
+
const ES256_IMPORT = { name: "ECDSA", namedCurve: "P-256" };
|
|
21
|
+
const ES256_SIGN = { name: "ECDSA", hash: "SHA-256" };
|
|
22
|
+
function configError(providerId, message) {
|
|
23
|
+
return new ExternalAuthError(providerId, "configuration", message);
|
|
24
|
+
}
|
|
25
|
+
function requireNonEmpty(providerId, value, field) {
|
|
26
|
+
const trimmed = value?.trim?.() ?? "";
|
|
27
|
+
if (!trimmed) {
|
|
28
|
+
throw configError(providerId, `Apple ${field} is empty — set it from the Apple Developer console.`);
|
|
29
|
+
}
|
|
30
|
+
return trimmed;
|
|
31
|
+
}
|
|
32
|
+
function pemToPkcs8(providerId, pem) {
|
|
33
|
+
const body = pem
|
|
34
|
+
.replace(/-----BEGIN [^-]+-----/g, "")
|
|
35
|
+
.replace(/-----END [^-]+-----/g, "")
|
|
36
|
+
.replace(/\s+/g, "");
|
|
37
|
+
if (!body) {
|
|
38
|
+
throw configError(providerId, `the Apple private key (.p8) is empty. Paste the PKCS#8 PEM contents ` +
|
|
39
|
+
`of the AuthKey_*.p8 file Apple lets you download once.`);
|
|
40
|
+
}
|
|
41
|
+
try {
|
|
42
|
+
return decodeBase64(body);
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
throw configError(providerId, `the Apple private key (.p8) is not valid base64/PEM ` +
|
|
46
|
+
`(${error instanceof Error ? error.message : String(error)}).`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
async function importPrivateKey(providerId, pem) {
|
|
50
|
+
const der = pemToPkcs8(providerId, pem);
|
|
51
|
+
try {
|
|
52
|
+
return await crypto.subtle.importKey("pkcs8", toArrayBuffer(der), ES256_IMPORT, false, ["sign"]);
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
throw configError(providerId, `the Apple private key (.p8) could not be imported as a P-256 (ES256) ` +
|
|
56
|
+
`key (${error instanceof Error ? error.message : String(error)}). ` +
|
|
57
|
+
`Confirm you downloaded a "Sign in with Apple" key and pasted it ` +
|
|
58
|
+
`unmodified.`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Generate a single Sign in with Apple client secret: an ES256 JWT with
|
|
63
|
+
* `iss` = team id, `sub` = client id (Services ID), `aud` = Apple, and an `exp`
|
|
64
|
+
* bounded by {@link APPLE_CLIENT_SECRET_MAX_TTL_SECONDS}. Prefer
|
|
65
|
+
* {@link createAppleClientSecretFactory} in a long-lived process so the secret
|
|
66
|
+
* is cached and renewed rather than re-signed on every sign-in.
|
|
67
|
+
*
|
|
68
|
+
* @throws {ExternalAuthError} `configuration` naming the offending input when
|
|
69
|
+
* the team id / key id / client id is empty, the `.p8` is not a valid P-256
|
|
70
|
+
* key, or the requested TTL exceeds Apple's cap.
|
|
71
|
+
*
|
|
72
|
+
* @example
|
|
73
|
+
* ```ts
|
|
74
|
+
* const secret = await generateAppleClientSecret({
|
|
75
|
+
* teamId: "ABCDE12345",
|
|
76
|
+
* keyId: "KEY1234567",
|
|
77
|
+
* clientId: "com.example.web",
|
|
78
|
+
* privateKey: Deno.env.get("APPLE_PRIVATE_KEY")!,
|
|
79
|
+
* });
|
|
80
|
+
* ```
|
|
81
|
+
*/
|
|
82
|
+
export async function generateAppleClientSecret(options) {
|
|
83
|
+
const providerId = options.providerId ?? "apple";
|
|
84
|
+
const teamId = requireNonEmpty(providerId, options.teamId, "team id");
|
|
85
|
+
const keyId = requireNonEmpty(providerId, options.keyId, "key id");
|
|
86
|
+
const clientId = requireNonEmpty(providerId, options.clientId, "client id (Services ID)");
|
|
87
|
+
const ttl = options.expiresInSeconds ??
|
|
88
|
+
DEFAULT_APPLE_CLIENT_SECRET_TTL_SECONDS;
|
|
89
|
+
if (ttl <= 0 || ttl > APPLE_CLIENT_SECRET_MAX_TTL_SECONDS) {
|
|
90
|
+
throw configError(providerId, `Apple client secret TTL must be between 1 and ` +
|
|
91
|
+
`${APPLE_CLIENT_SECRET_MAX_TTL_SECONDS} seconds (6 months); ` +
|
|
92
|
+
`got ${ttl}.`);
|
|
93
|
+
}
|
|
94
|
+
const privateKey = await importPrivateKey(providerId, options.privateKey);
|
|
95
|
+
const now = Math.floor(Date.now() / 1000);
|
|
96
|
+
const header = { alg: "ES256", kid: keyId };
|
|
97
|
+
const payload = {
|
|
98
|
+
iss: teamId,
|
|
99
|
+
iat: now,
|
|
100
|
+
exp: now + ttl,
|
|
101
|
+
aud: APPLE_AUDIENCE,
|
|
102
|
+
sub: clientId,
|
|
103
|
+
};
|
|
104
|
+
const signingInput = `${encodeSegment(header)}.${encodeSegment(payload)}`;
|
|
105
|
+
const signature = await crypto.subtle.sign(ES256_SIGN, privateKey, encoder.encode(signingInput));
|
|
106
|
+
return `${signingInput}.${base64urlEncode(new Uint8Array(signature))}`;
|
|
107
|
+
}
|
|
108
|
+
function encodeSegment(value) {
|
|
109
|
+
return base64urlEncode(encoder.encode(JSON.stringify(value)));
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Build a self-renewing Apple client-secret source. It signs a secret with
|
|
113
|
+
* {@link generateAppleClientSecret} on first call, caches it, and re-signs
|
|
114
|
+
* automatically once the cached secret is within `renewBeforeSeconds` of expiry
|
|
115
|
+
* — so a long-lived {@link appleProvider} never presents an expired assertion.
|
|
116
|
+
* The key material is validated on the first call, surfacing a misconfiguration
|
|
117
|
+
* as a `configuration` {@link ExternalAuthError}.
|
|
118
|
+
*/
|
|
119
|
+
export function createAppleClientSecretFactory(options) {
|
|
120
|
+
const ttl = options.expiresInSeconds ??
|
|
121
|
+
DEFAULT_APPLE_CLIENT_SECRET_TTL_SECONDS;
|
|
122
|
+
const renewBefore = options.renewBeforeSeconds ?? 60 * 60;
|
|
123
|
+
let cached = null;
|
|
124
|
+
return async () => {
|
|
125
|
+
const now = Math.floor(Date.now() / 1000);
|
|
126
|
+
if (cached && now < cached.expiresAt - renewBefore) {
|
|
127
|
+
return cached.secret;
|
|
128
|
+
}
|
|
129
|
+
const secret = await generateAppleClientSecret(options);
|
|
130
|
+
cached = { secret, expiresAt: now + ttl };
|
|
131
|
+
return secret;
|
|
132
|
+
};
|
|
133
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sign in with Apple connector. Apple is OpenID-Connect-shaped but does not fit
|
|
3
|
+
* {@link oidcProvider}: its client secret is a signed ES256 JWT rather than a
|
|
4
|
+
* static string (see {@link generateAppleClientSecret}), it authenticates the
|
|
5
|
+
* client in the token body (`client_secret_post`), and it requires
|
|
6
|
+
* `response_mode=form_post` whenever profile scopes are requested. This is a
|
|
7
|
+
* dedicated {@link ExternalProvider} that exchanges the code, then verifies the
|
|
8
|
+
* `id_token` signature against Apple's JWKS.
|
|
9
|
+
*
|
|
10
|
+
* The user's name is returned by Apple **only** on the first authorization, in
|
|
11
|
+
* the `form_post` body's `user` field — never in the `id_token`. This connector
|
|
12
|
+
* returns what the `id_token` carries (stable subject, email); capturing the
|
|
13
|
+
* first-authorization name is the callback route's responsibility, since only
|
|
14
|
+
* it sees the form body.
|
|
15
|
+
*
|
|
16
|
+
* @module
|
|
17
|
+
*/
|
|
18
|
+
import { type AppleClientSecretFactory, type AppleClientSecretOptions } from "./apple-client-secret.js";
|
|
19
|
+
import type { ExternalProvider } from "./provider.js";
|
|
20
|
+
/** Options for {@link appleProvider}. */
|
|
21
|
+
export interface AppleProviderOptions extends Pick<AppleClientSecretOptions, "teamId" | "keyId" | "privateKey"> {
|
|
22
|
+
/** The Services ID configured for web Sign in with Apple (the `client_id`). */
|
|
23
|
+
clientId: string;
|
|
24
|
+
/** Scopes requested by default. Defaults to `["name", "email"]`. */
|
|
25
|
+
scopes?: string[];
|
|
26
|
+
/**
|
|
27
|
+
* Client-secret lifetime in seconds. Defaults to Apple's practical maximum;
|
|
28
|
+
* see {@link AppleClientSecretOptions.expiresInSeconds}.
|
|
29
|
+
*/
|
|
30
|
+
expiresInSeconds?: number;
|
|
31
|
+
/**
|
|
32
|
+
* Override the client-secret source — inject a stub in tests instead of real
|
|
33
|
+
* `.p8` key material. Defaults to a cached factory over the key material.
|
|
34
|
+
*/
|
|
35
|
+
clientSecret?: AppleClientSecretFactory;
|
|
36
|
+
/** Fetch implementation for all provider traffic. Defaults to `globalThis.fetch`. */
|
|
37
|
+
fetch?: typeof fetch;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Creates a Sign in with Apple connector with id `"apple"`.
|
|
41
|
+
*
|
|
42
|
+
* The flow requests `response_mode=form_post` (Apple's requirement when `name`
|
|
43
|
+
* or `email` scope is present), exchanges the code at Apple's token endpoint
|
|
44
|
+
* with a freshly-signed client-secret JWT, and verifies the returned
|
|
45
|
+
* `id_token`'s RS256 signature against Apple's published keys before trusting
|
|
46
|
+
* its claims (`iss`, `aud`, `azp`, `exp`, and the `nonce` when one was sent,
|
|
47
|
+
* checked exactly as {@link oidcProvider} checks them). Apple's
|
|
48
|
+
* private-relay addresses (`@privaterelay.appleid.com`) are ordinary verified
|
|
49
|
+
* emails and are returned as-is. The raw `id_token` claims — including
|
|
50
|
+
* `is_private_email` — stay on {@link ExternalProfile.raw}.
|
|
51
|
+
*
|
|
52
|
+
* @example
|
|
53
|
+
* ```ts
|
|
54
|
+
* const flow = new ExternalAuthFlow({
|
|
55
|
+
* provider: appleProvider({
|
|
56
|
+
* clientId: "com.example.web",
|
|
57
|
+
* teamId: Deno.env.get("APPLE_TEAM_ID")!,
|
|
58
|
+
* keyId: Deno.env.get("APPLE_KEY_ID")!,
|
|
59
|
+
* privateKey: Deno.env.get("APPLE_PRIVATE_KEY")!,
|
|
60
|
+
* }),
|
|
61
|
+
* });
|
|
62
|
+
* ```
|
|
63
|
+
*/
|
|
64
|
+
export declare function appleProvider(options: AppleProviderOptions): ExternalProvider;
|