@_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
@@ -54,6 +54,10 @@ export type AuditEvent =
54
54
  subjectId: string
55
55
  /** Hash of the issued refresh token id — never the token itself. */
56
56
  refreshTokenIdHash?: string
57
+ /** OIDC Core §2 — set when the response carried an `id_token`. */
58
+ idTokenIssued?: boolean
59
+ /** RFC 9449 — set when the access token is DPoP-bound. */
60
+ dpopBound?: boolean
57
61
  }
58
62
  | {
59
63
  kind: "token_refreshed"
@@ -87,6 +91,44 @@ export type AuditEvent =
87
91
  clientId: string
88
92
  family: string
89
93
  }
94
+ | {
95
+ /**
96
+ * Emitted after a logout is processed. Two channels, discriminated
97
+ * by `via`:
98
+ *
99
+ * - `rp_initiated` (or absent — the default): `/end_session`
100
+ * (OIDC RP-Initiated Logout 1.0 §2), regardless of whether a
101
+ * `post_logout_redirect_uri` was supplied. `subjectId` present
102
+ * when an `id_token_hint` verified; absent otherwise.
103
+ * - `upstream_slo`: an upstream IdP notified us a federated
104
+ * session ended (SAML front-channel Single Logout). `methodId`
105
+ * / `methodKind` identify the federation connection;
106
+ * `subjectId` present only when the host's `onLogout` returned
107
+ * a subject to revoke.
108
+ *
109
+ * `via` is general (OIDC back-channel logout would reuse
110
+ * `upstream_slo`), not SAML-specific surface.
111
+ */
112
+ kind: "session_logout"
113
+ tenantId: TenantId
114
+ clientId?: string
115
+ subjectId?: string
116
+ via?: "rp_initiated" | "upstream_slo"
117
+ methodId?: string
118
+ methodKind?: string
119
+ }
120
+ | {
121
+ /**
122
+ * Emitted when a DPoP proof's `jti` is presented within the replay
123
+ * window (RFC 9449 §11.1). The request was rejected with
124
+ * `invalid_dpop_proof`. Operators / SIEM use this to spot
125
+ * stolen-key replay attempts.
126
+ */
127
+ kind: "dpop_replay_detected"
128
+ tenantId: TenantId | null
129
+ /** First-half of the offending jti so logs can correlate without storing it. */
130
+ jtiPrefix: string
131
+ }
90
132
  | {
91
133
  kind: "flow_replay_attempt"
92
134
  tenantId: TenantId | null
@@ -83,6 +83,67 @@ export type SessionStore = {
83
83
  ): Promise<Result<void>>
84
84
  readSession?(sessionId: string): Promise<Result<SessionRecord>>
85
85
  revokeSession?(sessionId: string): Promise<Result<void>>
86
+
87
+ /**
88
+ * Optional: Pushed Authorization Request (RFC 9126) storage. Persists the
89
+ * pre-parsed `/authorize` parameters under an opaque `request_uri` for
90
+ * one-shot retrieval at `/authorize` time. Strong consistency + atomic
91
+ * delete-on-read — same semantics as `saveFlow` / `consumeFlow`.
92
+ *
93
+ * Adapters without these methods cannot satisfy `/par`; the framework's
94
+ * `/par` handler returns `invalid_request` when called against such a
95
+ * store. Implement both methods together (the framework only exposes
96
+ * the endpoint when both are present).
97
+ */
98
+ savePar?(
99
+ requestUri: string,
100
+ payload: ParRecord,
101
+ ttl: number,
102
+ ): Promise<Result<void>>
103
+ consumePar?(requestUri: string): Promise<Result<ParRecord>>
104
+
105
+ /**
106
+ * Optional: per-method-instance scratch storage. Survives across flows
107
+ * (unlike `methodState`, which is per-flow). Use cases include cross-flow
108
+ * deduplication state — e.g., a SAML SP method remembering recently-seen
109
+ * assertion IDs for replay protection.
110
+ *
111
+ * The framework scopes keys per `(tenantId, methodId)` before calling
112
+ * these — adapters see opaque, already-namespaced keys and store the
113
+ * UTF-8 string value verbatim. Methods JSON-encode if they want object
114
+ * state.
115
+ *
116
+ * Strong consistency + TTL respect (same semantics as flow records).
117
+ * `readScratch` returns `unknown_state` if the key is missing or
118
+ * expired. `deleteScratch` is idempotent.
119
+ *
120
+ * Adapters without these methods cannot host methods that depend on
121
+ * scratch; the framework surfaces a clear `unsupported` error through
122
+ * `MethodContext.methodScratch` at call time. Implement all three
123
+ * methods together — partial implementations are not supported.
124
+ */
125
+ saveScratch?(
126
+ key: string,
127
+ value: string,
128
+ ttlMs: number,
129
+ ): Promise<Result<void>>
130
+ readScratch?(key: string): Promise<Result<string>>
131
+ deleteScratch?(key: string): Promise<Result<void>>
132
+ }
133
+
134
+ /**
135
+ * Stored PAR payload. The `params` blob is the raw form/query record from
136
+ * `POST /par`, kept verbatim so the `/authorize` rehydrate path can feed
137
+ * it through the same Zod parser the direct path uses.
138
+ */
139
+ export type ParRecord = {
140
+ requestUri: string
141
+ /** Raw key/value record as posted to `/par` (excluding auth fields). */
142
+ params: Record<string, string>
143
+ clientId: string
144
+ /** Wall-clock issuance + absolute expiry (ms). */
145
+ issuedAt: number
146
+ expiresAt: number
86
147
  }
