@_mustachio/openauth 0.11.0 → 0.13.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 (208) 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/scim/discovery.js +206 -0
  32. package/dist/esm/domain/scim/filter.js +177 -0
  33. package/dist/esm/domain/scim/handle.js +338 -0
  34. package/dist/esm/domain/scim/patch.js +568 -0
  35. package/dist/esm/domain/scim/resource.js +256 -0
  36. package/dist/esm/domain/state-envelope.js +2 -2
  37. package/dist/esm/domain/token.js +5 -5
  38. package/dist/esm/error.js +8 -8
  39. package/dist/esm/http/cookies.js +19 -2
  40. package/dist/esm/http/errors.js +4 -4
  41. package/dist/esm/http/handlers/authorize.js +3 -19
  42. package/dist/esm/http/handlers/callback.js +7 -1
  43. package/dist/esm/http/handlers/metadata.js +2 -2
  44. package/dist/esm/http/handlers/method-route.js +47 -2
  45. package/dist/esm/http/handlers/revocation.js +2 -2
  46. package/dist/esm/http/handlers/scim.js +59 -0
  47. package/dist/esm/http/middleware/tenant.js +31 -6
  48. package/dist/esm/http/router.js +3 -0
  49. package/dist/esm/http/schemas/authorize.js +2 -2
  50. package/dist/esm/http/schemas/revocation.js +2 -2
  51. package/dist/esm/http/schemas/token.js +7 -7
  52. package/dist/esm/index.js +34 -32
  53. package/dist/esm/methods/oauth2-factory.js +2 -2
  54. package/dist/esm/methods/password-hash.js +2 -2
  55. package/dist/esm/methods/providers/index.js +30 -14
  56. package/dist/esm/methods/saml-sp/acs.js +232 -0
  57. package/dist/esm/methods/saml-sp/attributes.js +77 -0
  58. package/dist/esm/methods/saml-sp/authnrequest.js +63 -0
  59. package/dist/esm/methods/saml-sp/cache-provider.js +33 -0
  60. package/dist/esm/methods/saml-sp/cert-rotation.js +13 -0
  61. package/dist/esm/methods/saml-sp/factory.js +81 -0
  62. package/dist/esm/methods/saml-sp/index.js +7 -0
  63. package/dist/esm/methods/saml-sp/metadata.js +71 -0
  64. package/dist/esm/methods/saml-sp/method.js +29 -0
  65. package/dist/esm/methods/saml-sp/parse-idp-metadata.js +102 -0
  66. package/dist/esm/methods/saml-sp/saml-instance.js +59 -0
  67. package/dist/esm/methods/saml-sp/slo-initiate.js +84 -0
  68. package/dist/esm/methods/saml-sp/sls.js +154 -0
  69. package/dist/esm/methods/saml-sp/types.js +0 -0
  70. package/dist/esm/ports/scim-directory.js +0 -0
  71. package/dist/esm/types/error.js +5 -0
  72. package/dist/esm/types/result.js +3 -3
  73. package/dist/esm/types/scim.js +0 -0
  74. package/dist/esm/ui/forms.js +3 -3
  75. package/dist/esm/ui/index.js +8 -5
  76. package/dist/esm/ui/picker.js +2 -2
  77. package/dist/types/adapters/d1/migrations.d.ts.map +1 -1
  78. package/dist/types/adapters/d1/session-store.d.ts +3 -0
  79. package/dist/types/adapters/d1/session-store.d.ts.map +1 -1
  80. package/dist/types/adapters/durable-object/session-store.d.ts +3 -0
  81. package/dist/types/adapters/durable-object/session-store.d.ts.map +1 -1
  82. package/dist/types/adapters/dynamo/session-store.d.ts +3 -0
  83. package/dist/types/adapters/dynamo/session-store.d.ts.map +1 -1
  84. package/dist/types/adapters/memory/session-store.d.ts +3 -0
  85. package/dist/types/adapters/memory/session-store.d.ts.map +1 -1
  86. package/dist/types/adapters/postgres/migrations.d.ts +1 -1
  87. package/dist/types/adapters/postgres/migrations.d.ts.map +1 -1
  88. package/dist/types/adapters/postgres/session-store.d.ts +3 -0
  89. package/dist/types/adapters/postgres/session-store.d.ts.map +1 -1
  90. package/dist/types/domain/callback.d.ts +16 -1
  91. package/dist/types/domain/callback.d.ts.map +1 -1
  92. package/dist/types/domain/method-dispatch.d.ts.map +1 -1
  93. package/dist/types/domain/method-route.d.ts +57 -1
  94. package/dist/types/domain/method-route.d.ts.map +1 -1
  95. package/dist/types/domain/scim/discovery.d.ts +11 -0
  96. package/dist/types/domain/scim/discovery.d.ts.map +1 -0
  97. package/dist/types/domain/scim/filter.d.ts +51 -0
  98. package/dist/types/domain/scim/filter.d.ts.map +1 -0
  99. package/dist/types/domain/scim/handle.d.ts +35 -0
  100. package/dist/types/domain/scim/handle.d.ts.map +1 -0
  101. package/dist/types/domain/scim/patch.d.ts +73 -0
  102. package/dist/types/domain/scim/patch.d.ts.map +1 -0
  103. package/dist/types/domain/scim/resource.d.ts +56 -0
  104. package/dist/types/domain/scim/resource.d.ts.map +1 -0
  105. package/dist/types/http/context.d.ts +6 -1
  106. package/dist/types/http/context.d.ts.map +1 -1
  107. package/dist/types/http/cookies.d.ts +9 -1
  108. package/dist/types/http/cookies.d.ts.map +1 -1
  109. package/dist/types/http/handlers/callback.d.ts.map +1 -1
  110. package/dist/types/http/handlers/method-route.d.ts.map +1 -1
  111. package/dist/types/http/handlers/scim.d.ts +3 -0
  112. package/dist/types/http/handlers/scim.d.ts.map +1 -0
  113. package/dist/types/http/middleware/tenant.d.ts.map +1 -1
  114. package/dist/types/http/router.d.ts.map +1 -1
  115. package/dist/types/index.d.ts +3 -1
  116. package/dist/types/index.d.ts.map +1 -1
  117. package/dist/types/methods/saml-sp/acs.d.ts +4 -0
  118. package/dist/types/methods/saml-sp/acs.d.ts.map +1 -0
  119. package/dist/types/methods/saml-sp/attributes.d.ts +45 -0
  120. package/dist/types/methods/saml-sp/attributes.d.ts.map +1 -0
  121. package/dist/types/methods/saml-sp/authnrequest.d.ts +4 -0
  122. package/dist/types/methods/saml-sp/authnrequest.d.ts.map +1 -0
  123. package/dist/types/methods/saml-sp/cache-provider.d.ts +38 -0
  124. package/dist/types/methods/saml-sp/cache-provider.d.ts.map +1 -0
  125. package/dist/types/methods/saml-sp/cert-rotation.d.ts +23 -0
  126. package/dist/types/methods/saml-sp/cert-rotation.d.ts.map +1 -0
  127. package/dist/types/methods/saml-sp/factory.d.ts +920 -0
  128. package/dist/types/methods/saml-sp/factory.d.ts.map +1 -0
  129. package/dist/types/methods/saml-sp/index.d.ts +24 -0
  130. package/dist/types/methods/saml-sp/index.d.ts.map +1 -0
  131. package/dist/types/methods/saml-sp/metadata.d.ts +50 -0
  132. package/dist/types/methods/saml-sp/metadata.d.ts.map +1 -0
  133. package/dist/types/methods/saml-sp/method.d.ts +17 -0
  134. package/dist/types/methods/saml-sp/method.d.ts.map +1 -0
  135. package/dist/types/methods/saml-sp/parse-idp-metadata.d.ts +4 -0
  136. package/dist/types/methods/saml-sp/parse-idp-metadata.d.ts.map +1 -0
  137. package/dist/types/methods/saml-sp/saml-instance.d.ts +93 -0
  138. package/dist/types/methods/saml-sp/saml-instance.d.ts.map +1 -0
  139. package/dist/types/methods/saml-sp/slo-initiate.d.ts +4 -0
  140. package/dist/types/methods/saml-sp/slo-initiate.d.ts.map +1 -0
  141. package/dist/types/methods/saml-sp/sls.d.ts +4 -0
  142. package/dist/types/methods/saml-sp/sls.d.ts.map +1 -0
  143. package/dist/types/methods/saml-sp/types.d.ts +299 -0
  144. package/dist/types/methods/saml-sp/types.d.ts.map +1 -0
  145. package/dist/types/ports/audit-log.d.ts +18 -5
  146. package/dist/types/ports/audit-log.d.ts.map +1 -1
  147. package/dist/types/ports/scim-directory.d.ts +104 -0
  148. package/dist/types/ports/scim-directory.d.ts.map +1 -0
  149. package/dist/types/ports/session-store.d.ts +23 -0
  150. package/dist/types/ports/session-store.d.ts.map +1 -1
  151. package/dist/types/types/error.d.ts +5 -0
  152. package/dist/types/types/error.d.ts.map +1 -1
  153. package/dist/types/types/idp.d.ts +83 -0
  154. package/dist/types/types/idp.d.ts.map +1 -1
  155. package/dist/types/types/method.d.ts +109 -0
  156. package/dist/types/types/method.d.ts.map +1 -1
  157. package/dist/types/types/scim.d.ts +269 -0
  158. package/dist/types/types/scim.d.ts.map +1 -0
  159. package/dist/types/types/tenant.d.ts +10 -0
  160. package/dist/types/types/tenant.d.ts.map +1 -1
  161. package/package.json +9 -1
  162. package/src/adapters/d1/migrations.ts +5 -0
  163. package/src/adapters/d1/session-store.ts +68 -0
  164. package/src/adapters/durable-object/session-store.ts +61 -0
  165. package/src/adapters/dynamo/session-store.ts +63 -0
  166. package/src/adapters/memory/session-store.ts +39 -0
  167. package/src/adapters/postgres/migrations.ts +6 -0
  168. package/src/adapters/postgres/session-store.ts +69 -0
  169. package/src/domain/callback.ts +215 -7
  170. package/src/domain/method-dispatch.ts +53 -1
  171. package/src/domain/method-route.ts +190 -1
  172. package/src/domain/scim/discovery.ts +262 -0
  173. package/src/domain/scim/filter.ts +295 -0
  174. package/src/domain/scim/handle.ts +597 -0
  175. package/src/domain/scim/patch.ts +807 -0
  176. package/src/domain/scim/resource.ts +358 -0
  177. package/src/http/context.ts +9 -0
  178. package/src/http/cookies.ts +19 -1
  179. package/src/http/handlers/authorize.ts +2 -22
  180. package/src/http/handlers/callback.ts +9 -1
  181. package/src/http/handlers/method-route.ts +72 -2
  182. package/src/http/handlers/scim.ts +107 -0
  183. package/src/http/middleware/tenant.ts +47 -1
  184. package/src/http/router.ts +9 -0
  185. package/src/index.ts +23 -0
  186. package/src/methods/saml-sp/acs.ts +449 -0
  187. package/src/methods/saml-sp/attributes.ts +146 -0
  188. package/src/methods/saml-sp/authnrequest.ts +129 -0
  189. package/src/methods/saml-sp/cache-provider.ts +76 -0
  190. package/src/methods/saml-sp/cert-rotation.ts +34 -0
  191. package/src/methods/saml-sp/factory.ts +139 -0
  192. package/src/methods/saml-sp/index.ts +34 -0
  193. package/src/methods/saml-sp/metadata.ts +215 -0
  194. package/src/methods/saml-sp/method.ts +65 -0
  195. package/src/methods/saml-sp/parse-idp-metadata.ts +158 -0
  196. package/src/methods/saml-sp/saml-instance.ts +210 -0
  197. package/src/methods/saml-sp/slo-initiate.ts +180 -0
  198. package/src/methods/saml-sp/sls.ts +307 -0
  199. package/src/methods/saml-sp/types.ts +298 -0
  200. package/src/ports/CONSISTENCY.md +52 -24
  201. package/src/ports/audit-log.ts +18 -5
  202. package/src/ports/scim-directory.ts +166 -0
  203. package/src/ports/session-store.ts +24 -0
  204. package/src/types/error.ts +11 -0
  205. package/src/types/idp.ts +86 -0
  206. package/src/types/method.ts +110 -0
  207. package/src/types/scim.ts +280 -0
  208. package/src/types/tenant.ts +10 -0
