@_mustachio/openauth 0.13.3 → 0.15.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 (61) hide show
  1. package/dist/esm/client.js +61 -62
  2. package/dist/esm/domain/authorize.js +10 -0
  3. package/dist/esm/domain/callback.js +21 -19
  4. package/dist/esm/domain/client-credentials.js +16 -0
  5. package/dist/esm/domain/method-route.js +10 -0
  6. package/dist/esm/domain/refresh.js +16 -5
  7. package/dist/esm/domain/register.js +8 -2
  8. package/dist/esm/domain/state-envelope.js +19 -0
  9. package/dist/esm/domain/subject.js +35 -0
  10. package/dist/esm/domain/token.js +28 -0
  11. package/dist/esm/http/handlers/token.js +6 -0
  12. package/dist/esm/http/middleware/tenant.js +5 -20
  13. package/dist/esm/index.js +2 -0
  14. package/dist/types/client.d.ts +61 -40
  15. package/dist/types/client.d.ts.map +1 -1
  16. package/dist/types/domain/authorize.d.ts.map +1 -1
  17. package/dist/types/domain/callback.d.ts.map +1 -1
  18. package/dist/types/domain/client-credentials.d.ts +6 -2
  19. package/dist/types/domain/client-credentials.d.ts.map +1 -1
  20. package/dist/types/domain/method-route.d.ts.map +1 -1
  21. package/dist/types/domain/refresh.d.ts +3 -0
  22. package/dist/types/domain/refresh.d.ts.map +1 -1
  23. package/dist/types/domain/register.d.ts.map +1 -1
  24. package/dist/types/domain/state-envelope.d.ts +22 -0
  25. package/dist/types/domain/state-envelope.d.ts.map +1 -1
  26. package/dist/types/domain/subject.d.ts +48 -0
  27. package/dist/types/domain/subject.d.ts.map +1 -0
  28. package/dist/types/domain/token-exchange.d.ts +3 -1
  29. package/dist/types/domain/token-exchange.d.ts.map +1 -1
  30. package/dist/types/domain/token.d.ts +11 -2
  31. package/dist/types/domain/token.d.ts.map +1 -1
  32. package/dist/types/http/context.d.ts +6 -0
  33. package/dist/types/http/context.d.ts.map +1 -1
  34. package/dist/types/http/handlers/token.d.ts.map +1 -1
  35. package/dist/types/http/middleware/tenant.d.ts.map +1 -1
  36. package/dist/types/http/schemas/revocation.d.ts +4 -4
  37. package/dist/types/http/schemas/token.d.ts +12 -12
  38. package/dist/types/index.d.ts +1 -1
  39. package/dist/types/index.d.ts.map +1 -1
  40. package/dist/types/ports/audit-log.d.ts +25 -1
  41. package/dist/types/ports/audit-log.d.ts.map +1 -1
  42. package/dist/types/types/idp.d.ts +105 -38
  43. package/dist/types/types/idp.d.ts.map +1 -1
  44. package/package.json +1 -1
  45. package/src/client.ts +145 -129
  46. package/src/domain/authorize.ts +10 -0
  47. package/src/domain/callback.ts +22 -29
  48. package/src/domain/client-credentials.ts +27 -2
  49. package/src/domain/method-route.ts +10 -0
  50. package/src/domain/refresh.ts +42 -10
  51. package/src/domain/register.ts +27 -8
  52. package/src/domain/state-envelope.ts +40 -0
  53. package/src/domain/subject.ts +103 -0
  54. package/src/domain/token-exchange.ts +3 -1
  55. package/src/domain/token.ts +54 -2
  56. package/src/http/context.ts +6 -0
  57. package/src/http/handlers/token.ts +6 -0
  58. package/src/http/middleware/tenant.ts +5 -24
  59. package/src/index.ts +3 -2
  60. package/src/ports/audit-log.ts +26 -1
  61. package/src/types/idp.ts +107 -41
@@ -37,59 +37,59 @@ function createClient(input) {
37
37
  jwksCache.set(issuer, result);
38
38
  return result;
39
39
  }