87
148
 
88
149
  /** Optional long-lived session payload (used by `createSession` family). */
@@ -100,4 +100,17 @@ export type TokenStore = {
100
100
  * preferred; documented eventual lag acceptable.
101
101
  */
102
102
  revokeBySubject(tenantId: TenantId, subjectId: string): Promise<Result<void>>
103
+
104
+ /**
105
+ * Optional: DPoP proof replay protection (RFC 9449 §11.1). Atomically
106
+ * record the proof's `jti` and reject if it's been seen within the TTL
107
+ * window. Returns `invalid_grant` (with `reuseSignal` omitted) when a
108
+ * `jti` is already present.
109
+ *
110
+ * Adapters without this method cannot satisfy DPoP at the token /
111
+ * userinfo endpoints; the framework's DPoP verifier returns
112
+ * `invalid_dpop_proof` when called against such a store. Memory adapter
113
+ * implements; production adapters follow in subsequent commits.
114
+ */
115
+ recordDpopJti?(jti: string, ttlMs: number): Promise<Result<void>>
103
116
  }
@@ -37,6 +37,31 @@ export type AuthorizationRequest = {
37
37
  uiLocales?: string[]
38
38
  /** Standard OIDC. */
39
39
  nonce?: string
40
+ /**
41
+ * OIDC Core §5.5 — RP-requested claims, parsed from the `claims`
42
+ * parameter's JSON value. The library currently honors claim **names**
43
+ * (additive, bypasses scope gating) but does not enforce the
44
+ * `essential` / `value` / `values` qualifiers.
45
+ */
46
+ claimsRequest?: ClaimsRequest
47
+ }
48
+
49
+ /**
50
+ * OIDC Core §5.5 `claims` parameter. Each section maps claim name to an
51
+ * optional qualifier object (`{essential, value, values}`) or `null`
52
+ * meaning "just request the claim without qualifiers".
53
+ */
54
+ export type ClaimsRequest = {
55
+ /** Claims to include in the `/userinfo` response. */
56
+ userinfo?: Record<string, ClaimRequestEntry | null>
57
+ /** Claims to include in the `id_token`. */
58
+ id_token?: Record<string, ClaimRequestEntry | null>
59
+ }
60
+
61
+ export type ClaimRequestEntry = {
62
+ essential?: boolean
63
+ value?: unknown
64
+ values?: unknown[]
40
65
  }