@@ -46,12 +46,12 @@ var tokenRequestSchema = z.discriminatedUnion("grant_type", [
46
46
  tokenExchangeGrantSchema
47
47
  ]);
48
48
  export {
49
- tokenRequestSchema,
50
- tokenExchangeGrantSchema,
51
- refreshTokenGrantSchema,
52
- clientCredentialsGrantSchema,
53
- authorizationCodeGrantSchema,
54
- TOKEN_EXCHANGE_GRANT_TYPE,
49
+ ACCESS_TOKEN_TYPE,
55
50
  REFRESH_TOKEN_TYPE,
56
- ACCESS_TOKEN_TYPE
51
+ TOKEN_EXCHANGE_GRANT_TYPE,
52
+ authorizationCodeGrantSchema,
53
+ clientCredentialsGrantSchema,
54
+ refreshTokenGrantSchema,
55
+ tokenExchangeGrantSchema,
56
+ tokenRequestSchema
57
57
  };
package/dist/esm/index.js CHANGED
@@ -58,6 +58,7 @@ function createIdP(opts) {
58
58
  sessionStore: opts.sessionStore,
59
59
  keyStore: opts.keyStore,
60
60
  ...opts.methodStore ? { methodStore: opts.methodStore } : {},
61
+ ...opts.scimDirectory ? { scimDirectory: opts.scimDirectory } : {},
61
62
  ...auditLog ? { auditLog } : {},
62
63
  methodCache,
63
64
  stateKeys: opts.stateKeys,
@@ -65,6 +66,7 @@ function createIdP(opts) {
65
66
  ...opts.callbackHostFor ? { callbackHostFor: opts.callbackHostFor } : {},
66
67
  resolveTenant: opts.resolveTenant,
67
68
  success: opts.success,
69
+ ...opts.onLogout ? { onLogout: opts.onLogout } : {},
68
70
  ...opts.persistUpstreamTokens ? { persistUpstreamTokens: opts.persistUpstreamTokens } : {},
69
71
  ...opts.exchangeAudience ? { exchangeAudience: opts.exchangeAudience } : {},
70
72
  ...opts.renderPicker ? { renderPicker: opts.renderPicker } : {},
@@ -95,37 +97,37 @@ function createIdP(opts) {
95
97
  };
96
98
  }
97
99
  export {
98
- yahooFactory,
99
- xFactory,
100
- twitchFactory,
101
- spotifyFactory,
102
- slackFactory,
103
- revokeAllForSubject,
104
- passwordMethod,
105
- passkeyMethod,
106
- ok,
107
- oidcFactory,
108
- oauth2Factory,
109
- microsoftFactory,
110
- m2mMethod,
111
- linkedinFactory,
112
- keycloakFactory,
113
- jumpcloudFactory,
114
- isOk,
115
- isErr,
116
- googleFactory,
117
- githubFactory,
118
- facebookFactory,
119
- err,
120
- discordFactory,
121
- createIdP,
122
- cognitoFactory,
123
- codeMethod,
124
- buildOidcMethod,
125
- buildOauth2Method,
126
- authError,
127
- asTenantId,
128
- argon2idHasher,
100
+ DEFAULT_ARGON2ID_PARAMS,
129
101
  appleFactory,
130
- DEFAULT_ARGON2ID_PARAMS
102
+ argon2idHasher,
103
+ asTenantId,
104
+ authError,
105
+ buildOauth2Method,
106
+ buildOidcMethod,
107
+ codeMethod,
108
+ cognitoFactory,
109
+ createIdP,
110
+ discordFactory,
111
+ err,
112
+ facebookFactory,
113
+ githubFactory,
114
+ googleFactory,
115
+ isErr,
116
+ isOk,
117
+ jumpcloudFactory,
118
+ keycloakFactory,
119
+ linkedinFactory,
120
+ m2mMethod,
121
+ microsoftFactory,
122
+ oauth2Factory,
123
+ oidcFactory,
124
+ ok,
125
+ passkeyMethod,
126
+ passwordMethod,
127
+ revokeAllForSubject,
128
+ slackFactory,
129
+ spotifyFactory,
130
+ twitchFactory,
131
+ xFactory,
132
+ yahooFactory
131
133
  };
@@ -79,6 +79,6 @@ var oidcFactory = {
79
79
  })
