@_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.
Files changed (277) hide show
  1. package/dist/esm/adapters/d1/index.js +18 -9
  2. package/dist/esm/adapters/d1/migrations.js +7 -2
  3. package/dist/esm/adapters/d1/session-store.js +39 -0
  4. package/dist/esm/adapters/d1/session.js +2 -2
  5. package/dist/esm/adapters/durable-object/index.js +4 -0
  6. package/dist/esm/adapters/durable-object/session-store.js +42 -0
  7. package/dist/esm/adapters/dynamo/index.js +24 -7
  8. package/dist/esm/adapters/dynamo/session-store.js +49 -0
  9. package/dist/esm/adapters/kms/index.js +6 -2
  10. package/dist/esm/adapters/kv/index.js +6 -2
  11. package/dist/esm/adapters/memory/index.js +24 -7
  12. package/dist/esm/adapters/memory/session-store.js +45 -0
  13. package/dist/esm/adapters/memory/token-store.js +13 -0
  14. package/dist/esm/adapters/postgres/executor.js +2 -2
  15. package/dist/esm/adapters/postgres/index.js +33 -10
  16. package/dist/esm/adapters/postgres/migrations.js +8 -2
  17. package/dist/esm/adapters/postgres/session-store.js +42 -0
  18. package/dist/esm/client.js +35 -28
  19. package/dist/esm/domain/authorize.js +7 -2
  20. package/dist/esm/domain/callback.js +124 -7
  21. package/dist/esm/domain/client-auth.js +2 -2
  22. package/dist/esm/domain/crypto.js +14 -14
  23. package/dist/esm/domain/discovery.js +46 -4
  24. package/dist/esm/domain/dpop.js +122 -0
  25. package/dist/esm/domain/id-token.js +94 -0
  26. package/dist/esm/domain/introspect.js +5 -1
  27. package/dist/esm/domain/jwt.js +30 -2
  28. package/dist/esm/domain/logout.js +75 -0
  29. package/dist/esm/domain/method-dispatch.js +27 -1
  30. package/dist/esm/domain/method-route.js +83 -1
  31. package/dist/esm/domain/par.js +51 -0
  32. package/dist/esm/domain/pkce.js +2 -2
  33. package/dist/esm/domain/refresh.js +12 -1
  34. package/dist/esm/domain/register.js +74 -0
  35. package/dist/esm/domain/revoke.js +2 -2
  36. package/dist/esm/domain/state-envelope.js +2 -2
  37. package/dist/esm/domain/token-exchange.js +1 -0
  38. package/dist/esm/domain/token.js +53 -12
  39. package/dist/esm/domain/userinfo.js +38 -3
  40. package/dist/esm/error.js +8 -8
  41. package/dist/esm/http/cookies.js +19 -2
  42. package/dist/esm/http/errors.js +4 -4
  43. package/dist/esm/http/handlers/authorize.js +92 -21
  44. package/dist/esm/http/handlers/callback.js +7 -1
  45. package/dist/esm/http/handlers/end-session.js +64 -0
  46. package/dist/esm/http/handlers/metadata.js +7 -4
  47. package/dist/esm/http/handlers/method-route.js +47 -2
  48. package/dist/esm/http/handlers/par.js +53 -0
  49. package/dist/esm/http/handlers/register.js +46 -0
  50. package/dist/esm/http/handlers/revocation.js +2 -2
  51. package/dist/esm/http/handlers/token.js +36 -6
  52. package/dist/esm/http/handlers/userinfo.js +35 -29
  53. package/dist/esm/http/middleware/tenant.js +6 -6
  54. package/dist/esm/http/router.js +10 -0
  55. package/dist/esm/http/schemas/authorize.js +8 -2
  56. package/dist/esm/http/schemas/end-session.js +13 -0
  57. package/dist/esm/http/schemas/par.js +9 -0
  58. package/dist/esm/http/schemas/register.js +16 -0
  59. package/dist/esm/http/schemas/revocation.js +2 -2
  60. package/dist/esm/http/schemas/token.js +7 -7
  61. package/dist/esm/index.js +40 -34
  62. package/dist/esm/methods/oauth2-factory.js +2 -2
  63. package/dist/esm/methods/password-hash.js +2 -2
  64. package/dist/esm/methods/providers/index.js +30 -14
  65. package/dist/esm/methods/saml-sp/acs.js +208 -0
  66. package/dist/esm/methods/saml-sp/attributes.js +75 -0
  67. package/dist/esm/methods/saml-sp/authnrequest.js +63 -0
  68. package/dist/esm/methods/saml-sp/cache-provider.js +33 -0
  69. package/dist/esm/methods/saml-sp/cert-rotation.js +13 -0
  70. package/dist/esm/methods/saml-sp/factory.js +70 -0
  71. package/dist/esm/methods/saml-sp/index.js +7 -0
  72. package/dist/esm/methods/saml-sp/metadata.js +69 -0
  73. package/dist/esm/methods/saml-sp/method.js +29 -0
  74. package/dist/esm/methods/saml-sp/parse-idp-metadata.js +102 -0
  75. package/dist/esm/methods/saml-sp/saml-instance.js +49 -0
  76. package/dist/esm/methods/saml-sp/slo-initiate.js +84 -0
  77. package/dist/esm/methods/saml-sp/sls.js +154 -0
  78. package/dist/esm/methods/saml-sp/types.js +0 -0
  79. package/dist/esm/types/error.js +5 -0
  80. package/dist/esm/types/result.js +3 -3
  81. package/dist/esm/ui/forms.js +3 -3
  82. package/dist/esm/ui/index.js +8 -5
  83. package/dist/esm/ui/picker.js +2 -2
  84. package/dist/types/adapters/d1/migrations.d.ts.map +1 -1
  85. package/dist/types/adapters/d1/session-store.d.ts +3 -0
  86. package/dist/types/adapters/d1/session-store.d.ts.map +1 -1
  87. package/dist/types/adapters/durable-object/session-store.d.ts +3 -0
  88. package/dist/types/adapters/durable-object/session-store.d.ts.map +1 -1
  89. package/dist/types/adapters/dynamo/session-store.d.ts +3 -0
  90. package/dist/types/adapters/dynamo/session-store.d.ts.map +1 -1
  91. package/dist/types/adapters/memory/session-store.d.ts +6 -1
  92. package/dist/types/adapters/memory/session-store.d.ts.map +1 -1
  93. package/dist/types/adapters/memory/token-store.d.ts +1 -0
  94. package/dist/types/adapters/memory/token-store.d.ts.map +1 -1
  95. package/dist/types/adapters/postgres/migrations.d.ts +1 -1
  96. package/dist/types/adapters/postgres/migrations.d.ts.map +1 -1
  97. package/dist/types/adapters/postgres/session-store.d.ts +3 -0
  98. package/dist/types/adapters/postgres/session-store.d.ts.map +1 -1
  99. package/dist/types/client.d.ts +19 -0
  100. package/dist/types/client.d.ts.map +1 -1
  101. package/dist/types/domain/authorize.d.ts.map +1 -1
  102. package/dist/types/domain/callback.d.ts +16 -1
  103. package/dist/types/domain/callback.d.ts.map +1 -1
  104. package/dist/types/domain/client-credentials.d.ts +2 -0
  105. package/dist/types/domain/client-credentials.d.ts.map +1 -1
  106. package/dist/types/domain/discovery.d.ts +37 -0
  107. package/dist/types/domain/discovery.d.ts.map +1 -1
  108. package/dist/types/domain/dpop.d.ts +61 -0
  109. package/dist/types/domain/dpop.d.ts.map +1 -0
  110. package/dist/types/domain/id-token.d.ts +83 -0
  111. package/dist/types/domain/id-token.d.ts.map +1 -0
  112. package/dist/types/domain/introspect.d.ts +8 -0
  113. package/dist/types/domain/introspect.d.ts.map +1 -1
  114. package/dist/types/domain/jwt.d.ts +27 -1
  115. package/dist/types/domain/jwt.d.ts.map +1 -1
  116. package/dist/types/domain/logout.d.ts +69 -0
  117. package/dist/types/domain/logout.d.ts.map +1 -0
  118. package/dist/types/domain/method-dispatch.d.ts.map +1 -1
  119. package/dist/types/domain/method-route.d.ts +57 -1
  120. package/dist/types/domain/method-route.d.ts.map +1 -1
  121. package/dist/types/domain/par.d.ts +62 -0
  122. package/dist/types/domain/par.d.ts.map +1 -0
  123. package/dist/types/domain/refresh.d.ts +9 -0
  124. package/dist/types/domain/refresh.d.ts.map +1 -1
  125. package/dist/types/domain/register.d.ts +33 -0
  126. package/dist/types/domain/register.d.ts.map +1 -0
  127. package/dist/types/domain/token-exchange.d.ts +2 -0
  128. package/dist/types/domain/token-exchange.d.ts.map +1 -1
  129. package/dist/types/domain/token.d.ts +41 -1
  130. package/dist/types/domain/token.d.ts.map +1 -1
  131. package/dist/types/domain/userinfo.d.ts +44 -5
  132. package/dist/types/domain/userinfo.d.ts.map +1 -1
  133. package/dist/types/http/context.d.ts +6 -1
  134. package/dist/types/http/context.d.ts.map +1 -1
  135. package/dist/types/http/cookies.d.ts +9 -1
  136. package/dist/types/http/cookies.d.ts.map +1 -1
  137. package/dist/types/http/handlers/authorize.d.ts.map +1 -1
  138. package/dist/types/http/handlers/callback.d.ts.map +1 -1
  139. package/dist/types/http/handlers/end-session.d.ts +3 -0
  140. package/dist/types/http/handlers/end-session.d.ts.map +1 -0
  141. package/dist/types/http/handlers/metadata.d.ts +1 -1
  142. package/dist/types/http/handlers/metadata.d.ts.map +1 -1
  143. package/dist/types/http/handlers/method-route.d.ts.map +1 -1
  144. package/dist/types/http/handlers/par.d.ts +3 -0
  145. package/dist/types/http/handlers/par.d.ts.map +1 -0
  146. package/dist/types/http/handlers/register.d.ts +3 -0
  147. package/dist/types/http/handlers/register.d.ts.map +1 -0
  148. package/dist/types/http/handlers/token.d.ts.map +1 -1
  149. package/dist/types/http/handlers/userinfo.d.ts.map +1 -1
  150. package/dist/types/http/router.d.ts.map +1 -1
  151. package/dist/types/http/schemas/authorize.d.ts +40 -0
  152. package/dist/types/http/schemas/authorize.d.ts.map +1 -1
  153. package/dist/types/http/schemas/end-session.d.ts +31 -0
  154. package/dist/types/http/schemas/end-session.d.ts.map +1 -0
  155. package/dist/types/http/schemas/par.d.ts +20 -0
  156. package/dist/types/http/schemas/par.d.ts.map +1 -0
  157. package/dist/types/http/schemas/register.d.ts +42 -0
  158. package/dist/types/http/schemas/register.d.ts.map +1 -0
  159. package/dist/types/index.d.ts +3 -3
  160. package/dist/types/index.d.ts.map +1 -1
  161. package/dist/types/methods/saml-sp/acs.d.ts +4 -0
  162. package/dist/types/methods/saml-sp/acs.d.ts.map +1 -0
  163. package/dist/types/methods/saml-sp/attributes.d.ts +40 -0
  164. package/dist/types/methods/saml-sp/attributes.d.ts.map +1 -0
  165. package/dist/types/methods/saml-sp/authnrequest.d.ts +4 -0
  166. package/dist/types/methods/saml-sp/authnrequest.d.ts.map +1 -0
  167. package/dist/types/methods/saml-sp/cache-provider.d.ts +38 -0
  168. package/dist/types/methods/saml-sp/cache-provider.d.ts.map +1 -0
  169. package/dist/types/methods/saml-sp/cert-rotation.d.ts +23 -0
  170. package/dist/types/methods/saml-sp/cert-rotation.d.ts.map +1 -0
  171. package/dist/types/methods/saml-sp/factory.d.ts +704 -0
  172. package/dist/types/methods/saml-sp/factory.d.ts.map +1 -0
  173. package/dist/types/methods/saml-sp/index.d.ts +24 -0
  174. package/dist/types/methods/saml-sp/index.d.ts.map +1 -0
  175. package/dist/types/methods/saml-sp/metadata.d.ts +42 -0
  176. package/dist/types/methods/saml-sp/metadata.d.ts.map +1 -0
  177. package/dist/types/methods/saml-sp/method.d.ts +17 -0
  178. package/dist/types/methods/saml-sp/method.d.ts.map +1 -0
  179. package/dist/types/methods/saml-sp/parse-idp-metadata.d.ts +4 -0
  180. package/dist/types/methods/saml-sp/parse-idp-metadata.d.ts.map +1 -0
  181. package/dist/types/methods/saml-sp/saml-instance.d.ts +81 -0
  182. package/dist/types/methods/saml-sp/saml-instance.d.ts.map +1 -0
  183. package/dist/types/methods/saml-sp/slo-initiate.d.ts +4 -0
  184. package/dist/types/methods/saml-sp/slo-initiate.d.ts.map +1 -0
  185. package/dist/types/methods/saml-sp/sls.d.ts +4 -0
  186. package/dist/types/methods/saml-sp/sls.d.ts.map +1 -0
  187. package/dist/types/methods/saml-sp/types.d.ts +204 -0
  188. package/dist/types/methods/saml-sp/types.d.ts.map +1 -0
  189. package/dist/types/ports/audit-log.d.ts +40 -0
  190. package/dist/types/ports/audit-log.d.ts.map +1 -1
  191. package/dist/types/ports/session-store.d.ts +50 -0
  192. package/dist/types/ports/session-store.d.ts.map +1 -1
  193. package/dist/types/ports/token-store.d.ts +12 -0
  194. package/dist/types/ports/token-store.d.ts.map +1 -1
  195. package/dist/types/types/authorization.d.ts +23 -0
  196. package/dist/types/types/authorization.d.ts.map +1 -1
  197. package/dist/types/types/error.d.ts +9 -0
  198. package/dist/types/types/error.d.ts.map +1 -1
  199. package/dist/types/types/flow.d.ts +10 -0
  200. package/dist/types/types/flow.d.ts.map +1 -1
  201. package/dist/types/types/idp.d.ts +162 -1
  202. package/dist/types/types/idp.d.ts.map +1 -1
  203. package/dist/types/types/method.d.ts +109 -0
  204. package/dist/types/types/method.d.ts.map +1 -1
  205. package/dist/types/types/tenant.d.ts +37 -0
  206. package/dist/types/types/tenant.d.ts.map +1 -1
  207. package/dist/types/types/token.d.ts +128 -0
  208. package/dist/types/types/token.d.ts.map +1 -1
  209. package/package.json +9 -1
  210. package/src/adapters/d1/migrations.ts +5 -0
  211. package/src/adapters/d1/session-store.ts +68 -0
  212. package/src/adapters/durable-object/session-store.ts +61 -0
  213. package/src/adapters/dynamo/session-store.ts +63 -0
  214. package/src/adapters/memory/session-store.ts +83 -1
  215. package/src/adapters/memory/token-store.ts +16 -0
  216. package/src/adapters/postgres/migrations.ts +6 -0
  217. package/src/adapters/postgres/session-store.ts +69 -0
  218. package/src/client.ts +32 -0
  219. package/src/domain/authorize.ts +9 -0
  220. package/src/domain/callback.ts +220 -7
  221. package/src/domain/client-credentials.ts +2 -0
  222. package/src/domain/discovery.ts +80 -2
  223. package/src/domain/dpop.ts +293 -0
  224. package/src/domain/id-token.ts +213 -0
  225. package/src/domain/introspect.ts +10 -0
  226. package/src/domain/jwt.ts +80 -1
  227. package/src/domain/logout.ts +180 -0
  228. package/src/domain/method-dispatch.ts +53 -1
  229. package/src/domain/method-route.ts +195 -1
  230. package/src/domain/par.ts +141 -0
  231. package/src/domain/refresh.ts +41 -0
  232. package/src/domain/register.ts +168 -0
  233. package/src/domain/token-exchange.ts +11 -0
  234. package/src/domain/token.ts +134 -8
  235. package/src/domain/userinfo.ts +121 -6
  236. package/src/http/context.ts +10 -0
  237. package/src/http/cookies.ts +19 -1
  238. package/src/http/handlers/authorize.ts +146 -24
  239. package/src/http/handlers/callback.ts +9 -1
  240. package/src/http/handlers/end-session.ts +106 -0
  241. package/src/http/handlers/metadata.ts +7 -2
  242. package/src/http/handlers/method-route.ts +75 -2
  243. package/src/http/handlers/par.ts +84 -0
  244. package/src/http/handlers/register.ts +70 -0
  245. package/src/http/handlers/token.ts +56 -0
  246. package/src/http/handlers/userinfo.ts +62 -38
  247. package/src/http/router.ts +19 -0
  248. package/src/http/schemas/authorize.ts +27 -0
  249. package/src/http/schemas/end-session.ts +23 -0
  250. package/src/http/schemas/par.ts +23 -0
  251. package/src/http/schemas/register.ts +29 -0
  252. package/src/index.ts +21 -1
  253. package/src/methods/saml-sp/acs.ts +399 -0
  254. package/src/methods/saml-sp/attributes.ts +137 -0
  255. package/src/methods/saml-sp/authnrequest.ts +128 -0
  256. package/src/methods/saml-sp/cache-provider.ts +76 -0
  257. package/src/methods/saml-sp/cert-rotation.ts +34 -0
  258. package/src/methods/saml-sp/factory.ts +118 -0
  259. package/src/methods/saml-sp/index.ts +34 -0
  260. package/src/methods/saml-sp/metadata.ts +204 -0
  261. package/src/methods/saml-sp/method.ts +65 -0
  262. package/src/methods/saml-sp/parse-idp-metadata.ts +164 -0
  263. package/src/methods/saml-sp/saml-instance.ts +173 -0
  264. package/src/methods/saml-sp/slo-initiate.ts +177 -0
  265. package/src/methods/saml-sp/sls.ts +306 -0
  266. package/src/methods/saml-sp/types.ts +203 -0
  267. package/src/ports/CONSISTENCY.md +77 -21
  268. package/src/ports/audit-log.ts +42 -0
  269. package/src/ports/session-store.ts +61 -0
  270. package/src/ports/token-store.ts +13 -0
  271. package/src/types/authorization.ts +25 -0
  272. package/src/types/error.ts +23 -0
  273. package/src/types/flow.ts +10 -0
  274. package/src/types/idp.ts +170 -0
  275. package/src/types/method.ts +110 -0
  276. package/src/types/tenant.ts +37 -0
  277. package/src/types/token.ts +135 -0
