@_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.
Files changed (88) hide show
  1. package/dist/esm/client.js +61 -62
  2. package/dist/esm/domain/authorize.js +21 -3
  3. package/dist/esm/domain/callback.js +30 -21
  4. package/dist/esm/domain/client-credentials.js +16 -0
  5. package/dist/esm/domain/method-dispatch.js +1 -0
  6. package/dist/esm/domain/method-route.js +12 -0
  7. package/dist/esm/domain/mount.js +29 -0
  8. package/dist/esm/domain/refresh.js +16 -5
  9. package/dist/esm/domain/register.js +8 -2
  10. package/dist/esm/domain/state-envelope.js +19 -0
  11. package/dist/esm/domain/subject.js +35 -0
  12. package/dist/esm/domain/token.js +15 -0
  13. package/dist/esm/http/handlers/method-route.js +8 -3
  14. package/dist/esm/http/handlers/token.js +2 -0
  15. package/dist/esm/http/middleware/tenant.js +5 -20
  16. package/dist/esm/index.js +4 -0
  17. package/dist/esm/methods/code.js +9 -8
  18. package/dist/esm/methods/oauth2-generic.js +2 -1
  19. package/dist/esm/methods/passkey.js +3 -2
  20. package/dist/esm/methods/password.js +9 -8
  21. package/dist/esm/methods/saml-sp/metadata.js +2 -1
  22. package/dist/types/client.d.ts +61 -40
  23. package/dist/types/client.d.ts.map +1 -1
  24. package/dist/types/domain/authorize.d.ts.map +1 -1
  25. package/dist/types/domain/callback.d.ts +6 -5
  26. package/dist/types/domain/callback.d.ts.map +1 -1
  27. package/dist/types/domain/client-credentials.d.ts +3 -1
  28. package/dist/types/domain/client-credentials.d.ts.map +1 -1
  29. package/dist/types/domain/method-dispatch.d.ts +5 -0
  30. package/dist/types/domain/method-dispatch.d.ts.map +1 -1
  31. package/dist/types/domain/method-route.d.ts +6 -0
  32. package/dist/types/domain/method-route.d.ts.map +1 -1
  33. package/dist/types/domain/mount.d.ts +90 -0
  34. package/dist/types/domain/mount.d.ts.map +1 -0
  35. package/dist/types/domain/refresh.d.ts.map +1 -1
  36. package/dist/types/domain/register.d.ts.map +1 -1
  37. package/dist/types/domain/state-envelope.d.ts +22 -0
  38. package/dist/types/domain/state-envelope.d.ts.map +1 -1
  39. package/dist/types/domain/subject.d.ts +48 -0
  40. package/dist/types/domain/subject.d.ts.map +1 -0
  41. package/dist/types/domain/token.d.ts +7 -1
  42. package/dist/types/domain/token.d.ts.map +1 -1
  43. package/dist/types/http/context.d.ts +3 -0
  44. package/dist/types/http/context.d.ts.map +1 -1
  45. package/dist/types/http/handlers/method-route.d.ts.map +1 -1
  46. package/dist/types/http/handlers/token.d.ts.map +1 -1
  47. package/dist/types/http/middleware/tenant.d.ts.map +1 -1
  48. package/dist/types/http/schemas/revocation.d.ts +4 -4
  49. package/dist/types/http/schemas/token.d.ts +12 -12
  50. package/dist/types/index.d.ts +17 -1
  51. package/dist/types/index.d.ts.map +1 -1
  52. package/dist/types/methods/code.d.ts.map +1 -1
  53. package/dist/types/methods/oauth2-generic.d.ts.map +1 -1
  54. package/dist/types/methods/passkey.d.ts.map +1 -1
  55. package/dist/types/methods/password.d.ts.map +1 -1
  56. package/dist/types/methods/saml-sp/metadata.d.ts.map +1 -1
  57. package/dist/types/ports/audit-log.d.ts +25 -1
  58. package/dist/types/ports/audit-log.d.ts.map +1 -1
  59. package/dist/types/types/idp.d.ts +49 -39
  60. package/dist/types/types/idp.d.ts.map +1 -1
  61. package/dist/types/types/method.d.ts +11 -0
  62. package/dist/types/types/method.d.ts.map +1 -1
  63. package/package.json +2 -1
  64. package/src/client.ts +145 -129
  65. package/src/domain/authorize.ts +25 -4
  66. package/src/domain/callback.ts +40 -36
  67. package/src/domain/client-credentials.ts +20 -1
  68. package/src/domain/method-dispatch.ts +6 -0
  69. package/src/domain/method-route.ts +18 -0
  70. package/src/domain/mount.ts +113 -0
  71. package/src/domain/refresh.ts +39 -10
  72. package/src/domain/register.ts +27 -8
  73. package/src/domain/state-envelope.ts +40 -0
  74. package/src/domain/subject.ts +103 -0
  75. package/src/domain/token.ts +25 -1
  76. package/src/http/context.ts +3 -0
  77. package/src/http/handlers/method-route.ts +9 -5
  78. package/src/http/handlers/token.ts +2 -0
  79. package/src/http/middleware/tenant.ts +5 -24
  80. package/src/index.ts +18 -2
  81. package/src/methods/code.ts +15 -6
  82. package/src/methods/oauth2-generic.ts +6 -1
  83. package/src/methods/passkey.ts +6 -2
  84. package/src/methods/password.ts +19 -4
  85. package/src/methods/saml-sp/metadata.ts +5 -1
  86. package/src/ports/audit-log.ts +26 -1
  87. package/src/types/idp.ts +49 -42
  88. 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.13.2",
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
- * @internal
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 `undefined` when the verify is successful.
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?: undefined
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 autorization flow. For example, in SSR sites.
398
+ * Start the authorization code flow.
369
399
  *