80
80
  };
81
81
  export {
82
- oidcFactory,
83
- oauth2Factory
82
+ oauth2Factory,
83
+ oidcFactory
84
84
  };
@@ -74,6 +74,6 @@ function parsePhc(stored) {
74
74
  }
75
75
  }
76
76
  export {
77
- argon2idHasher,
78
- DEFAULT_ARGON2ID_PARAMS
77
+ DEFAULT_ARGON2ID_PARAMS,
78
+ argon2idHasher
79
79
  };
@@ -1,17 +1,33 @@
1
+ // src/methods/providers/index.ts
2
+ import { googleFactory } from "./google";
3
+ import { githubFactory } from "./github";
4
+ import { appleFactory } from "./apple";
5
+ import { microsoftFactory } from "./microsoft";
6
+ import { discordFactory } from "./discord";
7
+ import { facebookFactory } from "./facebook";
8
+ import { linkedinFactory } from "./linkedin";
9
+ import { slackFactory } from "./slack";
10
+ import { spotifyFactory } from "./spotify";
11
+ import { twitchFactory } from "./twitch";
12
+ import { xFactory } from "./x";
13
+ import { yahooFactory } from "./yahoo";
14
+ import { jumpcloudFactory } from "./jumpcloud";
15
+ import { keycloakFactory } from "./keycloak";
16
+ import { cognitoFactory } from "./cognito";
1
17
  export {
2
- yahooFactory,
3
- xFactory,
4
- twitchFactory,
5
- spotifyFactory,
6
- slackFactory,
7
- microsoftFactory,
8
- linkedinFactory,
9
- keycloakFactory,
10
- jumpcloudFactory,
11
- googleFactory,
12
- githubFactory,
13
- facebookFactory,
14
- discordFactory,
18
+ appleFactory,
15
19
  cognitoFactory,
16
- appleFactory
20
+ discordFactory,
21
+ facebookFactory,
22
+ githubFactory,
23
+ googleFactory,
24
+ jumpcloudFactory,
25
+ keycloakFactory,
26
+ linkedinFactory,
27
+ microsoftFactory,
28
+ slackFactory,
29
+ spotifyFactory,
30
+ twitchFactory,
31
+ xFactory,
32
+ yahooFactory
17
33
  };