@@ -13,7 +13,8 @@ async function dispatchMethod(input) {
13
13
  flow: input.flow,
14
14
  methodState: input.flow?.methodState ?? null,
15
15
  cookies: input.cookies,
16
- dispatch: input.dispatch
16
+ dispatch: input.dispatch,
17
+ methodScratch: buildMethodScratch(input.sessionStore, input.tenant.id, input.method.id)
17
18
  };
18
19
  let result;
19
20
  try {
@@ -31,6 +32,31 @@ async function dispatchMethod(input) {
31
32
  }
32
33
  return ok(result);
33
34
  }
35
+ function buildMethodScratch(store, tenantId, methodId) {
36
+ const prefix = `scratch:${tenantId}:${methodId}:`;
37
+ const scope = (key) => `${prefix}${key}`;
38
+ const unsupported = (op) => authError.internalError(`SessionStore does not implement ${op}; methodScratch is unavailable on this adapter`);
39
+ return {
40
+ put: async (key, value, ttlMs) => {
41
+ if (!store.saveScratch)
42
+ return err(unsupported("saveScratch"));
43
+ if (ttlMs <= 0) {
44
+ return err(authError.internalError(`methodScratch.put: ttlMs must be positive, got ${ttlMs}`));
45
+ }
46
+ return store.saveScratch(scope(key), value, ttlMs);
47
+ },
48
+ get: async (key) => {
49
+ if (!store.readScratch)
50
+ return err(unsupported("readScratch"));
51
+ return store.readScratch(scope(key));
52
+ },
53
+ delete: async (key) => {
54
+ if (!store.deleteScratch)
55
+ return err(unsupported("deleteScratch"));
56
+ return store.deleteScratch(scope(key));
57
+ }
58
+ };
59
+ }
34
60
  export {
35
61
  dispatchMethod
36
62
  };