41
66
 
42
67
  /**
@@ -50,6 +50,21 @@ export type AuthError =
50
50
  // endpoints. Use for control-flow signalling that should never escape to a
51
51
  // standards-compliant client.
52
52
  | { code: "internal_error"; description: string; cause?: unknown }
53
+ // RFC 9449 §5.2 — DPoP proof verification failed (bad signature, htm/htu
54
+ // mismatch, iat outside window, replayed jti, missing/mismatched cnf.jkt).
55
+ // Returned as a 400 with `error="invalid_dpop_proof"` on form-body
56
+ // endpoints; on resource-server endpoints it becomes a 401 with
57
+ // `WWW-Authenticate: DPoP error="invalid_dpop_proof"`.
58
+ //
59
+ // `replaySignal`, when present, indicates `recordDpopJti` reported a
60
+ // jti already seen within the replay window. The HTTP layer uses this
61
+ // to emit a `dpop_replay_detected` audit event distinct from other
62
+ // proof failures.
63
+ | {
64
+ code: "invalid_dpop_proof"
65
+ description: string
66
+ replaySignal?: { jti: string }
67
+ }
53
68
 
54
69
  export type AuthErrorCode = AuthError["code"]
55
70
 
@@ -120,4 +135,12 @@ export const authError = {
120
135
  description,
121
136
  ...(cause !== undefined ? { cause } : {}),
122
137
  }),
138
+ invalidDpopProof: (
139
+ description: string,
140
+ replaySignal?: { jti: string },
141
+ ): AuthError => ({
142
+ code: "invalid_dpop_proof",
143
+ description,
144
+ ...(replaySignal !== undefined ? { replaySignal } : {}),
145
+ }),
123
146
  }
package/src/types/flow.ts CHANGED
@@ -19,6 +19,7 @@
19
19
  * needed at `/token` into the auth-code payload; `methodState` is
20
20
  * dropped.
21
21
  */
22
+ import type { ClaimsRequest } from "./authorization"
22
23
  import type { TenantId } from "./tenant"
23
24
 