370
400
  * ```ts
371
- * const { url } = await client.authorize(<redirect_uri>, "code")
401
+ * const { challenge, url } = await client.authorize(<redirect_uri>)
402
+ * // store `challenge`, then redirect the user to `url`
372
403
  * ```
373
404
  *
374
- * This takes a redirect URI and the type of flow you want to use. The redirect URI is the
375
- * location where the user will be redirected to after the flow is complete.
376
- *
377
- * Supports both the _code_ and _token_ flows. We recommend using the _code_ flow as it's more
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
- * For SPA apps, we recommend using the PKCE flow.
388
- *
389
- * ```ts {4}
390
- * const { challenge, url } = await client.authorize(
391
- * <redirect_uri>,
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
- * This returns a redirect URL and a challenge that you need to use later to verify the code.
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
- const result = {
597
- async authorize(
598
- redirectURI: string,
599
- response: "code" | "token",
600
- opts?: AuthorizeOptions,
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
- const result = new URL(issuer + "/authorize")
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
- result.searchParams.set("client_id", input.clientID)
607
- result.searchParams.set("redirect_uri", redirectURI)
608
- result.searchParams.set("response_type", response)
609
- result.searchParams.set("state", challenge.state)
610
- if (opts?.provider) result.searchParams.set("provider", opts.provider)
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) result.searchParams.set("scope", 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 tokens = await f(issuer + "/token", {
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
- "Content-Type": "application/x-www-form-urlencoded",
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 tokens = await f(issuer + "/token", {
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
- "Content-Type": "application/x-www-form-urlencoded",
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
- // Two issuer shapes are supported:
737
- //
738
- // 1. New `createIdP` — `AccessTokenClaims.claim = { type,
739
- // properties }`. No `mode` claim.
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
- // Prefer the nested shape, fall back to the top-level shape.
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
- let subjectType: keyof T | undefined
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(subjectProperties)
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,
@@ -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
- const callbackHost =
204
- deps.callbackHostFor?.(tenant.id) ?? new URL(deps.issuerUrl).host
205
- const callbackPath = `/cb/${methodId}`
206
- const callbackUrl = `${new URL(deps.issuerUrl).protocol}//${callbackHost}${callbackPath}`
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,
@@ -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). 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.
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?: string
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 || !input.issuerUrl) return null
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
- const callbackUrl = `${url.protocol}//${url.host}/cb/${methodId}`
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