@@ -5,6 +5,7 @@ import { safeAudit } from "./audit";
5
5
  import { AUTH_CODE_TTL_MS } from "./authorize";
6
6
  import { randomToken } from "./crypto";
7
7
  import { dispatchMethod } from "./method-dispatch";
8
+ import { revokeAllForSubject } from "./revoke";
8
9
  import { saveEncryptedCode } from "./token";
9
10
  async function handleMethodRoute(input, deps) {
10
11
  const flowRes = await deps.sessionStore.readFlow(input.flowId);
@@ -65,6 +66,9 @@ async function translate(result, flow, deps) {
65
66
  context: final.context ?? null,
66
67
  providerSubject: result.providerSubject,
67
68
  properties: result.properties,
69
+ ...final.appNonce !== undefined ? { appNonce: final.appNonce } : {},
70
+ ...final.claimsRequest !== undefined ? { claimsRequest: final.claimsRequest } : {},
71
+ authTime: Math.floor(now / 1000),
68
72
  expiresAt: now + AUTH_CODE_TTL_MS
69
73
  }, AUTH_CODE_TTL_MS, { keyStore: deps.keyStore, tokenStore: deps.tokenStore });
70
74
  if (isErr(saved))
@@ -103,6 +107,84 @@ async function translate(result, flow, deps) {
103
107
  return err(result.error);
104
108
  }