24
25
  export type FlowRecord = {
@@ -56,6 +57,15 @@ export type FlowRecord = {
56
57
  * `appState`. Compared at callback against the consumed record.
57
58
  */
58
59
  nonce: string
60
+ /**
61
+ * Relying party's OIDC `nonce` parameter (OIDC Core §3.1.2.1). Distinct
62
+ * from `nonce` (which is the framework's CSRF nonce for state-MAC
63
+ * binding). When present, must be echoed in the issued `id_token`
64
+ * (OIDC Core §2). Snapshotted into `CodePayload.appNonce` at success.
65
+ */
66
+ appNonce?: string
67
+ /** OIDC Core §5.5 — RP-requested claims, parsed at `/authorize`. */
68
+ claimsRequest?: ClaimsRequest
59
69
  /**
60
70
  * Relying-party → IdP PKCE. The RP generates the verifier and sends the
61
71
  * challenge to `/authorize`. Verified at `/token`.
package/src/types/idp.ts CHANGED
@@ -15,6 +15,7 @@ import type { AnyAuthMethodFactory } from "./method"
15
15
  import type { Result } from "./result"
16
16
  import type { SubjectClaim, SubjectSchema } from "./subject"
17
17
  import type {
18
+ ClientConfig,
18
19
  StateKeyRing,
19
20
  TenantContext,
20
21
  TenantId,
@@ -68,6 +69,56 @@ export type FailureEvent = {
68
69
  error: AuthError
69
70
  }
70
71
 
72
+ /**
73
+ * Input to the optional `IdPOptions.onLogout` hook.
74
+ *
75
+ * Fires when an upstream provider notifies this IdP that a federated
76
+ * session has ended — today, a SAML front-channel `LogoutRequest`
77
+ * delivered to the SP's SLS endpoint. By the time this runs the library
78
+ * has already cryptographically verified the upstream logout message
79
+ * (XML-DSig via the SAML method), so the hook is purely the host's
80
+ * teardown point.
81
+ *
82
+ * The library deliberately does **not** know which OIDC `subject` an
83
+ * upstream identifier (`nameId`) maps to — that mapping lives in the
84
+ * host's `success` callback, not the library. So the host clears its
85
+ * own session and returns the subject (if any) whose library-issued
86
+ * tokens should be revoked; the library then runs the same
87
+ * `revokeAllForSubject` primitive `/end_session` uses. Returning
88
+ * nothing skips library-side revocation (the host handled everything,
89
+ * or there is nothing to revoke).
90
+ *
91
+ * Method-agnostic on purpose: any federation method that can verify an
92
+ * upstream logout signal reuses this. SAML SLO is the first caller;
93
+ * OIDC back-channel logout would be the next.
94
+ */
95
+ export type LogoutEventInput = {
96
+ tenant: TenantContext
97
+ methodId: string
98
+ methodKind: string
99
+ /** What kind of upstream logout this is. Extensible discriminant. */
100
+ reason: "upstream_slo"
101
+ /**
102
+ * Upstream subject identifier from the verified logout message
103
+ * (SAML `LogoutRequest/NameID`). Absent if the message omitted it.
104
+ */
105
+ nameId?: string
106
+ /**
107
+ * Upstream session index from the verified logout message
108
+ * (SAML `SessionIndex`), when present. Lets a host that tracks
109
+ * per-session state scope its teardown.
110
+ */
111
+ sessionIndex?: string
112
+ }
113
+
114
+ /**
115
+ * Return of `IdPOptions.onLogout`. `revokeSubject` names the OIDC
116
+ * subject whose library-issued refresh tokens the library should
117
+ * revoke (the host resolves it from `nameId` — only the host has that
118
+ * map). Omit / return nothing to skip library-side revocation.
119
+ */
120
+ export type LogoutHookResult = { revokeSubject?: string } | void
121
+
71
122
  /**
72
123
  * Optional hook called at `/token` time, after PKCE has succeeded and
73
124
  * after the `success` callback has produced a `SubjectClaim`, but
@@ -119,6 +170,69 @@ export type ExchangeAudience = (
119
170
  },
120
171
  ) => Promise<SubjectClaim | AuthError>
121
172
 
173
+ /**
174
+ * RFC 7591 Dynamic Client Registration request body, normalized to the
175
+ * library's type system. The framework's `/register` handler parses the
176
+ * raw JSON, validates structure, then calls the host's `registerClient`
177
+ * hook with this shape. The host owns persistence — writing through its
178
+ * own `ConfigStore` — and returns the final `ClientConfig`.
179
+ */
180
+ export type RegisterClientRequest = {
181
+ client_name?: string
182
+ redirect_uris: string[]
183
+ /** RFC 7591 §2 — defaults to `["authorization_code"]`. */
184
+ grant_types?: string[]
185
+ /** RFC 7591 §2 — defaults to `["code"]`. */
186
+ response_types?: string[]
187
+ /** RFC 7591 §2 — `"none"` = public client, otherwise confidential. */
188
+ token_endpoint_auth_method?:
189
+ | "none"
190
+ | "client_secret_basic"
191
+ | "client_secret_post"
192
+ scope?: string
193
+ /** OIDC RP-Initiated Logout 1.0 §2. */
194
+ post_logout_redirect_uris?: string[]
195
+ /** OIDC Core §8.1. */
196
+ sector_identifier_uri?: string
197
+ /** Free-form metadata the host may interpret. */
198
+ contacts?: string[]
199
+ }
200
+
201
+ /**
202
+ * RFC 7591 §3.2.1 response body. Returned verbatim from `/register` when
203
+ * the host hook produces a `ClientConfig`. `client_secret` is included
204
+ * only for confidential clients; public clients omit it.
205
+ */
206
+ export type RegisterClientResponse = {
207
+ client_id: string
208
+ client_secret?: string
209
+ client_id_issued_at: number
210
+ client_secret_expires_at?: number
211
+ redirect_uris: string[]
212
+ grant_types?: string[]
213
+ response_types?: string[]
214
+ token_endpoint_auth_method?: string
215
+ client_name?: string
216
+ }
217
+
218
+ /**
219
+ * Optional Dynamic Client Registration hook. Hosts that want to expose
220
+ * RFC 7591 client provisioning supply this; the framework validates the
221
+ * wire format, then defers persistence to the host. If absent, the
222
+ * `/register` endpoint returns `invalid_request` so RPs receive a clear
223
+ * "not enabled" signal rather than a 404.
224
+ *
225
+ * The hook receives the parsed request, the resolved tenant, and the
226
+ * plaintext client secret (if any) the framework minted — hosts hash it
227
+ * with `hashClientSecret` before storing on `ClientConfig.secretHash`,
228
+ * then return the final `ClientConfig` along with the secret in the
229
+ * `RegisterClientResponse` so the RP can record it.
230
+ */
231
+ export type RegisterClient = (input: {
232
+ tenant: TenantContext
233
+ request: RegisterClientRequest
234
+ }) => Promise<Result<{ client: ClientConfig; secret?: string }, AuthError>>
235
+
122
236
  /**
123
237
  * Optional override for the default provider picker shown when an
124
238
  * `/authorize` request has multiple enabled methods and no `method_id`.
@@ -195,6 +309,30 @@ export type IdPOptions = {
195
309
  onFailure?: (event: FailureEvent) => Promise<void>
196
310
  }
197
311
 
312
+ /**
313
+ * Optional hook fired when an upstream provider signals that a
314
+ * federated session ended — SAML front-channel Single Logout today.
315
+ * The library has already verified the signed logout message; this
316
+ * hook is where the host tears down its own session and names the
317
+ * OIDC subject (if any) whose library-issued tokens to revoke. See
318
+ * the `LogoutEventInput` / `LogoutHookResult` type docs.
319
+ *
320
+ * Unlike `hooks.onSuccess`/`onFailure` (observation only) this hook
321
+ * **influences** library behaviour — its return drives token
322
+ * revocation — so it sits at the top level alongside `success`.
323
+ *
324
+ * Absent ⇒ the library still verifies the logout, emits a
325
+ * `session_logout` audit event, and returns the protocol
326
+ * `LogoutResponse`, but performs no token revocation (it cannot map
327
+ * the upstream id to a subject without the host).
328
+ *
329
+ * If it throws, the SLS endpoint fails closed with an internal error
330
+ * rather than acknowledging a logout it could not fully process.
331
+ */
332
+ onLogout?: (
333
+ input: LogoutEventInput,
334
+ ) => Promise<LogoutHookResult> | LogoutHookResult
335
+
198
336
  /**
199
337
  * Optional escape hatch for high-sensitivity deployments — see the
200
338
  * `PersistUpstreamTokens` type doc.
@@ -213,6 +351,13 @@ export type IdPOptions = {
213
351
  */
214
352
  renderPicker?: RenderPicker
215
353
 
354
+ /**
355
+ * Optional RFC 7591 Dynamic Client Registration hook. See
356
+ * `RegisterClient` for the contract. When absent, `/register` rejects
357
+ * with `invalid_request: "dynamic client registration is not enabled"`.
358
+ */
359
+ registerClient?: RegisterClient
360
+
216
361
  /**
217
362
  * Optional hook that builds the `TenantContext.request.custom` blob for
218
363
  * each request the framework processes (the initial `/authorize`, the
@@ -234,6 +379,25 @@ export type IdPOptions = {
234
379
  req: Request,
235
380
  ) => Record<string, unknown> | Promise<Record<string, unknown>>
236
381
 
382
+ /**
383
+ * Optional vendor scope → claim-names map. Merged on top of OIDC Core
384
+ * §5.4 when building the id_token + `/userinfo` response. Lets a host
385
+ * expose its own identity vocabulary (`tenant_id`, `org_role`, etc.)
386
+ * via custom scope names. The standard §5.4 mapping always wins on
387
+ * collision, so an entry for `email` is silently ignored.
388
+ *
389
+ * Per-client scope allowlist still applies — a client must list a
390
+ * custom scope in `ClientConfig.scopes` to be allowed to request it.
391
+ *
392
+ * ```ts
393
+ * customScopeClaims: {
394
+ * tenant: ["tenant_id", "tenant_role", "tenant_roles"],
395
+ * org: ["organization_id", "org_role"],
396
+ * }
397
+ * ```
398
+ */
399
+ customScopeClaims?: Record<string, ReadonlyArray<string>>
400
+
237
401
  /**
238
402
  * Optional override for the framework's `Set-Cookie` defaults.
239
403
  *
@@ -266,4 +430,10 @@ export type IdP = {
266
430
  discovery: (req: Request) => Promise<Response>
267
431
  revoke: (req: Request) => Promise<Response>
268
432
  introspect: (req: Request) => Promise<Response>
433
+ /** OIDC RP-Initiated Logout 1.0. */
434
+ endSession: (req: Request) => Promise<Response>
435
+ /** RFC 9126 Pushed Authorization Requests. */
436
+ par: (req: Request) => Promise<Response>
437
+ /** RFC 7591 Dynamic Client Registration. */
438
+ register: (req: Request) => Promise<Response>
269
439
  }
@@ -56,6 +56,37 @@ export type AuthMethod<P = unknown, S = unknown> = {
56
56
  * same factory get distinct URL spaces).
57
57
  */
58
58
  routes: Record<string, MethodHandler<P, S>>
59
+ /**
60
+ * Opt-in allowlist of route keys (same `"GET /metadata"` string form
61
+ * as `routes` keys) that the framework dispatches **without** a flow
62
+ * cookie or flow record — anonymous, unauthenticated GETs. The sole
63
+ * intended use is publishing static, per-instance descriptive
64
+ * documents (SAML SP metadata XML). The handler receives
65
+ * `ctx.flow === null` and `ctx.methodState === null` and MUST be a
66
+ * pure function of `ctx.tenant` + `ctx.dispatch` + its captured
67
+ * config — it must not assume an authenticated principal.
68
+ *
69
+ * Absent (the default for every method) ⇒ behaviour is unchanged:
70
+ * every `/m/<id>/*` request requires the `idp.flow` cookie. The
71
+ * cookie gate is skipped *only* for a route key a method explicitly
72
+ * lists here — fail-closed by construction.
73
+ */
74
+ publicRoutes?: ReadonlyArray<string>
75
+ /**
76
+ * Opt-in: this method instance handles **unsolicited** upstream
77
+ * callbacks — a `POST /cb/<methodId>` carrying a provider assertion
78
+ * with no framework state envelope and no prior flow (SAML
79
+ * IdP-initiated SSO). When `true`, the framework, instead of
80
+ * rejecting a stateless callback, dispatches `GET /callback` with
81
+ * `flow === null` and a derived `dispatch` (issuer/ACS); the handler
82
+ * verifies the assertion and returns `success` **with
83
+ * `unsolicitedBinding`**. Absent (every method's default) ⇒ a
84
+ * stateless callback stays an `invalid_request` (the conservative
85
+ * default — many deployments do not want IdP-initiated). General
86
+ * capability, not SAML-specific; set per instance (a SAML instance
87
+ * sets it only when its config enables IdP-initiated).
88
+ */
89
+ unsolicitedCallback?: boolean
59
90
  /**
60
91
  * Token-exchange function for the `/token` endpoint when the method
61
92
  * participates in client-credentials-style flows (e.g. `m2m`). Most
@@ -102,6 +133,47 @@ export type MethodContext<S = unknown> = {
102
133
  * and the relevant data is on `flow`).
103
134
  */
104
135
  dispatch: MethodDispatchData | null
136
+ /**
137
+ * Per-method-instance scratch storage scoped to
138
+ * `(tenant.id, method.id)`. Survives across flows — distinct from
139
+ * `methodState`, which is per-flow.
140
+ *
141
+ * Most methods do NOT need this. It exists for cross-flow
142
+ * deduplication patterns such as SAML SP assertion-ID replay
143
+ * protection.
144
+ *
145
+ * Backed by `SessionStore.{saveScratch,readScratch,deleteScratch}`
146
+ * when those optional methods are implemented. Against adapters that
147
+ * don't implement them, every call returns
148
+ * `{ ok: false, error: unsupported }` — the method should surface a
149
+ * `MethodResult.error` with a clear message, not silently degrade.
150
+ */
151
+ methodScratch: MethodScratch
152
+ }
153
+
154
+ /**
155
+ * Caller-facing API for per-method-instance scratch. The framework
156
+ * scopes user-supplied keys with a `(tenantId, methodId)` prefix before
157
+ * delegating to `SessionStore` — adapters never see raw method keys.
158
+ *
159
+ * Values are UTF-8 strings; methods JSON-encode if they want to stash
160
+ * objects. Keeping the port-level type narrow simplifies adapter
161
+ * implementations (one TEXT column, one Dynamo `S` attribute, etc.).
162
+ */
163
+ export type MethodScratch = {
164
+ /**
165
+ * Persist `value` under `key` with the given TTL. Overwrites prior
166
+ * value for the same key. `ttlMs` must be positive.
167
+ */
168
+ put(key: string, value: string, ttlMs: number): Promise<Result<void>>
169
+ /**
170
+ * Read the value previously stored at `key`. Returns `unknown_state`
171
+ * if the key is missing or expired (the underlying adapter MAY
172
+ * lazily evict expired entries on read).
173
+ */
174
+ get(key: string): Promise<Result<string>>
175
+ /** Idempotent. Resolves `ok` whether the key existed or not. */
176
+ delete(key: string): Promise<Result<void>>
105
177
  }
