@_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
@@ -6,6 +6,7 @@
6
6
  * Per AD9, access tokens are JWTs (ES256 by default, Ed25519 optional per
7
7
  * AD11) and refresh tokens are opaque server-side records.
8
8
  */
9
+ import type { ClaimsRequest } from "./authorization";
9
10
  import type { SubjectClaim } from "./subject";
10
11
  import type { TenantId } from "./tenant";
11
12
  /**
@@ -39,9 +40,98 @@ export type CodePayload = {
39
40
  providerSubject: string;
40
41
  /** Typed-per-method properties from `MethodResult.success`. */
41
42
  properties: unknown;
43
+ /**
44
+ * Relying party's OIDC `nonce` (OIDC Core §3.1.2.1). Snapshotted from
45
+ * `FlowRecord.appNonce`. Echoed into the issued `id_token` when present.
46
+ */
47
+ appNonce?: string;
48
+ /**
49
+ * Seconds-since-epoch when end-user authentication completed
50
+ * (OIDC Core §2 `auth_time`). Stamped at the moment the method's
51
+ * `MethodResult.success` fires. Carried into the `id_token` and forward
52
+ * across refresh-token rotations.
53
+ */
54
+ authTime: number;
55
+ /** OIDC Core §5.5 — RP-requested claims; honored at id_token + /userinfo. */
56
+ claimsRequest?: ClaimsRequest;
42
57
  /** Auth-code TTL is 60 s — framework refuses anything longer. */
43
58
  expiresAt: number;
44
59
  };
60
+ /**
61
+ * OIDC Core §5.1 `address` claim. Structured object value rather than a
62
+ * scalar — distinct from the rest of the profile fields.
63
+ */
64
+ export type AddressClaim = {
65
+ formatted?: string;
66
+ street_address?: string;
67
+ locality?: string;
68
+ region?: string;
69
+ postal_code?: string;
70
+ country?: string;
71
+ };
72
+ /**
73
+ * OIDC Core §5.1 profile / email / phone / address claims, gated by the
74
+ * `profile` / `email` / `phone` / `address` scopes via §5.4. Reused by
75
+ * `IdTokenClaims` and the `/userinfo` response so the two surfaces share
76
+ * exactly one source of truth for scope→claim mapping.
77
+ *
78
+ * Every field is optional: presence depends on (a) which scopes were
79
+ * granted and (b) whether the host populated the matching key in
80
+ * `SubjectClaim.properties`.
81
+ */
82
+ export type ScopedProfileClaims = {
83
+ name?: string;
84
+ given_name?: string;
85
+ family_name?: string;
86
+ middle_name?: string;
87
+ nickname?: string;
88
+ preferred_username?: string;
89
+ profile?: string;
90
+ picture?: string;
91
+ website?: string;
92
+ gender?: string;
93
+ birthdate?: string;
94
+ zoneinfo?: string;
95
+ locale?: string;
96
+ updated_at?: number;
97
+ email?: string;
98
+ email_verified?: boolean;
99
+ phone_number?: string;
100
+ phone_number_verified?: boolean;
101
+ address?: AddressClaim;
102
+ };
103
+ /**
104
+ * OIDC `id_token` JWT claims this IdP issues at `/token` when `openid`
105
+ * scope is granted (OIDC Core §2). Distinct from access-token claims:
106
+ *
107
+ * - `aud` is the relying-party `client_id` (not the API audience).
108
+ * - `nonce` echoes the RP's `/authorize` `nonce` parameter when present.
109
+ * - `auth_time` is stable across refresh-token rotations (OIDC Core §12).
110
+ * - `at_hash` is recommended whenever an id_token and access_token are
111
+ * returned in the same response (§3.1.3.6).
112
+ *
113
+ * Standard OIDC claim names use snake_case on the wire; TypeScript field
114
+ * names match to keep the marshalling 1:1.
115
+ */
116
+ export type IdTokenClaims = ScopedProfileClaims & {
117
+ iss: string;
118
+ sub: string;
119
+ aud: string;
120
+ exp: number;
121
+ iat: number;
122
+ /** Seconds-since-epoch when end-user auth completed. */
123
+ auth_time?: number;
124
+ /** RP-supplied OIDC nonce, when present. MUST equal the `/authorize` value. */
125
+ nonce?: string;
126
+ /** Authentication Methods References (RFC 8176). */
127
+ amr?: string[];
128
+ /** Authentication Context Class Reference. */
129
+ acr?: string;
130
+ /** Authorized party — the client_id of the party the id_token is for. */
131
+ azp?: string;
132
+ /** Access-token hash (OIDC Core §3.1.3.6). */
133
+ at_hash?: string;
134
+ };
45
135
  /** Access-token JWT claims this IdP issues. */