105
109
  }
110
+ async function handlePublicMethodRoute(input, deps) {
111
+ if (!input.method.publicRoutes?.includes(input.route)) {
112
+ return err(authError.invalidRequest(`route "${input.route}" is not public on method "${input.method.id}"`, "path"));
113
+ }
114
+ const dispatched = await dispatchMethod({
115
+ method: input.method,
116
+ route: input.route,
117
+ tenant: input.tenant,
118
+ request: input.rawRequest,
119
+ subPath: input.subPath,
120
+ flow: null,
121
+ cookies: new Map,
122
+ sessionStore: deps.sessionStore,
123
+ dispatch: input.dispatch
124
+ });
125
+ if (isErr(dispatched))
126
+ return err(dispatched.error);
127
+ const r = dispatched.value;
128
+ switch (r.kind) {
129
+ case "challenge": {
130
+ if (r.logout) {
131
+ const sideEffect = await runUpstreamLogout(input, deps, r.logout);
132
+ if (isErr(sideEffect))
133
+ return err(sideEffect.error);
134
+ }
135
+ return ok({
136
+ kind: "challenge",
137
+ response: r.response,
138
+ ...r.cache !== undefined ? { cache: r.cache } : {}
139
+ });
140
+ }
141
+ case "denied":
142
+ return ok({ kind: "denied", reason: r.reason });
143
+ case "success":
144
+ return err(authError.internalError(`public route "${input.route}" on method "${input.method.id}" ` + `returned success — a flowless route cannot authenticate`));
145
+ case "error":
146
+ return err(r.error);
147
+ }
148
+ }
149
+ async function runUpstreamLogout(input, deps, logout) {
150
+ let hookResult = undefined;
151
+ if (deps.onLogout) {
152
+ const evt = {
153
+ tenant: input.tenant,
154
+ methodId: input.method.id,
155
+ methodKind: input.method.kind,
156
+ reason: "upstream_slo",
157
+ ...logout.nameId !== undefined ? { nameId: logout.nameId } : {},
158
+ ...logout.sessionIndex !== undefined ? { sessionIndex: logout.sessionIndex } : {}
159
+ };
160
+ try {
161
+ hookResult = await deps.onLogout(evt);
162
+ } catch (e) {
163
+ return err(authError.internalError(`onLogout hook threw during upstream Single Logout for method ` + `"${input.method.id}"`, e));
164
+ }
165
+ }
166
+ const revokeSubject = hookResult && typeof hookResult === "object" ? hookResult.revokeSubject : undefined;
167
+ if (revokeSubject) {
168
+ const revoked = await revokeAllForSubject(input.tenant.id, revokeSubject, {
169
+ tokenStore: deps.tokenStore,
170
+ ...deps.auditLog ? { auditLog: deps.auditLog } : {},
171
+ clock: deps.clock
172
+ });
173
+ if (isErr(revoked))
174
+ return err(revoked.error);
175
+ }
176
+ await safeAudit(deps, {
177
+ kind: "session_logout",
178
+ tenantId: input.tenant.id,
179
+ via: "upstream_slo",
180
+ methodId: input.method.id,
181
+ methodKind: input.method.kind,
182
+ ...revokeSubject ? { subjectId: revokeSubject } : {},
183
+ timestamp: deps.clock()
184
+ });
185
+ return ok(undefined);
186
+ }
106
187
  export {
107
- handleMethodRoute
188
+ handleMethodRoute,
189
+ handlePublicMethodRoute
108
190
  };