106
178
 
107
179
  /** Framework-supplied data available to the method at `/authorize` time. */
@@ -143,6 +215,27 @@ export type MethodResult<P = unknown, S = unknown> =
143
215
  saveMethodState?: S
144
216
  /** Serialized into a `Cache-Control` header by the framework. */
145
217
  cache?: CachePolicy
218
+ /**
219
+ * Verified upstream-logout notification. Set **only** by a
220
+ * flowless **public** logout route (e.g. SAML front-channel SLS)
221
+ * once it has cryptographically verified the inbound logout
222
+ * message. The method stays port-free — it proves authenticity
223
+ * and builds the protocol response (`response` = the signed
224
+ * `LogoutResponse` redirect / ack); the privileged side effect
225
+ * runs in the framework.
226
+ *
227
+ * When present on a public route the framework fires
228
+ * `IdPOptions.onLogout` and, if that returns `{ revokeSubject }`,
229
+ * runs `revokeAllForSubject` — then returns `response`. Ignored
230
+ * on non-public (flow-bearing) routes: an authenticated method
231
+ * route never logs anyone out. Mirrors the Phase 2 V′ pattern
232
+ * (`success.unsolicitedBinding`): an optional field on an
233
+ * existing variant, not a new `MethodResult` kind.
234
+ */
235
+ logout?: {
236
+ nameId?: string
237
+ sessionIndex?: string
238
+ }
146
239
  }
