@_mustachio/openauth 0.9.1 → 0.10.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/dist/esm/adapters/d1/audit-log.js +15 -0
- package/dist/esm/adapters/d1/config-store.js +45 -0
- package/dist/esm/adapters/d1/index.js +12 -0
- package/dist/esm/adapters/d1/key-store.js +116 -0
- package/dist/esm/adapters/d1/method-store.js +52 -0
- package/dist/esm/adapters/d1/migrations.js +77 -0
- package/dist/esm/adapters/d1/session-store.js +134 -0
- package/dist/esm/adapters/d1/session.js +18 -0
- package/dist/esm/adapters/d1/token-store.js +140 -0
- package/dist/esm/adapters/durable-object/index.js +3 -0
- package/dist/esm/adapters/durable-object/session-store.js +139 -0
- package/dist/esm/adapters/durable-object/types.js +0 -0
- package/dist/esm/adapters/dynamo/audit-log.js +23 -0
- package/dist/esm/adapters/dynamo/client.js +0 -0
- package/dist/esm/adapters/dynamo/config-store.js +57 -0
- package/dist/esm/adapters/dynamo/index.js +10 -0
- package/dist/esm/adapters/dynamo/key-store.js +188 -0
- package/dist/esm/adapters/dynamo/method-store.js +72 -0
- package/dist/esm/adapters/dynamo/passkey-credential-store.js +83 -0
- package/dist/esm/adapters/dynamo/sdk.js +128 -0
- package/dist/esm/adapters/dynamo/session-store.js +173 -0
- package/dist/esm/adapters/dynamo/token-store.js +178 -0
- package/dist/esm/adapters/kms/in-memory-backing.js +28 -0
- package/dist/esm/adapters/kms/index.js +5 -0
- package/dist/esm/adapters/kms/key-store.js +129 -0
- package/dist/esm/adapters/kms/sdk.js +23 -0
- package/dist/esm/adapters/kms/types.js +0 -0
- package/dist/esm/adapters/kv/audit-log.js +21 -0
- package/dist/esm/adapters/kv/config-store.js +43 -0
- package/dist/esm/adapters/kv/index.js +5 -0
- package/dist/esm/adapters/kv/method-store.js +63 -0
- package/dist/esm/adapters/kv/types.js +0 -0
- package/dist/esm/adapters/memory/audit-log.js +16 -0
- package/dist/esm/adapters/memory/clock.js +5 -0
- package/dist/esm/adapters/memory/config-store.js +31 -0
- package/dist/esm/adapters/memory/index.js +10 -0
- package/dist/esm/adapters/memory/key-store.js +78 -0
- package/dist/esm/adapters/memory/method-store.js +43 -0
- package/dist/esm/adapters/memory/passkey-credential-store.js +53 -0
- package/dist/esm/adapters/memory/session-store.js +82 -0
- package/dist/esm/adapters/memory/token-store.js +91 -0
- package/dist/esm/adapters/postgres/audit-log.js +16 -0
- package/dist/esm/adapters/postgres/config-store.js +48 -0
- package/dist/esm/adapters/postgres/executor.js +21 -0
- package/dist/esm/adapters/postgres/index.js +13 -0
- package/dist/esm/adapters/postgres/key-store.js +191 -0
- package/dist/esm/adapters/postgres/method-store.js +61 -0
- package/dist/esm/adapters/postgres/migrations.js +111 -0
- package/dist/esm/adapters/postgres/passkey-credential-store.js +80 -0
- package/dist/esm/adapters/postgres/session-store.js +135 -0
- package/dist/esm/adapters/postgres/token-store.js +151 -0
- package/dist/esm/client.js +26 -10
- package/dist/esm/domain/audit.js +13 -0
- package/dist/esm/domain/authorize.js +212 -0
- package/dist/esm/domain/callback.js +173 -0
- package/dist/esm/domain/client-auth.js +61 -0
- package/dist/esm/domain/client-credentials.js +91 -0
- package/dist/esm/domain/crypto.js +97 -0
- package/dist/esm/domain/discovery.js +41 -0
- package/dist/esm/domain/introspect.js +51 -0
- package/dist/esm/domain/jwt.js +43 -0
- package/dist/esm/domain/method-cache.js +136 -0
- package/dist/esm/domain/method-dispatch.js +36 -0
- package/dist/esm/domain/method-route.js +108 -0
- package/dist/esm/domain/pkce.js +16 -0
- package/dist/esm/domain/refresh.js +91 -0
- package/dist/esm/domain/revoke.js +81 -0
- package/dist/esm/domain/state-envelope.js +80 -0
- package/dist/esm/domain/token-exchange.js +101 -0
- package/dist/esm/domain/token.js +205 -0
- package/dist/esm/domain/userinfo.js +27 -0
- package/dist/esm/http/context.js +7 -0
- package/dist/esm/http/cookies.js +98 -0
- package/dist/esm/http/errors.js +71 -0
- package/dist/esm/http/handlers/authorize.js +163 -0
- package/dist/esm/http/handlers/callback.js +53 -0
- package/dist/esm/http/handlers/metadata.js +34 -0
- package/dist/esm/http/handlers/method-route.js +87 -0
- package/dist/esm/http/handlers/revocation.js +98 -0
- package/dist/esm/http/handlers/token.js +167 -0
- package/dist/esm/http/handlers/userinfo.js +54 -0
- package/dist/esm/http/middleware/error.js +15 -0
- package/dist/esm/http/middleware/tenant.js +102 -0
- package/dist/esm/http/resolver.js +15 -0
- package/dist/esm/http/router.js +35 -0
- package/dist/esm/http/schemas/authorize.js +24 -0
- package/dist/esm/http/schemas/revocation.js +18 -0
- package/dist/esm/http/schemas/token.js +57 -0
- package/dist/esm/index.js +121 -9
- package/dist/esm/methods/code.js +221 -0
- package/dist/esm/methods/m2m.js +41 -0
- package/dist/esm/methods/oauth2-factory.js +84 -0
- package/dist/esm/methods/oauth2-generic.js +196 -0
- package/dist/esm/methods/oidc-generic.js +53 -0
- package/dist/esm/methods/passkey.js +275 -0
- package/dist/esm/methods/password-hash.js +79 -0
- package/dist/esm/methods/password.js +166 -0
- package/dist/esm/methods/providers/apple.js +29 -0
- package/dist/esm/methods/providers/cognito.js +37 -0
- package/dist/esm/methods/providers/discord.js +28 -0
- package/dist/esm/methods/providers/facebook.js +42 -0
- package/dist/esm/methods/providers/github.js +29 -0
- package/dist/esm/methods/providers/google.js +29 -0
- package/dist/esm/methods/providers/index.js +17 -0
- package/dist/esm/methods/providers/jumpcloud.js +27 -0
- package/dist/esm/methods/providers/keycloak.js +32 -0
- package/dist/esm/methods/providers/linkedin.js +28 -0
- package/dist/esm/methods/providers/microsoft.js +28 -0
- package/dist/esm/methods/providers/slack.js +30 -0
- package/dist/esm/methods/providers/spotify.js +28 -0
- package/dist/esm/methods/providers/twitch.js +28 -0
- package/dist/esm/methods/providers/x.js +28 -0
- package/dist/esm/methods/providers/yahoo.js +27 -0
- package/dist/esm/pkce.js +4 -16
- package/dist/esm/ports/audit-log.js +0 -0
- package/dist/esm/ports/config-store.js +0 -0
- package/dist/esm/ports/key-store.js +0 -0
- package/dist/esm/ports/method-store.js +0 -0
- package/dist/esm/ports/session-store.js +0 -0
- package/dist/esm/ports/token-store.js +0 -0
- package/dist/esm/types/authorization.js +0 -0
- package/dist/esm/types/error.js +65 -0
- package/dist/esm/types/flow.js +0 -0
- package/dist/esm/types/idp.js +0 -0
- package/dist/esm/types/method.js +0 -0
- package/dist/esm/types/picker.js +0 -0
- package/dist/esm/types/result.js +11 -0
- package/dist/esm/types/subject.js +0 -0
- package/dist/esm/types/tenant.js +5 -0
- package/dist/esm/types/token.js +0 -0
- package/dist/esm/ui/forms.js +253 -0
- package/dist/esm/ui/index.js +8 -0
- package/dist/esm/ui/picker.js +49 -0
- package/dist/types/adapters/d1/audit-log.d.ts +19 -0
- package/dist/types/adapters/d1/audit-log.d.ts.map +1 -0
- package/dist/types/adapters/d1/config-store.d.ts +20 -0
- package/dist/types/adapters/d1/config-store.d.ts.map +1 -0
- package/dist/types/adapters/d1/index.d.ts +35 -0
- package/dist/types/adapters/d1/index.d.ts.map +1 -0
- package/dist/types/adapters/d1/key-store.d.ts +17 -0
- package/dist/types/adapters/d1/key-store.d.ts.map +1 -0
- package/dist/types/adapters/d1/method-store.d.ts +21 -0
- package/dist/types/adapters/d1/method-store.d.ts.map +1 -0
- package/dist/types/adapters/d1/migrations.d.ts +18 -0
- package/dist/types/adapters/d1/migrations.d.ts.map +1 -0
- package/dist/types/adapters/d1/session-store.d.ts +20 -0
- package/dist/types/adapters/d1/session-store.d.ts.map +1 -0
- package/dist/types/adapters/d1/session.d.ts +18 -0
- package/dist/types/adapters/d1/session.d.ts.map +1 -0
- package/dist/types/adapters/d1/token-store.d.ts +29 -0
- package/dist/types/adapters/d1/token-store.d.ts.map +1 -0
- package/dist/types/adapters/d1/types.d.ts +43 -0
- package/dist/types/adapters/d1/types.d.ts.map +1 -0
- package/dist/types/adapters/durable-object/index.d.ts +17 -0
- package/dist/types/adapters/durable-object/index.d.ts.map +1 -0
- package/dist/types/adapters/durable-object/session-store.d.ts +21 -0
- package/dist/types/adapters/durable-object/session-store.d.ts.map +1 -0
- package/dist/types/adapters/durable-object/types.d.ts +23 -0
- package/dist/types/adapters/durable-object/types.d.ts.map +1 -0
- package/dist/types/adapters/dynamo/audit-log.d.ts +19 -0
- package/dist/types/adapters/dynamo/audit-log.d.ts.map +1 -0
- package/dist/types/adapters/dynamo/client.d.ts +111 -0
- package/dist/types/adapters/dynamo/client.d.ts.map +1 -0
- package/dist/types/adapters/dynamo/config-store.d.ts +21 -0
- package/dist/types/adapters/dynamo/config-store.d.ts.map +1 -0
- package/dist/types/adapters/dynamo/index.d.ts +60 -0
- package/dist/types/adapters/dynamo/index.d.ts.map +1 -0
- package/dist/types/adapters/dynamo/key-store.d.ts +23 -0
- package/dist/types/adapters/dynamo/key-store.d.ts.map +1 -0
- package/dist/types/adapters/dynamo/method-store.d.ts +20 -0
- package/dist/types/adapters/dynamo/method-store.d.ts.map +1 -0
- package/dist/types/adapters/dynamo/passkey-credential-store.d.ts +47 -0
- package/dist/types/adapters/dynamo/passkey-credential-store.d.ts.map +1 -0
- package/dist/types/adapters/dynamo/sdk.d.ts +23 -0
- package/dist/types/adapters/dynamo/sdk.d.ts.map +1 -0
- package/dist/types/adapters/dynamo/session-store.d.ts +20 -0
- package/dist/types/adapters/dynamo/session-store.d.ts.map +1 -0
- package/dist/types/adapters/dynamo/token-store.d.ts +29 -0
- package/dist/types/adapters/dynamo/token-store.d.ts.map +1 -0
- package/dist/types/adapters/kms/in-memory-backing.d.ts +9 -0
- package/dist/types/adapters/kms/in-memory-backing.d.ts.map +1 -0
- package/dist/types/adapters/kms/index.d.ts +35 -0
- package/dist/types/adapters/kms/index.d.ts.map +1 -0
- package/dist/types/adapters/kms/key-store.d.ts +20 -0
- package/dist/types/adapters/kms/key-store.d.ts.map +1 -0
- package/dist/types/adapters/kms/sdk.d.ts +16 -0
- package/dist/types/adapters/kms/sdk.d.ts.map +1 -0
- package/dist/types/adapters/kms/types.d.ts +58 -0
- package/dist/types/adapters/kms/types.d.ts.map +1 -0
- package/dist/types/adapters/kv/audit-log.d.ts +22 -0
- package/dist/types/adapters/kv/audit-log.d.ts.map +1 -0
- package/dist/types/adapters/kv/config-store.d.ts +25 -0
- package/dist/types/adapters/kv/config-store.d.ts.map +1 -0
- package/dist/types/adapters/kv/index.d.ts +21 -0
- package/dist/types/adapters/kv/index.d.ts.map +1 -0
- package/dist/types/adapters/kv/method-store.d.ts +23 -0
- package/dist/types/adapters/kv/method-store.d.ts.map +1 -0
- package/dist/types/adapters/kv/types.d.ts +28 -0
- package/dist/types/adapters/kv/types.d.ts.map +1 -0
- package/dist/types/adapters/memory/audit-log.d.ts +18 -0
- package/dist/types/adapters/memory/audit-log.d.ts.map +1 -0
- package/dist/types/adapters/memory/clock.d.ts +7 -0
- package/dist/types/adapters/memory/clock.d.ts.map +1 -0
- package/dist/types/adapters/memory/config-store.d.ts +19 -0
- package/dist/types/adapters/memory/config-store.d.ts.map +1 -0
- package/dist/types/adapters/memory/index.d.ts +15 -0
- package/dist/types/adapters/memory/index.d.ts.map +1 -0
- package/dist/types/adapters/memory/key-store.d.ts +17 -0
- package/dist/types/adapters/memory/key-store.d.ts.map +1 -0
- package/dist/types/adapters/memory/method-store.d.ts +26 -0
- package/dist/types/adapters/memory/method-store.d.ts.map +1 -0
- package/dist/types/adapters/memory/passkey-credential-store.d.ts +43 -0
- package/dist/types/adapters/memory/passkey-credential-store.d.ts.map +1 -0
- package/dist/types/adapters/memory/session-store.d.ts +19 -0
- package/dist/types/adapters/memory/session-store.d.ts.map +1 -0
- package/dist/types/adapters/memory/token-store.d.ts +28 -0
- package/dist/types/adapters/memory/token-store.d.ts.map +1 -0
- package/dist/types/adapters/postgres/audit-log.d.ts +18 -0
- package/dist/types/adapters/postgres/audit-log.d.ts.map +1 -0
- package/dist/types/adapters/postgres/config-store.d.ts +21 -0
- package/dist/types/adapters/postgres/config-store.d.ts.map +1 -0
- package/dist/types/adapters/postgres/executor.d.ts +48 -0
- package/dist/types/adapters/postgres/executor.d.ts.map +1 -0
- package/dist/types/adapters/postgres/index.d.ts +39 -0
- package/dist/types/adapters/postgres/index.d.ts.map +1 -0
- package/dist/types/adapters/postgres/key-store.d.ts +23 -0
- package/dist/types/adapters/postgres/key-store.d.ts.map +1 -0
- package/dist/types/adapters/postgres/method-store.d.ts +21 -0
- package/dist/types/adapters/postgres/method-store.d.ts.map +1 -0
- package/dist/types/adapters/postgres/migrations.d.ts +22 -0
- package/dist/types/adapters/postgres/migrations.d.ts.map +1 -0
- package/dist/types/adapters/postgres/passkey-credential-store.d.ts +48 -0
- package/dist/types/adapters/postgres/passkey-credential-store.d.ts.map +1 -0
- package/dist/types/adapters/postgres/session-store.d.ts +20 -0
- package/dist/types/adapters/postgres/session-store.d.ts.map +1 -0
- package/dist/types/adapters/postgres/token-store.d.ts +30 -0
- package/dist/types/adapters/postgres/token-store.d.ts.map +1 -0
- package/dist/types/client.d.ts +1 -1
- package/dist/types/client.d.ts.map +1 -1
- package/dist/types/domain/audit.d.ts +17 -0
- package/dist/types/domain/audit.d.ts.map +1 -0
- package/dist/types/domain/authorize.d.ts +125 -0
- package/dist/types/domain/authorize.d.ts.map +1 -0
- package/dist/types/domain/callback.d.ts +70 -0
- package/dist/types/domain/callback.d.ts.map +1 -0
- package/dist/types/domain/client-auth.d.ts +57 -0
- package/dist/types/domain/client-auth.d.ts.map +1 -0
- package/dist/types/domain/client-credentials.d.ts +52 -0
- package/dist/types/domain/client-credentials.d.ts.map +1 -0
- package/dist/types/domain/crypto.d.ts +53 -0
- package/dist/types/domain/crypto.d.ts.map +1 -0
- package/dist/types/domain/discovery.d.ts +44 -0
- package/dist/types/domain/discovery.d.ts.map +1 -0
- package/dist/types/domain/introspect.d.ts +71 -0
- package/dist/types/domain/introspect.d.ts.map +1 -0
- package/dist/types/domain/jwt.d.ts +48 -0
- package/dist/types/domain/jwt.d.ts.map +1 -0
- package/dist/types/domain/method-cache.d.ts +42 -0
- package/dist/types/domain/method-cache.d.ts.map +1 -0
- package/dist/types/domain/method-dispatch.d.ts +37 -0
- package/dist/types/domain/method-dispatch.d.ts.map +1 -0
- package/dist/types/domain/method-route.d.ts +77 -0
- package/dist/types/domain/method-route.d.ts.map +1 -0
- package/dist/types/domain/pkce.d.ts +15 -0
- package/dist/types/domain/pkce.d.ts.map +1 -0
- package/dist/types/domain/refresh.d.ts +43 -0
- package/dist/types/domain/refresh.d.ts.map +1 -0
- package/dist/types/domain/revoke.d.ts +61 -0
- package/dist/types/domain/revoke.d.ts.map +1 -0
- package/dist/types/domain/state-envelope.d.ts +30 -0
- package/dist/types/domain/state-envelope.d.ts.map +1 -0
- package/dist/types/domain/token-exchange.d.ts +67 -0
- package/dist/types/domain/token-exchange.d.ts.map +1 -0
- package/dist/types/domain/token.d.ts +100 -0
- package/dist/types/domain/token.d.ts.map +1 -0
- package/dist/types/domain/userinfo.d.ts +22 -0
- package/dist/types/domain/userinfo.d.ts.map +1 -0
- package/dist/types/http/context.d.ts +63 -0
- package/dist/types/http/context.d.ts.map +1 -0
- package/dist/types/http/cookies.d.ts +45 -0
- package/dist/types/http/cookies.d.ts.map +1 -0
- package/dist/types/http/errors.d.ts +36 -0
- package/dist/types/http/errors.d.ts.map +1 -0
- package/dist/types/http/handlers/authorize.d.ts +3 -0
- package/dist/types/http/handlers/authorize.d.ts.map +1 -0
- package/dist/types/http/handlers/callback.d.ts +3 -0
- package/dist/types/http/handlers/callback.d.ts.map +1 -0
- package/dist/types/http/handlers/metadata.d.ts +4 -0
- package/dist/types/http/handlers/metadata.d.ts.map +1 -0
- package/dist/types/http/handlers/method-route.d.ts +3 -0
- package/dist/types/http/handlers/method-route.d.ts.map +1 -0
- package/dist/types/http/handlers/revocation.d.ts +4 -0
- package/dist/types/http/handlers/revocation.d.ts.map +1 -0
- package/dist/types/http/handlers/token.d.ts +3 -0
- package/dist/types/http/handlers/token.d.ts.map +1 -0
- package/dist/types/http/handlers/userinfo.d.ts +3 -0
- package/dist/types/http/handlers/userinfo.d.ts.map +1 -0
- package/dist/types/http/middleware/error.d.ts +9 -0
- package/dist/types/http/middleware/error.d.ts.map +1 -0
- package/dist/types/http/middleware/tenant.d.ts +30 -0
- package/dist/types/http/middleware/tenant.d.ts.map +1 -0
- package/dist/types/http/resolver.d.ts +22 -0
- package/dist/types/http/resolver.d.ts.map +1 -0
- package/dist/types/http/router.d.ts +21 -0
- package/dist/types/http/router.d.ts.map +1 -0
- package/dist/types/http/schemas/authorize.d.ts +52 -0
- package/dist/types/http/schemas/authorize.d.ts.map +1 -0
- package/dist/types/http/schemas/revocation.d.ts +41 -0
- package/dist/types/http/schemas/revocation.d.ts.map +1 -0
- package/dist/types/http/schemas/token.d.ts +227 -0
- package/dist/types/http/schemas/token.d.ts.map +1 -0
- package/dist/types/index.d.ts +63 -13
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/methods/code.d.ts +96 -0
- package/dist/types/methods/code.d.ts.map +1 -0
- package/dist/types/methods/m2m.d.ts +41 -0
- package/dist/types/methods/m2m.d.ts.map +1 -0
- package/dist/types/methods/oauth2-factory.d.ts +133 -0
- package/dist/types/methods/oauth2-factory.d.ts.map +1 -0
- package/dist/types/methods/oauth2-generic.d.ts +75 -0
- package/dist/types/methods/oauth2-generic.d.ts.map +1 -0
- package/dist/types/methods/oidc-generic.d.ts +26 -0
- package/dist/types/methods/oidc-generic.d.ts.map +1 -0
- package/dist/types/methods/passkey.d.ts +106 -0
- package/dist/types/methods/passkey.d.ts.map +1 -0
- package/dist/types/methods/password-hash.d.ts +25 -0
- package/dist/types/methods/password-hash.d.ts.map +1 -0
- package/dist/types/methods/password.d.ts +70 -0
- package/dist/types/methods/password.d.ts.map +1 -0
- package/dist/types/methods/providers/apple.d.ts +29 -0
- package/dist/types/methods/providers/apple.d.ts.map +1 -0
- package/dist/types/methods/providers/cognito.d.ts +36 -0
- package/dist/types/methods/providers/cognito.d.ts.map +1 -0
- package/dist/types/methods/providers/discord.d.ts +22 -0
- package/dist/types/methods/providers/discord.d.ts.map +1 -0
- package/dist/types/methods/providers/facebook.d.ts +29 -0
- package/dist/types/methods/providers/facebook.d.ts.map +1 -0
- package/dist/types/methods/providers/github.d.ts +25 -0
- package/dist/types/methods/providers/github.d.ts.map +1 -0
- package/dist/types/methods/providers/google.d.ts +28 -0
- package/dist/types/methods/providers/google.d.ts.map +1 -0
- package/dist/types/methods/providers/index.d.ts +24 -0
- package/dist/types/methods/providers/index.d.ts.map +1 -0
- package/dist/types/methods/providers/jumpcloud.d.ts +22 -0
- package/dist/types/methods/providers/jumpcloud.d.ts.map +1 -0
- package/dist/types/methods/providers/keycloak.d.ts +30 -0
- package/dist/types/methods/providers/keycloak.d.ts.map +1 -0
- package/dist/types/methods/providers/linkedin.d.ts +22 -0
- package/dist/types/methods/providers/linkedin.d.ts.map +1 -0
- package/dist/types/methods/providers/microsoft.d.ts +30 -0
- package/dist/types/methods/providers/microsoft.d.ts.map +1 -0
- package/dist/types/methods/providers/slack.d.ts +25 -0
- package/dist/types/methods/providers/slack.d.ts.map +1 -0
- package/dist/types/methods/providers/spotify.d.ts +22 -0
- package/dist/types/methods/providers/spotify.d.ts.map +1 -0
- package/dist/types/methods/providers/twitch.d.ts +23 -0
- package/dist/types/methods/providers/twitch.d.ts.map +1 -0
- package/dist/types/methods/providers/x.d.ts +25 -0
- package/dist/types/methods/providers/x.d.ts.map +1 -0
- package/dist/types/methods/providers/yahoo.d.ts +22 -0
- package/dist/types/methods/providers/yahoo.d.ts.map +1 -0
- package/dist/types/pkce.d.ts +1 -2
- package/dist/types/pkce.d.ts.map +1 -1
- package/dist/types/ports/audit-log.d.ts +139 -0
- package/dist/types/ports/audit-log.d.ts.map +1 -0
- package/dist/types/ports/config-store.d.ts +32 -0
- package/dist/types/ports/config-store.d.ts.map +1 -0
- package/dist/types/ports/key-store.d.ts +98 -0
- package/dist/types/ports/key-store.d.ts.map +1 -0
- package/dist/types/ports/method-store.d.ts +29 -0
- package/dist/types/ports/method-store.d.ts.map +1 -0
- package/dist/types/ports/session-store.d.ts +82 -0
- package/dist/types/ports/session-store.d.ts.map +1 -0
- package/dist/types/ports/token-store.d.ts +93 -0
- package/dist/types/ports/token-store.d.ts.map +1 -0
- package/dist/types/types/authorization.d.ts +52 -0
- package/dist/types/types/authorization.d.ts.map +1 -0
- package/dist/types/types/error.d.ts +92 -0
- package/dist/types/types/error.d.ts.map +1 -0
- package/dist/types/types/flow.d.ts +87 -0
- package/dist/types/types/flow.d.ts.map +1 -0
- package/dist/types/types/idp.d.ts +234 -0
- package/dist/types/types/idp.d.ts.map +1 -0
- package/dist/types/types/method.d.ts +256 -0
- package/dist/types/types/method.d.ts.map +1 -0
- package/dist/types/types/picker.d.ts +31 -0
- package/dist/types/types/picker.d.ts.map +1 -0
- package/dist/types/types/result.d.ts +29 -0
- package/dist/types/types/result.d.ts.map +1 -0
- package/dist/types/types/subject.d.ts +47 -0
- package/dist/types/types/subject.d.ts.map +1 -0
- package/dist/types/types/tenant.d.ts +239 -0
- package/dist/types/types/tenant.d.ts.map +1 -0
- package/dist/types/types/token.d.ts +103 -0
- package/dist/types/types/token.d.ts.map +1 -0
- package/dist/types/ui/forms.d.ts +35 -0
- package/dist/types/ui/forms.d.ts.map +1 -0
- package/dist/types/ui/index.d.ts +14 -0
- package/dist/types/ui/index.d.ts.map +1 -0
- package/dist/types/ui/picker.d.ts +18 -0
- package/dist/types/ui/picker.d.ts.map +1 -0
- package/package.json +41 -6
- package/src/adapters/d1/audit-log.ts +32 -0
- package/src/adapters/d1/config-store.ts +67 -0
- package/src/adapters/d1/index.ts +41 -0
- package/src/adapters/d1/key-store.ts +218 -0
- package/src/adapters/d1/method-store.ts +103 -0
- package/src/adapters/d1/migrations.ts +89 -0
- package/src/adapters/d1/session-store.ts +230 -0
- package/src/adapters/d1/session.ts +38 -0
- package/src/adapters/d1/token-store.ts +251 -0
- package/src/adapters/d1/types.ts +53 -0
- package/src/adapters/durable-object/index.ts +23 -0
- package/src/adapters/durable-object/session-store.ts +227 -0
- package/src/adapters/durable-object/types.ts +25 -0
- package/src/adapters/dynamo/audit-log.ts +35 -0
- package/src/adapters/dynamo/client.ts +118 -0
- package/src/adapters/dynamo/config-store.ts +72 -0
- package/src/adapters/dynamo/index.ts +81 -0
- package/src/adapters/dynamo/key-store.ts +267 -0
- package/src/adapters/dynamo/method-store.ts +105 -0
- package/src/adapters/dynamo/passkey-credential-store.ts +132 -0
- package/src/adapters/dynamo/sdk.ts +189 -0
- package/src/adapters/dynamo/session-store.ts +229 -0
- package/src/adapters/dynamo/token-store.ts +248 -0
- package/src/adapters/kms/in-memory-backing.ts +36 -0
- package/src/adapters/kms/index.ts +39 -0
- package/src/adapters/kms/key-store.ts +176 -0
- package/src/adapters/kms/sdk.ts +39 -0
- package/src/adapters/kms/types.ts +61 -0
- package/src/adapters/kv/audit-log.ts +40 -0
- package/src/adapters/kv/config-store.ts +63 -0
- package/src/adapters/kv/index.ts +20 -0
- package/src/adapters/kv/method-store.ts +98 -0
- package/src/adapters/kv/types.ts +26 -0
- package/src/adapters/memory/audit-log.ts +28 -0
- package/src/adapters/memory/clock.ts +7 -0
- package/src/adapters/memory/config-store.ts +41 -0
- package/src/adapters/memory/index.ts +26 -0
- package/src/adapters/memory/key-store.ts +102 -0
- package/src/adapters/memory/method-store.ts +75 -0
- package/src/adapters/memory/passkey-credential-store.ts +97 -0
- package/src/adapters/memory/session-store.ts +128 -0
- package/src/adapters/memory/token-store.ts +165 -0
- package/src/adapters/postgres/audit-log.ts +34 -0
- package/src/adapters/postgres/config-store.ts +72 -0
- package/src/adapters/postgres/executor.ts +76 -0
- package/src/adapters/postgres/index.ts +60 -0
- package/src/adapters/postgres/key-store.ts +334 -0
- package/src/adapters/postgres/method-store.ts +104 -0
- package/src/adapters/postgres/migrations.ts +131 -0
- package/src/adapters/postgres/passkey-credential-store.ts +150 -0
- package/src/adapters/postgres/session-store.ts +216 -0
- package/src/adapters/postgres/token-store.ts +263 -0
- package/src/client.ts +50 -16
- package/src/domain/audit.ts +28 -0
- package/src/domain/authorize.ts +405 -0
- package/src/domain/callback.ts +263 -0
- package/src/domain/client-auth.ts +116 -0
- package/src/domain/client-credentials.ts +178 -0
- package/src/domain/crypto.ts +168 -0
- package/src/domain/discovery.ts +83 -0
- package/src/domain/introspect.ts +145 -0
- package/src/domain/jwt.ts +106 -0
- package/src/domain/method-cache.ts +220 -0
- package/src/domain/method-dispatch.ts +95 -0
- package/src/domain/method-route.ts +208 -0
- package/src/domain/pkce.ts +37 -0
- package/src/domain/refresh.ts +169 -0
- package/src/domain/revoke.ts +161 -0
- package/src/domain/state-envelope.ts +142 -0
- package/src/domain/token-exchange.ts +232 -0
- package/src/domain/token.ts +357 -0
- package/src/domain/userinfo.ts +56 -0
- package/src/http/context.ts +86 -0
- package/src/http/cookies.ts +167 -0
- package/src/http/errors.ts +98 -0
- package/src/http/handlers/authorize.ts +221 -0
- package/src/http/handlers/callback.ts +77 -0
- package/src/http/handlers/metadata.ts +44 -0
- package/src/http/handlers/method-route.ts +115 -0
- package/src/http/handlers/revocation.ts +151 -0
- package/src/http/handlers/token.ts +244 -0
- package/src/http/handlers/userinfo.ts +66 -0
- package/src/http/middleware/error.ts +26 -0
- package/src/http/middleware/tenant.ts +165 -0
- package/src/http/resolver.ts +34 -0
- package/src/http/router.ts +66 -0
- package/src/http/schemas/authorize.ts +58 -0
- package/src/http/schemas/revocation.ts +23 -0
- package/src/http/schemas/token.ts +85 -0
- package/src/index.ts +266 -20
- package/src/methods/code.ts +359 -0
- package/src/methods/m2m.ts +82 -0
- package/src/methods/oauth2-factory.ts +151 -0
- package/src/methods/oauth2-generic.ts +360 -0
- package/src/methods/oidc-generic.ts +109 -0
- package/src/methods/passkey.ts +419 -0
- package/src/methods/password-hash.ts +134 -0
- package/src/methods/password.ts +298 -0
- package/src/methods/providers/apple.ts +42 -0
- package/src/methods/providers/cognito.ts +55 -0
- package/src/methods/providers/discord.ts +37 -0
- package/src/methods/providers/facebook.ts +54 -0
- package/src/methods/providers/github.ts +42 -0
- package/src/methods/providers/google.ts +43 -0
- package/src/methods/providers/index.ts +23 -0
- package/src/methods/providers/jumpcloud.ts +36 -0
- package/src/methods/providers/keycloak.ts +42 -0
- package/src/methods/providers/linkedin.ts +37 -0
- package/src/methods/providers/microsoft.ts +42 -0
- package/src/methods/providers/slack.ts +42 -0
- package/src/methods/providers/spotify.ts +37 -0
- package/src/methods/providers/twitch.ts +38 -0
- package/src/methods/providers/x.ts +40 -0
- package/src/methods/providers/yahoo.ts +36 -0
- package/src/pkce.ts +20 -21
- package/src/ports/CONSISTENCY.md +88 -0
- package/src/ports/audit-log.ts +148 -0
- package/src/ports/config-store.ts +34 -0
- package/src/ports/key-store.ts +106 -0
- package/src/ports/method-store.ts +41 -0
- package/src/ports/session-store.ts +98 -0
- package/src/ports/token-store.ts +103 -0
- package/src/types/authorization.ts +53 -0
- package/src/types/error.ts +123 -0
- package/src/types/flow.ts +89 -0
- package/src/types/idp.ts +269 -0
- package/src/types/method.ts +270 -0
- package/src/types/picker.ts +32 -0
- package/src/types/result.ts +23 -0
- package/src/types/subject.ts +50 -0
- package/src/types/tenant.ts +248 -0
- package/src/types/token.ts +101 -0
- package/src/ui/forms.ts +289 -0
- package/src/ui/index.ts +14 -0
- package/src/ui/picker.ts +63 -0
- package/dist/esm/issuer.js +0 -691
- package/dist/esm/jwt.js +0 -16
- package/dist/esm/keys.js +0 -113
- package/dist/esm/provider/apple.js +0 -28
- package/dist/esm/provider/arctic.js +0 -43
- package/dist/esm/provider/code.js +0 -58
- package/dist/esm/provider/cognito.js +0 -16
- package/dist/esm/provider/discord.js +0 -15
- package/dist/esm/provider/facebook.js +0 -24
- package/dist/esm/provider/github.js +0 -15
- package/dist/esm/provider/google.js +0 -25
- package/dist/esm/provider/index.js +0 -3
- package/dist/esm/provider/jumpcloud.js +0 -15
- package/dist/esm/provider/keycloak.js +0 -15
- package/dist/esm/provider/linkedin.js +0 -15
- package/dist/esm/provider/m2m.js +0 -16
- package/dist/esm/provider/microsoft.js +0 -24
- package/dist/esm/provider/oauth2.js +0 -119
- package/dist/esm/provider/oidc.js +0 -69
- package/dist/esm/provider/passkey.js +0 -315
- package/dist/esm/provider/password.js +0 -306
- package/dist/esm/provider/provider.js +0 -10
- package/dist/esm/provider/slack.js +0 -15
- package/dist/esm/provider/spotify.js +0 -15
- package/dist/esm/provider/twitch.js +0 -15
- package/dist/esm/provider/x.js +0 -16
- package/dist/esm/provider/yahoo.js +0 -15
- package/dist/esm/random.js +0 -27
- package/dist/esm/storage/aws.js +0 -39
- package/dist/esm/storage/cloudflare.js +0 -42
- package/dist/esm/storage/dynamo.js +0 -116
- package/dist/esm/storage/memory.js +0 -88
- package/dist/esm/storage/storage.js +0 -36
- package/dist/esm/subject.js +0 -7
- package/dist/esm/ui/base.js +0 -407
- package/dist/esm/ui/code.js +0 -151
- package/dist/esm/ui/form.js +0 -43
- package/dist/esm/ui/icon.js +0 -92
- package/dist/esm/ui/passkey.js +0 -329
- package/dist/esm/ui/password.js +0 -338
- package/dist/esm/ui/select.js +0 -187
- package/dist/esm/ui/theme.js +0 -115
- package/dist/esm/util.js +0 -54
- package/dist/types/issuer.d.ts +0 -607
- package/dist/types/issuer.d.ts.map +0 -1
- package/dist/types/jwt.d.ts +0 -6
- package/dist/types/jwt.d.ts.map +0 -1
- package/dist/types/keys.d.ts +0 -18
- package/dist/types/keys.d.ts.map +0 -1
- package/dist/types/provider/apple.d.ts +0 -108
- package/dist/types/provider/apple.d.ts.map +0 -1
- package/dist/types/provider/arctic.d.ts +0 -16
- package/dist/types/provider/arctic.d.ts.map +0 -1
- package/dist/types/provider/code.d.ts +0 -74
- package/dist/types/provider/code.d.ts.map +0 -1
- package/dist/types/provider/cognito.d.ts +0 -64
- package/dist/types/provider/cognito.d.ts.map +0 -1
- package/dist/types/provider/discord.d.ts +0 -38
- package/dist/types/provider/discord.d.ts.map +0 -1
- package/dist/types/provider/facebook.d.ts +0 -74
- package/dist/types/provider/facebook.d.ts.map +0 -1
- package/dist/types/provider/github.d.ts +0 -38
- package/dist/types/provider/github.d.ts.map +0 -1
- package/dist/types/provider/google.d.ts +0 -74
- package/dist/types/provider/google.d.ts.map +0 -1
- package/dist/types/provider/index.d.ts +0 -4
- package/dist/types/provider/index.d.ts.map +0 -1
- package/dist/types/provider/jumpcloud.d.ts +0 -38
- package/dist/types/provider/jumpcloud.d.ts.map +0 -1
- package/dist/types/provider/keycloak.d.ts +0 -67
- package/dist/types/provider/keycloak.d.ts.map +0 -1
- package/dist/types/provider/linkedin.d.ts +0 -6
- package/dist/types/provider/linkedin.d.ts.map +0 -1
- package/dist/types/provider/m2m.d.ts +0 -34
- package/dist/types/provider/m2m.d.ts.map +0 -1
- package/dist/types/provider/microsoft.d.ts +0 -89
- package/dist/types/provider/microsoft.d.ts.map +0 -1
- package/dist/types/provider/oauth2.d.ts +0 -133
- package/dist/types/provider/oauth2.d.ts.map +0 -1
- package/dist/types/provider/oidc.d.ts +0 -91
- package/dist/types/provider/oidc.d.ts.map +0 -1
- package/dist/types/provider/passkey.d.ts +0 -143
- package/dist/types/provider/passkey.d.ts.map +0 -1
- package/dist/types/provider/password.d.ts +0 -210
- package/dist/types/provider/password.d.ts.map +0 -1
- package/dist/types/provider/provider.d.ts +0 -29
- package/dist/types/provider/provider.d.ts.map +0 -1
- package/dist/types/provider/slack.d.ts +0 -59
- package/dist/types/provider/slack.d.ts.map +0 -1
- package/dist/types/provider/spotify.d.ts +0 -38
- package/dist/types/provider/spotify.d.ts.map +0 -1
- package/dist/types/provider/twitch.d.ts +0 -38
- package/dist/types/provider/twitch.d.ts.map +0 -1
- package/dist/types/provider/x.d.ts +0 -38
- package/dist/types/provider/x.d.ts.map +0 -1
- package/dist/types/provider/yahoo.d.ts +0 -38
- package/dist/types/provider/yahoo.d.ts.map +0 -1
- package/dist/types/random.d.ts +0 -3
- package/dist/types/random.d.ts.map +0 -1
- package/dist/types/storage/aws.d.ts +0 -4
- package/dist/types/storage/aws.d.ts.map +0 -1
- package/dist/types/storage/cloudflare.d.ts +0 -34
- package/dist/types/storage/cloudflare.d.ts.map +0 -1
- package/dist/types/storage/dynamo.d.ts +0 -65
- package/dist/types/storage/dynamo.d.ts.map +0 -1
- package/dist/types/storage/memory.d.ts +0 -49
- package/dist/types/storage/memory.d.ts.map +0 -1
- package/dist/types/storage/storage.d.ts +0 -15
- package/dist/types/storage/storage.d.ts.map +0 -1
- package/dist/types/subject.d.ts +0 -122
- package/dist/types/subject.d.ts.map +0 -1
- package/dist/types/ui/base.d.ts +0 -5
- package/dist/types/ui/base.d.ts.map +0 -1
- package/dist/types/ui/code.d.ts +0 -104
- package/dist/types/ui/code.d.ts.map +0 -1
- package/dist/types/ui/form.d.ts +0 -6
- package/dist/types/ui/form.d.ts.map +0 -1
- package/dist/types/ui/icon.d.ts +0 -6
- package/dist/types/ui/icon.d.ts.map +0 -1
- package/dist/types/ui/passkey.d.ts +0 -5
- package/dist/types/ui/passkey.d.ts.map +0 -1
- package/dist/types/ui/password.d.ts +0 -139
- package/dist/types/ui/password.d.ts.map +0 -1
- package/dist/types/ui/select.d.ts +0 -55
- package/dist/types/ui/select.d.ts.map +0 -1
- package/dist/types/ui/theme.d.ts +0 -207
- package/dist/types/ui/theme.d.ts.map +0 -1
- package/dist/types/util.d.ts +0 -8
- package/dist/types/util.d.ts.map +0 -1
- package/src/css.d.ts +0 -4
- package/src/issuer.ts +0 -1628
- package/src/jwt.ts +0 -17
- package/src/keys.ts +0 -139
- package/src/provider/apple.ts +0 -127
- package/src/provider/arctic.ts +0 -66
- package/src/provider/code.ts +0 -227
- package/src/provider/cognito.ts +0 -74
- package/src/provider/discord.ts +0 -45
- package/src/provider/facebook.ts +0 -84
- package/src/provider/github.ts +0 -45
- package/src/provider/google.ts +0 -85
- package/src/provider/index.ts +0 -3
- package/src/provider/jumpcloud.ts +0 -45
- package/src/provider/keycloak.ts +0 -75
- package/src/provider/linkedin.ts +0 -12
- package/src/provider/m2m.ts +0 -56
- package/src/provider/microsoft.ts +0 -100
- package/src/provider/oauth2.ts +0 -297
- package/src/provider/oidc.ts +0 -179
- package/src/provider/passkey.ts +0 -655
- package/src/provider/password.ts +0 -672
- package/src/provider/provider.ts +0 -33
- package/src/provider/slack.ts +0 -67
- package/src/provider/spotify.ts +0 -45
- package/src/provider/twitch.ts +0 -45
- package/src/provider/x.ts +0 -46
- package/src/provider/yahoo.ts +0 -45
- package/src/random.ts +0 -24
- package/src/storage/aws.ts +0 -59
- package/src/storage/cloudflare.ts +0 -77
- package/src/storage/dynamo.ts +0 -193
- package/src/storage/memory.ts +0 -135
- package/src/storage/storage.ts +0 -46
- package/src/subject.ts +0 -130
- package/src/ui/base.tsx +0 -118
- package/src/ui/code.tsx +0 -215
- package/src/ui/form.tsx +0 -40
- package/src/ui/icon.tsx +0 -95
- package/src/ui/passkey.tsx +0 -321
- package/src/ui/password.tsx +0 -405
- package/src/ui/select.tsx +0 -221
- package/src/ui/theme.ts +0 -319
- package/src/ui/ui.css +0 -252
- package/src/util.ts +0 -58
- /package/dist/esm/{css.d.js → adapters/d1/types.js} +0 -0
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `/token` endpoint logic for `grant_type=refresh_token`.
|
|
3
|
+
*
|
|
4
|
+
* Rotation semantics (plan §"Cross-cutting Decisions" + §"Port consistency
|
|
5
|
+
* contracts"):
|
|
6
|
+
*
|
|
7
|
+
* - Refresh tokens rotate on every use. The presented token is consumed
|
|
8
|
+
* (CAS); a fresh refresh token is issued, keyed by the same `family`.
|
|
9
|
+
* - If the same token is presented a second time within the reuse window
|
|
10
|
+
* (default 60 s), the family is revoked and the request is rejected
|
|
11
|
+
* with `invalid_grant`. Audit: `refresh_reuse_detected`.
|
|
12
|
+
* - Outside the reuse window the row should be GC'd; presentation
|
|
13
|
+
* returns `invalid_grant` ("unknown token").
|
|
14
|
+
*/
|
|
15
|
+
import type { AuditLog } from "../ports/audit-log"
|
|
16
|
+
import type { ConfigStore } from "../ports/config-store"
|
|
17
|
+
import type { KeyStore } from "../ports/key-store"
|
|
18
|
+
import type { TokenStore } from "../ports/token-store"
|
|
19
|
+
import { authError, type AuthError } from "../types/error"
|
|
20
|
+
import type { Result } from "../types/result"
|
|
21
|
+
import { err, isErr } from "../types/result"
|
|
22
|
+
import type { TenantContext } from "../types/tenant"
|
|
23
|
+
import type { TokenResponse } from "../types/token"
|
|
24
|
+
|
|
25
|
+
import { safeAudit } from "./audit"
|
|
26
|
+
import { verifyClientCredentials } from "./client-auth"
|
|
27
|
+
import { mintTokens } from "./token"
|
|
28
|
+
|
|
29
|
+
export type RefreshGrantRequest = {
|
|
30
|
+
grantType: "refresh_token"
|
|
31
|
+
refreshToken: string
|
|
32
|
+
/** Optional per RFC 6749 — narrow scope. If absent, original scopes are reused. */
|
|
33
|
+
scope?: string
|
|
34
|
+
/** Confidential clients authenticate at /token. */
|
|
35
|
+
clientId?: string
|
|
36
|
+
clientSecret?: string
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export type RefreshTokensDeps = {
|
|
40
|
+
configStore: ConfigStore
|
|
41
|
+
tokenStore: TokenStore
|
|
42
|
+
keyStore: KeyStore
|
|
43
|
+
auditLog?: AuditLog
|
|
44
|
+
issuerUrl: string
|
|
45
|
+
clock: () => number
|
|
46
|
+
/** Reuse-detection window (ms). Default 60 s. */
|
|
47
|
+
reuseWindowMs?: number
|
|
48
|
+
newRefreshToken?: () => string
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export async function refreshTokens(
|
|
52
|
+
req: RefreshGrantRequest,
|
|
53
|
+
deps: RefreshTokensDeps,
|
|
54
|
+
): Promise<Result<TokenResponse, AuthError>> {
|
|
55
|
+
// Peek before consuming so we can authenticate the client without
|
|
56
|
+
// burning the token on auth failures (RFC 6749 §6).
|
|
57
|
+
const peek = await deps.tokenStore.peekRefresh(req.refreshToken)
|
|
58
|
+
if (isErr(peek)) return err(peek.error)
|
|
59
|
+
const peekedPayload = peek.value
|
|
60
|
+
|
|
61
|
+
const tenantCfg = await deps.configStore.getTenantConfig(
|
|
62
|
+
peekedPayload.tenantId,
|
|
63
|
+
)
|
|
64
|
+
if (isErr(tenantCfg)) return err(tenantCfg.error)
|
|
65
|
+
|
|
66
|
+
// Authenticate the *presenting* client first, then check ownership.
|
|
67
|
+
// Wrong-secret, wrong-client, and unknown-presenting-client all collapse
|
|
68
|
+
// to the same `invalid_grant` response so an attacker holding a stolen
|
|
69
|
+
// refresh token cannot probe candidate `client_id` values: every
|
|
70
|
+
// misroute looks identical on the wire (RFC 6749 §5.2; H4 of the
|
|
71
|
+
// post-rebuild review).
|
|
72
|
+
const INVALID_REFRESH_DESC = "refresh token is invalid"
|
|
73
|
+
const client = tenantCfg.value.clients.find(
|
|
74
|
+
(c) => c.id === peekedPayload.clientId,
|
|
75
|
+
)
|
|
76
|
+
if (!client) {
|
|
77
|
+
return err(authError.invalidGrant(INVALID_REFRESH_DESC))
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (req.clientId !== undefined) {
|
|
81
|
+
const presenting = tenantCfg.value.clients.find(
|
|
82
|
+
(c) => c.id === req.clientId,
|
|
83
|
+
)
|
|
84
|
+
if (!presenting) {
|
|
85
|
+
return err(authError.invalidGrant(INVALID_REFRESH_DESC))
|
|
86
|
+
}
|
|
87
|
+
const authErr = await verifyClientCredentials(presenting, req.clientSecret)
|
|
88
|
+
if (authErr) return err(authError.invalidGrant(INVALID_REFRESH_DESC))
|
|
89
|
+
if (req.clientId !== peekedPayload.clientId) {
|
|
90
|
+
return err(authError.invalidGrant(INVALID_REFRESH_DESC))
|
|
91
|
+
}
|
|
92
|
+
} else if (client.type === "confidential") {
|
|
93
|
+
return err(authError.invalidGrant(INVALID_REFRESH_DESC))
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const consumed = await deps.tokenStore.consumeRefresh(req.refreshToken, {
|
|
97
|
+
reuseWindowMs: deps.reuseWindowMs,
|
|
98
|
+
})
|
|
99
|
+
if (isErr(consumed)) {
|
|
100
|
+
if (consumed.error.code === "invalid_grant" && consumed.error.reuseSignal) {
|
|
101
|
+
// Reuse-detection signal arrives typed on the AuthError. tenantId /
|
|
102
|
+
// subjectId come from the signal when the adapter emits it; we fall
|
|
103
|
+
// back to the peeked payload's fields for adapters that don't
|
|
104
|
+
// (older 4th-party adapters that satisfy the port without the
|
|
105
|
+
// structured carrier). `family` always prefers the signal — it's
|
|
106
|
+
// the discriminating identifier the adapter knows.
|
|
107
|
+
const signal = consumed.error.reuseSignal
|
|
108
|
+
await safeAudit(deps, {
|
|
109
|
+
kind: "refresh_reuse_detected",
|
|
110
|
+
tenantId: peekedPayload.tenantId,
|
|
111
|
+
clientId: peekedPayload.clientId,
|
|
112
|
+
family: signal.family,
|
|
113
|
+
timestamp: deps.clock(),
|
|
114
|
+
})
|
|
115
|
+
}
|
|
116
|
+
return err(consumed.error)
|
|
117
|
+
}
|
|
118
|
+
const payload = consumed.value
|
|
119
|
+
|
|
120
|
+
// Narrow scope if requested (must be subset of original).
|
|
121
|
+
const requestedScopes = req.scope
|
|
122
|
+
? req.scope.split(" ").filter(Boolean)
|
|
123
|
+
: payload.scopes
|
|
124
|
+
for (const s of requestedScopes) {
|
|
125
|
+
if (!payload.scopes.includes(s)) {
|
|
126
|
+
return err(
|
|
127
|
+
authError.invalidScope(
|
|
128
|
+
`requested scope "${s}" not granted by original refresh token`,
|
|
129
|
+
),
|
|
130
|
+
)
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const tenant: TenantContext = {
|
|
135
|
+
id: payload.tenantId,
|
|
136
|
+
config: tenantCfg.value,
|
|
137
|
+
request: { raw: new Request("about:blank"), custom: {} },
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return mintTokens({
|
|
141
|
+
tenant,
|
|
142
|
+
claim: payload.claim,
|
|
143
|
+
payload: {
|
|
144
|
+
tenantId: payload.tenantId,
|
|
145
|
+
clientId: payload.clientId,
|
|
146
|
+
// Carry original method provenance forward — descendant tokens
|
|
147
|
+
// surface the method that started the chain via `mid` / `mkind`,
|
|
148
|
+
// not the grant-type literal "refresh".
|
|
149
|
+
methodId: payload.methodId,
|
|
150
|
+
methodKind: payload.methodKind,
|
|
151
|
+
scopes: requestedScopes,
|
|
152
|
+
audience: payload.audience,
|
|
153
|
+
},
|
|
154
|
+
family: payload.family,
|
|
155
|
+
deps,
|
|
156
|
+
}).then(async (result) => {
|
|
157
|
+
if (result.ok) {
|
|
158
|
+
await safeAudit(deps, {
|
|
159
|
+
kind: "token_refreshed",
|
|
160
|
+
tenantId: payload.tenantId,
|
|
161
|
+
clientId: payload.clientId,
|
|
162
|
+
subjectId: payload.subjectId,
|
|
163
|
+
family: payload.family,
|
|
164
|
+
timestamp: deps.clock(),
|
|
165
|
+
})
|
|
166
|
+
}
|
|
167
|
+
return result
|
|
168
|
+
})
|
|
169
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `/revoke` endpoint logic (RFC 7009).
|
|
3
|
+
*
|
|
4
|
+
* Access tokens are stateless JWTs in this IdP; revocation has to wait
|
|
5
|
+
* for natural expiry. `tokenTypeHint: "access_token"` resolves to a
|
|
6
|
+
* no-op per RFC 7009 §2.2 (invalid hint is ignored).
|
|
7
|
+
*
|
|
8
|
+
* Refresh tokens are server-side state. RFC 7009 §2.2 requires the
|
|
9
|
+
* authorization server to verify that the presented token was issued to
|
|
10
|
+
* the authenticated client and refuse otherwise. We implement this via
|
|
11
|
+
* `peekRefresh` (non-destructive) → ownership check → `consumeRefresh`
|
|
12
|
+
* (destructive). The peek/consume race is benign: if a concurrent caller
|
|
13
|
+
* consumes the token in between, our consume returns `invalid_grant`,
|
|
14
|
+
* which RFC 7009 §2.2 says we treat as a successful no-op.
|
|
15
|
+
*
|
|
16
|
+
* Anonymous revoke (no `client_id`) is permitted only for tokens issued
|
|
17
|
+
* to public clients. Confidential-client tokens MUST authenticate per
|
|
18
|
+
* RFC 7009 §2.1.
|
|
19
|
+
*/
|
|
20
|
+
import type { AuditLog } from "../ports/audit-log"
|
|
21
|
+
import type { ConfigStore } from "../ports/config-store"
|
|
22
|
+
import type { TokenStore } from "../ports/token-store"
|
|
23
|
+
import type { AuthError } from "../types/error"
|
|
24
|
+
import { authError } from "../types/error"
|
|
25
|
+
import type { Result } from "../types/result"
|
|
26
|
+
import { err, isErr, ok } from "../types/result"
|
|
27
|
+
import type { TenantId } from "../types/tenant"
|
|
28
|
+
|
|
29
|
+
import { safeAudit } from "./audit"
|
|
30
|
+
import { verifyClientCredentials } from "./client-auth"
|
|
31
|
+
|
|
32
|
+
export type RevokeRequest = {
|
|
33
|
+
token: string
|
|
34
|
+
/** RFC 7009 hint: `"access_token"` or `"refresh_token"`. */
|
|
35
|
+
tokenTypeHint?: "access_token" | "refresh_token"
|
|
36
|
+
/** Optional — required when revoking a confidential client's token. */
|
|
37
|
+
clientId?: string
|
|
38
|
+
clientSecret?: string
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export type RevokeDeps = {
|
|
42
|
+
tokenStore: TokenStore
|
|
43
|
+
configStore: ConfigStore
|
|
44
|
+
auditLog?: AuditLog
|
|
45
|
+
clock: () => number
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Revoke a single refresh token. Anonymous revokes are accepted for
|
|
50
|
+
* public-client tokens; confidential-client tokens require valid
|
|
51
|
+
* credentials.
|
|
52
|
+
*/
|
|
53
|
+
export async function revokeToken(
|
|
54
|
+
req: RevokeRequest,
|
|
55
|
+
deps: RevokeDeps,
|
|
56
|
+
): Promise<Result<void, AuthError>> {
|
|
57
|
+
if (req.tokenTypeHint === "access_token") {
|
|
58
|
+
return ok(undefined)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// RFC 7009 §2.2: unknown tokens are successful no-ops. Peek tells us
|
|
62
|
+
// the token's owner without consuming so we can authenticate first.
|
|
63
|
+
const peek = await deps.tokenStore.peekRefresh(req.token)
|
|
64
|
+
if (isErr(peek)) {
|
|
65
|
+
return ok(undefined)
|
|
66
|
+
}
|
|
67
|
+
const payload = peek.value
|
|
68
|
+
|
|
69
|
+
// Load the issuing client so we can decide whether auth is required.
|
|
70
|
+
const tenantCfg = await deps.configStore.getTenantConfig(payload.tenantId)
|
|
71
|
+
if (isErr(tenantCfg)) {
|
|
72
|
+
// Tenant evaporated. Treat as no-op — caller can't usefully retry.
|
|
73
|
+
return ok(undefined)
|
|
74
|
+
}
|
|
75
|
+
const client = tenantCfg.value.clients.find((c) => c.id === payload.clientId)
|
|
76
|
+
if (!client) {
|
|
77
|
+
return ok(undefined)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Confidential clients MUST authenticate per RFC 7009 §2.1.
|
|
81
|
+
if (client.type === "confidential") {
|
|
82
|
+
if (!req.clientId || !req.clientSecret) {
|
|
83
|
+
return err(
|
|
84
|
+
authError.invalidClient(
|
|
85
|
+
"confidential client must authenticate to revoke its tokens",
|
|
86
|
+
),
|
|
87
|
+
)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// If a client_id was presented but it doesn't own the token, RFC 7009
|
|
92
|
+
// §2.2 still wants a successful no-op response — surfacing
|
|
93
|
+
// `invalid_grant` here would let a different client probe for token
|
|
94
|
+
// existence. Audit the attempt so operators can spot abuse.
|
|
95
|
+
if (req.clientId !== undefined && req.clientId !== payload.clientId) {
|
|
96
|
+
await safeAudit(deps, {
|
|
97
|
+
kind: "custom",
|
|
98
|
+
type: "revoke_wrong_client_attempt",
|
|
99
|
+
tenantId: payload.tenantId,
|
|
100
|
+
data: {
|
|
101
|
+
presentedClientId: req.clientId,
|
|
102
|
+
tokenClientId: payload.clientId,
|
|
103
|
+
subjectId: payload.subjectId,
|
|
104
|
+
},
|
|
105
|
+
timestamp: deps.clock(),
|
|
106
|
+
})
|
|
107
|
+
return ok(undefined)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Credentials presented by the owning client must validate. Anonymous
|
|
111
|
+
// revoke of a public-client token is permitted.
|
|
112
|
+
if (req.clientId !== undefined) {
|
|
113
|
+
const authErr = await verifyClientCredentials(client, req.clientSecret)
|
|
114
|
+
if (authErr) return err(authErr)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const consumed = await deps.tokenStore.consumeRefresh(req.token, {
|
|
118
|
+
reuseWindowMs: 0,
|
|
119
|
+
})
|
|
120
|
+
if (isErr(consumed)) {
|
|
121
|
+
return ok(undefined)
|
|
122
|
+
}
|
|
123
|
+
await safeAudit(deps, {
|
|
124
|
+
kind: "token_revoked",
|
|
125
|
+
tenantId: payload.tenantId,
|
|
126
|
+
clientId: payload.clientId,
|
|
127
|
+
subjectId: payload.subjectId,
|
|
128
|
+
family: payload.family,
|
|
129
|
+
reason: "client_revoke",
|
|
130
|
+
timestamp: deps.clock(),
|
|
131
|
+
})
|
|
132
|
+
return ok(undefined)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Revoke every refresh token for a `(tenantId, subjectId)` pair and emit
|
|
137
|
+
* a `token_revoked` audit event with `reason: "subject_revoke"`.
|
|
138
|
+
*
|
|
139
|
+
* Public primitive exposed for host-driven "log me out of all devices"
|
|
140
|
+
* flows. Not called from the reuse-detection path — that stays at
|
|
141
|
+
* family-scope per OAuth 2.0 Security BCP §4.13.2 (revoking sibling
|
|
142
|
+
* families would punish legitimate other-device sessions sharing the
|
|
143
|
+
* same subject). See ARCHITECTURE.md §"Reuse detection scope".
|
|
144
|
+
*/
|
|
145
|
+
export async function revokeAllForSubject(
|
|
146
|
+
tenantId: TenantId,
|
|
147
|
+
subjectId: string,
|
|
148
|
+
deps: { tokenStore: TokenStore; auditLog?: AuditLog; clock: () => number },
|
|
149
|
+
): Promise<Result<void, AuthError>> {
|
|
150
|
+
const res = await deps.tokenStore.revokeBySubject(tenantId, subjectId)
|
|
151
|
+
if (isErr(res)) return err(res.error)
|
|
152
|
+
await safeAudit(deps, {
|
|
153
|
+
kind: "token_revoked",
|
|
154
|
+
tenantId,
|
|
155
|
+
clientId: null,
|
|
156
|
+
subjectId,
|
|
157
|
+
reason: "subject_revoke",
|
|
158
|
+
timestamp: deps.clock(),
|
|
159
|
+
})
|
|
160
|
+
return ok(undefined)
|
|
161
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MAC-signed `state` envelope used by the tenant-recovery chain.
|
|
3
|
+
*
|
|
4
|
+
* Format (intentionally compact, well under 256 base64url bytes):
|
|
5
|
+
*
|
|
6
|
+
* `<b64u(payloadJson)>.<b64u(hmacSha256(payloadJson, key[kid]))>`
|
|
7
|
+
*
|
|
8
|
+
* where `payloadJson` is canonical JSON over `{tenantId, flowId, nonce, kid}`.
|
|
9
|
+
*
|
|
10
|
+
* **Nothing sensitive is in the envelope** — only the minimal recovery
|
|
11
|
+
* tuple. Sensitive data (`clientPkce`, `appRedirectUri`, scopes, etc.)
|
|
12
|
+
* lives in the server-side `FlowRecord`, identified by `flowId`.
|
|
13
|
+
*
|
|
14
|
+
* See `ARCHITECTURE.md` §"Tenant recovery across redirects" and plan
|
|
15
|
+
* §"Crypto — HMAC-SHA-256".
|
|
16
|
+
*/
|
|
17
|
+
import { authError, type AuthError } from "../types/error"
|
|
18
|
+
import type { Result } from "../types/result"
|
|
19
|
+
import { err, ok } from "../types/result"
|
|
20
|
+
import type {
|
|
21
|
+
StateEnvelope,
|
|
22
|
+
StateKey,
|
|
23
|
+
StateKeyRing,
|
|
24
|
+
TenantId,
|
|
25
|
+
} from "../types/tenant"
|
|
26
|
+
import { asTenantId } from "../types/tenant"
|
|
27
|
+
|
|
28
|
+
import { base64url, hmacSign, hmacVerify, importHmacKey, utf8 } from "./crypto"
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Serialize envelope fields in a fixed order so the MAC input is
|
|
32
|
+
* deterministic regardless of object-key insertion order.
|
|
33
|
+
*/
|
|
34
|
+
function canonicalize(env: StateEnvelope): string {
|
|
35
|
+
return JSON.stringify({
|
|
36
|
+
tenantId: env.tenantId,
|
|
37
|
+
flowId: env.flowId,
|
|
38
|
+
nonce: env.nonce,
|
|
39
|
+
kid: env.kid,
|
|
40
|
+
})
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Mint a signed state string for the given envelope under the active key. */
|
|
44
|
+
export async function mintStateEnvelope(
|
|
45
|
+
envelope: Omit<StateEnvelope, "kid">,
|
|
46
|
+
keyRing: StateKeyRing,
|
|
47
|
+
): Promise<string> {
|
|
48
|
+
const active = keyRing.active
|
|
49
|
+
const stamped: StateEnvelope = { ...envelope, kid: active.kid }
|
|
50
|
+
const payloadBytes = utf8.encode(canonicalize(stamped))
|
|
51
|
+
const key = await importHmacKey(active.key, ["sign"])
|
|
52
|
+
const sig = await hmacSign(key, payloadBytes)
|
|
53
|
+
return `${base64url.encode(payloadBytes)}.${base64url.encode(sig)}`
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Verify and parse a state string. Returns `unknown_state` on malformed
|
|
58
|
+
* input, unknown `kid`, or signature mismatch.
|
|
59
|
+
*
|
|
60
|
+
* The MAC verification step uses `crypto.subtle.verify`, which is
|
|
61
|
+
* timing-safe by spec.
|
|
62
|
+
*/
|
|
63
|
+
export async function verifyStateEnvelope(
|
|
64
|
+
state: string,
|
|
65
|
+
keyRing: StateKeyRing,
|
|
66
|
+
): Promise<Result<StateEnvelope, AuthError>> {
|
|
67
|
+
if (typeof state !== "string" || !state.includes(".")) {
|
|
68
|
+
return err(authError.unknownState("malformed state"))
|
|
69
|
+
}
|
|
70
|
+
const dotIdx = state.indexOf(".")
|
|
71
|
+
const headerB64u = state.slice(0, dotIdx)
|
|
72
|
+
const sigB64u = state.slice(dotIdx + 1)
|
|
73
|
+
if (!headerB64u || !sigB64u || sigB64u.includes(".")) {
|
|
74
|
+
return err(authError.unknownState("malformed state"))
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
let payloadBytes: Uint8Array
|
|
78
|
+
let sigBytes: Uint8Array
|
|
79
|
+
try {
|
|
80
|
+
payloadBytes = base64url.decode(headerB64u)
|
|
81
|
+
sigBytes = base64url.decode(sigB64u)
|
|
82
|
+
} catch {
|
|
83
|
+
return err(authError.unknownState("malformed state encoding"))
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
let parsed: unknown
|
|
87
|
+
try {
|
|
88
|
+
parsed = JSON.parse(utf8.decode(payloadBytes))
|
|
89
|
+
} catch {
|
|
90
|
+
return err(authError.unknownState("malformed state payload"))
|
|
91
|
+
}
|
|
92
|
+
if (!isEnvelopeShape(parsed)) {
|
|
93
|
+
return err(authError.unknownState("state payload missing fields"))
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const candidate = findKey(keyRing, parsed.kid)
|
|
97
|
+
if (!candidate) {
|
|
98
|
+
return err(authError.unknownState(`unknown state kid "${parsed.kid}"`))
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const key = await importHmacKey(candidate.key, ["verify"])
|
|
102
|
+
const okSig = await hmacVerify(key, sigBytes, payloadBytes)
|
|
103
|
+
if (!okSig) {
|
|
104
|
+
return err(authError.unknownState("state signature mismatch"))
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Cross-check that the payload's `kid` matches the verifying key — guards
|
|
108
|
+
// against a malicious sender re-encoding the header to claim a different
|
|
109
|
+
// key was used while the MAC was actually computed under another.
|
|
110
|
+
if (parsed.kid !== candidate.kid) {
|
|
111
|
+
return err(authError.unknownState("state kid drift"))
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return ok({
|
|
115
|
+
tenantId: asTenantId(parsed.tenantId),
|
|
116
|
+
flowId: parsed.flowId,
|
|
117
|
+
nonce: parsed.nonce,
|
|
118
|
+
kid: parsed.kid,
|
|
119
|
+
})
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function findKey(ring: StateKeyRing, kid: string): StateKey | undefined {
|
|
123
|
+
return ring.verify.find((k) => k.kid === kid)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
type EnvelopeShape = {
|
|
127
|
+
tenantId: TenantId & string
|
|
128
|
+
flowId: string
|
|
129
|
+
nonce: string
|
|
130
|
+
kid: string
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function isEnvelopeShape(value: unknown): value is EnvelopeShape {
|
|
134
|
+
if (!value || typeof value !== "object") return false
|
|
135
|
+
const v = value as Record<string, unknown>
|
|
136
|
+
return (
|
|
137
|
+
typeof v.tenantId === "string" &&
|
|
138
|
+
typeof v.flowId === "string" &&
|
|
139
|
+
typeof v.nonce === "string" &&
|
|
140
|
+
typeof v.kid === "string"
|
|
141
|
+
)
|
|
142
|
+
}
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `/token` logic for `grant_type=urn:ietf:params:oauth:grant-type:token-exchange`
|
|
3
|
+
* (RFC 8693).
|
|
4
|
+
*
|
|
5
|
+
* Scope: **audience-switch / impersonation only.** A subject who holds an
|
|
6
|
+
* access token for tenant A can present it here to receive a fresh
|
|
7
|
+
* access + refresh pair scoped to tenant B (the `audience` parameter).
|
|
8
|
+
* Delegation (`actor_token`) is rejected at the HTTP layer.
|
|
9
|
+
*
|
|
10
|
+
* Flow:
|
|
11
|
+
* 1. Verify the `subject_token` JWT against the IdP's own signing keys.
|
|
12
|
+
* Expired / wrong-issuer / bad-sig tokens fail with `invalid_grant`
|
|
13
|
+
* (per RFC 8693 §2.4).
|
|
14
|
+
* 2. Refuse if the host hasn't configured `exchangeAudience` — return
|
|
15
|
+
* `unsupported_grant_type` for graceful degradation.
|
|
16
|
+
* 3. Narrow scope if the caller requested a subset.
|
|
17
|
+
* 4. Call `exchangeAudience(currentClaim, request)`. The host returns
|
|
18
|
+
* either the SubjectClaim that should anchor the new tokens at the
|
|
19
|
+
* target tenant, or an `AuthError` (typically `invalid_target`).
|
|
20
|
+
* 5. Load the target tenant config via `ConfigStore.getTenantConfig`.
|
|
21
|
+
* This validates that the audience is a real partition.
|
|
22
|
+
* 6. Mint a fresh access + refresh pair with `tid = audience` and a
|
|
23
|
+
* brand-new refresh family. The previous family lives untouched.
|
|
24
|
+
* 7. Audit `token_exchanged`.
|
|
25
|
+
*
|
|
26
|
+
* Notes:
|
|
27
|
+
* - The new tokens' `clientId` is the **authenticated** client (the
|
|
28
|
+
* one doing the exchange), not the original audience of the
|
|
29
|
+
* subject_token. Hosts that need a different client must register
|
|
30
|
+
* one in the target tenant.
|
|
31
|
+
* - `methodId` / `methodKind` are preserved from the subject_token so
|
|
32
|
+
* the audit trail still names the original authentication method.
|
|
33
|
+
*/
|
|
34
|
+
import type { AuditLog } from "../ports/audit-log"
|
|
35
|
+
import type { ConfigStore } from "../ports/config-store"
|
|
36
|
+
import type { KeyStore } from "../ports/key-store"
|
|
37
|
+
import type { TokenStore } from "../ports/token-store"
|
|
38
|
+
import { authError, type AuthError } from "../types/error"
|
|
39
|
+
import type { ExchangeAudience } from "../types/idp"
|
|
40
|
+
import type { Result } from "../types/result"
|
|
41
|
+
import { err, isErr, ok } from "../types/result"
|
|
42
|
+
import type { SubjectClaim } from "../types/subject"
|
|
43
|
+
import type { ClientConfig, TenantContext, TenantId } from "../types/tenant"
|
|
44
|
+
import type { AccessTokenClaims, TokenResponse } from "../types/token"
|
|
45
|
+
|
|
46
|
+
import { safeAudit } from "./audit"
|
|
47
|
+
import { verifyClientCredentials } from "./client-auth"
|
|
48
|
+
import { randomId } from "./crypto"
|
|
49
|
+
import { verifyAccessToken } from "./jwt"
|
|
50
|
+
import { mintTokens } from "./token"
|
|
51
|
+
|
|
52
|
+
export type TokenExchangeRequest = {
|
|
53
|
+
grantType: "urn:ietf:params:oauth:grant-type:token-exchange"
|
|
54
|
+
subjectToken: string
|
|
55
|
+
audience: string
|
|
56
|
+
/** Confidential clients authenticate. Public clients use the subject_token alone. */
|
|
57
|
+
clientId?: string
|
|
58
|
+
clientSecret?: string
|
|
59
|
+
/** Optional subset of subject_token scopes. */
|
|
60
|
+
scope?: string
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export type TokenExchangeResponse = TokenResponse & {
|
|
64
|
+
/** RFC 8693 §2.2.1. Always `access_token` for this grant. */
|
|
65
|
+
issued_token_type: "urn:ietf:params:oauth:token-type:access_token"
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export type ExchangeTokenDeps = {
|
|
69
|
+
configStore: ConfigStore
|
|
70
|
+
tokenStore: TokenStore
|
|
71
|
+
keyStore: KeyStore
|
|
72
|
+
auditLog?: AuditLog
|
|
73
|
+
exchangeAudience?: ExchangeAudience
|
|
74
|
+
issuerUrl: string
|
|
75
|
+
clock: () => number
|
|
76
|
+
newRefreshFamily?: () => string
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export async function exchangeToken(
|
|
80
|
+
req: TokenExchangeRequest,
|
|
81
|
+
deps: ExchangeTokenDeps,
|
|
82
|
+
): Promise<Result<TokenExchangeResponse, AuthError>> {
|
|
83
|
+
// 1. Graceful degradation: refuse cleanly if not configured.
|
|
84
|
+
if (!deps.exchangeAudience) {
|
|
85
|
+
return err(
|
|
86
|
+
authError.unsupportedGrantType(
|
|
87
|
+
"token-exchange is not configured on this IdP",
|
|
88
|
+
),
|
|
89
|
+
)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// 2. Verify the subject_token.
|
|
93
|
+
const keysRes = await deps.keyStore.signingKeys()
|
|
94
|
+
if (isErr(keysRes)) return err(keysRes.error)
|
|
95
|
+
let subjectClaims: AccessTokenClaims
|
|
96
|
+
try {
|
|
97
|
+
subjectClaims = await verifyAccessToken(req.subjectToken, keysRes.value, {
|
|
98
|
+
issuer: deps.issuerUrl,
|
|
99
|
+
})
|
|
100
|
+
} catch (e) {
|
|
101
|
+
return err(
|
|
102
|
+
authError.invalidGrant(
|
|
103
|
+
`subject_token verification failed: ${
|
|
104
|
+
e instanceof Error ? e.message : "unknown"
|
|
105
|
+
}`,
|
|
106
|
+
),
|
|
107
|
+
)
|
|
108
|
+
}
|
|
109
|
+
if (!subjectClaims.claim) {
|
|
110
|
+
return err(
|
|
111
|
+
authError.invalidGrant(
|
|
112
|
+
"subject_token does not carry a SubjectClaim — was it issued by this IdP?",
|
|
113
|
+
),
|
|
114
|
+
)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// 3. Load the issuing tenant + client so we can authenticate the caller.
|
|
118
|
+
const fromTenantId = subjectClaims.tid
|
|
119
|
+
const fromTenantCfg = await deps.configStore.getTenantConfig(fromTenantId)
|
|
120
|
+
if (isErr(fromTenantCfg)) return err(fromTenantCfg.error)
|
|
121
|
+
// The client doing the exchange — defaults to the subject_token's `aud`
|
|
122
|
+
// when not presented (public-client case).
|
|
123
|
+
const callerClientId = req.clientId ?? subjectClaims.aud
|
|
124
|
+
const callerClient = fromTenantCfg.value.clients.find(
|
|
125
|
+
(c: ClientConfig) => c.id === callerClientId,
|
|
126
|
+
)
|
|
127
|
+
if (!callerClient) {
|
|
128
|
+
return err(authError.invalidClient(`unknown client "${callerClientId}"`))
|
|
129
|
+
}
|
|
130
|
+
// Same client-auth rules as auth_code / refresh: confidential clients
|
|
131
|
+
// MUST authenticate; public clients MUST NOT present a secret.
|
|
132
|
+
const authErr = await verifyClientCredentials(callerClient, req.clientSecret)
|
|
133
|
+
if (authErr) return err(authErr)
|
|
134
|
+
|
|
135
|
+
// 4. Scope subset check (RFC 8693 §2.1 — `scope` MAY be requested).
|
|
136
|
+
const subjectScopes = (subjectClaims.scope ?? "")
|
|
137
|
+
.split(" ")
|
|
138
|
+
.filter((s) => s.length > 0)
|
|
139
|
+
const requestedScopes = req.scope
|
|
140
|
+
? req.scope.split(" ").filter((s) => s.length > 0)
|
|
141
|
+
: subjectScopes
|
|
142
|
+
for (const s of requestedScopes) {
|
|
143
|
+
if (!subjectScopes.includes(s)) {
|
|
144
|
+
return err(
|
|
145
|
+
authError.invalidScope(
|
|
146
|
+
`requested scope "${s}" not granted by subject_token`,
|
|
147
|
+
),
|
|
148
|
+
)
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// 5. Host decision — does the subject get to access `audience`?
|
|
153
|
+
const audienceClaim = await deps.exchangeAudience(subjectClaims.claim, {
|
|
154
|
+
audience: req.audience,
|
|
155
|
+
...(req.scope !== undefined ? { requestedScopes } : {}),
|
|
156
|
+
clientId: callerClient.id,
|
|
157
|
+
fromTenantId,
|
|
158
|
+
})
|
|
159
|
+
// The hook may return either a SubjectClaim (success) or an
|
|
160
|
+
// AuthError (rejection). Distinguish by checking for the `code`
|
|
161
|
+
// discriminator — AuthError is a closed taxonomy, all variants have
|
|
162
|
+
// `code: string`.
|
|
163
|
+
if (
|
|
164
|
+
typeof (audienceClaim as { code?: unknown }).code === "string" &&
|
|
165
|
+
typeof (audienceClaim as { description?: unknown }).description === "string"
|
|
166
|
+
) {
|
|
167
|
+
return err(audienceClaim as AuthError)
|
|
168
|
+
}
|
|
169
|
+
const newClaim = audienceClaim as SubjectClaim
|
|
170
|
+
|
|
171
|
+
// 6. Load the target tenant config so mintTokens has a TenantContext.
|
|
172
|
+
const targetTenantId = req.audience as TenantId
|
|
173
|
+
const targetTenantCfg = await deps.configStore.getTenantConfig(targetTenantId)
|
|
174
|
+
if (isErr(targetTenantCfg)) return err(targetTenantCfg.error)
|
|
175
|
+
// Verify the calling client exists at the target tenant — otherwise
|
|
176
|
+
// the issued token's `aud` would name a client the target tenant
|
|
177
|
+
// doesn't know about.
|
|
178
|
+
const targetClient = targetTenantCfg.value.clients.find(
|
|
179
|
+
(c: ClientConfig) => c.id === callerClient.id,
|
|
180
|
+
)
|
|
181
|
+
if (!targetClient) {
|
|
182
|
+
return err(
|
|
183
|
+
authError.invalidTarget(
|
|
184
|
+
`client "${callerClient.id}" is not registered in target tenant "${targetTenantId}"`,
|
|
185
|
+
),
|
|
186
|
+
)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// 7. Mint. Fresh family — previous family is untouched.
|
|
190
|
+
const targetTenant: TenantContext = {
|
|
191
|
+
id: targetTenantId,
|
|
192
|
+
config: targetTenantCfg.value,
|
|
193
|
+
request: { raw: new Request("about:blank"), custom: {} },
|
|
194
|
+
}
|
|
195
|
+
const minted = await mintTokens({
|
|
196
|
+
tenant: targetTenant,
|
|
197
|
+
claim: newClaim,
|
|
198
|
+
payload: {
|
|
199
|
+
tenantId: targetTenantId,
|
|
200
|
+
clientId: callerClient.id,
|
|
201
|
+
// Preserve provenance — operators can trace exchanged tokens back
|
|
202
|
+
// to the originating method through the audit chain.
|
|
203
|
+
methodId: subjectClaims.mid ?? "token_exchange",
|
|
204
|
+
methodKind: subjectClaims.mkind ?? "token_exchange",
|
|
205
|
+
scopes: requestedScopes,
|
|
206
|
+
...(subjectClaims.aud !== undefined &&
|
|
207
|
+
subjectClaims.aud !== callerClient.id
|
|
208
|
+
? { audience: subjectClaims.aud }
|
|
209
|
+
: {}),
|
|
210
|
+
},
|
|
211
|
+
family: (deps.newRefreshFamily ?? randomId)(),
|
|
212
|
+
deps,
|
|
213
|
+
})
|
|
214
|
+
if (isErr(minted)) return err(minted.error)
|
|
215
|
+
|
|
216
|
+
// 8. Audit. mintTokens already emits `token_issued`; the additional
|
|
217
|
+
// `token_exchanged` event records the cross-tenant context.
|
|
218
|
+
await safeAudit(deps, {
|
|
219
|
+
kind: "token_exchanged",
|
|
220
|
+
tenantId: targetTenantId,
|
|
221
|
+
fromTenantId,
|
|
222
|
+
clientId: callerClient.id,
|
|
223
|
+
subjectId: subjectClaims.sub,
|
|
224
|
+
family: "exchanged",
|
|
225
|
+
timestamp: deps.clock(),
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
return ok({
|
|
229
|
+
...minted.value,
|
|
230
|
+
issued_token_type: "urn:ietf:params:oauth:token-type:access_token",
|
|
231
|
+
})
|
|
232
|
+
}
|