@@ -0,0 +1,51 @@
1
+ // src/domain/par.ts
2
+ import { authError } from "../types/error";
3
+ import { err, isErr, ok } from "../types/result";
4
+ import { verifyClientCredentials } from "./client-auth";
5
+ import { randomToken } from "./crypto";
6
+ var PAR_URI_PREFIX = "urn:ietf:params:oauth:request_uri:";
7
+ var DEFAULT_PAR_TTL_MS = 60 * 1000;
8
+ async function pushAuthorizationRequest(req, tenant, deps) {
9
+ if (!deps.sessionStore.savePar || !deps.sessionStore.consumePar) {
10
+ return err(authError.invalidRequest("session adapter does not implement PAR storage"));
11
+ }
12
+ const client = tenant.config.clients.find((c) => c.id === req.clientId);
13
+ if (!client) {
14
+ return err(authError.invalidClient(`unknown client "${req.clientId}"`));
15
+ }
16
+ if (client.type === "confidential") {
17
+ const authErr = await verifyClientCredentials(client, req.clientSecret);
18
+ if (authErr)
19
+ return err(authErr);
20
+ } else if (req.clientSecret !== undefined) {
21
+ return err(authError.invalidClient("public client must not present client_secret"));
22
+ }
23
+ if (req.params.client_id && req.params.client_id !== req.clientId) {
24
+ return err(authError.invalidRequest("client_id in body conflicts with authenticated client", "client_id"));
25
+ }
26
+ if ("request_uri" in req.params) {
27
+ return err(authError.invalidRequest("request_uri MUST NOT be present in a PAR request", "request_uri"));
28
+ }
29
+ const suffix = (deps.newRequestUriSuffix ?? randomToken)();
30
+ const requestUri = PAR_URI_PREFIX + suffix;
31
+ const ttl = deps.ttlMs ?? DEFAULT_PAR_TTL_MS;
32
+ const now = deps.clock();
33
+ const saved = await deps.sessionStore.savePar(requestUri, {
34
+ requestUri,
35
+ params: { ...req.params, client_id: req.clientId },
36
+ clientId: req.clientId,
37
+ issuedAt: now,
38
+ expiresAt: now + ttl
39
+ }, ttl);
40
+ if (isErr(saved))
41
+ return err(saved.error);
42
+ return ok({
43
+ request_uri: requestUri,
44
+ expires_in: Math.floor(ttl / 1000)
45
+ });
46
+ }
47
+ export {
48
+ DEFAULT_PAR_TTL_MS,
49
+ PAR_URI_PREFIX,
50
+ pushAuthorizationRequest
51
+ };
@@ -11,6 +11,6 @@ async function validatePkce(verifier, challenge) {
11
11
  return timingSafeEqualStr(computed, challenge);
12
12
  }
13
13
  export {
14
- validatePkce,
15
- s256Challenge
14
+ s256Challenge,
15
+ validatePkce
16
16
  };
@@ -31,6 +31,14 @@ async function refreshTokens(req, deps) {
31
31
  } else if (client.type === "confidential") {
32
32
  return err(authError.invalidGrant(INVALID_REFRESH_DESC));
33
33
  }
34
+ if (peekedPayload.dpopJkt !== undefined) {
35
+ if (req.dpopJkt === undefined) {
36
+ return err(authError.invalidDpopProof("refresh token is DPoP-bound; request is missing a DPoP proof"));
37
+ }
38
+ if (req.dpopJkt !== peekedPayload.dpopJkt) {
39
+ return err(authError.invalidDpopProof("refresh token DPoP jkt does not match the original binding"));
40
+ }
41
+ }
34
42
  const consumed = await deps.tokenStore.consumeRefresh(req.refreshToken, {
35
43
  reuseWindowMs: deps.reuseWindowMs
36
44
  });
@@ -68,7 +76,10 @@ async function refreshTokens(req, deps) {
68
76
  methodId: payload.methodId,
69
77
  methodKind: payload.methodKind,
70
78
  scopes: requestedScopes,
71
- audience: payload.audience
79
+ audience: payload.audience,
80
+ authTime: payload.authTime,
81
+ ...payload.dpopJkt !== undefined ? { dpopJkt: payload.dpopJkt } : {},
82
+ ...payload.claimsRequest !== undefined ? { claimsRequest: payload.claimsRequest } : {}
72
83
  },
73
84
  family: payload.family,
74
85
  deps