@@ -0,0 +1,232 @@
1
+ // src/methods/saml-sp/acs.ts
2
+ import xmldom from "@xmldom/xmldom";
3
+ import { authError } from "../../types/error";
4
+ import { mapProfile } from "./attributes";
5
+ import { buildSamlInstance, resolveSpEntityId } from "./saml-instance";
6
+ var { DOMParser } = xmldom;
7
+ function parseVerifiedAssertion(profile) {
8
+ if (typeof profile.getAssertionXml !== "function") {
9
+ return {
10
+ failure: {
11
+ kind: "error",
12
+ error: authError.internalError("saml-sp: node-saml profile exposes no getAssertionXml(); cannot " + "perform the Recipient binding check. Refusing to authenticate.")
13
+ }
14
+ };
15
+ }
16
+ try {
17
+ return {
18
+ doc: new DOMParser().parseFromString(profile.getAssertionXml(), "text/xml")
19
+ };
20
+ } catch (e) {
21
+ return {
22
+ failure: {
23
+ kind: "error",
24
+ error: authError.internalError("saml-sp: failed to parse the verified assertion", e)
25
+ }
26
+ };
27
+ }
28
+ }
29
+ function checkRecipient(doc, acsUrl) {
30
+ const nodes = doc.getElementsByTagNameNS("*", "SubjectConfirmationData");
31
+ const recipients = [];
32
+ for (let i = 0;i < nodes.length; i++) {
33
+ const r = nodes[i]?.getAttribute("Recipient");
34
+ if (r)
35
+ recipients.push(r);
36
+ }
37
+ if (recipients.length === 0) {
38
+ return {
39
+ kind: "denied",
40
+ reason: "assertion has no SubjectConfirmationData/@Recipient binding it " + "to this ACS"
41
+ };
42
+ }
43
+ if (!recipients.includes(acsUrl)) {
44
+ return {
45
+ kind: "denied",
46
+ reason: "recipient mismatch: assertion not addressed to this ACS"
47
+ };
48
+ }
49
+ return null;
50
+ }
51
+ function extractReplayInfo(doc) {
52
+ const root = doc.documentElement;
53
+ const assertionId = root?.getAttribute("ID") ?? "";
54
+ if (!assertionId)
55
+ return null;
56
+ let earliest = null;
57
+ for (const tag of ["Conditions", "SubjectConfirmationData"]) {
58
+ const els = doc.getElementsByTagNameNS("*", tag);
59
+ for (let i = 0;i < els.length; i++) {
60
+ const v = els[i]?.getAttribute("NotOnOrAfter");
61
+ if (!v)
62
+ continue;
63
+ const ms = Date.parse(v);
64
+ if (!Number.isNaN(ms))
65
+ earliest = earliest === null ? ms : Math.min(earliest, ms);
66
+ }
67
+ }
68
+ return { assertionId, notOnOrAfterMs: earliest };
69
+ }
70
+ function extractAuthnStatement(doc) {
71
+ const out = {};
72
+ const stmt = doc.getElementsByTagNameNS("*", "AuthnStatement")[0];
73
+ if (!stmt)
74
+ return out;
75
+ const instant = stmt.getAttribute("AuthnInstant");
76
+ if (instant)
77
+ out.authnInstant = instant;
78
+ const sessionExpiry = stmt.getAttribute("SessionNotOnOrAfter");
79
+ if (sessionExpiry) {
80
+ const ms = Date.parse(sessionExpiry);
81
+ if (!Number.isNaN(ms))
82
+ out.sessionNotOnOrAfter = ms;
83
+ }
84
+ const ref = stmt.getElementsByTagNameNS("*", "AuthnContextClassRef")[0]?.textContent?.trim();
85
+ if (ref)
86
+ out.authnContextClassRef = ref;
87
+ return out;
88
+ }
89
+ async function consumeAssertion(ctx, methodId, config) {
90
+ const idpInitiated = ctx.flow === null;
91
+ let spEntityId;
92
+ let acsUrl;
93
+ if (idpInitiated) {
94
+ if (!config.idpInitiated) {
95
+ return {
96
+ kind: "error",
97
+ error: authError.internalError("saml-sp: unsolicited Response reached a method with no " + "idpInitiated config")
98
+ };
99
+ }
100
+ if (!ctx.dispatch) {
101
+ return {
102
+ kind: "error",
103
+ error: authError.internalError("saml-sp: IdP-initiated ACS dispatched without issuer context")
104
+ };
105
+ }
106
+ spEntityId = resolveSpEntityId(config, ctx.dispatch.issuerUrl, ctx.tenant.id, methodId);
107
+ acsUrl = ctx.dispatch.callbackUrl;
108
+ } else {
109
+ const state = ctx.methodState;
110
+ if (!state || !state.spEntityId || !state.acsUrl) {
111
+ return {
112
+ kind: "error",
113
+ error: authError.internalError("saml-sp: ACS reached without AuthnRequest method state " + "(spEntityId / acsUrl). The flow did not originate from this method.")
114
+ };
115
+ }
116
+ spEntityId = state.spEntityId;
117
+ acsUrl = state.acsUrl;
118
+ }
119
+ let samlResponse;
120
+ let relayState;
121
+ try {
122
+ const form = new URLSearchParams(await ctx.request.text());
123
+ samlResponse = form.get("SAMLResponse");
124
+ relayState = form.get("RelayState");
125
+ } catch {
126
+ return {
127
+ kind: "error",
128
+ error: authError.internalError("saml-sp: ACS could not read POST body")
129
+ };
130
+ }
131
+ if (!samlResponse) {
132
+ return { kind: "denied", reason: "missing SAMLResponse" };
133
+ }
134
+ let saml;
135
+ try {
136
+ saml = buildSamlInstance(config, {
137
+ spEntityId,
138
+ acsUrl,
139
+ scratch: ctx.methodScratch,
140
+ ...idpInitiated ? { idpInitiated: true } : {},
141
+ ...config.allowEncryptedAssertions && config.decryptionKey ? { decryptionPvk: config.decryptionKey.privateKeyPem } : {}
142
+ }, Date.now());
143
+ } catch (e) {
144
+ return {
145
+ kind: "error",
146
+ error: authError.internalError(`saml-sp: cannot construct verifier: ${e instanceof Error ? e.message : String(e)}`, e)
147
+ };
148
+ }
149
+ let profile;
150
+ try {
151
+ const result = await saml.validatePostResponseAsync({
152
+ SAMLResponse: samlResponse,
153
+ ...relayState !== null ? { RelayState: relayState } : {}
154
+ });
155
+ profile = result.profile;
156
+ } catch (e) {
157
+ const msg = e instanceof Error ? e.message : String(e);
158
+ if (!config.allowEncryptedAssertions && msg.includes("No decryption key")) {
159
+ return {
160
+ kind: "denied",
161
+ reason: "encrypted assertion received but encrypted assertions are not " + "enabled for this SAML connection (set allowEncryptedAssertions " + "+ decryptionKey)"
162
+ };
163
+ }
164
+ return { kind: "denied", reason: `assertion rejected: ${msg}` };
165
+ }
166
+ if (!profile || !profile.nameID) {
167
+ return { kind: "denied", reason: "assertion produced no usable subject" };
168
+ }
169
+ const parsed = parseVerifiedAssertion(profile);
170
+ if ("failure" in parsed)
171
+ return parsed.failure;
172
+ const assertionDoc = parsed.doc;
173
+ const recipientFailure = checkRecipient(assertionDoc, acsUrl);
174
+ if (recipientFailure)
175
+ return recipientFailure;
176
+ if (idpInitiated) {
177
+ const replay = extractReplayInfo(assertionDoc);
178
+ if (!replay) {
179
+ return {
180
+ kind: "error",
181
+ error: authError.internalError("saml-sp: cannot read assertion @ID for IdP-initiated replay " + "protection. Refusing to authenticate.")
182
+ };
183
+ }
184
+ const key = `idp-replay:${replay.assertionId}`;
185
+ const seen = await ctx.methodScratch.get(key);
186
+ if (seen.ok) {
187
+ return {
188
+ kind: "denied",
189
+ reason: "assertion replay detected (assertion ID already seen)"
190
+ };
191
+ }
192
+ const skewMs = (config.clockSkewSeconds ?? 60) * 1000;
193
+ const now = Date.now();
194
+ const horizon = replay.notOnOrAfterMs !== null ? replay.notOnOrAfterMs - now + skewMs : 10 * 60000;
195
+ const ttlMs = Math.min(Math.max(horizon, skewMs, 60000), 24 * 60 * 60000);
196
+ const recorded = await ctx.methodScratch.put(key, "1", ttlMs);
197
+ if (!recorded.ok) {
198
+ return {
199
+ kind: "error",
200
+ error: authError.internalError("saml-sp: could not record assertion ID for replay protection")
201
+ };
202
+ }
203
+ }
204
+ const authnStatement = extractAuthnStatement(assertionDoc);
205
+ const verified = {
206
+ nameID: profile.nameID,
207
+ nameIDFormat: profile.nameIDFormat ?? "",
208
+ ...profile.sessionIndex !== undefined ? { sessionIndex: profile.sessionIndex } : {},
209
+ ...authnStatement,
210
+ attributes: profile.attributes ?? {},
211
+ responseXml: profile.getSamlResponseXml?.() ?? ""
212
+ };
213
+ const mapped = mapProfile(verified, config.attributeMapping);
214
+ if ("error" in mapped) {
215
+ return { kind: "denied", reason: mapped.error };
216
+ }
217
+ return {
218
+ kind: "success",
219
+ providerSubject: mapped.providerSubject,
220
+ properties: mapped.properties,
221
+ ...idpInitiated && config.idpInitiated ? {
222
+ unsolicitedBinding: {
223
+ clientId: config.idpInitiated.defaultClientId,
224
+ redirectUri: config.idpInitiated.defaultRedirectUri,
225
+ scopes: config.idpInitiated.defaultScopes ?? []
226
+ }
227
+ } : {}
228
+ };
229
+ }
230
+ export {
231
+ consumeAssertion
232
+ };
@@ -0,0 +1,77 @@
1
+ // src/methods/saml-sp/attributes.ts
2
+ var NAME_ID_FORMAT_BY_URN = {
3
+ "urn:oasis:names:tc:SAML:2.0:nameid-format:persistent": "persistent",
4
+ "urn:oasis:names:tc:SAML:2.0:nameid-format:transient": "transient",
5
+ "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress": "emailAddress",
6
+ "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified": "unspecified"
7
+ };
8
+ function normalizeNameIdFormat(urn) {
9
+ return NAME_ID_FORMAT_BY_URN[urn] ?? "unspecified";
10
+ }
11
+ function scalar(v) {
12
+ if (typeof v === "string")
13
+ return v;
14
+ if (Array.isArray(v) && v.length > 0 && typeof v[0] === "string") {
15
+ return v[0];
16
+ }
17
+ return;
18
+ }
19
+ function multi(v) {
20
+ if (typeof v === "string")
21
+ return v;
22
+ if (Array.isArray(v)) {
23
+ const strs = v.filter((x) => typeof x === "string");
24
+ return strs.length > 0 ? strs : undefined;
25
+ }
26
+ return;
27
+ }
28
+ function resolveRef(ref, profile) {
29
+ if (ref.source === "nameId")
30
+ return profile.nameID;
31
+ return multi(profile.attributes[ref.name]);
32
+ }
33
+ function mapProfile(profile, mapping) {
34
+ const subjectRef = mapping.subject ?? { source: "nameId" };
35
+ const subjectVal = scalar(resolveRef(subjectRef, profile));
36
+ if (!subjectVal) {
37
+ return {
38
+ error: subjectRef.source === "nameId" ? "assertion NameID is empty; cannot derive subject" : `subject attribute "${subjectRef.name}" missing from assertion`
39
+ };
40
+ }
41
+ const attributes = {};
42
+ const put = (key, ref) => {
43
+ if (!ref)
44
+ return;
45
+ const v = resolveRef(ref, profile);
46
+ if (v !== undefined)
47
+ attributes[key] = v;
48
+ };
49
+ put("email", mapping.email);
50
+ put("name", mapping.name);
51
+ put("groups", mapping.groups);
52
+ if (mapping.emailVerified) {
53
+ attributes["emailVerified"] = mapping.emailVerified.value ? "true" : "false";
54
+ }
55
+ for (const [key, ref] of Object.entries(mapping.custom ?? {})) {
56
+ put(key, ref);
57
+ }
58
+ const authnInstant = profile.authnInstant ? Date.parse(profile.authnInstant) : Date.now();
59
+ return {
60
+ providerSubject: subjectVal,
61
+ properties: {
62
+ nameId: {
63
+ value: profile.nameID,
64
+ format: normalizeNameIdFormat(profile.nameIDFormat)
65
+ },
66
+ attributes,
67
+ ...profile.sessionIndex !== undefined ? { sessionIndex: profile.sessionIndex } : {},
68
+ authnInstant: Number.isNaN(authnInstant) ? Date.now() : authnInstant,
69
+ ...profile.sessionNotOnOrAfter !== undefined ? { sessionNotOnOrAfter: profile.sessionNotOnOrAfter } : {},
70
+ ...profile.authnContextClassRef !== undefined ? { authnContextClassRef: profile.authnContextClassRef } : {},
71
+ raw: { responseXml: profile.responseXml }
72
+ }
73
+ };
74
+ }
75
+ export {
76
+ mapProfile
77
+ };
@@ -0,0 +1,63 @@
1
+ // src/methods/saml-sp/authnrequest.ts
2
+ import { authError } from "../../types/error";
3
+ import { isErr } from "../../types/result";
4
+ import { buildSamlInstance, resolveSpEntityId } from "./saml-instance";
5
+ async function buildAuthnRequestRedirect(ctx, methodId, config) {
6
+ if (!ctx.dispatch) {
7
+ return {
8
+ kind: "error",
9
+ error: authError.internalError("saml-sp: dispatch missing on /authorize")
10
+ };
11
+ }
12
+ if (config.signAuthnRequest && !config.signingKey) {
13
+ return {
14
+ kind: "error",
15
+ error: authError.internalError("saml-sp: signAuthnRequest is true but signingKey is missing " + "(config schema should have rejected this).")
16
+ };
17
+ }
18
+ const spEntityId = resolveSpEntityId(config, ctx.dispatch.issuerUrl, ctx.tenant.id, methodId);
19
+ const acsUrl = ctx.dispatch.callbackUrl;
20
+ const probe = await ctx.methodScratch.put("authnrequest-scratch-probe", "1", 1000);
21
+ if (isErr(probe)) {
22
+ return {
23
+ kind: "error",
24
+ error: authError.internalError("saml-sp: the configured SessionStore adapter does not support " + "methodScratch (saveScratch/readScratch/deleteScratch), which " + "SAML SP requires for InResponseTo replay protection. Deploy " + "SAML against an adapter that implements the scratch trio.")
25
+ };
26
+ }
27
+ let redirectUrl;
28
+ try {
29
+ const saml = buildSamlInstance(config, {
30
+ spEntityId,
31
+ acsUrl,
32
+ scratch: ctx.methodScratch,
33
+ ...config.signAuthnRequest && config.signingKey ? {
34
+ signing: {
35
+ privateKeyPem: config.signingKey.privateKeyPem,
36
+ certPem: config.signingKey.certPem
37
+ }
38
+ } : {}
39
+ }, Date.now());
40
+ redirectUrl = await saml.getAuthorizeUrlAsync(ctx.dispatch.state, undefined, {});
41
+ } catch (e) {
42
+ return {
43
+ kind: "error",
44
+ error: authError.internalError(`saml-sp: failed to build AuthnRequest: ${e instanceof Error ? e.message : String(e)}`, e)
45
+ };
46
+ }
47
+ return {
48
+ kind: "challenge",
49
+ response: new Response(null, {
50
+ status: 302,
51
+ headers: { location: redirectUrl }
52
+ }),
53
+ saveMethodState: {
54
+ relayState: ctx.dispatch.state,
55
+ issuedAt: Date.now(),
56
+ spEntityId,
57
+ acsUrl
58
+ }
59
+ };
60
+ }
61
+ export {
62
+ buildAuthnRequestRedirect
63
+ };
@@ -0,0 +1,33 @@
1
+ // src/methods/saml-sp/cache-provider.ts
2
+ import { isOk } from "../../types/result";
3
+ var KEY_PREFIX = "saml-inresponseto:";
4
+ function methodScratchCacheProvider(scratch, ttlMs) {
5
+ return {
6
+ async saveAsync(key, value) {
7
+ const createdAt = Date.now();
8
+ const item = { value, createdAt };
9
+ const r = await scratch.put(`${KEY_PREFIX}${key}`, JSON.stringify(item), ttlMs);
10
+ return isOk(r) ? item : null;
11
+ },
12
+ async getAsync(key) {
13
+ const r = await scratch.get(`${KEY_PREFIX}${key}`);
14
+ if (!isOk(r))
15
+ return null;
16
+ try {
17
+ const item = JSON.parse(r.value);
18
+ return item.value;
19
+ } catch {
20
+ return null;
21
+ }
22
+ },
23
+ async removeAsync(key) {
24
+ if (key === null)
25
+ return null;
26
+ const r = await scratch.delete(`${KEY_PREFIX}${key}`);
27
+ return isOk(r) ? key : null;
28
+ }
29
+ };
30
+ }
31
+ export {
32
+ methodScratchCacheProvider
33
+ };
@@ -0,0 +1,13 @@
1
+ // src/methods/saml-sp/cert-rotation.ts
2
+ function selectActiveCertPems(certs, nowMs) {
3
+ return certs.filter((c) => {
4
+ if (c.notBefore !== undefined && nowMs < c.notBefore)
5
+ return false;
6
+ if (c.notAfter !== undefined && nowMs >= c.notAfter)
7
+ return false;
8
+ return true;
9
+ }).map((c) => c.pem);
10
+ }
11
+ export {
12
+ selectActiveCertPems
13
+ };
@@ -0,0 +1,81 @@
1
+ // src/methods/saml-sp/factory.ts
2
+ import { z } from "zod";
3
+ import { buildSamlSpMethod } from "./method";
4
+ var attributeRefSchema = z.union([
5
+ z.object({ source: z.literal("nameId") }),
6
+ z.object({
7
+ source: z.literal("attribute"),
8
+ name: z.string().min(1),
9
+ format: z.string().optional()
10
+ })
11
+ ]);
12
+ var attributeMappingSchema = z.object({
13
+ subject: attributeRefSchema.optional(),
14
+ email: attributeRefSchema.optional(),
15
+ emailVerified: z.object({ source: z.literal("literal"), value: z.boolean() }).optional(),
16
+ name: attributeRefSchema.optional(),
17
+ groups: attributeRefSchema.optional(),
18
+ custom: z.record(attributeRefSchema).optional()
19
+ });
20
+ var signingCertSchema = z.object({
21
+ pem: z.string().min(1),
22
+ notBefore: z.number().optional(),
23
+ notAfter: z.number().optional()
24
+ });
25
+ var idpSchema = z.object({
26
+ entityId: z.string().min(1),
27
+ ssoUrl: z.string().url(),
28
+ sloUrl: z.string().url().optional(),
29
+ nameIdFormat: z.enum(["persistent", "transient", "emailAddress", "unspecified"]).optional(),
30
+ signingCerts: z.array(signingCertSchema).min(1)
31
+ });
32
+ var idpInitiatedSchema = z.object({
33
+ defaultClientId: z.string().min(1),
34
+ defaultRedirectUri: z.string().url(),
35
+ defaultScopes: z.array(z.string()).optional()
36
+ });
37
+ var samlSpConfigSchema = z.object({
38
+ idp: idpSchema,
39
+ attributeMapping: attributeMappingSchema,
40
+ signAuthnRequest: z.boolean().optional(),
41
+ signingKey: z.object({
42
+ privateKeyPem: z.string().min(1),
43
+ certPem: z.string().min(1)
44
+ }).optional(),
45
+ allowEncryptedAssertions: z.boolean().optional(),
46
+ decryptionKey: z.object({
47
+ privateKeyPem: z.string().min(1),
48
+ certPem: z.string().min(1)
49
+ }).optional(),
50
+ idpInitiated: idpInitiatedSchema.optional(),
51
+ clockSkewSeconds: z.number().int().nonnegative().optional(),
52
+ spEntityId: z.string().min(1).optional(),
53
+ forceAuthn: z.boolean().optional(),
54
+ requestedAuthnContext: z.object({
55
+ classRefs: z.array(z.string().min(1)).min(1),
56
+ comparison: z.enum(["exact", "minimum", "maximum", "better"]).optional()
57
+ }).optional(),
58
+ requireSignedAssertion: z.boolean().optional(),
59
+ requireSignedResponse: z.boolean().optional()
60
+ }).refine((c) => !c.signAuthnRequest || c.signingKey !== undefined, {
61
+ message: "signingKey is required when signAuthnRequest is true",
62
+ path: ["signingKey"]
63
+ }).refine((c) => !c.allowEncryptedAssertions || c.decryptionKey !== undefined, {
64
+ message: "decryptionKey is required when allowEncryptedAssertions is true",
65
+ path: ["decryptionKey"]
66
+ }).refine((c) => (c.requireSignedAssertion ?? true) || c.requireSignedResponse === true, {
67
+ message: "requireSignedAssertion may only be false when requireSignedResponse " + "is true — at least one signature is mandatory",
68
+ path: ["requireSignedAssertion"]
69
+ });
70
+ var samlSpFactory = {
71
+ kind: "saml-sp",
72
+ configSchema: samlSpConfigSchema,
73
+ build: async ({
74
+ id,
75
+ kind,
76
+ config
77
+ }) => buildSamlSpMethod(id, kind, config)
78
+ };
79
+ export {
80
+ samlSpFactory
81
+ };
@@ -0,0 +1,7 @@
1
+ // src/methods/saml-sp/index.ts
2
+ import { samlSpFactory } from "./factory";
3
+ import { parseSamlIdpMetadata } from "./parse-idp-metadata";
4
+ export {
5
+ parseSamlIdpMetadata,
6
+ samlSpFactory
7
+ };