@_mustachio/openauth 0.10.0 → 0.12.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/index.js +18 -9
- package/dist/esm/adapters/d1/migrations.js +7 -2
- package/dist/esm/adapters/d1/session-store.js +39 -0
- package/dist/esm/adapters/d1/session.js +2 -2
- package/dist/esm/adapters/durable-object/index.js +4 -0
- package/dist/esm/adapters/durable-object/session-store.js +42 -0
- package/dist/esm/adapters/dynamo/index.js +24 -7
- package/dist/esm/adapters/dynamo/session-store.js +49 -0
- package/dist/esm/adapters/kms/index.js +6 -2
- package/dist/esm/adapters/kv/index.js +6 -2
- package/dist/esm/adapters/memory/index.js +24 -7
- package/dist/esm/adapters/memory/session-store.js +45 -0
- package/dist/esm/adapters/memory/token-store.js +13 -0
- package/dist/esm/adapters/postgres/executor.js +2 -2
- package/dist/esm/adapters/postgres/index.js +33 -10
- package/dist/esm/adapters/postgres/migrations.js +8 -2
- package/dist/esm/adapters/postgres/session-store.js +42 -0
- package/dist/esm/client.js +35 -28
- package/dist/esm/domain/authorize.js +7 -2
- package/dist/esm/domain/callback.js +124 -7
- package/dist/esm/domain/client-auth.js +2 -2
- package/dist/esm/domain/crypto.js +14 -14
- package/dist/esm/domain/discovery.js +46 -4
- package/dist/esm/domain/dpop.js +122 -0
- package/dist/esm/domain/id-token.js +94 -0
- package/dist/esm/domain/introspect.js +5 -1
- package/dist/esm/domain/jwt.js +30 -2
- package/dist/esm/domain/logout.js +75 -0
- package/dist/esm/domain/method-dispatch.js +27 -1
- package/dist/esm/domain/method-route.js +83 -1
- package/dist/esm/domain/par.js +51 -0
- package/dist/esm/domain/pkce.js +2 -2
- package/dist/esm/domain/refresh.js +12 -1
- package/dist/esm/domain/register.js +74 -0
- package/dist/esm/domain/revoke.js +2 -2
- package/dist/esm/domain/state-envelope.js +2 -2
- package/dist/esm/domain/token-exchange.js +1 -0
- package/dist/esm/domain/token.js +53 -12
- package/dist/esm/domain/userinfo.js +38 -3
- package/dist/esm/error.js +8 -8
- package/dist/esm/http/cookies.js +19 -2
- package/dist/esm/http/errors.js +4 -4
- package/dist/esm/http/handlers/authorize.js +92 -21
- package/dist/esm/http/handlers/callback.js +7 -1
- package/dist/esm/http/handlers/end-session.js +64 -0
- package/dist/esm/http/handlers/metadata.js +7 -4
- package/dist/esm/http/handlers/method-route.js +47 -2
- package/dist/esm/http/handlers/par.js +53 -0
- package/dist/esm/http/handlers/register.js +46 -0
- package/dist/esm/http/handlers/revocation.js +2 -2
- package/dist/esm/http/handlers/token.js +36 -6
- package/dist/esm/http/handlers/userinfo.js +35 -29
- package/dist/esm/http/middleware/tenant.js +6 -6
- package/dist/esm/http/router.js +10 -0
- package/dist/esm/http/schemas/authorize.js +8 -2
- package/dist/esm/http/schemas/end-session.js +13 -0
- package/dist/esm/http/schemas/par.js +9 -0
- package/dist/esm/http/schemas/register.js +16 -0
- package/dist/esm/http/schemas/revocation.js +2 -2
- package/dist/esm/http/schemas/token.js +7 -7
- package/dist/esm/index.js +40 -34
- package/dist/esm/methods/oauth2-factory.js +2 -2
- package/dist/esm/methods/password-hash.js +2 -2
- package/dist/esm/methods/providers/index.js +30 -14
- package/dist/esm/methods/saml-sp/acs.js +208 -0
- package/dist/esm/methods/saml-sp/attributes.js +75 -0
- package/dist/esm/methods/saml-sp/authnrequest.js +63 -0
- package/dist/esm/methods/saml-sp/cache-provider.js +33 -0
- package/dist/esm/methods/saml-sp/cert-rotation.js +13 -0
- package/dist/esm/methods/saml-sp/factory.js +70 -0
- package/dist/esm/methods/saml-sp/index.js +7 -0
- package/dist/esm/methods/saml-sp/metadata.js +69 -0
- package/dist/esm/methods/saml-sp/method.js +29 -0
- package/dist/esm/methods/saml-sp/parse-idp-metadata.js +102 -0
- package/dist/esm/methods/saml-sp/saml-instance.js +49 -0
- package/dist/esm/methods/saml-sp/slo-initiate.js +84 -0
- package/dist/esm/methods/saml-sp/sls.js +154 -0
- package/dist/esm/methods/saml-sp/types.js +0 -0
- package/dist/esm/types/error.js +5 -0
- package/dist/esm/types/result.js +3 -3
- package/dist/esm/ui/forms.js +3 -3
- package/dist/esm/ui/index.js +8 -5
- package/dist/esm/ui/picker.js +2 -2
- package/dist/types/adapters/d1/migrations.d.ts.map +1 -1
- package/dist/types/adapters/d1/session-store.d.ts +3 -0
- package/dist/types/adapters/d1/session-store.d.ts.map +1 -1
- package/dist/types/adapters/durable-object/session-store.d.ts +3 -0
- package/dist/types/adapters/durable-object/session-store.d.ts.map +1 -1
- package/dist/types/adapters/dynamo/session-store.d.ts +3 -0
- package/dist/types/adapters/dynamo/session-store.d.ts.map +1 -1
- package/dist/types/adapters/memory/session-store.d.ts +6 -1
- package/dist/types/adapters/memory/session-store.d.ts.map +1 -1
- package/dist/types/adapters/memory/token-store.d.ts +1 -0
- package/dist/types/adapters/memory/token-store.d.ts.map +1 -1
- package/dist/types/adapters/postgres/migrations.d.ts +1 -1
- package/dist/types/adapters/postgres/migrations.d.ts.map +1 -1
- package/dist/types/adapters/postgres/session-store.d.ts +3 -0
- package/dist/types/adapters/postgres/session-store.d.ts.map +1 -1
- package/dist/types/client.d.ts +19 -0
- package/dist/types/client.d.ts.map +1 -1
- package/dist/types/domain/authorize.d.ts.map +1 -1
- package/dist/types/domain/callback.d.ts +16 -1
- package/dist/types/domain/callback.d.ts.map +1 -1
- package/dist/types/domain/client-credentials.d.ts +2 -0
- package/dist/types/domain/client-credentials.d.ts.map +1 -1
- package/dist/types/domain/discovery.d.ts +37 -0
- package/dist/types/domain/discovery.d.ts.map +1 -1
- package/dist/types/domain/dpop.d.ts +61 -0
- package/dist/types/domain/dpop.d.ts.map +1 -0
- package/dist/types/domain/id-token.d.ts +83 -0
- package/dist/types/domain/id-token.d.ts.map +1 -0
- package/dist/types/domain/introspect.d.ts +8 -0
- package/dist/types/domain/introspect.d.ts.map +1 -1
- package/dist/types/domain/jwt.d.ts +27 -1
- package/dist/types/domain/jwt.d.ts.map +1 -1
- package/dist/types/domain/logout.d.ts +69 -0
- package/dist/types/domain/logout.d.ts.map +1 -0
- package/dist/types/domain/method-dispatch.d.ts.map +1 -1
- package/dist/types/domain/method-route.d.ts +57 -1
- package/dist/types/domain/method-route.d.ts.map +1 -1
- package/dist/types/domain/par.d.ts +62 -0
- package/dist/types/domain/par.d.ts.map +1 -0
- package/dist/types/domain/refresh.d.ts +9 -0
- package/dist/types/domain/refresh.d.ts.map +1 -1
- package/dist/types/domain/register.d.ts +33 -0
- package/dist/types/domain/register.d.ts.map +1 -0
- package/dist/types/domain/token-exchange.d.ts +2 -0
- package/dist/types/domain/token-exchange.d.ts.map +1 -1
- package/dist/types/domain/token.d.ts +41 -1
- package/dist/types/domain/token.d.ts.map +1 -1
- package/dist/types/domain/userinfo.d.ts +44 -5
- package/dist/types/domain/userinfo.d.ts.map +1 -1
- package/dist/types/http/context.d.ts +6 -1
- package/dist/types/http/context.d.ts.map +1 -1
- package/dist/types/http/cookies.d.ts +9 -1
- package/dist/types/http/cookies.d.ts.map +1 -1
- package/dist/types/http/handlers/authorize.d.ts.map +1 -1
- package/dist/types/http/handlers/callback.d.ts.map +1 -1
- package/dist/types/http/handlers/end-session.d.ts +3 -0
- package/dist/types/http/handlers/end-session.d.ts.map +1 -0
- package/dist/types/http/handlers/metadata.d.ts +1 -1
- package/dist/types/http/handlers/metadata.d.ts.map +1 -1
- package/dist/types/http/handlers/method-route.d.ts.map +1 -1
- package/dist/types/http/handlers/par.d.ts +3 -0
- package/dist/types/http/handlers/par.d.ts.map +1 -0
- package/dist/types/http/handlers/register.d.ts +3 -0
- package/dist/types/http/handlers/register.d.ts.map +1 -0
- package/dist/types/http/handlers/token.d.ts.map +1 -1
- package/dist/types/http/handlers/userinfo.d.ts.map +1 -1
- package/dist/types/http/router.d.ts.map +1 -1
- package/dist/types/http/schemas/authorize.d.ts +40 -0
- package/dist/types/http/schemas/authorize.d.ts.map +1 -1
- package/dist/types/http/schemas/end-session.d.ts +31 -0
- package/dist/types/http/schemas/end-session.d.ts.map +1 -0
- package/dist/types/http/schemas/par.d.ts +20 -0
- package/dist/types/http/schemas/par.d.ts.map +1 -0
- package/dist/types/http/schemas/register.d.ts +42 -0
- package/dist/types/http/schemas/register.d.ts.map +1 -0
- package/dist/types/index.d.ts +3 -3
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/methods/saml-sp/acs.d.ts +4 -0
- package/dist/types/methods/saml-sp/acs.d.ts.map +1 -0
- package/dist/types/methods/saml-sp/attributes.d.ts +40 -0
- package/dist/types/methods/saml-sp/attributes.d.ts.map +1 -0
- package/dist/types/methods/saml-sp/authnrequest.d.ts +4 -0
- package/dist/types/methods/saml-sp/authnrequest.d.ts.map +1 -0
- package/dist/types/methods/saml-sp/cache-provider.d.ts +38 -0
- package/dist/types/methods/saml-sp/cache-provider.d.ts.map +1 -0
- package/dist/types/methods/saml-sp/cert-rotation.d.ts +23 -0
- package/dist/types/methods/saml-sp/cert-rotation.d.ts.map +1 -0
- package/dist/types/methods/saml-sp/factory.d.ts +704 -0
- package/dist/types/methods/saml-sp/factory.d.ts.map +1 -0
- package/dist/types/methods/saml-sp/index.d.ts +24 -0
- package/dist/types/methods/saml-sp/index.d.ts.map +1 -0
- package/dist/types/methods/saml-sp/metadata.d.ts +42 -0
- package/dist/types/methods/saml-sp/metadata.d.ts.map +1 -0
- package/dist/types/methods/saml-sp/method.d.ts +17 -0
- package/dist/types/methods/saml-sp/method.d.ts.map +1 -0
- package/dist/types/methods/saml-sp/parse-idp-metadata.d.ts +4 -0
- package/dist/types/methods/saml-sp/parse-idp-metadata.d.ts.map +1 -0
- package/dist/types/methods/saml-sp/saml-instance.d.ts +81 -0
- package/dist/types/methods/saml-sp/saml-instance.d.ts.map +1 -0
- package/dist/types/methods/saml-sp/slo-initiate.d.ts +4 -0
- package/dist/types/methods/saml-sp/slo-initiate.d.ts.map +1 -0
- package/dist/types/methods/saml-sp/sls.d.ts +4 -0
- package/dist/types/methods/saml-sp/sls.d.ts.map +1 -0
- package/dist/types/methods/saml-sp/types.d.ts +204 -0
- package/dist/types/methods/saml-sp/types.d.ts.map +1 -0
- package/dist/types/ports/audit-log.d.ts +40 -0
- package/dist/types/ports/audit-log.d.ts.map +1 -1
- package/dist/types/ports/session-store.d.ts +50 -0
- package/dist/types/ports/session-store.d.ts.map +1 -1
- package/dist/types/ports/token-store.d.ts +12 -0
- package/dist/types/ports/token-store.d.ts.map +1 -1
- package/dist/types/types/authorization.d.ts +23 -0
- package/dist/types/types/authorization.d.ts.map +1 -1
- package/dist/types/types/error.d.ts +9 -0
- package/dist/types/types/error.d.ts.map +1 -1
- package/dist/types/types/flow.d.ts +10 -0
- package/dist/types/types/flow.d.ts.map +1 -1
- package/dist/types/types/idp.d.ts +162 -1
- package/dist/types/types/idp.d.ts.map +1 -1
- package/dist/types/types/method.d.ts +109 -0
- package/dist/types/types/method.d.ts.map +1 -1
- package/dist/types/types/tenant.d.ts +37 -0
- package/dist/types/types/tenant.d.ts.map +1 -1
- package/dist/types/types/token.d.ts +128 -0
- package/dist/types/types/token.d.ts.map +1 -1
- package/package.json +9 -1
- package/src/adapters/d1/migrations.ts +5 -0
- package/src/adapters/d1/session-store.ts +68 -0
- package/src/adapters/durable-object/session-store.ts +61 -0
- package/src/adapters/dynamo/session-store.ts +63 -0
- package/src/adapters/memory/session-store.ts +83 -1
- package/src/adapters/memory/token-store.ts +16 -0
- package/src/adapters/postgres/migrations.ts +6 -0
- package/src/adapters/postgres/session-store.ts +69 -0
- package/src/client.ts +32 -0
- package/src/domain/authorize.ts +9 -0
- package/src/domain/callback.ts +220 -7
- package/src/domain/client-credentials.ts +2 -0
- package/src/domain/discovery.ts +80 -2
- package/src/domain/dpop.ts +293 -0
- package/src/domain/id-token.ts +213 -0
- package/src/domain/introspect.ts +10 -0
- package/src/domain/jwt.ts +80 -1
- package/src/domain/logout.ts +180 -0
- package/src/domain/method-dispatch.ts +53 -1
- package/src/domain/method-route.ts +195 -1
- package/src/domain/par.ts +141 -0
- package/src/domain/refresh.ts +41 -0
- package/src/domain/register.ts +168 -0
- package/src/domain/token-exchange.ts +11 -0
- package/src/domain/token.ts +134 -8
- package/src/domain/userinfo.ts +121 -6
- package/src/http/context.ts +10 -0
- package/src/http/cookies.ts +19 -1
- package/src/http/handlers/authorize.ts +146 -24
- package/src/http/handlers/callback.ts +9 -1
- package/src/http/handlers/end-session.ts +106 -0
- package/src/http/handlers/metadata.ts +7 -2
- package/src/http/handlers/method-route.ts +75 -2
- package/src/http/handlers/par.ts +84 -0
- package/src/http/handlers/register.ts +70 -0
- package/src/http/handlers/token.ts +56 -0
- package/src/http/handlers/userinfo.ts +62 -38
- package/src/http/router.ts +19 -0
- package/src/http/schemas/authorize.ts +27 -0
- package/src/http/schemas/end-session.ts +23 -0
- package/src/http/schemas/par.ts +23 -0
- package/src/http/schemas/register.ts +29 -0
- package/src/index.ts +21 -1
- package/src/methods/saml-sp/acs.ts +399 -0
- package/src/methods/saml-sp/attributes.ts +137 -0
- package/src/methods/saml-sp/authnrequest.ts +128 -0
- package/src/methods/saml-sp/cache-provider.ts +76 -0
- package/src/methods/saml-sp/cert-rotation.ts +34 -0
- package/src/methods/saml-sp/factory.ts +118 -0
- package/src/methods/saml-sp/index.ts +34 -0
- package/src/methods/saml-sp/metadata.ts +204 -0
- package/src/methods/saml-sp/method.ts +65 -0
- package/src/methods/saml-sp/parse-idp-metadata.ts +164 -0
- package/src/methods/saml-sp/saml-instance.ts +173 -0
- package/src/methods/saml-sp/slo-initiate.ts +177 -0
- package/src/methods/saml-sp/sls.ts +306 -0
- package/src/methods/saml-sp/types.ts +203 -0
- package/src/ports/CONSISTENCY.md +77 -21
- package/src/ports/audit-log.ts +42 -0
- package/src/ports/session-store.ts +61 -0
- package/src/ports/token-store.ts +13 -0
- package/src/types/authorization.ts +25 -0
- package/src/types/error.ts +23 -0
- package/src/types/flow.ts +10 -0
- package/src/types/idp.ts +170 -0
- package/src/types/method.ts +110 -0
- package/src/types/tenant.ts +37 -0
- package/src/types/token.ts +135 -0
package/src/client.ts
CHANGED
|
@@ -91,6 +91,14 @@ export interface Tokens {
|
|
|
91
91
|
* The number of seconds until the access token expires.
|
|
92
92
|
*/
|
|
93
93
|
expiresIn: number
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* The OIDC `id_token` (signed JWT), present when `scope=openid` was granted.
|
|
97
|
+
*
|
|
98
|
+
* Refresh-grant rotation reissues this with a stable `auth_time` per
|
|
99
|
+
* OIDC Core §12 and deliberately omits the original `nonce`.
|
|
100
|
+
*/
|
|
101
|
+
idToken?: string
|
|
94
102
|
}
|
|
95
103
|
|
|
96
104
|
interface ResponseLike {
|
|
@@ -178,6 +186,18 @@ export interface AuthorizeOptions {
|
|
|
178
186
|
* If there's only one provider configured, the user will be redirected to that.
|
|
179
187
|
*/
|
|
180
188
|
provider?: string
|
|
189
|
+
/**
|
|
190
|
+
* OAuth scopes to request, e.g. `["openid", "email", "profile"]`.
|
|
191
|
+
*
|
|
192
|
+
* Pass `openid` to request an `id_token` at `/token` (OIDC Core §2). Pass
|
|
193
|
+
* `email` / `profile` / `phone` / `address` to opt into the standard §5.4
|
|
194
|
+
* profile claim sets on the id_token and `/userinfo` response.
|
|
195
|
+
*
|
|
196
|
+
* Accepts an array or a pre-joined space-separated string. When omitted,
|
|
197
|
+
* no `scope` is sent — the IdP's default grant applies, which typically
|
|
198
|
+
* does NOT include `openid` and therefore does NOT issue an id_token.
|
|
199
|
+
*/
|
|
200
|
+
scope?: string | string[]
|
|
181
201
|
}
|
|
182
202
|
|
|
183
203
|
export interface AuthorizeResult {
|
|
@@ -588,6 +608,12 @@ export function createClient(input: ClientInput): Client {
|
|
|
588
608
|
result.searchParams.set("response_type", response)
|
|
589
609
|
result.searchParams.set("state", challenge.state)
|
|
590
610
|
if (opts?.provider) result.searchParams.set("provider", opts.provider)
|
|
611
|
+
if (opts?.scope !== undefined) {
|
|
612
|
+
const scope = Array.isArray(opts.scope)
|
|
613
|
+
? opts.scope.join(" ")
|
|
614
|
+
: opts.scope
|
|
615
|
+
if (scope) result.searchParams.set("scope", scope)
|
|
616
|
+
}
|
|
591
617
|
if (opts?.pkce && response === "code") {
|
|
592
618
|
const pkce = await generatePKCE()
|
|
593
619
|
result.searchParams.set("code_challenge_method", "S256")
|
|
@@ -648,6 +674,9 @@ export function createClient(input: ClientInput): Client {
|
|
|
648
674
|
access: json.access_token as string,
|
|
649
675
|
refresh: json.refresh_token as string,
|
|
650
676
|
expiresIn: json.expires_in as number,
|
|
677
|
+
...(typeof json.id_token === "string"
|
|
678
|
+
? { idToken: json.id_token as string }
|
|
679
|
+
: {}),
|
|
651
680
|
},
|
|
652
681
|
}
|
|
653
682
|
},
|
|
@@ -691,6 +720,9 @@ export function createClient(input: ClientInput): Client {
|
|
|
691
720
|
access: json.access_token as string,
|
|
692
721
|
refresh: json.refresh_token as string,
|
|
693
722
|
expiresIn: json.expires_in as number,
|
|
723
|
+
...(typeof json.id_token === "string"
|
|
724
|
+
? { idToken: json.id_token as string }
|
|
725
|
+
: {}),
|
|
694
726
|
},
|
|
695
727
|
}
|
|
696
728
|
},
|
package/src/domain/authorize.ts
CHANGED
|
@@ -221,6 +221,10 @@ export async function startAuthorize(
|
|
|
221
221
|
prompt: req.prompt,
|
|
222
222
|
uiLocales: req.uiLocales,
|
|
223
223
|
nonce,
|
|
224
|
+
...(req.nonce !== undefined ? { appNonce: req.nonce } : {}),
|
|
225
|
+
...(req.claimsRequest !== undefined
|
|
226
|
+
? { claimsRequest: req.claimsRequest }
|
|
227
|
+
: {}),
|
|
224
228
|
clientPkce: req.codeChallenge
|
|
225
229
|
? { challenge: req.codeChallenge, method: "S256" }
|
|
226
230
|
: undefined,
|
|
@@ -387,6 +391,11 @@ async function issueCodeFromInlineSuccess(
|
|
|
387
391
|
context: flow.context ?? null,
|
|
388
392
|
providerSubject: result.providerSubject,
|
|
389
393
|
properties: result.properties,
|
|
394
|
+
...(flow.appNonce !== undefined ? { appNonce: flow.appNonce } : {}),
|
|
395
|
+
...(flow.claimsRequest !== undefined
|
|
396
|
+
? { claimsRequest: flow.claimsRequest }
|
|
397
|
+
: {}),
|
|
398
|
+
authTime: Math.floor(now / 1000),
|
|
390
399
|
expiresAt: now + AUTH_CODE_TTL_MS,
|
|
391
400
|
},
|
|
392
401
|
AUTH_CODE_TTL_MS,
|
package/src/domain/callback.ts
CHANGED
|
@@ -58,6 +58,21 @@ export type HandleCallbackInput = {
|
|
|
58
58
|
/** The raw `Request` for the upstream provider's redirect. */
|
|
59
59
|
rawRequest: Request
|
|
60
60
|
cookies: ReadonlyMap<string, string>
|
|
61
|
+
/**
|
|
62
|
+
* Resolved tenant for this request. The SP-initiated path derives
|
|
63
|
+
* the tenant from the consumed flow and ignores this. The
|
|
64
|
+
* **IdP-initiated** path (unsolicited POST, no flow) has nothing to
|
|
65
|
+
* derive from, so it uses this — populated by the HTTP layer from
|
|
66
|
+
* the tenant middleware. Absent ⇒ IdP-initiated is not attempted.
|
|
67
|
+
*/
|
|
68
|
+
tenant?: TenantContext
|
|
69
|
+
/**
|
|
70
|
+
* Per-request issuer URL (HTTP layer). Used only by the
|
|
71
|
+
* IdP-initiated path to derive the SP entityID / ACS — the same
|
|
72
|
+
* derivation the AuthnRequest and metadata paths use, so the values
|
|
73
|
+
* cannot drift.
|
|
74
|
+
*/
|
|
75
|
+
issuerUrl?: string
|
|
61
76
|
}
|
|
62
77
|
|
|
63
78
|
export type HandleCallbackDeps = {
|
|
@@ -81,24 +96,73 @@ export type HandleCallbackDeps = {
|
|
|
81
96
|
) => Record<string, unknown> | Promise<Record<string, unknown>>
|
|
82
97
|
}
|
|
83
98
|
|
|
99
|
+
/**
|
|
100
|
+
* The framework's MAC state envelope normally rides `?state=` on the
|
|
101
|
+
* upstream redirect. POST-binding callbacks carry it in the form body
|
|
102
|
+
* instead: OAuth `response_mode=form_post` uses `state`, SAML's
|
|
103
|
+
* HTTP-POST binding uses `RelayState`. Read the query first (cheap,
|
|
104
|
+
* the common case) and fall back to a **cloned** body read so the
|
|
105
|
+
* downstream method handler still gets an unconsumed request body
|
|
106
|
+
* (it needs it for `code` / `SAMLResponse`).
|
|
107
|
+
*
|
|
108
|
+
* Any body-parse failure degrades to "no state" — identical to the
|
|
109
|
+
* pre-existing behaviour when the query param is absent.
|
|
110
|
+
*/
|
|
111
|
+
async function extractCallbackState(req: Request): Promise<string | null> {
|
|
112
|
+
const fromQuery = new URL(req.url).searchParams.get("state")
|
|
113
|
+
if (fromQuery) return fromQuery
|
|
114
|
+
if (req.method !== "POST") return null
|
|
115
|
+
const ct = req.headers.get("content-type") ?? ""
|
|
116
|
+
if (!ct.includes("application/x-www-form-urlencoded")) return null
|
|
117
|
+
try {
|
|
118
|
+
const body = await req.clone().text()
|
|
119
|
+
const form = new URLSearchParams(body)
|
|
120
|
+
const v = form.get("state") ?? form.get("RelayState")
|
|
121
|
+
return v && v.length > 0 ? v : null
|
|
122
|
+
} catch {
|
|
123
|
+
return null
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
84
127
|
export async function handleCallback(
|
|
85
128
|
input: HandleCallbackInput,
|
|
86
129
|
deps: HandleCallbackDeps,
|
|
87
130
|
): Promise<Result<CallbackOutput, AuthError>> {
|
|
88
131
|
const url = new URL(input.rawRequest.url)
|
|
89
|
-
const state =
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
132
|
+
const state = await extractCallbackState(input.rawRequest)
|
|
133
|
+
// A SAML IdP-initiated POST may legitimately carry a `RelayState`
|
|
134
|
+
// (Okta/Entra deep-link tokens), so "RelayState present" does NOT
|
|
135
|
+
// imply "framework state envelope present". The envelope is only
|
|
136
|
+
// real if it MAC-verifies. When there is no verifiable envelope —
|
|
137
|
+
// none at all, or a value that fails verification — first try
|
|
138
|
+
// IdP-initiated (the only flowless path). Only if that is not a
|
|
139
|
+
// clean candidate do we emit the original error/audit, unchanged
|
|
140
|
+
// for every method that did not opt into `unsolicitedCallback`.
|
|
141
|
+
const envelopeRes = state
|
|
142
|
+
? await verifyStateEnvelope(state, deps.stateKeys)
|
|
143
|
+
: null
|
|
144
|
+
if (envelopeRes === null || isErr(envelopeRes)) {
|
|
145
|
+
const idp = await tryIdpInitiated(input, deps, url)
|
|
146
|
+
if (idp) return idp
|
|
147
|
+
if (!state) {
|
|
148
|
+
return err(authError.invalidRequest("missing state parameter", "state"))
|
|
149
|
+
}
|
|
150
|
+
// State present but no valid envelope and not an IdP-init
|
|
151
|
+
// candidate — the original tampered/expired-envelope behaviour.
|
|
95
152
|
await safeAudit(deps, {
|
|
96
153
|
kind: "flow_replay_attempt",
|
|
97
154
|
tenantId: null,
|
|
98
155
|
flowId: "unknown",
|
|
99
156
|
timestamp: deps.clock(),
|
|
100
157
|
})
|
|
101
|
-
return err(
|
|
158
|
+
return err(
|
|
159
|
+
authError.invalidRequest(
|
|
160
|
+
envelopeRes && isErr(envelopeRes)
|
|
161
|
+
? envelopeRes.error.description
|
|
162
|
+
: "invalid state",
|
|
163
|
+
"state",
|
|
164
|
+
),
|
|
165
|
+
)
|
|
102
166
|
}
|
|
103
167
|
const envelope = envelopeRes.value
|
|
104
168
|
|
|
@@ -207,6 +271,11 @@ async function translate(
|
|
|
207
271
|
context: flow.context ?? null,
|
|
208
272
|
providerSubject: result.providerSubject,
|
|
209
273
|
properties: result.properties,
|
|
274
|
+
...(flow.appNonce !== undefined ? { appNonce: flow.appNonce } : {}),
|
|
275
|
+
...(flow.claimsRequest !== undefined
|
|
276
|
+
? { claimsRequest: flow.claimsRequest }
|
|
277
|
+
: {}),
|
|
278
|
+
authTime: Math.floor(now / 1000),
|
|
210
279
|
expiresAt: now + AUTH_CODE_TTL_MS,
|
|
211
280
|
},
|
|
212
281
|
AUTH_CODE_TTL_MS,
|
|
@@ -258,6 +327,150 @@ async function translate(
|
|
|
258
327
|
}
|
|
259
328
|
}
|
|
260
329
|
|
|
330
|
+
/**
|
|
331
|
+
* IdP-initiated (unsolicited) SAML SSO — SAML-AD7, the one carve-out
|
|
332
|
+
* vs. OAuth/OIDC methods. There is no AuthnRequest, no MAC state
|
|
333
|
+
* envelope, and no flow record, so none of `handleCallback`'s gates
|
|
334
|
+
* apply. Returns `null` when this is **not** a clean IdP-init candidate
|
|
335
|
+
* (so the caller emits the normal "missing state" error — the
|
|
336
|
+
* conservative default); otherwise a final `CallbackOutput`.
|
|
337
|
+
*
|
|
338
|
+
* Trust chain (no flow to lean on):
|
|
339
|
+
* - the assertion itself is signature/issuer/audience/conditions
|
|
340
|
+
* verified by the method (`consumeAssertion`, IdP-init mode) plus
|
|
341
|
+
* explicit assertion-ID replay dedup;
|
|
342
|
+
* - the RP binding is NOT attacker-influenced — it comes from the
|
|
343
|
+
* method's `unsolicitedBinding` (operator config), and is then
|
|
344
|
+
* re-validated here against the tenant's registered client +
|
|
345
|
+
* redirect URIs (open-redirect defence-in-depth). `RelayState` is
|
|
346
|
+
* never interpreted as a redirect.
|
|
347
|
+
*/
|
|
348
|
+
async function tryIdpInitiated(
|
|
349
|
+
input: HandleCallbackInput,
|
|
350
|
+
deps: HandleCallbackDeps,
|
|
351
|
+
url: URL,
|
|
352
|
+
): Promise<Result<CallbackOutput, AuthError> | null> {
|
|
353
|
+
if (input.rawRequest.method !== "POST") return null
|
|
354
|
+
if (!input.tenant || !input.issuerUrl) return null
|
|
355
|
+
|
|
356
|
+
const segments = url.pathname.split("/").filter(Boolean)
|
|
357
|
+
if (segments.length < 2 || segments[0] !== "cb") return null
|
|
358
|
+
const methodId = segments[1]!
|
|
359
|
+
|
|
360
|
+
const methodRes = await deps.methodCache.resolve(
|
|
361
|
+
input.tenant.config,
|
|
362
|
+
methodId,
|
|
363
|
+
)
|
|
364
|
+
if (isErr(methodRes)) return null
|
|
365
|
+
const method = methodRes.value
|
|
366
|
+
if (method.unsolicitedCallback !== true) return null
|
|
367
|
+
|
|
368
|
+
const callbackUrl = `${url.protocol}//${url.host}/cb/${methodId}`
|
|
369
|
+
const dispatched = await dispatchMethod({
|
|
370
|
+
method,
|
|
371
|
+
route: "GET /callback",
|
|
372
|
+
tenant: input.tenant,
|
|
373
|
+
request: input.rawRequest,
|
|
374
|
+
subPath: "/callback",
|
|
375
|
+
flow: null,
|
|
376
|
+
cookies: input.cookies,
|
|
377
|
+
sessionStore: deps.sessionStore,
|
|
378
|
+
dispatch: { state: "", callbackUrl, issuerUrl: input.issuerUrl },
|
|
379
|
+
})
|
|
380
|
+
if (isErr(dispatched)) return err(dispatched.error)
|
|
381
|
+
|
|
382
|
+
const result = dispatched.value
|
|
383
|
+
if (result.kind === "challenge") {
|
|
384
|
+
return ok({
|
|
385
|
+
kind: "challenge",
|
|
386
|
+
response: result.response,
|
|
387
|
+
setCookies: result.setCookies ?? [],
|
|
388
|
+
...(result.cache !== undefined ? { cache: result.cache } : {}),
|
|
389
|
+
})
|
|
390
|
+
}
|
|
391
|
+
if (result.kind === "denied") {
|
|
392
|
+
await safeAudit(deps, {
|
|
393
|
+
kind: "authorize_failed",
|
|
394
|
+
tenantId: input.tenant.id,
|
|
395
|
+
clientId: "idp-initiated",
|
|
396
|
+
methodId,
|
|
397
|
+
methodKind: method.kind,
|
|
398
|
+
flowId: "idp-initiated",
|
|
399
|
+
reason: result.reason,
|
|
400
|
+
timestamp: deps.clock(),
|
|
401
|
+
})
|
|
402
|
+
return ok({
|
|
403
|
+
kind: "denied",
|
|
404
|
+
reason: result.reason,
|
|
405
|
+
setCookies: result.setCookies ?? [],
|
|
406
|
+
})
|
|
407
|
+
}
|
|
408
|
+
if (result.kind === "error") return err(result.error)
|
|
409
|
+
|
|
410
|
+
// success — must carry the operator-configured RP binding.
|
|
411
|
+
const binding = result.unsolicitedBinding
|
|
412
|
+
if (!binding) {
|
|
413
|
+
return err(
|
|
414
|
+
authError.internalError(
|
|
415
|
+
`method "${methodId}" handled an unsolicited callback but returned ` +
|
|
416
|
+
`no unsolicitedBinding`,
|
|
417
|
+
),
|
|
418
|
+
)
|
|
419
|
+
}
|
|
420
|
+
// Open-redirect defence-in-depth: the binding is operator config, but
|
|
421
|
+
// still validate it against the tenant's registered client exactly
|
|
422
|
+
// as /authorize validates an RP's redirect_uri.
|
|
423
|
+
const client = input.tenant.config.clients.find(
|
|
424
|
+
(c) => c.id === binding.clientId,
|
|
425
|
+
)
|
|
426
|
+
if (!client) {
|
|
427
|
+
return err(
|
|
428
|
+
authError.invalidRequest(
|
|
429
|
+
`IdP-initiated binding references unknown client "${binding.clientId}"`,
|
|
430
|
+
),
|
|
431
|
+
)
|
|
432
|
+
}
|
|
433
|
+
if (!client.redirectUris.includes(binding.redirectUri)) {
|
|
434
|
+
return err(
|
|
435
|
+
authError.invalidRequest(
|
|
436
|
+
`IdP-initiated redirect_uri "${binding.redirectUri}" is not ` +
|
|
437
|
+
`registered for client "${binding.clientId}"`,
|
|
438
|
+
),
|
|
439
|
+
)
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
const code = (deps.newCodeId ?? randomToken)()
|
|
443
|
+
const now = deps.clock()
|
|
444
|
+
const saved = await saveEncryptedCode(
|
|
445
|
+
code,
|
|
446
|
+
{
|
|
447
|
+
tenantId: input.tenant.id,
|
|
448
|
+
clientId: binding.clientId,
|
|
449
|
+
appRedirectUri: binding.redirectUri,
|
|
450
|
+
// No RP-supplied OAuth state in an unsolicited flow; RelayState
|
|
451
|
+
// is opaque and intentionally NOT echoed as `state`.
|
|
452
|
+
appState: null,
|
|
453
|
+
scopes: binding.scopes,
|
|
454
|
+
methodId,
|
|
455
|
+
methodKind: method.kind,
|
|
456
|
+
context: input.tenant.request.custom ?? null,
|
|
457
|
+
providerSubject: result.providerSubject,
|
|
458
|
+
properties: result.properties,
|
|
459
|
+
authTime: Math.floor(now / 1000),
|
|
460
|
+
expiresAt: now + AUTH_CODE_TTL_MS,
|
|
461
|
+
},
|
|
462
|
+
AUTH_CODE_TTL_MS,
|
|
463
|
+
{ keyStore: deps.keyStore, tokenStore: deps.tokenStore },
|
|
464
|
+
)
|
|
465
|
+
if (isErr(saved)) return err(saved.error)
|
|
466
|
+
return ok({
|
|
467
|
+
kind: "issue-code",
|
|
468
|
+
code,
|
|
469
|
+
appRedirectUri: binding.redirectUri,
|
|
470
|
+
appState: null,
|
|
471
|
+
})
|
|
472
|
+
}
|
|
473
|
+
|
|
261
474
|
function normalizePath(p: string): string {
|
|
262
475
|
return p.replace(/\/+$/, "")
|
|
263
476
|
}
|
|
@@ -54,6 +54,8 @@ export type ClientCredentialsDeps = {
|
|
|
54
54
|
issuerUrl: string
|
|
55
55
|
clock: () => number
|
|
56
56
|
newRefreshFamily?: () => string
|
|
57
|
+
/** See `IdPOptions.customScopeClaims`. Forwarded to `mintTokens`. */
|
|
58
|
+
customScopeClaims?: Record<string, ReadonlyArray<string>>
|
|
57
59
|
}
|
|
58
60
|
|
|
59
61
|
export async function clientCredentialsGrant(
|
package/src/domain/discovery.ts
CHANGED
|
@@ -19,11 +19,37 @@ export type DiscoveryDocument = {
|
|
|
19
19
|
jwks_uri: string
|
|
20
20
|
revocation_endpoint: string
|
|
21
21
|
introspection_endpoint: string
|
|
22
|
+
/** OIDC RP-Initiated Logout 1.0 §2. */
|
|
23
|
+
end_session_endpoint: string
|
|
24
|
+
/** RFC 7591 §3.1. */
|
|
25
|
+
registration_endpoint: string
|
|
26
|
+
/** RFC 9126 §5. */
|
|
27
|
+
pushed_authorization_request_endpoint: string
|
|
28
|
+
/**
|
|
29
|
+
* RFC 9126 §5. True iff every client on this tenant is required to use
|
|
30
|
+
* PAR. We advertise `false` at the tenant level — the per-client toggle
|
|
31
|
+
* is read at request time.
|
|
32
|
+
*/
|
|
33
|
+
require_pushed_authorization_requests: boolean
|
|
34
|
+
/** RFC 9449 §5.1 — DPoP algorithms accepted on proof JWTs. */
|
|
35
|
+
dpop_signing_alg_values_supported: string[]
|
|
22
36
|
response_types_supported: string[]
|
|
23
37
|
grant_types_supported: string[]
|
|
24
38
|
subject_types_supported: string[]
|
|
25
39
|
id_token_signing_alg_values_supported: string[]
|
|
26
40
|
scopes_supported: string[]
|
|
41
|
+
/** OIDC Core §3 — registered claim names the IdP can return. */
|
|
42
|
+
claims_supported: string[]
|
|
43
|
+
/** OIDC Core §5.5 — `claims` request parameter is honored. */
|
|
44
|
+
claims_parameter_supported: boolean
|
|
45
|
+
/** OIDC Core §6.1 — JAR (`request` parameter). Not implemented. */
|
|
46
|
+
request_parameter_supported: boolean
|
|
47
|
+
/** OIDC Core §6.2 — JAR (`request_uri` parameter). Not implemented. */
|
|
48
|
+
request_uri_parameter_supported: boolean
|
|
49
|
+
/** OIDC Core §6.2 — whether `request_uri` values must be pre-registered. */
|
|
50
|
+
require_request_uri_registration: boolean
|
|
51
|
+
/** OIDC Core §3.1.2.1 — UI locales the OP can render. */
|
|
52
|
+
ui_locales_supported?: string[]
|
|
27
53
|
token_endpoint_auth_methods_supported: string[]
|
|
28
54
|
code_challenge_methods_supported: string[]
|
|
29
55
|
}
|
|
@@ -38,14 +64,30 @@ export type DiscoveryDeps = {
|
|
|
38
64
|
jwks: string
|
|
39
65
|
revoke: string
|
|
40
66
|
introspect: string
|
|
67
|
+
endSession: string
|
|
68
|
+
par: string
|
|
69
|
+
register: string
|
|
41
70
|
}>
|
|
42
71
|
/** Advertised scopes. Defaults to `["openid", "email", "profile"]`. */
|
|
43
72
|
scopes?: string[]
|
|
73
|
+
/** Advertised UI locales. Defaults to `["en"]`. */
|
|
74
|
+
uiLocales?: string[]
|
|
75
|
+
/**
|
|
76
|
+
* Host-supplied scope → claim-names map. Each key adds a scope to
|
|
77
|
+
* `scopes_supported`; the union of values augments `claims_supported`.
|
|
78
|
+
* Mirrors `IdPOptions.customScopeClaims`.
|
|
79
|
+
*/
|
|
80
|
+
customScopeClaims?: Record<string, ReadonlyArray<string>>
|
|
44
81
|
}
|
|
45
82
|
|
|
46
83
|
export function buildDiscoveryDocument(deps: DiscoveryDeps): DiscoveryDocument {
|
|
47
84
|
const base = deps.issuerUrl.replace(/\/+$/, "")
|
|
48
85
|
const p = deps.paths ?? {}
|
|
86
|
+
const customScopeKeys = Object.keys(deps.customScopeClaims ?? {})
|
|
87
|
+
const customClaimNames = new Set<string>()
|
|
88
|
+
for (const list of Object.values(deps.customScopeClaims ?? {})) {
|
|
89
|
+
for (const name of list) customClaimNames.add(name)
|
|
90
|
+
}
|
|
49
91
|
return {
|
|
50
92
|
issuer: deps.issuerUrl,
|
|
51
93
|
authorization_endpoint: `${base}${p.authorize ?? "/authorize"}`,
|
|
@@ -54,15 +96,51 @@ export function buildDiscoveryDocument(deps: DiscoveryDeps): DiscoveryDocument {
|
|
|
54
96
|
jwks_uri: `${base}${p.jwks ?? "/.well-known/jwks.json"}`,
|
|
55
97
|
revocation_endpoint: `${base}${p.revoke ?? "/revoke"}`,
|
|
56
98
|
introspection_endpoint: `${base}${p.introspect ?? "/introspect"}`,
|
|
99
|
+
end_session_endpoint: `${base}${p.endSession ?? "/end_session"}`,
|
|
100
|
+
registration_endpoint: `${base}${p.register ?? "/register"}`,
|
|
101
|
+
pushed_authorization_request_endpoint: `${base}${p.par ?? "/par"}`,
|
|
102
|
+
require_pushed_authorization_requests: false,
|
|
103
|
+
dpop_signing_alg_values_supported: ["ES256", "EdDSA"],
|
|
57
104
|
response_types_supported: ["code"],
|
|
58
105
|
grant_types_supported: [
|
|
59
106
|
"authorization_code",
|
|
60
107
|
"refresh_token",
|
|
61
108
|
"client_credentials",
|
|
62
109
|
],
|
|
63
|
-
subject_types_supported: ["public"],
|
|
110
|
+
subject_types_supported: ["public", "pairwise"],
|
|
64
111
|
id_token_signing_alg_values_supported: ["ES256", "EdDSA"],
|
|
65
|
-
scopes_supported:
|
|
112
|
+
scopes_supported: [
|
|
113
|
+
...(deps.scopes ?? ["openid", "email", "profile"]),
|
|
114
|
+
...customScopeKeys,
|
|
115
|
+
],
|
|
116
|
+
claims_supported: [
|
|
117
|
+
"sub",
|
|
118
|
+
"iss",
|
|
119
|
+
"aud",
|
|
120
|
+
"exp",
|
|
121
|
+
"iat",
|
|
122
|
+
"auth_time",
|
|
123
|
+
"nonce",
|
|
124
|
+
"amr",
|
|
125
|
+
"at_hash",
|
|
126
|
+
"email",
|
|
127
|
+
"email_verified",
|
|
128
|
+
"name",
|
|
129
|
+
"given_name",
|
|
130
|
+
"family_name",
|
|
131
|
+
"preferred_username",
|
|
132
|
+
"picture",
|
|
133
|
+
"locale",
|
|
134
|
+
"phone_number",
|
|
135
|
+
"phone_number_verified",
|
|
136
|
+
"address",
|
|
137
|
+
...customClaimNames,
|
|
138
|
+
],
|
|
139
|
+
claims_parameter_supported: true,
|
|
140
|
+
request_parameter_supported: false,
|
|
141
|
+
request_uri_parameter_supported: false,
|
|
142
|
+
require_request_uri_registration: false,
|
|
143
|
+
ui_locales_supported: deps.uiLocales ?? ["en"],
|
|
66
144
|
token_endpoint_auth_methods_supported: [
|
|
67
145
|
"client_secret_basic",
|
|
68
146
|
"client_secret_post",
|