@@ -0,0 +1,74 @@
1
+ // src/domain/register.ts
2
+ import { authError } from "../types/error";
3
+ import { err, isErr, ok } from "../types/result";
4
+ import { randomId, randomToken } from "./crypto";
5
+ import { hashClientSecret } from "./token";
6
+ async function registerNewClient(request, tenant, deps) {
7
+ if (!deps.registerClient) {
8
+ return err(authError.invalidRequest("dynamic client registration is not enabled on this deployment"));
9
+ }
10
+ if (!Array.isArray(request.redirect_uris) || request.redirect_uris.length === 0) {
11
+ return err(authError.invalidRequest("redirect_uris must be a non-empty array", "redirect_uris"));
12
+ }
13
+ for (const uri of request.redirect_uris) {
14
+ try {
15
+ new URL(uri);
16
+ } catch {
17
+ return err(authError.invalidRequest(`redirect_uri "${uri}" is not a valid absolute URI`, "redirect_uris"));
18
+ }
19
+ }
20
+ const authMethod = request.token_endpoint_auth_method ?? "client_secret_basic";
21
+ const isPublic = authMethod === "none";
22
+ const clientId = (deps.newClientId ?? randomId)();
23
+ const grantTypes = request.grant_types ?? ["authorization_code"];
24
+ const responseTypes = request.response_types ?? ["code"];
25
+ const scopes = request.scope ? request.scope.split(/\s+/).filter(Boolean) : ["openid"];
26
+ let secret;
27
+ let secretHash;
28
+ if (!isPublic) {
29
+ secret = (deps.newClientSecret ?? randomToken)();
30
+ secretHash = await hashClientSecret(secret);
31
+ }
32
+ const clientPartial = {
33
+ id: clientId,
34
+ name: request.client_name ?? clientId,
35
+ redirectUris: request.redirect_uris,
36
+ scopes,
37
+ grantTypes: grantTypes.filter((g) => g === "authorization_code" || g === "refresh_token" || g === "client_credentials"),
38
+ ...request.post_logout_redirect_uris !== undefined ? { postLogoutRedirectUris: request.post_logout_redirect_uris } : {},
39
+ ...request.sector_identifier_uri !== undefined ? { sectorIdentifier: request.sector_identifier_uri } : {}
40
+ };
41
+ const clientConfig = isPublic ? {
42
+ ...clientPartial,
43
+ type: "public",
44
+ pkceRequired: true
45
+ } : {
46
+ ...clientPartial,
47
+ type: "confidential",
48
+ secretHash,
49
+ pkceRequired: true
50
+ };
51
+ const hookResult = await deps.registerClient({
52
+ tenant,
53
+ request
54
+ });
55
+ if (isErr(hookResult))
56
+ return err(hookResult.error);
57
+ const persisted = hookResult.value.client;
58
+ const persistedSecret = hookResult.value.secret;
59
+ const issuedAt = Math.floor(deps.clock() / 1000);
60
+ return ok({
61
+ client_id: persisted.id,
62
+ ...persistedSecret !== undefined ? { client_secret: persistedSecret } : {},
63
+ client_id_issued_at: issuedAt,
64
+ client_secret_expires_at: 0,
65
+ redirect_uris: persisted.redirectUris,
66
+ grant_types: persisted.grantTypes,
67
+ response_types: responseTypes,
68
+ token_endpoint_auth_method: authMethod,
69
+ ...request.client_name !== undefined ? { client_name: request.client_name } : {}
70
+ });
71
+ }
72
+ export {
73
+ registerNewClient
74
+ };
@@ -76,6 +76,6 @@ async function revokeAllForSubject(tenantId, subjectId, deps) {
76
76
  return ok(undefined);
77
77
  }
78
78
  export {
79
- revokeToken,
80
- revokeAllForSubject
79
+ revokeAllForSubject,
80
+ revokeToken
81
81
  };
@@ -75,6 +75,6 @@ function isEnvelopeShape(value) {
75
75
  return typeof v.tenantId === "string" && typeof v.flowId === "string" && typeof v.nonce === "string" && typeof v.kid === "string";
76
76
  }
77
77
  export {
78
- verifyStateEnvelope,
79
- mintStateEnvelope
78
+ mintStateEnvelope,
79
+ verifyStateEnvelope
80
80
  };
@@ -75,6 +75,7 @@ async function exchangeToken(req, deps) {
75
75
  methodId: subjectClaims.mid ?? "token_exchange",
76
76
  methodKind: subjectClaims.mkind ?? "token_exchange",
77
77
  scopes: requestedScopes,
78
+ ...subjectClaims.auth_time !== undefined ? { authTime: subjectClaims.auth_time } : {},
78
79
  ...subjectClaims.aud !== undefined && subjectClaims.aud !== callerClient.id ? { audience: subjectClaims.aud } : {}
79
80
  },
80
81
  family: (deps.newRefreshFamily ?? randomId)(),
@@ -12,7 +12,8 @@ import {
12
12
  sha256,
13
13
  utf8
14
14
  } from "./crypto";
15
- import { signAccessToken } from "./jwt";
15
+ import { buildIdTokenClaims, shouldIssueIdToken } from "./id-token";
16
+ import { signAccessToken, signIdToken } from "./jwt";
16
17
  import { validatePkce } from "./pkce";
