@_mustachio/openauth 0.11.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 (164) 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 +23 -0
  13. package/dist/esm/adapters/postgres/executor.js +2 -2
  14. package/dist/esm/adapters/postgres/index.js +33 -10
  15. package/dist/esm/adapters/postgres/migrations.js +8 -2
  16. package/dist/esm/adapters/postgres/session-store.js +42 -0
  17. package/dist/esm/client.js +27 -27
  18. package/dist/esm/domain/authorize.js +2 -2
  19. package/dist/esm/domain/callback.js +121 -7
  20. package/dist/esm/domain/client-auth.js +2 -2
  21. package/dist/esm/domain/crypto.js +14 -14
  22. package/dist/esm/domain/discovery.js +2 -2
  23. package/dist/esm/domain/dpop.js +4 -4
  24. package/dist/esm/domain/id-token.js +5 -5
  25. package/dist/esm/domain/jwt.js +4 -4
  26. package/dist/esm/domain/method-dispatch.js +27 -1
  27. package/dist/esm/domain/method-route.js +80 -1
  28. package/dist/esm/domain/par.js +2 -2
  29. package/dist/esm/domain/pkce.js +2 -2
  30. package/dist/esm/domain/revoke.js +2 -2
  31. package/dist/esm/domain/state-envelope.js +2 -2
  32. package/dist/esm/domain/token.js +5 -5
  33. package/dist/esm/error.js +8 -8
  34. package/dist/esm/http/cookies.js +19 -2
  35. package/dist/esm/http/errors.js +4 -4
  36. package/dist/esm/http/handlers/authorize.js +3 -19
  37. package/dist/esm/http/handlers/callback.js +7 -1
  38. package/dist/esm/http/handlers/metadata.js +2 -2
  39. package/dist/esm/http/handlers/method-route.js +47 -2
  40. package/dist/esm/http/handlers/revocation.js +2 -2
  41. package/dist/esm/http/middleware/tenant.js +6 -6
  42. package/dist/esm/http/schemas/authorize.js +2 -2
  43. package/dist/esm/http/schemas/revocation.js +2 -2
  44. package/dist/esm/http/schemas/token.js +7 -7
  45. package/dist/esm/index.js +33 -32
  46. package/dist/esm/methods/oauth2-factory.js +2 -2
  47. package/dist/esm/methods/password-hash.js +2 -2
  48. package/dist/esm/methods/providers/index.js +30 -14
  49. package/dist/esm/methods/saml-sp/acs.js +208 -0
  50. package/dist/esm/methods/saml-sp/attributes.js +75 -0
  51. package/dist/esm/methods/saml-sp/authnrequest.js +63 -0
  52. package/dist/esm/methods/saml-sp/cache-provider.js +33 -0
  53. package/dist/esm/methods/saml-sp/cert-rotation.js +13 -0
  54. package/dist/esm/methods/saml-sp/factory.js +70 -0
  55. package/dist/esm/methods/saml-sp/index.js +7 -0
  56. package/dist/esm/methods/saml-sp/metadata.js +69 -0
  57. package/dist/esm/methods/saml-sp/method.js +29 -0
  58. package/dist/esm/methods/saml-sp/parse-idp-metadata.js +102 -0
  59. package/dist/esm/methods/saml-sp/saml-instance.js +49 -0
  60. package/dist/esm/methods/saml-sp/slo-initiate.js +84 -0
  61. package/dist/esm/methods/saml-sp/sls.js +154 -0
  62. package/dist/esm/methods/saml-sp/types.js +0 -0
  63. package/dist/esm/types/result.js +3 -3
  64. package/dist/esm/ui/forms.js +3 -3
  65. package/dist/esm/ui/index.js +8 -5
  66. package/dist/esm/ui/picker.js +2 -2
  67. package/dist/types/adapters/d1/migrations.d.ts.map +1 -1
  68. package/dist/types/adapters/d1/session-store.d.ts +3 -0
  69. package/dist/types/adapters/d1/session-store.d.ts.map +1 -1
  70. package/dist/types/adapters/durable-object/session-store.d.ts +3 -0
  71. package/dist/types/adapters/durable-object/session-store.d.ts.map +1 -1
  72. package/dist/types/adapters/dynamo/session-store.d.ts +3 -0
  73. package/dist/types/adapters/dynamo/session-store.d.ts.map +1 -1
  74. package/dist/types/adapters/memory/session-store.d.ts +3 -0
  75. package/dist/types/adapters/memory/session-store.d.ts.map +1 -1
  76. package/dist/types/adapters/postgres/migrations.d.ts +1 -1
  77. package/dist/types/adapters/postgres/migrations.d.ts.map +1 -1
  78. package/dist/types/adapters/postgres/session-store.d.ts +3 -0
  79. package/dist/types/adapters/postgres/session-store.d.ts.map +1 -1
  80. package/dist/types/domain/callback.d.ts +16 -1
  81. package/dist/types/domain/callback.d.ts.map +1 -1
  82. package/dist/types/domain/method-dispatch.d.ts.map +1 -1
  83. package/dist/types/domain/method-route.d.ts +57 -1
  84. package/dist/types/domain/method-route.d.ts.map +1 -1
  85. package/dist/types/http/context.d.ts +3 -1
  86. package/dist/types/http/context.d.ts.map +1 -1
  87. package/dist/types/http/cookies.d.ts +9 -1
  88. package/dist/types/http/cookies.d.ts.map +1 -1
  89. package/dist/types/http/handlers/callback.d.ts.map +1 -1
  90. package/dist/types/http/handlers/method-route.d.ts.map +1 -1
  91. package/dist/types/index.d.ts +1 -1
  92. package/dist/types/index.d.ts.map +1 -1
  93. package/dist/types/methods/saml-sp/acs.d.ts +4 -0
  94. package/dist/types/methods/saml-sp/acs.d.ts.map +1 -0
  95. package/dist/types/methods/saml-sp/attributes.d.ts +40 -0
  96. package/dist/types/methods/saml-sp/attributes.d.ts.map +1 -0
  97. package/dist/types/methods/saml-sp/authnrequest.d.ts +4 -0
  98. package/dist/types/methods/saml-sp/authnrequest.d.ts.map +1 -0
  99. package/dist/types/methods/saml-sp/cache-provider.d.ts +38 -0
  100. package/dist/types/methods/saml-sp/cache-provider.d.ts.map +1 -0
  101. package/dist/types/methods/saml-sp/cert-rotation.d.ts +23 -0
  102. package/dist/types/methods/saml-sp/cert-rotation.d.ts.map +1 -0
  103. package/dist/types/methods/saml-sp/factory.d.ts +704 -0
  104. package/dist/types/methods/saml-sp/factory.d.ts.map +1 -0
  105. package/dist/types/methods/saml-sp/index.d.ts +24 -0
  106. package/dist/types/methods/saml-sp/index.d.ts.map +1 -0
  107. package/dist/types/methods/saml-sp/metadata.d.ts +42 -0
  108. package/dist/types/methods/saml-sp/metadata.d.ts.map +1 -0
  109. package/dist/types/methods/saml-sp/method.d.ts +17 -0
  110. package/dist/types/methods/saml-sp/method.d.ts.map +1 -0
  111. package/dist/types/methods/saml-sp/parse-idp-metadata.d.ts +4 -0
  112. package/dist/types/methods/saml-sp/parse-idp-metadata.d.ts.map +1 -0
  113. package/dist/types/methods/saml-sp/saml-instance.d.ts +81 -0
  114. package/dist/types/methods/saml-sp/saml-instance.d.ts.map +1 -0
  115. package/dist/types/methods/saml-sp/slo-initiate.d.ts +4 -0
  116. package/dist/types/methods/saml-sp/slo-initiate.d.ts.map +1 -0
  117. package/dist/types/methods/saml-sp/sls.d.ts +4 -0
  118. package/dist/types/methods/saml-sp/sls.d.ts.map +1 -0
  119. package/dist/types/methods/saml-sp/types.d.ts +204 -0
  120. package/dist/types/methods/saml-sp/types.d.ts.map +1 -0
  121. package/dist/types/ports/audit-log.d.ts +18 -5
  122. package/dist/types/ports/audit-log.d.ts.map +1 -1
  123. package/dist/types/ports/session-store.d.ts +23 -0
  124. package/dist/types/ports/session-store.d.ts.map +1 -1
  125. package/dist/types/types/idp.d.ts +71 -0
  126. package/dist/types/types/idp.d.ts.map +1 -1
  127. package/dist/types/types/method.d.ts +109 -0
  128. package/dist/types/types/method.d.ts.map +1 -1
  129. package/package.json +9 -1
  130. package/src/adapters/d1/migrations.ts +5 -0
  131. package/src/adapters/d1/session-store.ts +68 -0
  132. package/src/adapters/durable-object/session-store.ts +61 -0
  133. package/src/adapters/dynamo/session-store.ts +63 -0
  134. package/src/adapters/memory/session-store.ts +39 -0
  135. package/src/adapters/postgres/migrations.ts +6 -0
  136. package/src/adapters/postgres/session-store.ts +69 -0
  137. package/src/domain/callback.ts +215 -7
  138. package/src/domain/method-dispatch.ts +53 -1
  139. package/src/domain/method-route.ts +190 -1
  140. package/src/http/context.ts +6 -0
  141. package/src/http/cookies.ts +19 -1
  142. package/src/http/handlers/authorize.ts +2 -21
  143. package/src/http/handlers/callback.ts +9 -1
  144. package/src/http/handlers/method-route.ts +75 -2
  145. package/src/index.ts +3 -0
  146. package/src/methods/saml-sp/acs.ts +399 -0
  147. package/src/methods/saml-sp/attributes.ts +137 -0
  148. package/src/methods/saml-sp/authnrequest.ts +128 -0
  149. package/src/methods/saml-sp/cache-provider.ts +76 -0
  150. package/src/methods/saml-sp/cert-rotation.ts +34 -0
  151. package/src/methods/saml-sp/factory.ts +118 -0
  152. package/src/methods/saml-sp/index.ts +34 -0
  153. package/src/methods/saml-sp/metadata.ts +204 -0
  154. package/src/methods/saml-sp/method.ts +65 -0
  155. package/src/methods/saml-sp/parse-idp-metadata.ts +164 -0
  156. package/src/methods/saml-sp/saml-instance.ts +173 -0
  157. package/src/methods/saml-sp/slo-initiate.ts +177 -0
  158. package/src/methods/saml-sp/sls.ts +306 -0
  159. package/src/methods/saml-sp/types.ts +203 -0
  160. package/src/ports/CONSISTENCY.md +25 -0
  161. package/src/ports/audit-log.ts +18 -5
  162. package/src/ports/session-store.ts +28 -0
  163. package/src/types/idp.ts +74 -0
  164. package/src/types/method.ts +110 -0
package/src/types/idp.ts CHANGED
@@ -69,6 +69,56 @@ export type FailureEvent = {
69
69
  error: AuthError
70
70
  }
71
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
+
72
122
  /**
73
123
  * Optional hook called at `/token` time, after PKCE has succeeded and
74
124
  * after the `success` callback has produced a `SubjectClaim`, but
@@ -259,6 +309,30 @@ export type IdPOptions = {
259
309
  onFailure?: (event: FailureEvent) => Promise<void>
260
310
  }
261
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
+
262
336
  /**
263
337
  * Optional escape hatch for high-sensitivity deployments — see the
264
338
  * `PersistUpstreamTokens` type doc.
@@ -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[] }