@_mustachio/openauth 0.13.2 → 0.14.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/client.js +61 -62
- package/dist/esm/domain/authorize.js +21 -3
- package/dist/esm/domain/callback.js +30 -21
- package/dist/esm/domain/client-credentials.js +16 -0
- package/dist/esm/domain/method-dispatch.js +1 -0
- package/dist/esm/domain/method-route.js +12 -0
- package/dist/esm/domain/mount.js +29 -0
- package/dist/esm/domain/refresh.js +16 -5
- package/dist/esm/domain/register.js +8 -2
- package/dist/esm/domain/state-envelope.js +19 -0
- package/dist/esm/domain/subject.js +35 -0
- package/dist/esm/domain/token.js +15 -0
- package/dist/esm/http/handlers/method-route.js +8 -3
- package/dist/esm/http/handlers/token.js +2 -0
- package/dist/esm/http/middleware/tenant.js +5 -20
- package/dist/esm/index.js +4 -0
- package/dist/esm/methods/code.js +9 -8
- package/dist/esm/methods/oauth2-generic.js +2 -1
- package/dist/esm/methods/passkey.js +3 -2
- package/dist/esm/methods/password.js +9 -8
- package/dist/esm/methods/saml-sp/metadata.js +2 -1
- package/dist/types/client.d.ts +61 -40
- 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 +6 -5
- package/dist/types/domain/callback.d.ts.map +1 -1
- package/dist/types/domain/client-credentials.d.ts +3 -1
- package/dist/types/domain/client-credentials.d.ts.map +1 -1
- package/dist/types/domain/method-dispatch.d.ts +5 -0
- package/dist/types/domain/method-dispatch.d.ts.map +1 -1
- package/dist/types/domain/method-route.d.ts +6 -0
- package/dist/types/domain/method-route.d.ts.map +1 -1
- package/dist/types/domain/mount.d.ts +90 -0
- package/dist/types/domain/mount.d.ts.map +1 -0
- package/dist/types/domain/refresh.d.ts.map +1 -1
- package/dist/types/domain/register.d.ts.map +1 -1
- package/dist/types/domain/state-envelope.d.ts +22 -0
- package/dist/types/domain/state-envelope.d.ts.map +1 -1
- package/dist/types/domain/subject.d.ts +48 -0
- package/dist/types/domain/subject.d.ts.map +1 -0
- package/dist/types/domain/token.d.ts +7 -1
- package/dist/types/domain/token.d.ts.map +1 -1
- package/dist/types/http/context.d.ts +3 -0
- package/dist/types/http/context.d.ts.map +1 -1
- package/dist/types/http/handlers/method-route.d.ts.map +1 -1
- package/dist/types/http/handlers/token.d.ts.map +1 -1
- package/dist/types/http/middleware/tenant.d.ts.map +1 -1
- package/dist/types/http/schemas/revocation.d.ts +4 -4
- package/dist/types/http/schemas/token.d.ts +12 -12
- package/dist/types/index.d.ts +17 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/methods/code.d.ts.map +1 -1
- package/dist/types/methods/oauth2-generic.d.ts.map +1 -1
- package/dist/types/methods/passkey.d.ts.map +1 -1
- package/dist/types/methods/password.d.ts.map +1 -1
- package/dist/types/methods/saml-sp/metadata.d.ts.map +1 -1
- package/dist/types/ports/audit-log.d.ts +25 -1
- package/dist/types/ports/audit-log.d.ts.map +1 -1
- package/dist/types/types/idp.d.ts +49 -39
- package/dist/types/types/idp.d.ts.map +1 -1
- package/dist/types/types/method.d.ts +11 -0
- package/dist/types/types/method.d.ts.map +1 -1
- package/package.json +2 -1
- package/src/client.ts +145 -129
- package/src/domain/authorize.ts +25 -4
- package/src/domain/callback.ts +40 -36
- package/src/domain/client-credentials.ts +20 -1
- package/src/domain/method-dispatch.ts +6 -0
- package/src/domain/method-route.ts +18 -0
- package/src/domain/mount.ts +113 -0
- package/src/domain/refresh.ts +39 -10
- package/src/domain/register.ts +27 -8
- package/src/domain/state-envelope.ts +40 -0
- package/src/domain/subject.ts +103 -0
- package/src/domain/token.ts +25 -1
- package/src/http/context.ts +3 -0
- package/src/http/handlers/method-route.ts +9 -5
- package/src/http/handlers/token.ts +2 -0
- package/src/http/middleware/tenant.ts +5 -24
- package/src/index.ts +18 -2
- package/src/methods/code.ts +15 -6
- package/src/methods/oauth2-generic.ts +6 -1
- package/src/methods/passkey.ts +6 -2
- package/src/methods/password.ts +19 -4
- package/src/methods/saml-sp/metadata.ts +5 -1
- package/src/ports/audit-log.ts +26 -1
- package/src/types/idp.ts +49 -42
- package/src/types/method.ts +11 -0
|
@@ -107,6 +107,17 @@ export type MethodHandler<P, S> = (ctx: MethodContext<S>) => Promise<MethodResul
|
|
|
107
107
|
export type MethodContext<S = unknown> = {
|
|
108
108
|
/** The raw Web Fetch `Request`. Web standard, not Hono `Context`. */
|
|
109
109
|
request: Request;
|
|
110
|
+
/**
|
|
111
|
+
* The issuer URL of this IdP, always present.
|
|
112
|
+
*
|
|
113
|
+
* Distinct from `dispatch.issuerUrl`, which is only populated for
|
|
114
|
+
* `GET /authorize`. Methods that render their own URLs — a form action
|
|
115
|
+
* re-rendered on a validation error, say — need it on every route, and
|
|
116
|
+
* must build those URLs with `mountedPath` so they carry the
|
|
117
|
+
* deployment's mount prefix. `ctx.request.url` is not a substitute: a
|
|
118
|
+
* proxy-stripped inbound URL has already lost that prefix.
|
|
119
|
+
*/
|
|
120
|
+
issuerUrl: string;
|
|
110
121
|
/** Path within the method's mount, e.g. `"/callback"` (without the `/<id>` prefix). */
|
|
111
122
|
subPath: string;
|
|
112
123
|
tenant: TenantContext;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"method.d.ts","sourceRoot":"","sources":["../../../src/types/method.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"method.d.ts","sourceRoot":"","sources":["../../../src/types/method.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@_mustachio/openauth",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
"build": "bun run script/build.ts",
|
|
12
12
|
"build:watch": "bun run script/build-watch.ts",
|
|
13
13
|
"test": "bun test",
|
|
14
|
+
"typecheck": "tsc --noEmit -p tsconfig.test.json",
|
|
14
15
|
"preview:ui": "bun --hot run script/preview-ui.ts"
|
|
15
16
|
},
|
|
16
17
|
"sideEffects": false,
|
package/src/client.ts
CHANGED
|
@@ -149,6 +149,37 @@ export interface ClientInput {
|
|
|
149
149
|
* ```
|
|
150
150
|
*/
|
|
151
151
|
issuer?: string
|
|
152
|
+
/**
|
|
153
|
+
* The client secret, for **confidential** clients only.
|
|
154
|
+
*
|
|
155
|
+
* Supply this for a server-side app registered as a confidential client;
|
|
156
|
+
* `exchange()` and `refresh()` then authenticate at `/token`. Omit it for
|
|
157
|
+
* public clients (SPA, mobile, CLI) — a secret cannot be kept in code
|
|
158
|
+
* that ships to users, and the IdP requires PKCE there instead.
|
|
159
|
+
*
|
|
160
|
+
* Without this, a confidential client's `exchange()` is rejected with
|
|
161
|
+
* `invalid_client`: the token endpoint has no way to authenticate it.
|
|
162
|
+
*
|
|
163
|
+
* @example
|
|
164
|
+
* ```ts
|
|
165
|
+
* {
|
|
166
|
+
* clientID: "my-server-app",
|
|
167
|
+
* clientSecret: process.env.CLIENT_SECRET
|
|
168
|
+
* }
|
|
169
|
+
* ```
|
|
170
|
+
*/
|
|
171
|
+
clientSecret?: string
|
|
172
|
+
/**
|
|
173
|
+
* How to present `clientSecret` at the token endpoint.
|
|
174
|
+
*
|
|
175
|
+
* `client_secret_basic` (the default) sends HTTP Basic credentials, which
|
|
176
|
+
* is what RFC 6749 §2.3.1 prefers and what the IdP parses first.
|
|
177
|
+
* `client_secret_post` puts them in the form body. Both are advertised in
|
|
178
|
+
* discovery as `token_endpoint_auth_methods_supported`.
|
|
179
|
+
*
|
|
180
|
+
* @default "client_secret_basic"
|
|
181
|
+
*/
|
|
182
|
+
tokenEndpointAuthMethod?: "client_secret_basic" | "client_secret_post"
|
|
152
183
|
/**
|
|
153
184
|
* Optionally, override the internally used fetch function.
|
|
154
185
|
*
|
|
@@ -159,18 +190,6 @@ export interface ClientInput {
|
|
|
159
190
|
}
|
|
160
191
|
|
|
161
192
|
export interface AuthorizeOptions {
|
|
162
|
-
/**
|
|
163
|
-
* Enable the PKCE flow. This is for SPA apps.
|
|
164
|
-
*
|
|
165
|
-
* ```ts
|
|
166
|
-
* {
|
|
167
|
-
* pkce: true
|
|
168
|
-
* }
|
|
169
|
-
* ```
|
|
170
|
-
*
|
|
171
|
-
* @default false
|
|
172
|
-
*/
|
|
173
|
-
pkce?: boolean
|
|
174
193
|
/**
|
|
175
194
|
* The provider you want to use for the OAuth flow.
|
|
176
195
|
*
|
|
@@ -306,7 +325,13 @@ export interface VerifyOptions {
|
|
|
306
325
|
*/
|
|
307
326
|
issuer?: string
|
|
308
327
|
/**
|
|
309
|
-
*
|
|
328
|
+
* The audience to require on the token.
|
|
329
|
+
*
|
|
330
|
+
* Defaults to this client's `clientID`, which is what the IdP puts in
|
|
331
|
+
* `aud` for an ordinary login. Set this when verifying a token minted
|
|
332
|
+
* for a **resource** — an `/authorize` call that passed `audience` puts
|
|
333
|
+
* that value in `aud` instead, so a resource server verifying it must
|
|
334
|
+
* name itself here.
|
|
310
335
|
*/
|
|
311
336
|
audience?: string
|
|
312
337
|
/**
|
|
@@ -320,9 +345,14 @@ export interface VerifyOptions {
|
|
|
320
345
|
|
|
321
346
|
export interface VerifyResult<T extends SubjectSchema> {
|
|
322
347
|
/**
|
|
323
|
-
* This is always `
|
|
348
|
+
* This is always `false` when the verify is successful.
|
|
349
|
+
*
|
|
350
|
+
* A literal, not an optional — `err?: undefined` would leave the
|
|
351
|
+
* property present on this arm, so `"err" in result` narrowed nothing
|
|
352
|
+
* and callers had to test truthiness instead. Matches `ExchangeSuccess`
|
|
353
|
+
* and `RefreshSuccess`.
|
|
324
354
|
*/
|
|
325
|
-
err
|
|
355
|
+
err: false
|
|
326
356
|
/**
|
|
327
357
|
* Returns the refreshed tokens only if they’ve been refreshed.
|
|
328
358
|
*
|
|
@@ -365,40 +395,30 @@ export interface VerifyError {
|
|
|
365
395
|
*/
|
|
366
396
|
export interface Client {
|
|
367
397
|
/**
|
|
368
|
-
* Start the
|
|
398
|
+
* Start the authorization code flow.
|
|
369
399
|
*
|
|
370
400
|
* ```ts
|
|
371
|
-
* const { url } = await client.authorize(<redirect_uri
|
|
401
|
+
* const { challenge, url } = await client.authorize(<redirect_uri>)
|
|
402
|
+
* // store `challenge`, then redirect the user to `url`
|
|
372
403
|
* ```
|
|
373
404
|
*
|
|
374
|
-
*
|
|
375
|
-
*
|
|
376
|
-
*
|
|
377
|
-
*
|
|
378
|
-
* secure.
|
|
379
|
-
*
|
|
380
|
-
* :::tip
|
|
381
|
-
* This returns a URL to redirect the user to. This starts the OAuth flow.
|
|
382
|
-
* :::
|
|
383
|
-
*
|
|
384
|
-
* This returns a URL to the auth server. You can redirect the user to the URL to start the
|
|
385
|
-
* OAuth flow.
|
|
405
|
+
* Returns the URL to send the user to, and a `challenge` carrying the
|
|
406
|
+
* CSRF `state` and the PKCE `verifier`. Persist the challenge (a cookie
|
|
407
|
+
* server-side, `sessionStorage` in a SPA) and hand the verifier back to
|
|
408
|
+
* {@link Client.exchange} when the user returns.
|
|
386
409
|
*
|
|
387
|
-
*
|
|
388
|
-
*
|
|
389
|
-
*
|
|
390
|
-
*
|
|
391
|
-
*
|
|
392
|
-
* "code",
|
|
393
|
-
* { pkce: true }
|
|
394
|
-
* )
|
|
395
|
-
* ```
|
|
410
|
+
* **PKCE is always used.** The IdP requires it for public clients, and
|
|
411
|
+
* OAuth 2.1 §7.5.1 recommends it for confidential ones too, so there is
|
|
412
|
+
* no reason to offer it as a toggle. Before 0.14.0 it was opt-in and
|
|
413
|
+
* off by default, which meant the documented server-side flow could not
|
|
414
|
+
* complete against a public client at all.
|
|
396
415
|
*
|
|
397
|
-
*
|
|
416
|
+
* Only the authorization code flow is supported. The implicit flow
|
|
417
|
+
* (`response_type=token`) is removed in OAuth 2.1 and the IdP rejects
|
|
418
|
+
* it with `unsupported_response_type`.
|
|
398
419
|
*/
|
|
399
420
|
authorize(
|
|
400
421
|
redirectURI: string,
|
|
401
|
-
response: "code" | "token",
|
|
402
422
|
opts?: AuthorizeOptions,
|
|
403
423
|
): Promise<AuthorizeResult>
|
|
404
424
|
/**
|
|
@@ -593,74 +613,76 @@ export function createClient(input: ClientInput): Client {
|
|
|
593
613
|
return result
|
|
594
614
|
}
|
|
595
615
|
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
616
|
+
/**
|
|
617
|
+
* Client authentication for the token endpoint (RFC 6749 §2.3.1).
|
|
618
|
+
* Applied only when a `clientSecret` is configured; public clients
|
|
619
|
+
* authenticate with PKCE instead and send neither.
|
|
620
|
+
*/
|
|
621
|
+
function applyClientAuth(
|
|
622
|
+
headers: Record<string, string>,
|
|
623
|
+
body: URLSearchParams,
|
|
624
|
+
) {
|
|
625
|
+
const secret = input.clientSecret
|
|
626
|
+
if (secret === undefined) return
|
|
627
|
+
if (
|
|
628
|
+
(input.tokenEndpointAuthMethod ?? "client_secret_basic") ===
|
|
629
|
+
"client_secret_post"
|
|
601
630
|
) {
|
|
602
|
-
|
|
631
|
+
body.set("client_secret", secret)
|
|
632
|
+
return
|
|
633
|
+
}
|
|
634
|
+
// §2.3.1 requires form-urlencoding each half before base64.
|
|
635
|
+
const cred = `${encodeURIComponent(input.clientID)}:${encodeURIComponent(secret)}`
|
|
636
|
+
headers["authorization"] = `Basic ${btoa(cred)}`
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
const result = {
|
|
640
|
+
async authorize(redirectURI: string, opts?: AuthorizeOptions) {
|
|
641
|
+
const wk = await getIssuer()
|
|
642
|
+
const url = new URL(wk.authorization_endpoint)
|
|
643
|
+
// PKCE unconditionally: required by the IdP for public clients, and
|
|
644
|
+
// recommended for confidential ones by OAuth 2.1 §7.5.1.
|
|
645
|
+
const pkce = await generatePKCE()
|
|
603
646
|
const challenge: Challenge = {
|
|
604
647
|
state: crypto.randomUUID(),
|
|
648
|
+
verifier: pkce.verifier,
|
|
605
649
|
}
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
650
|
+
url.searchParams.set("client_id", input.clientID)
|
|
651
|
+
url.searchParams.set("redirect_uri", redirectURI)
|
|
652
|
+
url.searchParams.set("response_type", "code")
|
|
653
|
+
url.searchParams.set("state", challenge.state)
|
|
654
|
+
url.searchParams.set("code_challenge_method", "S256")
|
|
655
|
+
url.searchParams.set("code_challenge", pkce.challenge)
|
|
656
|
+
if (opts?.provider) url.searchParams.set("provider", opts.provider)
|
|
611
657
|
if (opts?.scope !== undefined) {
|
|
612
658
|
const scope = Array.isArray(opts.scope)
|
|
613
659
|
? opts.scope.join(" ")
|
|
614
660
|
: opts.scope
|
|
615
|
-
if (scope)
|
|
616
|
-
}
|
|
617
|
-
if (opts?.pkce && response === "code") {
|
|
618
|
-
const pkce = await generatePKCE()
|
|
619
|
-
result.searchParams.set("code_challenge_method", "S256")
|
|
620
|
-
result.searchParams.set("code_challenge", pkce.challenge)
|
|
621
|
-
challenge.verifier = pkce.verifier
|
|
622
|
-
}
|
|
623
|
-
return {
|
|
624
|
-
challenge,
|
|
625
|
-
url: result.toString(),
|
|
661
|
+
if (scope) url.searchParams.set("scope", scope)
|
|
626
662
|
}
|
|
627
|
-
|
|
628
|
-
/**
|
|
629
|
-
* @deprecated use `authorize` instead, it will do pkce by default unless disabled with `opts.pkce = false`
|
|
630
|
-
*/
|
|
631
|
-
async pkce(
|
|
632
|
-
redirectURI: string,
|
|
633
|
-
opts?: {
|
|
634
|
-
provider?: string
|
|
635
|
-
},
|
|
636
|
-
) {
|
|
637
|
-
const result = new URL(issuer + "/authorize")
|
|
638
|
-
if (opts?.provider) result.searchParams.set("provider", opts.provider)
|
|
639
|
-
result.searchParams.set("client_id", input.clientID)
|
|
640
|
-
result.searchParams.set("redirect_uri", redirectURI)
|
|
641
|
-
result.searchParams.set("response_type", "code")
|
|
642
|
-
const pkce = await generatePKCE()
|
|
643
|
-
result.searchParams.set("code_challenge_method", "S256")
|
|
644
|
-
result.searchParams.set("code_challenge", pkce.challenge)
|
|
645
|
-
return [pkce.verifier, result.toString()]
|
|
663
|
+
return { challenge, url: url.toString() }
|
|
646
664
|
},
|
|
647
665
|
async exchange(
|
|
648
666
|
code: string,
|
|
649
667
|
redirectURI: string,
|
|
650
668
|
verifier?: string,
|
|
651
669
|
): Promise<ExchangeSuccess | ExchangeError> {
|
|
652
|
-
const
|
|
670
|
+
const wk = await getIssuer()
|
|
671
|
+
const body = new URLSearchParams({
|
|
672
|
+
code,
|
|
673
|
+
redirect_uri: redirectURI,
|
|
674
|
+
grant_type: "authorization_code",
|
|
675
|
+
client_id: input.clientID,
|
|
676
|
+
})
|
|
677
|
+
if (verifier) body.set("code_verifier", verifier)
|
|
678
|
+
const headers: Record<string, string> = {
|
|
679
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
680
|
+
}
|
|
681
|
+
applyClientAuth(headers, body)
|
|
682
|
+
const tokens = await f(wk.token_endpoint, {
|
|
653
683
|
method: "POST",
|
|
654
|
-
headers
|
|
655
|
-
|
|
656
|
-
},
|
|
657
|
-
body: new URLSearchParams({
|
|
658
|
-
code,
|
|
659
|
-
redirect_uri: redirectURI,
|
|
660
|
-
grant_type: "authorization_code",
|
|
661
|
-
client_id: input.clientID,
|
|
662
|
-
code_verifier: verifier || "",
|
|
663
|
-
}).toString(),
|
|
684
|
+
headers,
|
|
685
|
+
body: body.toString(),
|
|
664
686
|
})
|
|
665
687
|
const json = (await tokens.json()) as any
|
|
666
688
|
if (!tokens.ok) {
|
|
@@ -698,15 +720,24 @@ export function createClient(input: ClientInput): Client {
|
|
|
698
720
|
}
|
|
699
721
|
}
|
|
700
722
|
}
|
|
701
|
-
const
|
|
723
|
+
const wk = await getIssuer()
|
|
724
|
+
// `client_id` was previously omitted here too. The IdP rejects a
|
|
725
|
+
// confidential client's refresh outright when the request carries no
|
|
726
|
+
// client identity (RFC 6749 §6), so rotation was unreachable for
|
|
727
|
+
// exactly the clients that most need it.
|
|
728
|
+
const body = new URLSearchParams({
|
|
729
|
+
grant_type: "refresh_token",
|
|
730
|
+
refresh_token: refresh,
|
|
731
|
+
client_id: input.clientID,
|
|
732
|
+
})
|
|
733
|
+
const headers: Record<string, string> = {
|
|
734
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
735
|
+
}
|
|
736
|
+
applyClientAuth(headers, body)
|
|
737
|
+
const tokens = await f(wk.token_endpoint, {
|
|
702
738
|
method: "POST",
|
|
703
|
-
headers
|
|
704
|
-
|
|
705
|
-
},
|
|
706
|
-
body: new URLSearchParams({
|
|
707
|
-
grant_type: "refresh_token",
|
|
708
|
-
refresh_token: refresh,
|
|
709
|
-
}).toString(),
|
|
739
|
+
headers,
|
|
740
|
+
body: body.toString(),
|
|
710
741
|
})
|
|
711
742
|
const json = (await tokens.json()) as any
|
|
712
743
|
if (!tokens.ok) {
|
|
@@ -733,55 +764,40 @@ export function createClient(input: ClientInput): Client {
|
|
|
733
764
|
): Promise<VerifyResult<T> | VerifyError> {
|
|
734
765
|
const jwks = await getJWKS()
|
|
735
766
|
try {
|
|
736
|
-
//
|
|
737
|
-
//
|
|
738
|
-
//
|
|
739
|
-
//
|
|
740
|
-
// 2. Legacy `issuer({...})` — `payload.type` / `payload.properties`
|
|
741
|
-
// at the top level, gated by `payload.mode === "access"` so the
|
|
742
|
-
// same key ring couldn't sign a refresh token that verified as
|
|
743
|
-
// an access token.
|
|
767
|
+
// RFC 9068 §4 / RFC 7519 §4.1.3 — `aud` MUST be checked. Until
|
|
768
|
+
// 0.14.0 only `iss` was, so any client sharing an issuer accepted
|
|
769
|
+
// any other client's token: a confused deputy across every RP on
|
|
770
|
+
// the deployment.
|
|
744
771
|
//
|
|
745
|
-
//
|
|
772
|
+
// The IdP sets `aud` to the requested `audience` when one was
|
|
773
|
+
// asked for at /authorize, and to the client id otherwise — so a
|
|
774
|
+
// resource server verifying a resource-scoped token passes its own
|
|
775
|
+
// identifier via `options.audience`, and everyone else gets the
|
|
776
|
+
// right default for free.
|
|
746
777
|
const result = await jwtVerify<{
|
|
747
778
|
claim?: {
|
|
748
779
|
type: keyof T
|
|
749
780
|
properties: v1.InferInput<T[keyof T]>
|
|
750
781
|
}
|
|
751
|
-
mode?: "access" | "refresh"
|
|
752
|
-
type?: keyof T
|
|
753
|
-
properties?: v1.InferInput<T[keyof T]>
|
|
754
782
|
}>(token, jwks, {
|
|
755
783
|
issuer,
|
|
784
|
+
audience: options?.audience ?? input.clientID,
|
|
756
785
|
})
|
|
757
786
|
const claim = result.payload.claim
|
|
758
|
-
|
|
759
|
-
let subjectProperties: v1.InferInput<T[keyof T]> | undefined
|
|
760
|
-
if (claim && typeof claim.type === "string") {
|
|
761
|
-
// New shape — `mode` is not emitted; nested claim is authoritative.
|
|
762
|
-
subjectType = claim.type
|
|
763
|
-
subjectProperties = claim.properties
|
|
764
|
-
} else if (
|
|
765
|
-
result.payload.mode === "access" &&
|
|
766
|
-
typeof result.payload.type === "string"
|
|
767
|
-
) {
|
|
768
|
-
// Legacy shape — keep the mode gate so a refresh-token payload
|
|
769
|
-
// signed under the same keys does not verify as an access token.
|
|
770
|
-
subjectType = result.payload.type
|
|
771
|
-
subjectProperties = result.payload.properties
|
|
772
|
-
}
|
|
773
|
-
if (subjectType === undefined) {
|
|
787
|
+
if (!claim || typeof claim.type !== "string") {
|
|
774
788
|
return { err: new InvalidSubjectError() }
|
|
775
789
|
}
|
|
790
|
+
const subjectType = claim.type
|
|
776
791
|
const schema = subjects[subjectType]
|
|
777
792
|
if (!schema) {
|
|
778
793
|
return { err: new InvalidSubjectError() }
|
|
779
794
|
}
|
|
780
|
-
const validated = await schema["~standard"].validate(
|
|
795
|
+
const validated = await schema["~standard"].validate(claim.properties)
|
|
781
796
|
if (validated.issues) {
|
|
782
797
|
return { err: new InvalidSubjectError() }
|
|
783
798
|
}
|
|
784
799
|
return {
|
|
800
|
+
err: false,
|
|
785
801
|
aud: result.payload.aud as string,
|
|
786
802
|
subject: {
|
|
787
803
|
type: subjectType,
|
package/src/domain/authorize.ts
CHANGED
|
@@ -51,6 +51,7 @@ import { safeAudit } from "./audit"
|
|
|
51
51
|
import { randomId, randomToken } from "./crypto"
|
|
52
52
|
import { MethodCache } from "./method-cache"
|
|
53
53
|
import { dispatchMethod } from "./method-dispatch"
|
|
54
|
+
import { callbackTarget } from "./mount"
|
|
54
55
|
import { mintStateEnvelope } from "./state-envelope"
|
|
55
56
|
import { saveEncryptedCode } from "./token"
|
|
56
57
|
|
|
@@ -200,10 +201,19 @@ export async function startAuthorize(
|
|
|
200
201
|
const nonce = (deps.newNonce ?? randomToken)()
|
|
201
202
|
const flowTtl = deps.flowTtlMs ?? DEFAULT_FLOW_TTL_MS
|
|
202
203
|
const now = deps.clock()
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
204
|
+
// Source of truth for callback derivation — every other site that needs
|
|
205
|
+
// this URL goes through `callbackTarget` so they cannot drift apart.
|
|
206
|
+
// Note `path` is the *inbound* route (no mount prefix; the proxy strips
|
|
207
|
+
// it before we see the request) while `url` is the public form.
|
|
208
|
+
const {
|
|
209
|
+
host: callbackHost,
|
|
210
|
+
path: callbackPath,
|
|
211
|
+
url: callbackUrl,
|
|
212
|
+
} = callbackTarget({
|
|
213
|
+
issuerUrl: deps.issuerUrl,
|
|
214
|
+
methodId,
|
|
215
|
+
callbackHost: deps.callbackHostFor?.(tenant.id),
|
|
216
|
+
})
|
|
207
217
|
|
|
208
218
|
const record: FlowRecord = {
|
|
209
219
|
flowId,
|
|
@@ -262,6 +272,7 @@ export async function startAuthorize(
|
|
|
262
272
|
flow: record,
|
|
263
273
|
cookies: input.cookies,
|
|
264
274
|
sessionStore: deps.sessionStore,
|
|
275
|
+
issuerUrl: deps.issuerUrl,
|
|
265
276
|
dispatch: {
|
|
266
277
|
state: stateEnvelope,
|
|
267
278
|
callbackUrl,
|
|
@@ -402,6 +413,16 @@ async function issueCodeFromInlineSuccess(
|
|
|
402
413
|
{ keyStore: deps.keyStore, tokenStore: deps.tokenStore },
|
|
403
414
|
)
|
|
404
415
|
if (isErr(saved)) return err(saved.error)
|
|
416
|
+
await safeAudit(deps, {
|
|
417
|
+
kind: "authorize_succeeded",
|
|
418
|
+
tenantId: flow.tenantId,
|
|
419
|
+
clientId: flow.clientId,
|
|
420
|
+
methodId: flow.methodId,
|
|
421
|
+
methodKind: flow.methodKind,
|
|
422
|
+
flowId: flow.flowId,
|
|
423
|
+
providerSubject: result.providerSubject,
|
|
424
|
+
timestamp: now,
|
|
425
|
+
})
|
|
405
426
|
return ok({
|
|
406
427
|
kind: "issue-code",
|
|
407
428
|
code,
|
package/src/domain/callback.ts
CHANGED
|
@@ -35,8 +35,9 @@ import { safeAudit } from "./audit"
|
|
|
35
35
|
import { randomToken } from "./crypto"
|
|
36
36
|
import { MethodCache } from "./method-cache"
|
|
37
37
|
import { dispatchMethod } from "./method-dispatch"
|
|
38
|
+
import { callbackTarget } from "./mount"
|
|
38
39
|
import { saveEncryptedCode } from "./token"
|
|
39
|
-
import { verifyStateEnvelope } from "./state-envelope"
|
|
40
|
+
import { extractCallbackState, verifyStateEnvelope } from "./state-envelope"
|
|
40
41
|
import { AUTH_CODE_TTL_MS } from "./authorize"
|
|
41
42
|
|
|
42
43
|
export type CallbackOutput =
|
|
@@ -67,12 +68,13 @@ export type HandleCallbackInput = {
|
|
|
67
68
|
*/
|
|
68
69
|
tenant?: TenantContext
|
|
69
70
|
/**
|
|
70
|
-
* Per-request issuer URL (HTTP layer).
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
71
|
+
* Per-request issuer URL (HTTP layer). The IdP-initiated path derives
|
|
72
|
+
* the SP entityID / ACS from it — the same derivation the AuthnRequest
|
|
73
|
+
* and metadata paths use, so the values cannot drift — and every
|
|
74
|
+
* dispatched method receives it as `MethodContext.issuerUrl` to build
|
|
75
|
+
* mount-prefixed URLs of its own.
|
|
74
76
|
*/
|
|
75
|
-
issuerUrl
|
|
77
|
+
issuerUrl: string
|
|
76
78
|
}
|
|
77
79
|
|
|
78
80
|
export type HandleCallbackDeps = {
|
|
@@ -96,34 +98,6 @@ export type HandleCallbackDeps = {
|
|
|
96
98
|
) => Record<string, unknown> | Promise<Record<string, unknown>>
|
|
97
99
|
}
|
|
98
100
|
|
|
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
|
-
|
|
127
101
|
export async function handleCallback(
|
|
128
102
|
input: HandleCallbackInput,
|
|
129
103
|
deps: HandleCallbackDeps,
|
|
@@ -241,6 +215,7 @@ export async function handleCallback(
|
|
|
241
215
|
flow,
|
|
242
216
|
cookies: input.cookies,
|
|
243
217
|
sessionStore: deps.sessionStore,
|
|
218
|
+
issuerUrl: input.issuerUrl,
|
|
244
219
|
dispatch: null,
|
|
245
220
|
})
|
|
246
221
|
if (isErr(dispatched)) return err(dispatched.error)
|
|
@@ -282,6 +257,16 @@ async function translate(
|
|
|
282
257
|
{ keyStore: deps.keyStore, tokenStore: deps.tokenStore },
|
|
283
258
|
)
|
|
284
259
|
if (isErr(saved)) return err(saved.error)
|
|
260
|
+
await safeAudit(deps, {
|
|
261
|
+
kind: "authorize_succeeded",
|
|
262
|
+
tenantId: flow.tenantId,
|
|
263
|
+
clientId: flow.clientId,
|
|
264
|
+
methodId: flow.methodId,
|
|
265
|
+
methodKind: flow.methodKind,
|
|
266
|
+
flowId: flow.flowId,
|
|
267
|
+
providerSubject: result.providerSubject,
|
|
268
|
+
timestamp: now,
|
|
269
|
+
})
|
|
285
270
|
return ok({
|
|
286
271
|
kind: "issue-code",
|
|
287
272
|
code,
|
|
@@ -351,7 +336,7 @@ async function tryIdpInitiated(
|
|
|
351
336
|
url: URL,
|
|
352
337
|
): Promise<Result<CallbackOutput, AuthError> | null> {
|
|
353
338
|
if (input.rawRequest.method !== "POST") return null
|
|
354
|
-
if (!input.tenant
|
|
339
|
+
if (!input.tenant) return null
|
|
355
340
|
|
|
356
341
|
const segments = url.pathname.split("/").filter(Boolean)
|
|
357
342
|
if (segments.length < 2 || segments[0] !== "cb") return null
|
|
@@ -365,7 +350,14 @@ async function tryIdpInitiated(
|
|
|
365
350
|
const method = methodRes.value
|
|
366
351
|
if (method.unsolicitedCallback !== true) return null
|
|
367
352
|
|
|
368
|
-
|
|
353
|
+
// Derived from `issuerUrl`, not the inbound request URL: a path-mounted
|
|
354
|
+
// deployment sees a proxy-stripped pathname, so rebuilding from `url`
|
|
355
|
+
// would emit a callback URL missing the mount prefix.
|
|
356
|
+
const { url: callbackUrl } = callbackTarget({
|
|
357
|
+
issuerUrl: input.issuerUrl,
|
|
358
|
+
methodId,
|
|
359
|
+
callbackHost: url.host,
|
|
360
|
+
})
|
|
369
361
|
const dispatched = await dispatchMethod({
|
|
370
362
|
method,
|
|
371
363
|
route: "GET /callback",
|
|
@@ -375,6 +367,7 @@ async function tryIdpInitiated(
|
|
|
375
367
|
flow: null,
|
|
376
368
|
cookies: input.cookies,
|
|
377
369
|
sessionStore: deps.sessionStore,
|
|
370
|
+
issuerUrl: input.issuerUrl,
|
|
378
371
|
dispatch: { state: "", callbackUrl, issuerUrl: input.issuerUrl },
|
|
379
372
|
})
|
|
380
373
|
if (isErr(dispatched)) return err(dispatched.error)
|
|
@@ -463,6 +456,17 @@ async function tryIdpInitiated(
|
|
|
463
456
|
{ keyStore: deps.keyStore, tokenStore: deps.tokenStore },
|
|
464
457
|
)
|
|
465
458
|
if (isErr(saved)) return err(saved.error)
|
|
459
|
+
await safeAudit(deps, {
|
|
460
|
+
kind: "authorize_succeeded",
|
|
461
|
+
tenantId: input.tenant.id,
|
|
462
|
+
clientId: binding.clientId,
|
|
463
|
+
methodId,
|
|
464
|
+
methodKind: method.kind,
|
|
465
|
+
// Unsolicited: there is no FlowRecord, so no flowId to correlate on.
|
|
466
|
+
flowId: "",
|
|
467
|
+
providerSubject: result.providerSubject,
|
|
468
|
+
timestamp: now,
|
|
469
|
+
})
|
|
466
470
|
return ok({
|
|
467
471
|
kind: "issue-code",
|
|
468
472
|
code,
|
|
@@ -25,7 +25,7 @@ import { authError, type AuthError } from "../types/error"
|
|
|
25
25
|
import type { PersistUpstreamTokens, SuccessMapInput } from "../types/idp"
|
|
26
26
|
import type { Result } from "../types/result"
|
|
27
27
|
import { err, isErr, ok } from "../types/result"
|
|
28
|
-
import type { SubjectClaim } from "../types/subject"
|
|
28
|
+
import type { SubjectClaim, SubjectSchema } from "../types/subject"
|
|
29
29
|
import type { TenantContext, TenantId } from "../types/tenant"
|
|
30
30
|
import type { TokenResponse } from "../types/token"
|
|
31
31
|
|
|
@@ -33,6 +33,8 @@ import { verifyClientCredentials } from "./client-auth"
|
|
|
33
33
|
import { randomId } from "./crypto"
|
|
34
34
|
import { mintTokens } from "./token"
|
|
35
35
|
import { MethodCache } from "./method-cache"
|
|
36
|
+
import { safeAudit } from "./audit"
|
|
37
|
+
import { validateSubjectClaim } from "./subject"
|
|
36
38
|
|
|
37
39
|
export type ClientCredentialsRequest = {
|
|
38
40
|
grantType: "client_credentials"
|
|
@@ -50,6 +52,8 @@ export type ClientCredentialsDeps = {
|
|
|
50
52
|
auditLog?: AuditLog
|
|
51
53
|
methodCache: MethodCache
|
|
52
54
|
success: (input: SuccessMapInput) => Promise<SubjectClaim>
|
|
55
|
+
/** Host-declared subject schemas; the claim is validated against them. */
|
|
56
|
+
subjects: SubjectSchema
|
|
53
57
|
persistUpstreamTokens?: PersistUpstreamTokens
|
|
54
58
|
issuerUrl: string
|
|
55
59
|
clock: () => number
|
|
@@ -156,6 +160,21 @@ export async function clientCredentialsGrant(
|
|
|
156
160
|
return err(authError.serverError("success callback threw", e))
|
|
157
161
|
}
|
|
158
162
|
|
|
163
|
+
const checked = await validateSubjectClaim(deps.subjects, claim)
|
|
164
|
+
if (isErr(checked)) {
|
|
165
|
+
await safeAudit(deps, {
|
|
166
|
+
kind: "invalid_subject_claim",
|
|
167
|
+
tenantId: tenant.id,
|
|
168
|
+
clientId: client.id,
|
|
169
|
+
subjectType: checked.error.rejection.subjectType,
|
|
170
|
+
reason: checked.error.rejection.reason,
|
|
171
|
+
detail: checked.error.rejection.detail,
|
|
172
|
+
timestamp: deps.clock(),
|
|
173
|
+
})
|
|
174
|
+
return err(checked.error)
|
|
175
|
+
}
|
|
176
|
+
claim = checked.value
|
|
177
|
+
|
|
159
178
|
// 6. Mint access only — RFC 6749 §4.4.3 says client_credentials SHOULD
|
|
160
179
|
// NOT issue a refresh token. `skipRefresh: true` keeps the token-
|
|
161
180
|
// store from accumulating orphaned rows that the response would
|