46
136
  export type AccessTokenClaims = {
47
137
  iss: string;
@@ -56,10 +146,30 @@ export type AccessTokenClaims = {
56
146
  /** Factory kind that originated this token. */
57
147
  mkind?: string;
58
148
  scope?: string;
149
+ /**
150
+ * Seconds-since-epoch when the end-user originally authenticated
151
+ * (OIDC Core §2). Stable across refresh-token rotation and across
152
+ * RFC 8693 token-exchange (§12 — exchanging an audience does NOT
153
+ * re-authenticate the user). Absent on `client_credentials` grants
154
+ * where there is no end-user.
155
+ *
156
+ * Carried on the access token so RFC 8693 `subject_token` consumers
157
+ * can propagate it without needing the original id_token. Resource
158
+ * servers typically ignore it; it's there for the IdP's own
159
+ * downstream issuance.
160
+ */
161
+ auth_time?: number;
59
162
  /** DPoP confirmation claim (Phase 8). */
60
163
  cnf?: {
61
164
  jkt: string;
62
165
  };
166
+ /**
167
+ * OIDC Core §5.5 — list of claim names the RP requested for the
168
+ * `/userinfo` response via the `claims` parameter. The resource server
169
+ * surface uses this to bypass scope-gating per §5.5; it is empty /
170
+ * absent when the RP did not use the `claims` parameter.
171
+ */
172
+ uic?: string[];
63
173
  /** The structured subject (matches `SubjectClaim`). Inlined for resource servers. */
64
174
  claim: SubjectClaim;
65
175
  };
@@ -87,6 +197,24 @@ export type RefreshTokenPayload = {
87
197
  methodId: string;
88
198
  /** Factory kind (`MethodConfig.kind`) that originated the chain. */
89
199
  methodKind: string;
200
+ /**
201
+ * Seconds-since-epoch when end-user authentication originally completed
202
+ * (OIDC Core §2 `auth_time`). Stable across refresh-token rotations —
203
+ * `auth_time` does **not** advance on refresh, only on re-authentication.
204
+ */
205
+ authTime: number;
206
+ /**
207
+ * RFC 7638 JWK thumbprint of the client's DPoP key (RFC 9449 §6.1).
208
+ * Present when the original token grant was DPoP-bound. Refresh-grant
209
+ * rotation REQUIRES a fresh DPoP proof whose thumbprint matches; absent
210
+ * = the token is plain Bearer and Bearer is acceptable on refresh.
211
+ */
212
+ dpopJkt?: string;
213
+ /**
214
+ * OIDC Core §12 — the original RP-requested claims, preserved so
215
+ * refresh-grant id_token + /userinfo responses keep returning them.
216
+ */
217
+ claimsRequest?: ClaimsRequest;
90
218
  /** Wall-clock issuance and absolute-expiry timestamps. */
91
219
  issuedAt: number;
92
220
  expiresAt: number;
@@ -1 +1 @@
1
- {"version":3,"file":"token.d.ts","sourceRoot":"","sources":["../../../src/types/token.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,WAAW,CAAA;AAC7C,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAA;AAExC;;;;;;;GAOG;AACH,MAAM,MAAM,WAAW,GAAG;IACxB,QAAQ,EAAE,QAAQ,CAAA;IAClB,QAAQ,EAAE,MAAM,CAAA;IAChB,cAAc,EAAE,MAAM,CAAA;IACtB,6DAA6D;IAC7D,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,MAAM,EAAE,MAAM,EAAE,CAAA;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,gFAAgF;IAChF,UAAU,CAAC,EAAE;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAA;IAClD,2DAA2D;IAC3D,QAAQ,EAAE,MAAM,CAAA;IAChB,0CAA0C;IAC1C,UAAU,EAAE,MAAM,CAAA;IAClB,gDAAgD;IAChD,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAA;IACxC,yEAAyE;IACzE,eAAe,EAAE,MAAM,CAAA;IACvB,+DAA+D;IAC/D,UAAU,EAAE,OAAO,CAAA;IACnB,iEAAiE;IACjE,SAAS,EAAE,MAAM,CAAA;CAClB,CAAA;AAED,+CAA+C;AAC/C,MAAM,MAAM,iBAAiB,GAAG;IAC9B,GAAG,EAAE,MAAM,CAAA;IACX,GAAG,EAAE,MAAM,CAAA;IACX,GAAG,EAAE,MAAM,CAAA;IACX,GAAG,EAAE,MAAM,CAAA;IACX,GAAG,EAAE,MAAM,CAAA;IACX,iBAAiB;IACjB,GAAG,EAAE,QAAQ,CAAA;IACb,kEAAkE;IAClE,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,+CAA+C;IAC/C,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,yCAAyC;IACzC,GAAG,CAAC,EAAE;QAAE,GAAG,EAAE,MAAM,CAAA;KAAE,CAAA;IACrB,qFAAqF;IACrF,KAAK,EAAE,YAAY,CAAA;CACpB,CAAA;AAED;;;;GAIG;AACH,MAAM,MAAM,mBAAmB,GAAG;IAChC,QAAQ,EAAE,QAAQ,CAAA;IAClB,QAAQ,EAAE,MAAM,CAAA;IAChB,gEAAgE;IAChE,SAAS,EAAE,MAAM,CAAA;IACjB,KAAK,EAAE,YAAY,CAAA;IACnB,MAAM,EAAE,MAAM,EAAE,CAAA;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,kFAAkF;IAClF,MAAM,EAAE,MAAM,CAAA;IACd;;;;;OAKG;IACH,QAAQ,EAAE,MAAM,CAAA;IAChB,oEAAoE;IACpE,UAAU,EAAE,MAAM,CAAA;IAClB,0DAA0D;IAC1D,QAAQ,EAAE,MAAM,CAAA;IAChB,SAAS,EAAE,MAAM,CAAA;CAClB,CAAA;AAED,iDAAiD;AACjD,MAAM,MAAM,aAAa,GAAG;IAC1B,YAAY,EAAE,MAAM,CAAA;IACpB,UAAU,EAAE,QAAQ,GAAG,MAAM,CAAA;IAC7B,UAAU,EAAE,MAAM,CAAA;IAClB,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB,CAAA"}
1
+ {"version":3,"file":"token.d.ts","sourceRoot":"","sources":["../../../src/types/token.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@_mustachio/openauth",
3
- "version": "0.10.0",
3
+ "version": "0.12.0",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -37,6 +37,12 @@
37
37
  "import": "./dist/esm/adapters/*/index.js",
38
38
  "types": "./dist/types/adapters/*/index.d.ts"
39
39
  },
40
+ "./methods/saml-sp": {
41
+ "node": {
42
+ "import": "./dist/esm/methods/saml-sp/index.js",
43
+ "types": "./dist/types/methods/saml-sp/index.d.ts"
44
+ }
45
+ },
40
46
  "./*": {
41
47
  "import": "./dist/esm/*.js",
42
48
  "types": "./dist/types/*.d.ts"
@@ -73,8 +79,10 @@
73
79
  },
74
80
  "dependencies": {
75
81
  "@noble/hashes": "2.2.0",
82
+ "@node-saml/node-saml": "5.1.0",
76
83
  "@simplewebauthn/server": "13.1.1",
77
84
  "@standard-schema/spec": "1.0.0-beta.3",
85
+ "@xmldom/xmldom": "0.8.13",
78
86
  "aws4fetch": "1.0.20",
79
87
  "jose": "5.9.6",
80
88
  "oauth4webapi": "3.8.6",
@@ -67,6 +67,11 @@ export const INITIAL_SCHEMA_SQL = [
67
67
  payload TEXT NOT NULL,
68
68
  expires_at INTEGER NOT NULL
69
69
  )`,
70
+ `CREATE TABLE IF NOT EXISTS openauth_scratch (
71
+ scratch_key TEXT PRIMARY KEY,
72
+ value TEXT NOT NULL,
73
+ expires_at INTEGER NOT NULL
74
+ )`,
70
75
  `CREATE TABLE IF NOT EXISTS openauth_audit_events (
71
76
  id INTEGER PRIMARY KEY AUTOINCREMENT,
72
77
  ts INTEGER NOT NULL,
@@ -227,4 +227,72 @@ export class D1SessionStore implements SessionStore {
227
227
  }
228
228
  return ok(undefined)
229
229
  }
230
+
231
+ async saveScratch(
232
+ key: string,
233
+ value: string,
234
+ ttlMs: number,
235
+ ): Promise<Result<void>> {
236
+ if (ttlMs <= 0) {
237
+ return err(
238
+ authError.internalError(
239
+ `saveScratch: ttlMs must be positive, got ${ttlMs}`,
240
+ ),
241
+ )
242
+ }
243
+ const expiresAt = this.#clock() + ttlMs
244
+ try {
245
+ // Pinned to the primary — scratch backs InResponseTo replay
246
+ // protection, which is read-after-write sensitive at the ACS.
247
+ await primarySession(this.#db)
248
+ .prepare(
249
+ `INSERT INTO openauth_scratch (scratch_key, value, expires_at) VALUES (?1, ?2, ?3)
250
+ ON CONFLICT(scratch_key) DO UPDATE SET value = excluded.value, expires_at = excluded.expires_at`,
251
+ )
252
+ .bind(key, value, expiresAt)
253
+ .run()
254
+ } catch (e) {
255
+ return err(authError.internalError("saveScratch: upsert failed", e))
256
+ }
257
+ return ok(undefined)
258
+ }
259
+
260
+ async readScratch(key: string): Promise<Result<string>> {
261
+ let row: { value: string; expires_at: number } | null
262
+ try {
263
+ row = await primarySession(this.#db)
264
+ .prepare(
265
+ `SELECT value, expires_at FROM openauth_scratch WHERE scratch_key = ?1`,
266
+ )
267
+ .bind(key)
268
+ .first<{ value: string; expires_at: number }>()
269
+ } catch (e) {
270
+ return err(authError.internalError("readScratch: query failed", e))
271
+ }
272
+ if (!row) {
273
+ return err(authError.unknownState(`scratch "${key}" unknown`))
274
+ }
275
+ if (this.#clock() >= Number(row.expires_at)) {
276
+ try {
277
+ await primarySession(this.#db)
278
+ .prepare(`DELETE FROM openauth_scratch WHERE scratch_key = ?1`)
279
+ .bind(key)
280
+ .run()
281
+ } catch {}
282
+ return err(authError.unknownState(`scratch "${key}" expired`))
283
+ }
284
+ return ok(row.value)
285
+ }
286
+
287
+ async deleteScratch(key: string): Promise<Result<void>> {
288
+ try {
289
+ await primarySession(this.#db)
290
+ .prepare(`DELETE FROM openauth_scratch WHERE scratch_key = ?1`)
291
+ .bind(key)
292
+ .run()
293
+ } catch (e) {
294
+ return err(authError.internalError("deleteScratch: delete failed", e))
295
+ }
296
+ return ok(undefined)
297
+ }
230
298
  }
@@ -51,6 +51,11 @@ type StoredSession = {
51
51
  expiresAt: number
52
52
  }
53
53
 
54
+ type StoredScratch = {
55
+ value: string
56
+ expiresAt: number
57
+ }
58
+
54
59
  export type DurableObjectSessionStoreOptions = {
55
60
  storage: DurableObjectStorageLike
56
61
  clock?: () => number
@@ -218,10 +223,66 @@ export class DurableObjectSessionStore implements SessionStore {
218
223
  }
219
224
  return ok(undefined)
220
225
  }
226
+
227
+ async saveScratch(
228
+ key: string,
229
+ value: string,
230
+ ttlMs: number,
231
+ ): Promise<Result<void>> {
232
+ if (ttlMs <= 0) {
233
+ return err(
234
+ authError.internalError(
235
+ `saveScratch: ttlMs must be positive, got ${ttlMs}`,
236
+ ),
237
+ )
238
+ }
239
+ const stored: StoredScratch = {
240
+ value,
241
+ expiresAt: this.#clock() + ttlMs,
242
+ }
243
+ try {
244
+ // Plain put — scratch overwrites any prior value for the key. No
245
+ // transaction needed: it is a single write, and the DO serializes
246
+ // all operations on this instance.
247
+ await this.#storage.put(scratchKey(key), stored)
248
+ } catch (e) {
249
+ return err(authError.internalError("saveScratch: put failed", e))
250
+ }
251
+ return ok(undefined)
252
+ }
253
+
254
+ async readScratch(key: string): Promise<Result<string>> {
255
+ let stored: StoredScratch | undefined
256
+ try {
257
+ stored = await this.#storage.get<StoredScratch>(scratchKey(key))
258
+ } catch (e) {
259
+ return err(authError.internalError("readScratch: get failed", e))
260
+ }
261
+ if (!stored) {
262
+ return err(authError.unknownState(`scratch "${key}" unknown`))
263
+ }
264
+ if (this.#clock() >= stored.expiresAt) {
265
+ try {
266
+ await this.#storage.delete(scratchKey(key))
267
+ } catch {}
268
+ return err(authError.unknownState(`scratch "${key}" expired`))
269
+ }
270
+ return ok(stored.value)
271
+ }
272
+
273
+ async deleteScratch(key: string): Promise<Result<void>> {
274
+ try {
275
+ await this.#storage.delete(scratchKey(key))
276
+ } catch (e) {
277
+ return err(authError.internalError("deleteScratch: delete failed", e))
278
+ }
279
+ return ok(undefined)
280
+ }
221
281
  }
222
282
 
223
283
  const flowKey = (flowId: string) => `flow:${flowId}`
224
284
  const sessionKey = (sessionId: string) => `session:${sessionId}`
285
+ const scratchKey = (key: string) => `scratch:${key}`
225
286
 
226
287
  // Re-exported for adapter users wiring `storage.transaction` callbacks.
227
288
  export type { DurableObjectStorageLike, DurableObjectTransactionLike }
@@ -211,6 +211,69 @@ export class DynamoSessionStore implements SessionStore {
211
211
  }
212
212
  return ok(undefined)
213
213
  }
214
+
215
+ async saveScratch(
216
+ key: string,
217
+ value: string,
218
+ ttlMs: number,
219
+ ): Promise<Result<void>> {
220
+ if (ttlMs <= 0) {
221
+ return err(
222
+ authError.internalError(
223
+ `saveScratch: ttlMs must be positive, got ${ttlMs}`,
224
+ ),
225
+ )
226
+ }
227
+ const expiresAtMs = this.#clock() + ttlMs
228
+ try {
229
+ // Unconditional put — scratch overwrites any prior value for the key.
230
+ await this.#exec.put({
231
+ item: {
232
+ pk: "scratch",
233
+ sk: key,
234
+ value,
235
+ expires_at: expiresAtMs,
236
+ ttl: Math.floor(expiresAtMs / 1000),
237
+ },
238
+ })
239
+ } catch (e) {
240
+ return err(authError.internalError("saveScratch: put failed", e))
241
+ }
242
+ return ok(undefined)
243
+ }
244
+
245
+ async readScratch(key: string): Promise<Result<string>> {
246
+ let row: Record<string, unknown> | undefined
247
+ try {
248
+ row = await this.#exec.get({
249
+ key: { pk: "scratch", sk: key },
250
+ consistentRead: true,
251
+ })
252
+ } catch (e) {
253
+ return err(authError.internalError("readScratch: get failed", e))
254
+ }
255
+ if (!row) {
256
+ return err(authError.unknownState(`scratch "${key}" unknown`))
257
+ }
258
+ // Native DynamoDB TTL eviction is best-effort (can lag up to ~48h), so
259
+ // filter on the adapter clock for correctness.
260
+ if (this.#clock() >= Number(row.expires_at)) {
261
+ try {
262
+ await this.#exec.delete({ key: { pk: "scratch", sk: key } })
263
+ } catch {}
264
+ return err(authError.unknownState(`scratch "${key}" expired`))
265
+ }
266
+ return ok(String(row.value))
267
+ }
268
+
269
+ async deleteScratch(key: string): Promise<Result<void>> {
270
+ try {
271
+ await this.#exec.delete({ key: { pk: "scratch", sk: key } })
272
+ } catch (e) {
273
+ return err(authError.internalError("deleteScratch: delete failed", e))
274
+ }
275
+ return ok(undefined)
276
+ }
214
277
  }
215
278
 
216
279
  function parseFlowPayload(raw: unknown): FlowRecord {
@@ -8,7 +8,11 @@
8
8
  */
9
9
  import { authError } from "../../types/error"
10
10
  import type { FlowRecord } from "../../types/flow"
11
- import type { SessionRecord, SessionStore } from "../../ports/session-store"
11
+ import type {
12
+ ParRecord,
13
+ SessionRecord,
14
+ SessionStore,
15
+ } from "../../ports/session-store"
12
16
  import type { Result } from "../../types/result"
13
17
  import { err, ok } from "../../types/result"
14
18
  import type { Clock } from "./clock"
@@ -19,6 +23,16 @@ type StoredFlow = {
19
23
  expiresAt: number
20
24
  }
21
25
 
26
+ type StoredPar = {
27
+ record: ParRecord
28
+ expiresAt: number
29
+ }
30
+
31
+ type StoredScratch = {
32
+ value: string
33
+ expiresAt: number
34
+ }
35
+
22
36
  export type MemorySessionStoreOptions = {
23
37
  clock?: Clock
24
38
  }
@@ -27,6 +41,8 @@ export class MemorySessionStore implements SessionStore {
27
41
  #clock: Clock
28
42
  #flows = new Map<string, StoredFlow>()
29
43
  #sessions = new Map<string, SessionRecord>()
44
+ #par = new Map<string, StoredPar>()
45
+ #scratch = new Map<string, StoredScratch>()
30
46
 
31
47
  constructor(opts: MemorySessionStoreOptions = {}) {
32
48
  this.#clock = opts.clock ?? realClock
@@ -125,4 +141,70 @@ export class MemorySessionStore implements SessionStore {
125
141
  this.#sessions.delete(sessionId)
126
142
  return ok(undefined)
127
143
  }
144
+
145
+ async savePar(
146
+ requestUri: string,
147
+ payload: ParRecord,
148
+ ttl: number,
149
+ ): Promise<Result<void>> {
150
+ if (ttl <= 0) {
151
+ return err(
152
+ authError.internalError(`savePar: ttl must be positive, got ${ttl}`),
153
+ )
154
+ }
155
+ this.#par.set(requestUri, {
156
+ record: payload,
157
+ expiresAt: this.#clock() + ttl,
158
+ })
159
+ return ok(undefined)
160
+ }
161
+
162
+ async consumePar(requestUri: string): Promise<Result<ParRecord>> {
163
+ const stored = this.#par.get(requestUri)
164
+ if (!stored) {
165
+ return err(
166
+ authError.unknownState(
167
+ `par "${requestUri}" unknown or already consumed`,
168
+ ),
169
+ )
170
+ }
171
+ this.#par.delete(requestUri)
172
+ if (this.#clock() >= stored.expiresAt) {
173
+ return err(authError.unknownState(`par "${requestUri}" expired`))
174
+ }
175
+ return ok(stored.record)
176
+ }
177
+
178
+ async saveScratch(
179
+ key: string,
180
+ value: string,
181
+ ttlMs: number,
182
+ ): Promise<Result<void>> {
183
+ if (ttlMs <= 0) {
184
+ return err(
185
+ authError.internalError(
186
+ `saveScratch: ttlMs must be positive, got ${ttlMs}`,
187
+ ),
188
+ )
189
+ }
190
+ this.#scratch.set(key, { value, expiresAt: this.#clock() + ttlMs })
191
+ return ok(undefined)
192
+ }
193
+
194
+ async readScratch(key: string): Promise<Result<string>> {
195
+ const stored = this.#scratch.get(key)
196
+ if (!stored) {
197
+ return err(authError.unknownState(`scratch "${key}" unknown`))
198
+ }
199
+ if (this.#clock() >= stored.expiresAt) {
200
+ this.#scratch.delete(key)
201
+ return err(authError.unknownState(`scratch "${key}" expired`))
202
+ }
203
+ return ok(stored.value)
204
+ }
205
+
206
+ async deleteScratch(key: string): Promise<Result<void>> {
207
+ this.#scratch.delete(key)
208
+ return ok(undefined)
209
+ }
128
210
  }
@@ -43,6 +43,8 @@ export class MemoryTokenStore implements TokenStore {
43
43
  #clock: Clock
44
44
  #codes = new Map<string, StoredCode>()
45
45
  #refresh = new Map<string, StoredRefresh>()
46
+ /** DPoP proof `jti` replay-protection map. Value is expiry timestamp. */
47
+ #dpopJtis = new Map<string, number>()
46
48
 
47
49
  constructor(opts: MemoryTokenStoreOptions = {}) {
48
50
  this.#clock = opts.clock ?? realClock
@@ -162,4 +164,18 @@ export class MemoryTokenStore implements TokenStore {
162
164
  }
163
165
  return ok(undefined)
164
166
  }
167
+
168
+ async recordDpopJti(jti: string, ttlMs: number): Promise<Result<void>> {
169
+ const now = this.#clock()
170
+ // Sweep expired entries opportunistically — keeps the map bounded
171
+ // under sustained load without a separate GC pass.
172
+ for (const [k, exp] of this.#dpopJtis) {
173
+ if (exp <= now) this.#dpopJtis.delete(k)
174
+ }
175
+ if (this.#dpopJtis.has(jti)) {
176
+ return err(authError.invalidGrant(`dpop jti "${jti}" replayed`))
177
+ }
178
+ this.#dpopJtis.set(jti, now + ttlMs)
179
+ return ok(undefined)
180
+ }
165
181
  }
@@ -89,6 +89,12 @@ CREATE TABLE IF NOT EXISTS openauth_sessions (
89
89
  expires_at bigint NOT NULL
90
90
  );
91
91
 
92
+ CREATE TABLE IF NOT EXISTS openauth_scratch (
93
+ scratch_key text PRIMARY KEY,
94
+ value text NOT NULL,
95
+ expires_at bigint NOT NULL
96
+ );
97
+
92
98
  CREATE TABLE IF NOT EXISTS openauth_audit_events (
93
99
  id bigserial PRIMARY KEY,
94
100
  ts bigint NOT NULL,
@@ -203,6 +203,75 @@ export class PostgresSessionStore implements SessionStore {
203
203
  }
204
204
  return ok(undefined)
205
205
  }
206
+
207
+ async saveScratch(
208
+ key: string,
209
+ value: string,
210
+ ttlMs: number,
211
+ ): Promise<Result<void>> {
212
+ if (ttlMs <= 0) {
213
+ return err(
214
+ authError.internalError(
215
+ `saveScratch: ttlMs must be positive, got ${ttlMs}`,
216
+ ),
217
+ )
218
+ }
219
+ const expiresAt = this.#clock() + ttlMs
220
+ try {
221
+ await this.#exec.query(
222
+ `INSERT INTO openauth_scratch (scratch_key, value, expires_at)
223
+ VALUES ($1, $2, $3)
224
+ ON CONFLICT (scratch_key)
225
+ DO UPDATE SET value = EXCLUDED.value, expires_at = EXCLUDED.expires_at`,
226
+ [key, value, expiresAt],
227
+ )
228
+ } catch (e) {
229
+ return err(authError.internalError("saveScratch: upsert failed", e))
230
+ }
231
+ return ok(undefined)
232
+ }
233
+
234
+ async readScratch(key: string): Promise<Result<string>> {
235
+ let row: { value: string; expires_at: string | number } | undefined
236
+ try {
237
+ const result = await this.#exec.query<{
238
+ value: string
239
+ expires_at: string | number
240
+ }>(
241
+ `SELECT value, expires_at FROM openauth_scratch WHERE scratch_key = $1`,
242
+ [key],
243
+ )
244
+ row = result.rows[0]
245
+ } catch (e) {
246
+ return err(authError.internalError("readScratch: query failed", e))
247
+ }
248
+ if (!row) {
249
+ return err(authError.unknownState(`scratch "${key}" unknown`))
250
+ }
251
+ if (this.#clock() >= Number(row.expires_at)) {
252
+ // Lazy GC — best-effort, don't fail the read if the delete races.
253
+ try {
254
+ await this.#exec.query(
255
+ `DELETE FROM openauth_scratch WHERE scratch_key = $1`,
256
+ [key],
257
+ )
258
+ } catch {}
259
+ return err(authError.unknownState(`scratch "${key}" expired`))
260
+ }
261
+ return ok(row.value)
262
+ }
263
+
264
+ async deleteScratch(key: string): Promise<Result<void>> {
265
+ try {
266
+ await this.#exec.query(
267
+ `DELETE FROM openauth_scratch WHERE scratch_key = $1`,
268
+ [key],
269
+ )
270
+ } catch (e) {
271
+ return err(authError.internalError("deleteScratch: delete failed", e))
272
+ }
273
+ return ok(undefined)
274
+ }
206
275
  }
207
276
 
208
277
  function parseFlowRecord(raw: unknown): FlowRecord {