147
240
  /**
148
241
  * Authentication succeeded. The HTTP layer hands `providerSubject` +
@@ -155,6 +248,23 @@ export type MethodResult<P = unknown, S = unknown> =
155
248
  providerSubject: string
156
249
  properties: P
157
250
  setCookies?: SetCookie[]
251
+ /**
252
+ * RP binding for a **flowless** (unsolicited / IdP-initiated)
253
+ * authentication. Consulted **only** when the method ran with no
254
+ * `flow` (e.g. an unsolicited SAML Response — see
255
+ * `AuthMethod.unsolicitedCallback`). On the normal SP-initiated
256
+ * path a `flow` exists and this is ignored; on the flowless path
257
+ * it is **required** (the framework has no pending RP request to
258
+ * read `client_id` / `redirect_uri` / `scope` from, so the method
259
+ * supplies the operator-configured defaults). The framework still
260
+ * validates `clientId` / `redirectUri` against the tenant's
261
+ * registered client before issuing a code.
262
+ */
263
+ unsolicitedBinding?: {
264
+ clientId: string
265
+ redirectUri: string
266
+ scopes: string[]
267
+ }
158
268
  }
159
269
  /** User refused / failed auth in a non-error way (e.g. consent declined). */
160
270
  | { kind: "denied"; reason: string; setCookies?: SetCookie[] }