40
+ function applyClientAuth(headers, body) {
41
+ const secret = input.clientSecret;
42
+ if (secret === undefined)
43
+ return;
44
+ if ((input.tokenEndpointAuthMethod ?? "client_secret_basic") === "client_secret_post") {
45
+ body.set("client_secret", secret);
46
+ return;
47
+ }
48
+ const cred = `${encodeURIComponent(input.clientID)}:${encodeURIComponent(secret)}`;
49
+ headers["authorization"] = `Basic ${btoa(cred)}`;
50
+ }
40
51
  const result = {
41
- async authorize(redirectURI, response, opts) {
42
- const result = new URL(issuer + "/authorize");
52
+ async authorize(redirectURI, opts) {
53
+ const wk = await getIssuer();
54
+ const url = new URL(wk.authorization_endpoint);
55
+ const pkce = await generatePKCE();
43
56
  const challenge = {
44
- state: crypto.randomUUID()
57
+ state: crypto.randomUUID(),
58
+ verifier: pkce.verifier
45
59
  };
46
- result.searchParams.set("client_id", input.clientID);
47
- result.searchParams.set("redirect_uri", redirectURI);
48
- result.searchParams.set("response_type", response);
49
- result.searchParams.set("state", challenge.state);
60
+ url.searchParams.set("client_id", input.clientID);
61
+ url.searchParams.set("redirect_uri", redirectURI);
62
+ url.searchParams.set("response_type", "code");
63
+ url.searchParams.set("state", challenge.state);
64
+ url.searchParams.set("code_challenge_method", "S256");
65
+ url.searchParams.set("code_challenge", pkce.challenge);
50
66
  if (opts?.provider)
51
- result.searchParams.set("provider", opts.provider);
67
+ url.searchParams.set("provider", opts.provider);
52
68
  if (opts?.scope !== undefined) {
53
69
  const scope = Array.isArray(opts.scope) ? opts.scope.join(" ") : opts.scope;
54
70
  if (scope)
55
- result.searchParams.set("scope", scope);
56
- }
57
- if (opts?.pkce && response === "code") {
58
- const pkce = await generatePKCE();
59
- result.searchParams.set("code_challenge_method", "S256");
60
- result.searchParams.set("code_challenge", pkce.challenge);
61
- challenge.verifier = pkce.verifier;
71
+ url.searchParams.set("scope", scope);
62
72
  }
63
- return {
64
- challenge,
65
- url: result.toString()
66
- };
67
- },
68
- async pkce(redirectURI, opts) {
69
- const result = new URL(issuer + "/authorize");
70
- if (opts?.provider)
71
- result.searchParams.set("provider", opts.provider);
72
- result.searchParams.set("client_id", input.clientID);
73
- result.searchParams.set("redirect_uri", redirectURI);
74
- result.searchParams.set("response_type", "code");
75
- const pkce = await generatePKCE();
76
- result.searchParams.set("code_challenge_method", "S256");
77
- result.searchParams.set("code_challenge", pkce.challenge);
78
- return [pkce.verifier, result.toString()];
73
+ return { challenge, url: url.toString() };
79
74
  },
80
75
  async exchange(code, redirectURI, verifier) {
81
- const tokens = await f(issuer + "/token", {
76
+ const wk = await getIssuer();
77
+ const body = new URLSearchParams({
78
+ code,
79
+ redirect_uri: redirectURI,
80
+ grant_type: "authorization_code",
81
+ client_id: input.clientID
82
+ });
83
+ if (verifier)
84
+ body.set("code_verifier", verifier);
85
+ const headers = {
86
+ "Content-Type": "application/x-www-form-urlencoded"
87
+ };
88
+ applyClientAuth(headers, body);
89
+ const tokens = await f(wk.token_endpoint, {
82
90
  method: "POST",
83
- headers: {
84
- "Content-Type": "application/x-www-form-urlencoded"
85
- },
86
- body: new URLSearchParams({
87
- code,
88
- redirect_uri: redirectURI,
89
- grant_type: "authorization_code",
90
- client_id: input.clientID,
91
- code_verifier: verifier || ""
92
- }).toString()
91
+ headers,
92
+ body: body.toString()
93
93
  });
94
94
  const json = await tokens.json();
95
95
  if (!tokens.ok) {
@@ -121,15 +121,20 @@ function createClient(input) {
121
121
  };
122
122
  }
123
123
  }
124
- const tokens = await f(issuer + "/token", {
124
+ const wk = await getIssuer();
125
+ const body = new URLSearchParams({
126
+ grant_type: "refresh_token",
127
+ refresh_token: refresh,
128
+ client_id: input.clientID
129
+ });
130
+ const headers = {
131
+ "Content-Type": "application/x-www-form-urlencoded"
132
+ };
133
+ applyClientAuth(headers, body);
134
+ const tokens = await f(wk.token_endpoint, {
125
135
  method: "POST",
126
- headers: {
127
- "Content-Type": "application/x-www-form-urlencoded"
128
- },
129
- body: new URLSearchParams({
130
- grant_type: "refresh_token",
131
- refresh_token: refresh
132
- }).toString()
136
+ headers,
137
+ body: body.toString()
133
138
  });
134
139
  const json = await tokens.json();
135
140
  if (!tokens.ok) {
@@ -151,30 +156,24 @@ function createClient(input) {
151
156
  const jwks = await getJWKS();
152
157
  try {
153
158
  const result = await jwtVerify(token, jwks, {
154
- issuer
159
+ issuer,
160
+ audience: options?.audience ?? input.clientID
155
161
  });
156
162
  const claim = result.payload.claim;
157
- let subjectType;
158
- let subjectProperties;
159
- if (claim && typeof claim.type === "string") {
160
- subjectType = claim.type;
161
- subjectProperties = claim.properties;
162
- } else if (result.payload.mode === "access" && typeof result.payload.type === "string") {
163
- subjectType = result.payload.type;
164
- subjectProperties = result.payload.properties;
165
- }
166
- if (subjectType === undefined) {
163
+ if (!claim || typeof claim.type !== "string") {
167
164
  return { err: new InvalidSubjectError };
168
165
  }
166
+ const subjectType = claim.type;
169
167
  const schema = subjects[subjectType];
170
168
  if (!schema) {
171
169
  return { err: new InvalidSubjectError };
172
170
  }
173
- const validated = await schema["~standard"].validate(subjectProperties);
171
+ const validated = await schema["~standard"].validate(claim.properties);
174
172
  if (validated.issues) {
175
173
  return { err: new InvalidSubjectError };
176
174
  }
177
175
  return {
176
+ err: false,
178
177
  aud: result.payload.aud,
179
178
  subject: {
180
179
  type: subjectType,
@@ -211,6 +211,16 @@ async function issueCodeFromInlineSuccess(result, record, deps) {
211
211
  }, AUTH_CODE_TTL_MS, { keyStore: deps.keyStore, tokenStore: deps.tokenStore });
212
212
  if (isErr(saved))
213
213
  return err(saved.error);
214
+ await safeAudit(deps, {
215
+ kind: "authorize_succeeded",
216
+ tenantId: flow.tenantId,
217
+ clientId: flow.clientId,
218
+ methodId: flow.methodId,
219
+ methodKind: flow.methodKind,
220
+ flowId: flow.flowId,
221
+ providerSubject: result.providerSubject,
222
+ timestamp: now
223
+ });
214
224
  return ok({
215
225
  kind: "issue-code",
216
226
  code,
@@ -6,26 +6,8 @@ import { randomToken } from "./crypto";
6
6
  import { dispatchMethod } from "./method-dispatch";
7
7
  import { callbackTarget } from "./mount";
8
8
  import { saveEncryptedCode } from "./token";
9
- import { verifyStateEnvelope } from "./state-envelope";
9
+ import { extractCallbackState, verifyStateEnvelope } from "./state-envelope";
10
10
  import { AUTH_CODE_TTL_MS } from "./authorize";
11
- async function extractCallbackState(req) {
12
- const fromQuery = new URL(req.url).searchParams.get("state");
13
- if (fromQuery)
14
- return fromQuery;
15
- if (req.method !== "POST")
16
- return null;
17
- const ct = req.headers.get("content-type") ?? "";
18
- if (!ct.includes("application/x-www-form-urlencoded"))
19
- return null;
20
- try {
21
- const body = await req.clone().text();
22
- const form = new URLSearchParams(body);
23
- const v = form.get("state") ?? form.get("RelayState");
24
- return v && v.length > 0 ? v : null;
25
- } catch {
26
- return null;
27
- }
28
- }
29
11
  async function handleCallback(input, deps) {
30
12
  const url = new URL(input.rawRequest.url);
31
13
  const state = await extractCallbackState(input.rawRequest);
@@ -147,6 +129,16 @@ async function translate(result, flow, deps) {
147
129
  }, AUTH_CODE_TTL_MS, { keyStore: deps.keyStore, tokenStore: deps.tokenStore });
148
130
  if (isErr(saved))
149
131
  return err(saved.error);
132
+ await safeAudit(deps, {
133
+ kind: "authorize_succeeded",
134
+ tenantId: flow.tenantId,
135
+ clientId: flow.clientId,
136
+ methodId: flow.methodId,
137
+ methodKind: flow.methodKind,
138
+ flowId: flow.flowId,
139
+ providerSubject: result.providerSubject,
140
+ timestamp: now
141
+ });
150
142
  return ok({
151
143
  kind: "issue-code",
152
144
  code,
@@ -282,6 +274,16 @@ async function tryIdpInitiated(input, deps, url) {
282
274
  }, AUTH_CODE_TTL_MS, { keyStore: deps.keyStore, tokenStore: deps.tokenStore });
283
275
  if (isErr(saved))
284
276
  return err(saved.error);
277
+ await safeAudit(deps, {
278
+ kind: "authorize_succeeded",
279
+ tenantId: input.tenant.id,
280
+ clientId: binding.clientId,
281
+ methodId,
282
+ methodKind: method.kind,
283
+ flowId: "",
284
+ providerSubject: result.providerSubject,
285
+ timestamp: now
286
+ });
285
287
  return ok({
286
288
  kind: "issue-code",
287
289
  code,
@@ -4,6 +4,8 @@ import { err, isErr, ok } from "../types/result";
4
4
  import { verifyClientCredentials } from "./client-auth";
5
5
  import { randomId } from "./crypto";
6
6
  import { mintTokens } from "./token";
7
+ import { safeAudit } from "./audit";
8
+ import { validateSubjectClaim } from "./subject";
7
9
  async function clientCredentialsGrant(req, deps, tenantId) {
8
10
  const tenantCfg = await deps.configStore.getTenantConfig(tenantId);
9
11
  if (isErr(tenantCfg))
@@ -67,6 +69,20 @@ async function clientCredentialsGrant(req, deps, tenantId) {
67
69
  } catch (e) {
68
70
  return err(authError.serverError("success callback threw", e));
69
71
  }
72
+ const checked = await validateSubjectClaim(deps.subjects, claim);
73
+ if (isErr(checked)) {
74
+ await safeAudit(deps, {
75
+ kind: "invalid_subject_claim",
76
+ tenantId: tenant.id,
77
+ clientId: client.id,
78
+ subjectType: checked.error.rejection.subjectType,
79
+ reason: checked.error.rejection.reason,
80
+ detail: checked.error.rejection.detail,
81
+ timestamp: deps.clock()
82
+ });
83
+ return err(checked.error);
84
+ }
85
+ claim = checked.value;
70
86
  const minted = await mintTokens({
71
87
  tenant,
72
88
  claim,
@@ -74,6 +74,16 @@ async function translate(result, flow, deps) {
74
74
  }, AUTH_CODE_TTL_MS, { keyStore: deps.keyStore, tokenStore: deps.tokenStore });
75
75
  if (isErr(saved))
76
76
  return err(saved.error);
77
+ await safeAudit(deps, {
78
+ kind: "authorize_succeeded",
79
+ tenantId: final.tenantId,
80
+ clientId: final.clientId,
81
+ methodId: final.methodId,
82
+ methodKind: final.methodKind,
83
+ flowId: final.flowId,
84
+ providerSubject: result.providerSubject,
85
+ timestamp: now
86
+ });
77
87
  return ok({
78
88
  kind: "issue-code",
79
89
  code,
@@ -4,6 +4,16 @@ import { err, isErr } from "../types/result";
4
4
  import { safeAudit } from "./audit";
5
5
  import { verifyClientCredentials } from "./client-auth";
6
6
  import { mintTokens } from "./token";
7
+ function validateRequestedScopes(requested, granted) {
8
+ if (!requested)
9
+ return null;
10
+ for (const s of requested.split(" ").filter(Boolean)) {
11
+ if (!granted.includes(s)) {
12
+ return authError.invalidScope(`requested scope "${s}" not granted by original refresh token`);
13
+ }
14
+ }
15
+ return null;
16
+ }
7
17
  async function refreshTokens(req, deps) {
8
18
  const peek = await deps.tokenStore.peekRefresh(req.refreshToken);
9
19
  if (isErr(peek))
@@ -39,6 +49,9 @@ async function refreshTokens(req, deps) {
39
49
  return err(authError.invalidDpopProof("refresh token DPoP jkt does not match the original binding"));
40
50
  }
41
51
  }
52
+ const scopeErr = validateRequestedScopes(req.scope, peekedPayload.scopes);
53
+ if (scopeErr)
54
+ return err(scopeErr);
42
55
  const consumed = await deps.tokenStore.consumeRefresh(req.refreshToken, {
43
56
  reuseWindowMs: deps.reuseWindowMs
44
57
  });
@@ -56,12 +69,10 @@ async function refreshTokens(req, deps) {
56
69
  return err(consumed.error);
57
70
  }
58
71
  const payload = consumed.value;
72
+ const authoritativeScopeErr = validateRequestedScopes(req.scope, payload.scopes);
73
+ if (authoritativeScopeErr)
74
+ return err(authoritativeScopeErr);
59
75
  const requestedScopes = req.scope ? req.scope.split(" ").filter(Boolean) : payload.scopes;
60
- for (const s of requestedScopes) {
61
- if (!payload.scopes.includes(s)) {
62
- return err(authError.invalidScope(`requested scope "${s}" not granted by original refresh token`));
63
- }
64
- }
65
76
  const tenant = {
66
77
  id: payload.tenantId,
67
78
  config: tenantCfg.value,
@@ -50,12 +50,18 @@ async function registerNewClient(request, tenant, deps) {
50
50
  };
51
51
  const hookResult = await deps.registerClient({
52
52
  tenant,
53
- request
53
+ request,
54
+ client: clientConfig,
55
+ ...secret !== undefined ? { secret } : {}
54
56
  });
55
57
  if (isErr(hookResult))
56
58
  return err(hookResult.error);
57
59
  const persisted = hookResult.value.client;
58
- const persistedSecret = hookResult.value.secret;
60
+ const keptOurSecret = persisted.type === "confidential" && clientConfig.type === "confidential" && persisted.secretHash === clientConfig.secretHash;
61
+ const persistedSecret = hookResult.value.secret ?? (keptOurSecret ? secret : undefined);
62
+ if (persisted.type === "confidential" && persistedSecret === undefined) {
63
+ return err(authError.serverError("registerClient persisted a confidential client with a substituted " + "secretHash but returned no matching plaintext secret"));
64
+ }
59
65
  const issuedAt = Math.floor(deps.clock() / 1000);
60
66
  return ok({
61
67
  client_id: persisted.id,
@@ -74,7 +74,26 @@ function isEnvelopeShape(value) {
74
74
  const v = value;
75
75
  return typeof v.tenantId === "string" && typeof v.flowId === "string" && typeof v.nonce === "string" && typeof v.kid === "string";
76
76
  }
77
+ async function extractCallbackState(req) {
78
+ const fromQuery = new URL(req.url).searchParams.get("state");
79
+ if (fromQuery)
80
+ return fromQuery;
81
+ if (req.method !== "POST")
82
+ return null;
83
+ const ct = req.headers.get("content-type") ?? "";
84
+ if (!ct.toLowerCase().includes("application/x-www-form-urlencoded")) {
85
+ return null;
86
+ }
87
+ try {
88
+ const form = new URLSearchParams(await req.clone().text());
89
+ const v = form.get("state") ?? form.get("RelayState");
90
+ return v && v.length > 0 ? v : null;
91
+ } catch {
92
+ return null;
93
+ }
94
+ }
77
95
  export {
96
+ extractCallbackState,
78
97
  mintStateEnvelope,
79
98
  verifyStateEnvelope
80
99
  };
@@ -0,0 +1,35 @@
1
+ // src/domain/subject.ts
2
+ import { authError } from "../types/error";
3
+ import { err, ok } from "../types/result";
4
+ async function validateSubjectClaim(subjects, claim) {
5
+ const declared = Object.keys(subjects);
6
+ const subjectType = typeof claim?.type === "string" ? claim.type : "";
7
+ const schema = subjectType ? subjects[subjectType] : undefined;
8
+ if (!schema) {
9
+ return err(Object.assign(authError.serverError(`success() returned subject type "${subjectType}", which is not declared in \`subjects\``), {
10
+ rejection: {
11
+ reason: "unknown-type",
12
+ subjectType,
13
+ detail: `declared: ${declared.join(", ") || "(none)"}`
14
+ }
15
+ }));
16
+ }
17
+ const validated = await schema["~standard"].validate(claim.properties);
18
+ if (validated.issues) {
19
+ const detail = validated.issues.map((i) => (i.path ?? []).map(String).join(".") || "(root)").join(", ") || "(root)";
20
+ return err(Object.assign(authError.serverError(`success() returned properties that violate the "${subjectType}" schema`), {
21
+ rejection: {
22
+ reason: "invalid-properties",
23
+ subjectType,
24
+ detail
25
+ }
26
+ }));
27
+ }
28
+ return ok({
29
+ type: subjectType,
30
+ properties: validated.value
31
+ });
32
+ }
33
+ export {
34
+ validateSubjectClaim
35
+ };
@@ -15,6 +15,7 @@ import {
15
15
  import { buildIdTokenClaims, shouldIssueIdToken } from "./id-token";
16
16
  import { signAccessToken, signIdToken } from "./jwt";
17
17
  import { validatePkce } from "./pkce";
18
+ import { validateSubjectClaim } from "./subject";
18
19
  async function saveEncryptedCode(code, payload, ttl, deps) {
19
20
  const keyResult = await deps.keyStore.currentEncryptionKey();
20
21
  if (isErr(keyResult))
@@ -91,6 +92,20 @@ async function exchangeCode(req, deps) {
91
92
  } catch (e) {
92
93
  return err(authError.serverError("success callback threw", e));
93
94
  }
95
+ const checked = await validateSubjectClaim(deps.subjects, claim);
96
+ if (isErr(checked)) {
97
+ await safeAudit(deps, {
98
+ kind: "invalid_subject_claim",
99
+ tenantId: payload.tenantId,
100
+ clientId: payload.clientId,
101
+ subjectType: checked.error.rejection.subjectType,
102
+ reason: checked.error.rejection.reason,
103
+ detail: checked.error.rejection.detail,
104
+ timestamp: deps.clock()
105
+ });
106
+ return err(checked.error);
107
+ }
108
+ claim = checked.value;
94
109
  if (deps.persistUpstreamTokens) {
95
110
  try {
96
111
  await deps.persistUpstreamTokens({
@@ -124,6 +139,19 @@ async function mintTokens(args) {
124
139
  const now = deps.clock();
125
140
  const receivingClient = tenant.config.clients.find((c) => c.id === payload.clientId);
126
141
  const subjectId = await deriveSubjectId(claim, receivingClient?.sectorIdentifier);
142
+ if (deps.onTokenIssued) {
143
+ try {
144
+ await deps.onTokenIssued({
145
+ tenant,
146
+ clientId: payload.clientId,
147
+ subjectId,
148
+ claim,
149
+ ...skipRefresh ? {} : { family }
150
+ });
151
+ } catch (e) {
152
+ return err(authError.serverError("onTokenIssued hook threw", e));
153
+ }
154
+ }
127
155
  const keyRes = await deps.keyStore.currentSigningKey();
128
156
  if (isErr(keyRes))
129
157
  return err(keyRes.error);
@@ -67,9 +67,11 @@ function makeTokenHandler(deps) {
67
67
  keyStore: deps.keyStore,
68
68
  ...deps.auditLog ? { auditLog: deps.auditLog } : {},
69
69
  success: deps.success,
70
+ subjects: deps.subjects,
70
71
  ...deps.persistUpstreamTokens ? { persistUpstreamTokens: deps.persistUpstreamTokens } : {},
71
72
  issuerUrl: c.get("issuerUrl"),
72
73
  clock: deps.clock,
74
+ ...deps.onTokenIssued ? { onTokenIssued: deps.onTokenIssued } : {},
73
75
  ...deps.customScopeClaims !== undefined ? { customScopeClaims: deps.customScopeClaims } : {}
74
76
  });
75
77
  if (isErr(result))
@@ -99,9 +101,11 @@ function makeTokenHandler(deps) {
99
101
  ...deps.auditLog ? { auditLog: deps.auditLog } : {},
100
102
  methodCache: deps.methodCache,
101
103
  success: deps.success,
104
+ subjects: deps.subjects,
102
105
  ...deps.persistUpstreamTokens ? { persistUpstreamTokens: deps.persistUpstreamTokens } : {},
103
106
  issuerUrl: c.get("issuerUrl"),
104
107
  clock: deps.clock,
108
+ ...deps.onTokenIssued ? { onTokenIssued: deps.onTokenIssued } : {},
105
109
  ...deps.customScopeClaims !== undefined ? { customScopeClaims: deps.customScopeClaims } : {}
106
110
  }, tenantRes.value);
107
111
  if (isErr(result))
@@ -123,6 +127,7 @@ function makeTokenHandler(deps) {
123
127
  ...deps.auditLog ? { auditLog: deps.auditLog } : {},
124
128
  issuerUrl: c.get("issuerUrl"),
125
129
  clock: deps.clock,
130
+ ...deps.onTokenIssued ? { onTokenIssued: deps.onTokenIssued } : {},
126
131
  ...deps.customScopeClaims !== undefined ? { customScopeClaims: deps.customScopeClaims } : {}
127
132
  });
128
133
  if (isErr(refreshResult))
@@ -147,6 +152,7 @@ function makeTokenHandler(deps) {
147
152
  ...deps.exchangeAudience ? { exchangeAudience: deps.exchangeAudience } : {},
148
153
  issuerUrl: c.get("issuerUrl"),
149
154
  clock: deps.clock,
155
+ ...deps.onTokenIssued ? { onTokenIssued: deps.onTokenIssued } : {},
150
156
  ...deps.customScopeClaims !== undefined ? { customScopeClaims: deps.customScopeClaims } : {}
151
157
  });
152
158
  if (isErr(exchangeResult)) {
@@ -1,5 +1,8 @@
1
1
  // src/http/middleware/tenant.ts
2
- import { verifyStateEnvelope } from "../../domain/state-envelope";
2
+ import {
3
+ extractCallbackState,
4
+ verifyStateEnvelope
5
+ } from "../../domain/state-envelope";
3
6
  import { isErr } from "../../types/result";
4
7
  import { parseCookieHeader } from "../cookies";
5
8
  import {
@@ -90,7 +93,7 @@ function buildTenantContext(req, id, config, custom) {
90
93
  };
91
94
  }
92
95
  async function runCallbackRecovery(req, deps) {
93
- const state = await extractStateParam(req);
96
+ const state = await extractCallbackState(req);
94
97
  if (state) {
95
98
  const env = await verifyStateEnvelope(state, deps.stateKeys);
96
99
  if (env.ok) {
@@ -103,24 +106,6 @@ async function runCallbackRecovery(req, deps) {
103
106
  }
104
107
  return { kind: "fresh-request" };
105
108
  }
106
- async function extractStateParam(req) {
107
- const url = new URL(req.url);
108
- const fromQuery = url.searchParams.get("state");
109
- if (fromQuery)
110
- return fromQuery;
111
- if (req.method !== "POST")
112
- return null;
113
- const ct = req.headers.get("content-type") ?? "";
114
- if (!ct.toLowerCase().startsWith("application/x-www-form-urlencoded")) {
115
- return null;
116
- }
117
- try {
118
- const text = await req.clone().text();
119
- return new URLSearchParams(text).get("state");
120
- } catch {
121
- return null;
122
- }
123
- }
124
109
  export {
125
110
  bootstrapMiddleware,
126
111
  tenantMiddleware
package/dist/esm/index.js CHANGED
@@ -67,6 +67,8 @@ function createIdP(opts) {
67
67
  ...opts.callbackHostFor ? { callbackHostFor: opts.callbackHostFor } : {},
68
68
  resolveTenant: opts.resolveTenant,
69
69
  success: opts.success,
70
+ subjects: opts.subjects,
71
+ ...opts.onTokenIssued ? { onTokenIssued: opts.onTokenIssued } : {},
70
72
  ...opts.onLogout ? { onLogout: opts.onLogout } : {},
71
73
  ...opts.persistUpstreamTokens ? { persistUpstreamTokens: opts.persistUpstreamTokens } : {},
72
74
  ...opts.exchangeAudience ? { exchangeAudience: opts.exchangeAudience } : {},