@_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
@@ -17,18 +17,24 @@ import { isErr } from "../../types/result"
17
17
  import { startAuthorize } from "../../domain/authorize"
18
18
  import { asTenantId } from "../../types/tenant"
19
19
  import { authError } from "../../types/error"
20
- import type { AuthorizationRequest } from "../../types/authorization"
20
+ import type {
21
+ AuthorizationRequest,
22
+ ClaimsRequest,
23
+ } from "../../types/authorization"
21
24
  import type { PickerContext, PickerMethod } from "../../types/picker"
22
25
  import { renderPicker as renderDefaultPicker } from "../../ui/picker"
23
26
 
24
- import { applyResponsePolicy } from "../cookies"
27
+ import { applyResponsePolicy, cacheControlHeader } from "../cookies"
25
28
  import type { HttpContext, HttpDeps } from "../context"
26
29
  import {
27
30
  authorizeDirectErrorResponse,
28
31
  authorizeRedirectErrorResponse,
29
32
  isNonRecoverable,
30
33
  } from "../errors"
31
- import { authorizeQuerySchema } from "../schemas/authorize"
34
+ import {
35
+ authorizeQuerySchema,
36
+ authorizeRequestUriQuerySchema,
37
+ } from "../schemas/authorize"
32
38
 
33
39
  export function makeAuthorizeHandler(deps: HttpDeps) {
34
40
  return async (c: HttpContext): Promise<Response> => {
@@ -41,7 +47,51 @@ export function makeAuthorizeHandler(deps: HttpDeps) {
41
47
 
42
48
  const url = new URL(c.req.url)
43
49
  const raw = Object.fromEntries(url.searchParams.entries())
44
- const parsed = authorizeQuerySchema.safeParse(raw)
50
+
51
+ // RFC 9126 §4: rehydrate a PAR record when `request_uri` is present.
52
+ // The user-agent URL carries only `client_id` + `request_uri`; the
53
+ // stored params drive the rest of the request through the standard
54
+ // parser below.
55
+ let workingRaw: Record<string, string> = raw
56
+ if (raw.request_uri !== undefined) {
57
+ const parParsed = authorizeRequestUriQuerySchema.safeParse(raw)
58
+ if (!parParsed.success) {
59
+ return authorizeDirectErrorResponse(
60
+ authError.invalidRequest(
61
+ parParsed.error.issues[0]?.message ?? "invalid request",
62
+ parParsed.error.issues[0]?.path[0]?.toString() ?? "request",
63
+ ),
64
+ )
65
+ }
66
+ if (!deps.sessionStore.consumePar) {
67
+ return authorizeDirectErrorResponse(
68
+ authError.invalidRequest(
69
+ "session adapter does not support PAR",
70
+ "request_uri",
71
+ ),
72
+ )
73
+ }
74
+ const par = await deps.sessionStore.consumePar(parParsed.data.request_uri)
75
+ if (isErr(par)) {
76
+ return authorizeDirectErrorResponse(
77
+ authError.invalidRequest(
78
+ "unknown or expired request_uri",
79
+ "request_uri",
80
+ ),
81
+ )
82
+ }
83
+ if (par.value.clientId !== parParsed.data.client_id) {
84
+ return authorizeDirectErrorResponse(
85
+ authError.invalidRequest(
86
+ "request_uri belongs to a different client",
87
+ "request_uri",
88
+ ),
89
+ )
90
+ }
91
+ workingRaw = par.value.params
92
+ }
93
+
94
+ const parsed = authorizeQuerySchema.safeParse(workingRaw)
45
95
  if (!parsed.success) {
46
96
  const first = parsed.error.issues[0]
47
97
  const field = first?.path[0]?.toString() ?? "request"
@@ -51,6 +101,20 @@ export function makeAuthorizeHandler(deps: HttpDeps) {
51
101
  }
52
102
  const q = parsed.data
53
103
 
104
+ // RFC 9126 §2 per-client PAR enforcement: refuse direct `/authorize`
105
+ // when the client requires PAR and `request_uri` was absent.
106
+ if (raw.request_uri === undefined) {
107
+ const client = tenant.config.clients.find((c) => c.id === q.client_id)
108
+ if (client?.requirePushedAuthorizationRequests) {
109
+ return authorizeDirectErrorResponse(
110
+ authError.invalidRequest(
111
+ "this client requires Pushed Authorization Requests (RFC 9126)",
112
+ "request_uri",
113
+ ),
114
+ )
115
+ }
116
+ }
117
+
54
118
  if (q.response_type !== "code") {
55
119
  // OAuth 2.1: only `code` is supported. Implicit (`token`) is removed.
56
120
  return authorizeDirectErrorResponse({
@@ -60,6 +124,17 @@ export function makeAuthorizeHandler(deps: HttpDeps) {
60
124
  })
61
125
  }
62
126
 
127
+ let claimsRequest: ClaimsRequest | undefined
128
+ if (q.claims !== undefined) {
129
+ const parsedClaims = parseClaimsParameter(q.claims)
130
+ if (!parsedClaims.ok) {
131
+ return authorizeDirectErrorResponse(
132
+ authError.invalidRequest(parsedClaims.error, "claims"),
133
+ )
134
+ }
135
+ claimsRequest = parsedClaims.value
136
+ }
137
+
63
138
  const request: AuthorizationRequest = {
64
139
  tenantId: asTenantId(tenant.id),
65
140
  clientId: q.client_id,
@@ -78,6 +153,7 @@ export function makeAuthorizeHandler(deps: HttpDeps) {
78
153
  ...(q.prompt !== undefined ? { prompt: q.prompt } : {}),
79
154
  ...(q.ui_locales !== undefined ? { uiLocales: q.ui_locales } : {}),
80
155
  ...(q.nonce !== undefined ? { nonce: q.nonce } : {}),
156
+ ...(claimsRequest !== undefined ? { claimsRequest } : {}),
81
157
  }
82
158
 
83
159
  const result = await startAuthorize(
@@ -133,7 +209,7 @@ export function makeAuthorizeHandler(deps: HttpDeps) {
133
209
  return applyResponsePolicy(out.response, {
134
210
  setCookies: [flowCookie, ...out.setCookies],
135
211
  cookieDefaults: deps.cookieDefaults,
136
- cacheControl: cacheControlFor(out.cache),
212
+ cacheControl: cacheControlHeader(out.cache),
137
213
  })
138
214
  }
139
215
  case "issue-code": {
@@ -189,6 +265,71 @@ export function makeAuthorizeHandler(deps: HttpDeps) {
189
265
  }
190
266
  }
191
267
 
268
+ /**
269
+ * Parse + validate the OIDC Core §5.5 `claims` parameter. Accepts a
270
+ * JSON string; verifies it's an object whose `userinfo` / `id_token`
271
+ * values, if present, are objects mapping claim names to `null` or
272
+ * `{essential?, value?, values?}` entries. Returns a discriminated
273
+ * `{ok, value}` shape so the caller can map failure → `invalid_request`
274
+ * without dragging the AuthError import into this helper.
275
+ */
276
+ function parseClaimsParameter(
277
+ raw: string,
278
+ ): { ok: true; value: ClaimsRequest } | { ok: false; error: string } {
279
+ let parsed: unknown
280
+ try {
281
+ parsed = JSON.parse(raw)
282
+ } catch (e) {
283
+ return {
284
+ ok: false,
285
+ error: `claims parameter is not valid JSON: ${e instanceof Error ? e.message : String(e)}`,
286
+ }
287
+ }
288
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
289
+ return { ok: false, error: "claims parameter must be a JSON object" }
290
+ }
291
+ const obj = parsed as Record<string, unknown>
292
+ const result: ClaimsRequest = {}
293
+ for (const section of ["userinfo", "id_token"] as const) {
294
+ const v = obj[section]
295
+ if (v === undefined) continue
296
+ if (!v || typeof v !== "object" || Array.isArray(v)) {
297
+ return {
298
+ ok: false,
299
+ error: `claims.${section} must be a JSON object`,
300
+ }
301
+ }
302
+ const sectionMap: Record<
303
+ string,
304
+ { essential?: boolean; value?: unknown; values?: unknown[] } | null
305
+ > = {}
306
+ for (const [name, entry] of Object.entries(v)) {
307
+ if (entry === null) {
308
+ sectionMap[name] = null
309
+ continue
310
+ }
311
+ if (typeof entry !== "object" || Array.isArray(entry)) {
312
+ return {
313
+ ok: false,
314
+ error: `claims.${section}.${name} must be null or a JSON object`,
315
+ }
316
+ }
317
+ const e = entry as Record<string, unknown>
318
+ const normalized: {
319
+ essential?: boolean
320
+ value?: unknown
321
+ values?: unknown[]
322
+ } = {}
323
+ if (typeof e.essential === "boolean") normalized.essential = e.essential
324
+ if ("value" in e) normalized.value = e.value
325
+ if (Array.isArray(e.values)) normalized.values = e.values
326
+ sectionMap[name] = normalized
327
+ }
328
+ result[section] = sectionMap
329
+ }
330
+ return { ok: true, value: result }
331
+ }
332
+
192
333
  function clearFlowCookie() {
193
334
  return {
194
335
  name: "idp.flow",
@@ -200,22 +341,3 @@ function clearFlowCookie() {
200
341
  }
201
342
  }
202
343
 
203
- function cacheControlFor(
204
- cache:
205
- | {
206
- maxAge?: number
207
- sMaxAge?: number
208
- isPrivate?: boolean
209
- immutable?: boolean
210
- }
211
- | undefined,
212
- ): string {
213
- if (!cache) return "no-store"
214
- if ((cache.maxAge ?? -1) === 0) return "no-store"
215
- const parts: string[] = []
216
- if (cache.isPrivate) parts.push("private")
217
- if (cache.maxAge !== undefined) parts.push(`max-age=${cache.maxAge}`)
218
- if (cache.sMaxAge !== undefined) parts.push(`s-maxage=${cache.sMaxAge}`)
219
- if (cache.immutable) parts.push("immutable")
220
- return parts.length ? parts.join(", ") : "no-store"
221
- }
@@ -16,8 +16,16 @@ import { authorizeDirectErrorResponse } from "../errors"
16
16
 
17
17
  export function makeCallbackHandler(deps: HttpDeps) {
18
18
  return async (c: HttpContext): Promise<Response> => {
19
+ const tenant = c.get("tenant")
19
20
  const result = await handleCallback(
20
- { rawRequest: c.req.raw, cookies: c.get("cookies") },
21
+ {
22
+ rawRequest: c.req.raw,
23
+ cookies: c.get("cookies"),
24
+ // Only the IdP-initiated (no-state) path reads these; the
25
+ // SP-initiated path derives tenant from the consumed flow.
26
+ ...(tenant ? { tenant } : {}),
27
+ issuerUrl: c.get("issuerUrl"),
28
+ },
21
29
  {
22
30
  configStore: deps.configStore,
23
31
  sessionStore: deps.sessionStore,
@@ -0,0 +1,106 @@
1
+ /**
2
+ * `GET|POST /end_session` — OIDC RP-Initiated Logout 1.0 §2.
3
+ *
4
+ * Per spec, both GET (query) and POST (form body) are supported. The
5
+ * tenant is resolved via the standard `tenantMiddleware`; the issuer URL
6
+ * is taken from the per-request context.
7
+ *
8
+ * Outputs follow `domain/logout.ts`:
9
+ * - `redirect` → 302 to the validated `post_logout_redirect_uri` with
10
+ * `state` echoed when present.
11
+ * - `ok` → 200 with a minimal plain-text body. Hosts that need a styled
12
+ * "logged out" page can layer their own handler over `/end_session`.
13
+ * - errors → 400 with a JSON OAuth-style body (never 302; an unregistered
14
+ * `post_logout_redirect_uri` would otherwise be an open redirector).
15
+ */
16
+ import { endSession } from "../../domain/logout"
17
+ import { authError } from "../../types/error"
18
+ import { isErr } from "../../types/result"
19
+
20
+ import { applyResponsePolicy } from "../cookies"
21
+ import type { HttpContext, HttpDeps } from "../context"
22
+ import { tokenEndpointErrorResponse } from "../errors"
23
+ import { endSessionParamsSchema } from "../schemas/end-session"
24
+
25
+ export function makeEndSessionHandler(deps: HttpDeps) {
26
+ return async (c: HttpContext): Promise<Response> => {
27
+ const tenant = c.get("tenant")
28
+ if (!tenant) {
29
+ return tokenEndpointErrorResponse(
30
+ authError.invalidRequest("tenant unresolved"),
31
+ )
32
+ }
33
+
34
+ // GET → query; POST → form body. Anything else → 400.
35
+ let raw: Record<string, string>
36
+ const httpMethod = c.req.method.toUpperCase()
37
+ if (httpMethod === "GET") {
38
+ raw = Object.fromEntries(new URL(c.req.url).searchParams.entries())
39
+ } else if (httpMethod === "POST") {
40
+ const ct = (c.req.header("content-type") ?? "").toLowerCase()
41
+ if (!ct.startsWith("application/x-www-form-urlencoded")) {
42
+ return tokenEndpointErrorResponse(
43
+ authError.invalidRequest(
44
+ "POST /end_session requires application/x-www-form-urlencoded body",
45
+ ),
46
+ )
47
+ }
48
+ raw = Object.fromEntries(new URLSearchParams(await c.req.raw.text()))
49
+ } else {
50
+ return tokenEndpointErrorResponse(
51
+ authError.invalidRequest(`unsupported method "${httpMethod}"`),
52
+ )
53
+ }
54
+
55
+ const parsed = endSessionParamsSchema.safeParse(raw)
56
+ if (!parsed.success) {
57
+ return tokenEndpointErrorResponse(
58
+ authError.invalidRequest(
59
+ parsed.error.issues[0]?.message ?? "invalid request",
60
+ ),
61
+ )
62
+ }
63
+ const q = parsed.data
64
+
65
+ const res = await endSession(
66
+ {
67
+ ...(q.id_token_hint !== undefined
68
+ ? { idTokenHint: q.id_token_hint }
69
+ : {}),
70
+ ...(q.client_id !== undefined ? { clientId: q.client_id } : {}),
71
+ ...(q.post_logout_redirect_uri !== undefined
72
+ ? { postLogoutRedirectUri: q.post_logout_redirect_uri }
73
+ : {}),
74
+ ...(q.state !== undefined ? { state: q.state } : {}),
75
+ },
76
+ tenant,
77
+ {
78
+ configStore: deps.configStore,
79
+ tokenStore: deps.tokenStore,
80
+ keyStore: deps.keyStore,
81
+ ...(deps.auditLog ? { auditLog: deps.auditLog } : {}),
82
+ issuerUrl: c.get("issuerUrl"),
83
+ clock: deps.clock,
84
+ },
85
+ )
86
+
87
+ if (isErr(res)) return tokenEndpointErrorResponse(res.error)
88
+
89
+ if (res.value.kind === "redirect") {
90
+ return applyResponsePolicy(
91
+ new Response(null, {
92
+ status: 302,
93
+ headers: { location: res.value.url },
94
+ }),
95
+ { setCookies: [], cookieDefaults: deps.cookieDefaults },
96
+ )
97
+ }
98
+ return new Response("Logged out.", {
99
+ status: 200,
100
+ headers: {
101
+ "content-type": "text/plain; charset=utf-8",
102
+ "cache-control": "no-store",
103
+ },
104
+ })
105
+ }
106
+ }
@@ -16,9 +16,14 @@ import { isErr } from "../../types/result"
16
16
  import type { HttpContext, HttpDeps } from "../context"
17
17
  import { tokenEndpointErrorResponse } from "../errors"
18
18
 
19
- export function makeDiscoveryHandler(_deps: HttpDeps) {
19
+ export function makeDiscoveryHandler(deps: HttpDeps) {
20
20
  return async (c: HttpContext): Promise<Response> => {
21
- const doc = buildDiscoveryDocument({ issuerUrl: c.get("issuerUrl") })
21
+ const doc = buildDiscoveryDocument({
22
+ issuerUrl: c.get("issuerUrl"),
23
+ ...(deps.customScopeClaims !== undefined
24
+ ? { customScopeClaims: deps.customScopeClaims }
25
+ : {}),
26
+ })
22
27
  return new Response(JSON.stringify(doc), {
23
28
  status: 200,
24
29
  headers: {
@@ -7,11 +7,15 @@
7
7
  * `domain/method-route.handleMethodRoute` orchestrator handles flow lookup
8
8
  * + dispatch + result translation.
9
9
  */
10
- import { handleMethodRoute } from "../../domain/method-route"
10
+ import {
11
+ handleMethodRoute,
12
+ handlePublicMethodRoute,
13
+ } from "../../domain/method-route"
14
+ import type { RouteKey } from "../../domain/method-dispatch"
11
15
  import { authError } from "../../types/error"
12
16
  import { isErr } from "../../types/result"
13
17
 
14
- import { applyResponsePolicy } from "../cookies"
18
+ import { applyResponsePolicy, cacheControlHeader } from "../cookies"
15
19
  import type { HttpContext, HttpDeps } from "../context"
16
20
  import { authorizeDirectErrorResponse } from "../errors"
17
21
 
@@ -23,6 +27,75 @@ export function makeMethodRouteHandler(deps: HttpDeps) {
23
27
  authError.invalidRequest("tenant unresolved"),
24
28
  )
25
29
  }
30
+
31
+ // Public fast-path (additive — leaves the cookie-gated path below
32
+ // byte-identical for every non-public request). Only a clean hit —
33
+ // well-formed `/m/<id>/<sub>`, method resolves, and the resolved
34
+ // method explicitly lists this route key in `publicRoutes` — is
35
+ // diverted here, cookie-free. Anything else falls through to the
36
+ // original flow-cookie-gated logic with its original error
37
+ // ordering. `methodCache.resolve` is a cache lookup; the duplicate
38
+ // resolve on the gated path is a cache hit.
39
+ {
40
+ const u = new URL(c.req.url)
41
+ const segs = u.pathname.split("/").filter(Boolean)
42
+ if (segs.length >= 3 && segs[0] === "m") {
43
+ const methodId = segs[1]!
44
+ const subPath = "/" + segs.slice(2).join("/")
45
+ const httpMethod =
46
+ c.req.method.toUpperCase() === "POST" ? "POST" : "GET"
47
+ const routeKey = `${httpMethod} ${subPath}` as RouteKey
48
+ const resolved = await deps.methodCache.resolve(
49
+ tenant.config,
50
+ methodId,
51
+ )
52
+ if (
53
+ !isErr(resolved) &&
54
+ resolved.value.publicRoutes?.includes(routeKey)
55
+ ) {
56
+ const issuerUrl = c.get("issuerUrl")
57
+ const callbackHost =
58
+ deps.callbackHostFor?.(tenant.id) ?? new URL(issuerUrl).host
59
+ // Mirrors the ACS URL derivation in domain/authorize.ts
60
+ // (callbackHost/path/url). Source of truth is there; the
61
+ // metadata.test.ts anti-drift test asserts the emitted
62
+ // entityID/ACS equal what buildAuthnRequestRedirect derives.
63
+ const callbackUrl = `${new URL(issuerUrl).protocol}//${callbackHost}/cb/${methodId}`
64
+ const pub = await handlePublicMethodRoute(
65
+ {
66
+ rawRequest: c.req.raw,
67
+ tenant,
68
+ method: resolved.value,
69
+ route: routeKey,
70
+ subPath,
71
+ dispatch: { state: "", callbackUrl, issuerUrl },
72
+ },
73
+ {
74
+ sessionStore: deps.sessionStore,
75
+ tokenStore: deps.tokenStore,
76
+ clock: deps.clock,
77
+ ...(deps.auditLog ? { auditLog: deps.auditLog } : {}),
78
+ ...(deps.onLogout ? { onLogout: deps.onLogout } : {}),
79
+ },
80
+ )
81
+ if (isErr(pub)) return authorizeDirectErrorResponse(pub.error)
82
+ if (pub.value.kind === "denied") {
83
+ return applyResponsePolicy(
84
+ new Response(`access_denied: ${pub.value.reason}`, {
85
+ status: 403,
86
+ headers: { "content-type": "text/plain" },
87
+ }),
88
+ { cookieDefaults: deps.cookieDefaults },
89
+ )
90
+ }
91
+ return applyResponsePolicy(pub.value.response, {
92
+ cacheControl: cacheControlHeader(pub.value.cache),
93
+ cookieDefaults: deps.cookieDefaults,
94
+ })
95
+ }
96
+ }
97
+ }
98
+
26
99
  const cookies = c.get("cookies")
27
100
  const flowId = cookies.get("idp.flow")
28
101
  if (!flowId) {
@@ -0,0 +1,84 @@
1
+ /**
2
+ * `POST /par` — Pushed Authorization Requests (RFC 9126 §2).
3
+ *
4
+ * Body: `application/x-www-form-urlencoded` carrying the same parameter
5
+ * set the RP would otherwise put on `GET /authorize`, plus `client_id`
6
+ * and optionally `client_secret`. Client credentials may alternatively
7
+ * ride in `Authorization: Basic` (RFC 6749 §2.3.1).
8
+ *
9
+ * Response: HTTP 201, JSON `{request_uri, expires_in}` on success.
10
+ */
11
+ import { resolveClientCredentials } from "../../domain/client-auth"
12
+ import { pushAuthorizationRequest } from "../../domain/par"
13
+ import { authError } from "../../types/error"
14
+ import { isErr } from "../../types/result"
15
+
16
+ import type { HttpContext, HttpDeps } from "../context"
17
+ import { tokenEndpointErrorResponse } from "../errors"
18
+ import { parBodySchema } from "../schemas/par"
19
+
20
+ export function makeParHandler(deps: HttpDeps) {
21
+ return async (c: HttpContext): Promise<Response> => {
22
+ const tenant = c.get("tenant")
23
+ if (!tenant) {
24
+ return tokenEndpointErrorResponse(
25
+ authError.invalidRequest("tenant unresolved"),
26
+ )
27
+ }
28
+ const ct = (c.req.header("content-type") ?? "").toLowerCase()
29
+ if (!ct.startsWith("application/x-www-form-urlencoded")) {
30
+ return tokenEndpointErrorResponse(
31
+ authError.invalidRequest(
32
+ "body must be application/x-www-form-urlencoded",
33
+ ),
34
+ )
35
+ }
36
+ const raw = Object.fromEntries(
37
+ new URLSearchParams(await c.req.raw.text()).entries(),
38
+ )
39
+ const parsed = parBodySchema.safeParse(raw)
40
+ if (!parsed.success) {
41
+ return tokenEndpointErrorResponse(
42
+ authError.invalidRequest(
43
+ parsed.error.issues[0]?.message ?? "invalid request",
44
+ ),
45
+ )
46
+ }
47
+ const creds = resolveClientCredentials({
48
+ authorizationHeader: c.req.header("authorization") ?? null,
49
+ bodyClientId: parsed.data.client_id,
50
+ bodyClientSecret: parsed.data.client_secret,
51
+ })
52
+ const clientId = creds?.clientId ?? parsed.data.client_id
53
+
54
+ // Strip auth-only fields from the param record we persist — they
55
+ // belong to `/par`, not to the rehydrated `/authorize` request.
56
+ const { client_secret: _strip, ...paramsForAuthorize } = parsed.data
57
+ void _strip
58
+
59
+ const res = await pushAuthorizationRequest(
60
+ {
61
+ clientId,
62
+ ...(creds?.clientSecret !== undefined
63
+ ? { clientSecret: creds.clientSecret }
64
+ : {}),
65
+ params: paramsForAuthorize,
66
+ },
67
+ tenant,
68
+ {
69
+ configStore: deps.configStore,
70
+ sessionStore: deps.sessionStore,
71
+ ...(deps.auditLog ? { auditLog: deps.auditLog } : {}),
72
+ clock: deps.clock,
73
+ },
74
+ )
75
+ if (isErr(res)) return tokenEndpointErrorResponse(res.error)
76
+ return new Response(JSON.stringify(res.value), {
77
+ status: 201,
78
+ headers: {
79
+ "content-type": "application/json",
80
+ "cache-control": "no-store",
81
+ },
82
+ })
83
+ }
84
+ }
@@ -0,0 +1,70 @@
1
+ /**
2
+ * `POST /register` — RFC 7591 Dynamic Client Registration.
3
+ *
4
+ * Body: `application/json`. Tenant resolved via the standard middleware.
5
+ * The framework validates structure here and defers persistence to the
6
+ * `IdPOptions.registerClient` hook (in `domain/register.ts`).
7
+ *
8
+ * Per RFC 7591 §3.2.1, success returns HTTP 201 with JSON body.
9
+ */
10
+ import { registerNewClient } from "../../domain/register"
11
+ import { authError } from "../../types/error"
12
+ import type { RegisterClientRequest } from "../../types/idp"
13
+ import { isErr } from "../../types/result"
14
+
15
+ import type { HttpContext, HttpDeps } from "../context"
16
+ import { tokenEndpointErrorResponse } from "../errors"
17
+ import { registerBodySchema } from "../schemas/register"
18
+
19
+ export function makeRegisterHandler(deps: HttpDeps) {
20
+ return async (c: HttpContext): Promise<Response> => {
21
+ const tenant = c.get("tenant")
22
+ if (!tenant) {
23
+ return tokenEndpointErrorResponse(
24
+ authError.invalidRequest("tenant unresolved"),
25
+ )
26
+ }
27
+ const ct = (c.req.header("content-type") ?? "").toLowerCase()
28
+ if (!ct.startsWith("application/json")) {
29
+ return tokenEndpointErrorResponse(
30
+ authError.invalidRequest("body must be application/json"),
31
+ )
32
+ }
33
+ let raw: unknown
34
+ try {
35
+ raw = await c.req.raw.json()
36
+ } catch (e) {
37
+ const reason = e instanceof Error ? e.message : String(e)
38
+ return tokenEndpointErrorResponse(
39
+ authError.invalidRequest(`malformed JSON: ${reason}`),
40
+ )
41
+ }
42
+ const parsed = registerBodySchema.safeParse(raw)
43
+ if (!parsed.success) {
44
+ return tokenEndpointErrorResponse(
45
+ authError.invalidRequest(
46
+ parsed.error.issues[0]?.message ?? "invalid request",
47
+ ),
48
+ )
49
+ }
50
+ // Schema is `.passthrough()` (RFC 7591 allows extension metadata
51
+ // fields); the declared `RegisterClientRequest` fields are all
52
+ // statically present in `parsed.data`. This cast just drops the
53
+ // passthrough's `[k: string]: unknown` index signature so the
54
+ // downstream domain function sees the typed contract.
55
+ const request = parsed.data as RegisterClientRequest
56
+
57
+ const res = await registerNewClient(request, tenant, {
58
+ ...(deps.registerClient ? { registerClient: deps.registerClient } : {}),
59
+ clock: deps.clock,
60
+ })
61
+ if (isErr(res)) return tokenEndpointErrorResponse(res.error)
62
+ return new Response(JSON.stringify(res.value), {
63
+ status: 201,
64
+ headers: {
65
+ "content-type": "application/json",
66
+ "cache-control": "no-store",
67
+ },
68
+ })
69
+ }
70
+ }