@@ -128,6 +128,26 @@ export type PublicClientConfig = {
128
128
  scopes: string[]
129
129
  /** Must be `true` for public clients per OAuth 2.1 §2.1.1. */
130
130
  pkceRequired: true
131
+ /**
132
+ * Registered URIs to which `/end_session` may redirect after RP-initiated
133
+ * logout (OIDC RP-Initiated Logout 1.0 §2). Exact-match. If absent, the
134
+ * `/end_session` endpoint refuses any `post_logout_redirect_uri`.
135
+ */
136
+ postLogoutRedirectUris?: string[]
137
+ /**
138
+ * RFC 9126 §2: when `true`, the client MUST use Pushed Authorization
139
+ * Requests; a direct `GET /authorize` call without `request_uri` is
140
+ * rejected with `invalid_request`.
141
+ */
142
+ requirePushedAuthorizationRequests?: boolean
143
+ /**
144
+ * OIDC Core §8.1 — when set, the subject identifier (`sub`) is derived
145
+ * as a pairwise pseudonym keyed by this string. Two clients sharing
146
+ * the same `sectorIdentifier` will see the same `sub`; different
147
+ * values yield different `sub`s for the same end user. Absent =
148
+ * public subject (sub is identical across all RPs).
149
+ */
150
+ sectorIdentifier?: string
131
151
  /** Phase 8. */
132
152
  dpopRequired?: boolean
133
153
  }
@@ -146,6 +166,23 @@ export type ConfidentialClientConfig = {
146
166
  * strongly discouraged; OAuth 2.1 §2.1.1 recommends PKCE for every client.
147
167
  */
148
168
  pkceRequired: boolean
169
+ /**
170
+ * Registered URIs to which `/end_session` may redirect after RP-initiated
171
+ * logout (OIDC RP-Initiated Logout 1.0 §2). Exact-match.
172
+ */
173
+ postLogoutRedirectUris?: string[]
174
+ /**
175
+ * RFC 9126 §2: when `true`, the client MUST use Pushed Authorization
176
+ * Requests; a direct `GET /authorize` call without `request_uri` is
177
+ * rejected with `invalid_request`.
178
+ */
179
+ requirePushedAuthorizationRequests?: boolean
180
+ /**
181
+ * OIDC Core §8.1 — see `PublicClientConfig.sectorIdentifier` for
182
+ * semantics. Same field; duplicated on each branch because the
183
+ * `ClientConfig` discriminated union doesn't share optional fields.
184
+ */
185
+ sectorIdentifier?: string
149
186
  /** Phase 8. */
150
187
  dpopRequired?: boolean
151
188
  }