17
18
  async function saveEncryptedCode(code, payload, ttl, deps) {
18
19
  const keyResult = await deps.keyStore.currentEncryptionKey();
@@ -57,6 +58,9 @@ async function exchangeCode(req, deps) {
57
58
  const authResult = await verifyClientCredentials(client, req.clientSecret);
58
59
  if (authResult)
59
60
  return err(authResult);
61
+ if (client.dpopRequired && req.dpopJkt === undefined) {
62
+ return err(authError.invalidDpopProof(`client "${client.id}" requires DPoP-bound tokens`));
63
+ }
60
64
  if (payload.appRedirectUri !== req.redirectUri) {
61
65
  return err(authError.invalidGrant("redirect_uri mismatch with auth code"));
62
66
  }
@@ -104,7 +108,10 @@ async function exchangeCode(req, deps) {
104
108
  const minted = await mintTokens({
105
109
  tenant,
106
110
  claim,
107
- payload,
111
+ payload: {
112
+ ...payload,
113
+ ...req.dpopJkt !== undefined ? { dpopJkt: req.dpopJkt } : {}
114
+ },
108
115
  deps,
109
116
  family: (deps.newRefreshFamily ?? randomId)()
110
117
  });
@@ -115,11 +122,13 @@ async function mintTokens(args) {
115
122
  const accessTtl = tenant.config.accessTtl !== undefined ? tenant.config.accessTtl * 1000 : DEFAULT_ACCESS_TTL_MS;
116
123
  const refreshTtl = tenant.config.refreshTtl !== undefined ? tenant.config.refreshTtl * 1000 : DEFAULT_REFRESH_TTL_MS;
117
124
  const now = deps.clock();
118
- const subjectId = await deriveSubjectId(claim);
125
+ const receivingClient = tenant.config.clients.find((c) => c.id === payload.clientId);
126
+ const subjectId = await deriveSubjectId(claim, receivingClient?.sectorIdentifier);
119
127
  const keyRes = await deps.keyStore.currentSigningKey();
120
128
  if (isErr(keyRes))
121
129
  return err(keyRes.error);
122
130
  const signingKey = keyRes.value;
131
+ const userinfoClaimNames = Object.keys(payload.claimsRequest?.userinfo ?? {});
123
132
  const claims = {
124
133
  iss: deps.issuerUrl,
125
134
  sub: subjectId,
@@ -130,7 +139,10 @@ async function mintTokens(args) {
130
139
  mid: payload.methodId,
131
140
  mkind: payload.methodKind,
132
141
  scope: payload.scopes.join(" "),
133
- claim
142
+ claim,
143
+ ...payload.authTime !== undefined ? { auth_time: payload.authTime } : {},
144
+ ...payload.dpopJkt !== undefined ? { cnf: { jkt: payload.dpopJkt } } : {},
145
+ ...userinfoClaimNames.length > 0 ? { uic: userinfoClaimNames } : {}
134
146
  };
135
147
  let accessToken;
136
148
  try {
@@ -151,6 +163,9 @@ async function mintTokens(args) {
151
163
  family,
152
164
  methodId: payload.methodId,
153
165
  methodKind: payload.methodKind,
166
+ authTime: payload.authTime ?? Math.floor(now / 1000),
167
+ ...payload.dpopJkt !== undefined ? { dpopJkt: payload.dpopJkt } : {},
168
+ ...payload.claimsRequest !== undefined ? { claimsRequest: payload.claimsRequest } : {},
154
169
  issuedAt: now,
155
170
  expiresAt: now + refreshTtl
156
171
  };
@@ -158,6 +173,28 @@ async function mintTokens(args) {
158
173
  if (isErr(saved))
159
174
  return err(saved.error);
160
175
  }
176
+ let idToken;
177
+ if (payload.authTime !== undefined && shouldIssueIdToken(payload.scopes)) {
178
+ const idClaims = await buildIdTokenClaims({
179
+ issuerUrl: deps.issuerUrl,
180
+ audience: payload.clientId,
181
+ subjectId,
182
+ claim,
183
+ scopes: payload.scopes,
184
+ authTime: payload.authTime,
185
+ ...payload.appNonce !== undefined ? { appNonce: payload.appNonce } : {},
186
+ now,
187
+ methodKind: payload.methodKind,
188
+ accessToken,
189
+ ...payload.claimsRequest !== undefined ? { claimsRequest: payload.claimsRequest } : {},
190
+ ...deps.customScopeClaims !== undefined ? { customScopeClaims: deps.customScopeClaims } : {}
191
+ });
192
+ try {
193
+ idToken = await signIdToken(idClaims, signingKey.privateKeyRef, signingKey.alg, signingKey.kid);
194
+ } catch (e) {
195
+ return err(authError.serverError("id_token sign failed", e));
196
+ }
197
+ }
161
198
  await safeAudit(deps, {
162
199
  kind: "token_issued",
163
200
  tenantId: payload.tenantId,
@@ -166,23 +203,27 @@ async function mintTokens(args) {
166
203
  methodKind: payload.methodKind,
167
204
  subjectId,
168
205
  refreshTokenIdHash: refresh ? await hashTokenForAudit(refresh) : "",
206
+ ...idToken !== undefined ? { idTokenIssued: true } : {},
207
+ ...payload.dpopJkt !== undefined ? { dpopBound: true } : {},
169
208
  timestamp: now
170
209
  });
171
210
  return ok({
172
211
  access_token: accessToken,
173
- token_type: "Bearer",
212
+ token_type: payload.dpopJkt !== undefined ? "DPoP" : "Bearer",
174
213
  expires_in: Math.floor(accessTtl / 1000),
175
214
  ...refresh !== undefined ? { refresh_token: refresh } : {},
215
+ ...idToken !== undefined ? { id_token: idToken } : {},
176
216
  scope: payload.scopes.join(" ")
177
217
  });
178
218
  }
179
219
  async function hashClientSecret(plain) {
180
220
  return base64url.encode(await sha256(utf8.encode(plain)));
181
221
  }
182
- async function deriveSubjectId(claim) {
222
+ async function deriveSubjectId(claim, sectorIdentifier) {
183
223
  const c = claim;
184
224
  const ordered = canonicalize(c.properties);
185
- return base64url.encode(await sha256(`${c.type}\x00${ordered}`)).slice(0, 22);
225
+ const seed = sectorIdentifier !== undefined ? `${sectorIdentifier}\x00${c.type}\x00${ordered}` : `${c.type}\x00${ordered}`;
226
+ return base64url.encode(await sha256(seed)).slice(0, 22);
186
227
  }
187
228
  function canonicalize(value) {
188
229
  if (value === null || typeof value !== "object" || Array.isArray(value)) {
@@ -196,10 +237,10 @@ async function hashTokenForAudit(token) {
196
237
  return base64url.encode(await sha256(utf8.encode(token))).slice(0, 16);
197
238
  }
198
239
  export {
199
- saveEncryptedCode,
200
- mintTokens,
201
- hashClientSecret,
202
- exchangeCode,
240
+ DEFAULT_ACCESS_TTL_MS,
203
241
  DEFAULT_REFRESH_TTL_MS,
204
- DEFAULT_ACCESS_TTL_MS
242
+ exchangeCode,
243
+ hashClientSecret,
244
+ mintTokens,
245
+ saveEncryptedCode
205
246
  };
@@ -1,25 +1,60 @@
1
1
  // src/domain/userinfo.ts
2
2
  import { authError } from "../types/error";
3
3
  import { err, isErr, ok } from "../types/result";
4
+ import { computeAth, verifyDpopProof } from "./dpop";
5
+ import { pickScopedClaims } from "./id-token";
4
6
  import { verifyAccessToken } from "./jwt";
5
- async function userinfo(bearerToken, deps) {
7
+ async function userinfo(input, deps) {
6
8
  const keysRes = await deps.keyStore.signingKeys();
7
9
  if (isErr(keysRes))
8
10
  return err(keysRes.error);
9
11
  let claims;
10
12
  try {
11
- claims = await verifyAccessToken(bearerToken, keysRes.value, {
13
+ claims = await verifyAccessToken(input.accessToken, keysRes.value, {
12
14
  ...deps.issuerUrl ? { issuer: deps.issuerUrl } : {}
13
15
  });
14
16
  } catch {
15
17
  return err(authError.invalidGrant("access token invalid or expired"));
16
18
  }
19
+ const boundJkt = claims.cnf?.jkt;
20
+ if (boundJkt !== undefined) {
21
+ if (input.scheme !== "DPoP") {
22
+ return err(authError.invalidDpopProof('access token is DPoP-bound; Authorization scheme must be "DPoP"'));
23
+ }
24
+ if (!input.dpopProof) {
25
+ return err(authError.invalidDpopProof("access token is DPoP-bound; request is missing a DPoP proof"));
26
+ }
27
+ if (!input.htu || !input.htm || input.nowSec === undefined) {
28
+ return err(authError.invalidDpopProof("userinfo dpop verification requires htu, htm, and nowSec"));
29
+ }
30
+ if (!deps.tokenStore) {
31
+ return err(authError.invalidDpopProof("userinfo dpop verification requires a token-store (jti replay protection)"));
32
+ }
33
+ const expectedAth = await computeAth(input.accessToken);
34
+ const dpopRes = await verifyDpopProof({
35
+ proofJwt: input.dpopProof,
36
+ htu: input.htu,
37
+ htm: input.htm,
38
+ nowSec: input.nowSec,
39
+ expectedAth
40
+ }, { tokenStore: deps.tokenStore });
41
+ if (isErr(dpopRes))
42
+ return err(dpopRes.error);
43
+ if (dpopRes.value.jkt !== boundJkt) {
44
+ return err(authError.invalidDpopProof("dpop proof key does not match access token cnf.jkt"));
45
+ }
46
+ } else if (input.scheme === "DPoP") {
47
+ return err(authError.invalidGrant("access token is not DPoP-bound; use Authorization: Bearer"));
48
+ }
17
49
  const claim = claims.claim;
50
+ const scopes = claims.scope ? claims.scope.split(" ").filter(Boolean) : [];
51
+ const scopedClaims = pickScopedClaims(claim, scopes, claims.uic ?? [], deps.customScopeClaims ?? {});
18
52
  return ok({
19
53
  sub: claims.sub,
20
54
  subject_type: claim.type,
21
55
  properties: claim.properties,
22
- scope: claims.scope
56
+ scope: claims.scope,
57
+ ...scopedClaims
23
58
  });
24
59
  }
25
60
  export {
package/dist/esm/error.js CHANGED
@@ -61,13 +61,13 @@ class InvalidAuthorizationCodeError extends Error {
61
61
  }
62
62
  }
63
63
  export {
64
- UnknownStateError,
65
- UnauthorizedClientError,
66
- OauthError,
67
- MissingProviderError,
68
- MissingParameterError,
69
- InvalidSubjectError,
70
- InvalidRefreshTokenError,
64
+ InvalidAccessTokenError,
71
65
  InvalidAuthorizationCodeError,
72
- InvalidAccessTokenError
66
+ InvalidRefreshTokenError,
67
+ InvalidSubjectError,
68
+ MissingParameterError,
69
+ MissingProviderError,
70
+ OauthError,
71
+ UnauthorizedClientError,
72
+ UnknownStateError
73
73
  };
@@ -67,6 +67,22 @@ var STRIPPED_HEADERS = [
67
67
  "permissions-policy",
68
68
  "cache-control"
69
69
  ];
70
+ function cacheControlHeader(cache) {
71
+ if (!cache)
72
+ return "no-store";
73
+ if ((cache.maxAge ?? -1) === 0)
74
+ return "no-store";
75
+ const parts = [];
76
+ if (cache.isPrivate)
77
+ parts.push("private");
78
+ if (cache.maxAge !== undefined)
79
+ parts.push(`max-age=${cache.maxAge}`);
80
+ if (cache.sMaxAge !== undefined)
81
+ parts.push(`s-maxage=${cache.sMaxAge}`);
82
+ if (cache.immutable)
83
+ parts.push("immutable");
84
+ return parts.length ? parts.join(", ") : "no-store";
85
+ }
70
86
  function applyResponsePolicy(response, opts = {}) {
71
87
  const headers = new Headers(response.headers);
72
88
  const stripped = [];
@@ -92,7 +108,8 @@ function applyResponsePolicy(response, opts = {}) {
92
108
  });
93
109
  }
94
110
  export {
95
- serializeSetCookie,
111
+ applyResponsePolicy,
112
+ cacheControlHeader,
96
113
  parseCookieHeader,
97
- applyResponsePolicy
114
+ serializeSetCookie
98
115
  };
@@ -63,9 +63,9 @@ function isNonRecoverable(error) {
63
63
  return false;
64
64
  }
65
65
  export {
66
- tokenEndpointErrorResponse,
67
- publicErrorCode,
68
- isNonRecoverable,
66
+ authorizeDirectErrorResponse,
69
67
  authorizeRedirectErrorResponse,
70
- authorizeDirectErrorResponse
68
+ isNonRecoverable,
69
+ publicErrorCode,
70
+ tokenEndpointErrorResponse
71
71
  };