@better-auth/sso 1.4.7 → 1.4.8-beta.1
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.
- package/.turbo/turbo-build.log +6 -6
- package/dist/client.d.mts +1 -1
- package/dist/{index-B9WMxRdD.d.mts → index-DNWhGQW-.d.mts} +81 -69
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +462 -264
- package/package.json +3 -3
- package/src/constants.ts +42 -0
- package/src/domain-verification.test.ts +1 -0
- package/src/index.ts +38 -11
- package/src/linking/index.ts +2 -0
- package/src/linking/org-assignment.ts +158 -0
- package/src/linking/types.ts +10 -0
- package/src/routes/sso.ts +338 -332
- package/src/saml/algorithms.test.ts +205 -0
- package/src/saml/algorithms.ts +259 -0
- package/src/saml/index.ts +9 -0
- package/src/saml.test.ts +350 -127
- package/src/types.ts +24 -16
- package/src/authn-request-store.ts +0 -76
- package/src/authn-request.test.ts +0 -99
package/dist/index.mjs
CHANGED
|
@@ -1,56 +1,102 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { APIError, createAuthEndpoint, createAuthMiddleware, sessionMiddleware } from "better-auth/api";
|
|
2
|
+
import { XMLParser, XMLValidator } from "fast-xml-parser";
|
|
2
3
|
import * as saml from "samlify";
|
|
3
|
-
import { APIError, createAuthEndpoint, sessionMiddleware } from "better-auth/api";
|
|
4
4
|
import { generateRandomString } from "better-auth/crypto";
|
|
5
|
-
import * as z from "zod/v4";
|
|
5
|
+
import * as z$1 from "zod/v4";
|
|
6
|
+
import z from "zod/v4";
|
|
6
7
|
import { BetterFetchError, betterFetch } from "@better-fetch/fetch";
|
|
7
8
|
import { HIDE_METADATA, createAuthorizationURL, generateState, parseState, validateAuthorizationCode, validateToken } from "better-auth";
|
|
8
9
|
import { setSessionCookie } from "better-auth/cookies";
|
|
9
10
|
import { handleOAuthUserInfo } from "better-auth/oauth2";
|
|
10
11
|
import { decodeJwt } from "jose";
|
|
11
12
|
|
|
12
|
-
//#region src/
|
|
13
|
+
//#region src/linking/org-assignment.ts
|
|
13
14
|
/**
|
|
14
|
-
*
|
|
15
|
-
*
|
|
15
|
+
* Assigns a user to an organization based on the SSO provider's organizationId.
|
|
16
|
+
* Used in SSO flows (OIDC, SAML) where the provider is already linked to an org.
|
|
16
17
|
*/
|
|
17
|
-
|
|
18
|
+
async function assignOrganizationFromProvider(ctx, options) {
|
|
19
|
+
const { user, profile, provider, token, provisioningOptions } = options;
|
|
20
|
+
if (!provider.organizationId) return;
|
|
21
|
+
if (provisioningOptions?.disabled) return;
|
|
22
|
+
if (!ctx.context.options.plugins?.find((plugin) => plugin.id === "organization")) return;
|
|
23
|
+
if (await ctx.context.adapter.findOne({
|
|
24
|
+
model: "member",
|
|
25
|
+
where: [{
|
|
26
|
+
field: "organizationId",
|
|
27
|
+
value: provider.organizationId
|
|
28
|
+
}, {
|
|
29
|
+
field: "userId",
|
|
30
|
+
value: user.id
|
|
31
|
+
}]
|
|
32
|
+
})) return;
|
|
33
|
+
const role = provisioningOptions?.getRole ? await provisioningOptions.getRole({
|
|
34
|
+
user,
|
|
35
|
+
userInfo: profile.rawAttributes || {},
|
|
36
|
+
token,
|
|
37
|
+
provider
|
|
38
|
+
}) : provisioningOptions?.defaultRole || "member";
|
|
39
|
+
await ctx.context.adapter.create({
|
|
40
|
+
model: "member",
|
|
41
|
+
data: {
|
|
42
|
+
organizationId: provider.organizationId,
|
|
43
|
+
userId: user.id,
|
|
44
|
+
role,
|
|
45
|
+
createdAt: /* @__PURE__ */ new Date()
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
}
|
|
18
49
|
/**
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
50
|
+
* Assigns a user to an organization based on their email domain.
|
|
51
|
+
* Looks up SSO providers that match the user's email domain and assigns
|
|
52
|
+
* the user to the associated organization.
|
|
53
|
+
*
|
|
54
|
+
* This enables domain-based org assignment for non-SSO sign-in methods
|
|
55
|
+
* (e.g., Google OAuth with @acme.com email gets added to Acme's org).
|
|
23
56
|
*/
|
|
24
|
-
function
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
57
|
+
async function assignOrganizationByDomain(ctx, options) {
|
|
58
|
+
const { user, provisioningOptions } = options;
|
|
59
|
+
if (provisioningOptions?.disabled) return;
|
|
60
|
+
if (!ctx.context.options.plugins?.find((plugin) => plugin.id === "organization")) return;
|
|
61
|
+
const domain = user.email.split("@")[1];
|
|
62
|
+
if (!domain) return;
|
|
63
|
+
const ssoProvider = await ctx.context.adapter.findOne({
|
|
64
|
+
model: "ssoProvider",
|
|
65
|
+
where: [{
|
|
66
|
+
field: "domain",
|
|
67
|
+
value: domain
|
|
68
|
+
}]
|
|
69
|
+
});
|
|
70
|
+
if (!ssoProvider || !ssoProvider.organizationId) return;
|
|
71
|
+
if (await ctx.context.adapter.findOne({
|
|
72
|
+
model: "member",
|
|
73
|
+
where: [{
|
|
74
|
+
field: "organizationId",
|
|
75
|
+
value: ssoProvider.organizationId
|
|
76
|
+
}, {
|
|
77
|
+
field: "userId",
|
|
78
|
+
value: user.id
|
|
79
|
+
}]
|
|
80
|
+
})) return;
|
|
81
|
+
const role = provisioningOptions?.getRole ? await provisioningOptions.getRole({
|
|
82
|
+
user,
|
|
83
|
+
userInfo: {},
|
|
84
|
+
provider: ssoProvider
|
|
85
|
+
}) : provisioningOptions?.defaultRole || "member";
|
|
86
|
+
await ctx.context.adapter.create({
|
|
87
|
+
model: "member",
|
|
88
|
+
data: {
|
|
89
|
+
organizationId: ssoProvider.organizationId,
|
|
90
|
+
userId: user.id,
|
|
91
|
+
role,
|
|
92
|
+
createdAt: /* @__PURE__ */ new Date()
|
|
47
93
|
}
|
|
48
|
-
};
|
|
94
|
+
});
|
|
49
95
|
}
|
|
50
96
|
|
|
51
97
|
//#endregion
|
|
52
98
|
//#region src/routes/domain-verification.ts
|
|
53
|
-
const domainVerificationBodySchema = z.object({ providerId: z.string() });
|
|
99
|
+
const domainVerificationBodySchema = z$1.object({ providerId: z$1.string() });
|
|
54
100
|
const requestDomainVerification = (options) => {
|
|
55
101
|
return createAuthEndpoint("/sso/request-domain-verification", {
|
|
56
102
|
method: "POST",
|
|
@@ -223,6 +269,38 @@ const verifyDomain = (options) => {
|
|
|
223
269
|
});
|
|
224
270
|
};
|
|
225
271
|
|
|
272
|
+
//#endregion
|
|
273
|
+
//#region src/constants.ts
|
|
274
|
+
/**
|
|
275
|
+
* SAML Constants
|
|
276
|
+
*
|
|
277
|
+
* Centralized constants for SAML SSO functionality.
|
|
278
|
+
*/
|
|
279
|
+
/** Prefix for AuthnRequest IDs used in InResponseTo validation */
|
|
280
|
+
const AUTHN_REQUEST_KEY_PREFIX = "saml-authn-request:";
|
|
281
|
+
/** Prefix for used Assertion IDs used in replay protection */
|
|
282
|
+
const USED_ASSERTION_KEY_PREFIX = "saml-used-assertion:";
|
|
283
|
+
/**
|
|
284
|
+
* Default TTL for AuthnRequest records (5 minutes).
|
|
285
|
+
* This should be sufficient for most IdPs while protecting against stale requests.
|
|
286
|
+
*/
|
|
287
|
+
const DEFAULT_AUTHN_REQUEST_TTL_MS = 300 * 1e3;
|
|
288
|
+
/**
|
|
289
|
+
* Default TTL for used assertion records (15 minutes).
|
|
290
|
+
* This should match the maximum expected NotOnOrAfter window plus clock skew.
|
|
291
|
+
*/
|
|
292
|
+
const DEFAULT_ASSERTION_TTL_MS = 900 * 1e3;
|
|
293
|
+
/**
|
|
294
|
+
* Default clock skew tolerance (5 minutes).
|
|
295
|
+
* Allows for minor time differences between IdP and SP servers.
|
|
296
|
+
*
|
|
297
|
+
* Accommodates:
|
|
298
|
+
* - Network latency and processing time
|
|
299
|
+
* - Clock synchronization differences (NTP drift)
|
|
300
|
+
* - Distributed systems across timezones
|
|
301
|
+
*/
|
|
302
|
+
const DEFAULT_CLOCK_SKEW_MS = 300 * 1e3;
|
|
303
|
+
|
|
226
304
|
//#endregion
|
|
227
305
|
//#region src/oidc/types.ts
|
|
228
306
|
/**
|
|
@@ -569,6 +647,146 @@ function mapDiscoveryErrorToAPIError(error) {
|
|
|
569
647
|
}
|
|
570
648
|
}
|
|
571
649
|
|
|
650
|
+
//#endregion
|
|
651
|
+
//#region src/saml/algorithms.ts
|
|
652
|
+
const SignatureAlgorithm = {
|
|
653
|
+
RSA_SHA1: "http://www.w3.org/2000/09/xmldsig#rsa-sha1",
|
|
654
|
+
RSA_SHA256: "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256",
|
|
655
|
+
RSA_SHA384: "http://www.w3.org/2001/04/xmldsig-more#rsa-sha384",
|
|
656
|
+
RSA_SHA512: "http://www.w3.org/2001/04/xmldsig-more#rsa-sha512",
|
|
657
|
+
ECDSA_SHA256: "http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha256",
|
|
658
|
+
ECDSA_SHA384: "http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha384",
|
|
659
|
+
ECDSA_SHA512: "http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha512"
|
|
660
|
+
};
|
|
661
|
+
const DigestAlgorithm = {
|
|
662
|
+
SHA1: "http://www.w3.org/2000/09/xmldsig#sha1",
|
|
663
|
+
SHA256: "http://www.w3.org/2001/04/xmlenc#sha256",
|
|
664
|
+
SHA384: "http://www.w3.org/2001/04/xmldsig-more#sha384",
|
|
665
|
+
SHA512: "http://www.w3.org/2001/04/xmlenc#sha512"
|
|
666
|
+
};
|
|
667
|
+
const KeyEncryptionAlgorithm = {
|
|
668
|
+
RSA_1_5: "http://www.w3.org/2001/04/xmlenc#rsa-1_5",
|
|
669
|
+
RSA_OAEP: "http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p",
|
|
670
|
+
RSA_OAEP_SHA256: "http://www.w3.org/2009/xmlenc11#rsa-oaep"
|
|
671
|
+
};
|
|
672
|
+
const DataEncryptionAlgorithm = {
|
|
673
|
+
TRIPLEDES_CBC: "http://www.w3.org/2001/04/xmlenc#tripledes-cbc",
|
|
674
|
+
AES_128_CBC: "http://www.w3.org/2001/04/xmlenc#aes128-cbc",
|
|
675
|
+
AES_192_CBC: "http://www.w3.org/2001/04/xmlenc#aes192-cbc",
|
|
676
|
+
AES_256_CBC: "http://www.w3.org/2001/04/xmlenc#aes256-cbc",
|
|
677
|
+
AES_128_GCM: "http://www.w3.org/2009/xmlenc11#aes128-gcm",
|
|
678
|
+
AES_192_GCM: "http://www.w3.org/2009/xmlenc11#aes192-gcm",
|
|
679
|
+
AES_256_GCM: "http://www.w3.org/2009/xmlenc11#aes256-gcm"
|
|
680
|
+
};
|
|
681
|
+
const DEPRECATED_SIGNATURE_ALGORITHMS = [SignatureAlgorithm.RSA_SHA1];
|
|
682
|
+
const DEPRECATED_KEY_ENCRYPTION_ALGORITHMS = [KeyEncryptionAlgorithm.RSA_1_5];
|
|
683
|
+
const DEPRECATED_DATA_ENCRYPTION_ALGORITHMS = [DataEncryptionAlgorithm.TRIPLEDES_CBC];
|
|
684
|
+
const SECURE_SIGNATURE_ALGORITHMS = [
|
|
685
|
+
SignatureAlgorithm.RSA_SHA256,
|
|
686
|
+
SignatureAlgorithm.RSA_SHA384,
|
|
687
|
+
SignatureAlgorithm.RSA_SHA512,
|
|
688
|
+
SignatureAlgorithm.ECDSA_SHA256,
|
|
689
|
+
SignatureAlgorithm.ECDSA_SHA384,
|
|
690
|
+
SignatureAlgorithm.ECDSA_SHA512
|
|
691
|
+
];
|
|
692
|
+
const xmlParser = new XMLParser({
|
|
693
|
+
ignoreAttributes: false,
|
|
694
|
+
attributeNamePrefix: "@_",
|
|
695
|
+
removeNSPrefix: true
|
|
696
|
+
});
|
|
697
|
+
function findNode(obj, nodeName) {
|
|
698
|
+
if (!obj || typeof obj !== "object") return null;
|
|
699
|
+
const record = obj;
|
|
700
|
+
if (nodeName in record) return record[nodeName];
|
|
701
|
+
for (const value of Object.values(record)) if (Array.isArray(value)) for (const item of value) {
|
|
702
|
+
const found = findNode(item, nodeName);
|
|
703
|
+
if (found) return found;
|
|
704
|
+
}
|
|
705
|
+
else if (typeof value === "object" && value !== null) {
|
|
706
|
+
const found = findNode(value, nodeName);
|
|
707
|
+
if (found) return found;
|
|
708
|
+
}
|
|
709
|
+
return null;
|
|
710
|
+
}
|
|
711
|
+
function extractEncryptionAlgorithms(xml) {
|
|
712
|
+
try {
|
|
713
|
+
const parsed = xmlParser.parse(xml);
|
|
714
|
+
const keyAlg = (findNode(parsed, "EncryptedKey")?.EncryptionMethod)?.["@_Algorithm"];
|
|
715
|
+
const dataAlg = (findNode(parsed, "EncryptedData")?.EncryptionMethod)?.["@_Algorithm"];
|
|
716
|
+
return {
|
|
717
|
+
keyEncryption: keyAlg || null,
|
|
718
|
+
dataEncryption: dataAlg || null
|
|
719
|
+
};
|
|
720
|
+
} catch {
|
|
721
|
+
return {
|
|
722
|
+
keyEncryption: null,
|
|
723
|
+
dataEncryption: null
|
|
724
|
+
};
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
function hasEncryptedAssertion(xml) {
|
|
728
|
+
try {
|
|
729
|
+
return findNode(xmlParser.parse(xml), "EncryptedAssertion") !== null;
|
|
730
|
+
} catch {
|
|
731
|
+
return false;
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
function handleDeprecatedAlgorithm(message, behavior, errorCode) {
|
|
735
|
+
switch (behavior) {
|
|
736
|
+
case "reject": throw new APIError("BAD_REQUEST", {
|
|
737
|
+
message,
|
|
738
|
+
code: errorCode
|
|
739
|
+
});
|
|
740
|
+
case "warn":
|
|
741
|
+
console.warn(`[SAML Security Warning] ${message}`);
|
|
742
|
+
break;
|
|
743
|
+
case "allow": break;
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
function validateSignatureAlgorithm(algorithm, options = {}) {
|
|
747
|
+
if (!algorithm) return;
|
|
748
|
+
const { onDeprecated = "warn", allowedSignatureAlgorithms } = options;
|
|
749
|
+
if (allowedSignatureAlgorithms) {
|
|
750
|
+
if (!allowedSignatureAlgorithms.includes(algorithm)) throw new APIError("BAD_REQUEST", {
|
|
751
|
+
message: `SAML signature algorithm not in allow-list: ${algorithm}`,
|
|
752
|
+
code: "SAML_ALGORITHM_NOT_ALLOWED"
|
|
753
|
+
});
|
|
754
|
+
return;
|
|
755
|
+
}
|
|
756
|
+
if (DEPRECATED_SIGNATURE_ALGORITHMS.includes(algorithm)) {
|
|
757
|
+
handleDeprecatedAlgorithm(`SAML response uses deprecated signature algorithm: ${algorithm}. Please configure your IdP to use SHA-256 or stronger.`, onDeprecated, "SAML_DEPRECATED_ALGORITHM");
|
|
758
|
+
return;
|
|
759
|
+
}
|
|
760
|
+
if (!SECURE_SIGNATURE_ALGORITHMS.includes(algorithm)) throw new APIError("BAD_REQUEST", {
|
|
761
|
+
message: `SAML signature algorithm not recognized: ${algorithm}`,
|
|
762
|
+
code: "SAML_UNKNOWN_ALGORITHM"
|
|
763
|
+
});
|
|
764
|
+
}
|
|
765
|
+
function validateEncryptionAlgorithms(algorithms, options = {}) {
|
|
766
|
+
const { onDeprecated = "warn", allowedKeyEncryptionAlgorithms, allowedDataEncryptionAlgorithms } = options;
|
|
767
|
+
const { keyEncryption, dataEncryption } = algorithms;
|
|
768
|
+
if (keyEncryption) {
|
|
769
|
+
if (allowedKeyEncryptionAlgorithms) {
|
|
770
|
+
if (!allowedKeyEncryptionAlgorithms.includes(keyEncryption)) throw new APIError("BAD_REQUEST", {
|
|
771
|
+
message: `SAML key encryption algorithm not in allow-list: ${keyEncryption}`,
|
|
772
|
+
code: "SAML_ALGORITHM_NOT_ALLOWED"
|
|
773
|
+
});
|
|
774
|
+
} else if (DEPRECATED_KEY_ENCRYPTION_ALGORITHMS.includes(keyEncryption)) handleDeprecatedAlgorithm(`SAML response uses deprecated key encryption algorithm: ${keyEncryption}. Please configure your IdP to use RSA-OAEP.`, onDeprecated, "SAML_DEPRECATED_ALGORITHM");
|
|
775
|
+
}
|
|
776
|
+
if (dataEncryption) {
|
|
777
|
+
if (allowedDataEncryptionAlgorithms) {
|
|
778
|
+
if (!allowedDataEncryptionAlgorithms.includes(dataEncryption)) throw new APIError("BAD_REQUEST", {
|
|
779
|
+
message: `SAML data encryption algorithm not in allow-list: ${dataEncryption}`,
|
|
780
|
+
code: "SAML_ALGORITHM_NOT_ALLOWED"
|
|
781
|
+
});
|
|
782
|
+
} else if (DEPRECATED_DATA_ENCRYPTION_ALGORITHMS.includes(dataEncryption)) handleDeprecatedAlgorithm(`SAML response uses deprecated data encryption algorithm: ${dataEncryption}. Please configure your IdP to use AES-GCM.`, onDeprecated, "SAML_DEPRECATED_ALGORITHM");
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
function validateSAMLAlgorithms(response, options) {
|
|
786
|
+
validateSignatureAlgorithm(response.sigAlg, options);
|
|
787
|
+
if (hasEncryptedAssertion(response.samlContent)) validateEncryptionAlgorithms(extractEncryptionAlgorithms(response.samlContent), options);
|
|
788
|
+
}
|
|
789
|
+
|
|
572
790
|
//#endregion
|
|
573
791
|
//#region src/utils.ts
|
|
574
792
|
/**
|
|
@@ -599,9 +817,6 @@ const validateEmailDomain = (email, domain) => {
|
|
|
599
817
|
|
|
600
818
|
//#endregion
|
|
601
819
|
//#region src/routes/sso.ts
|
|
602
|
-
const AUTHN_REQUEST_KEY_PREFIX = "saml-authn-request:";
|
|
603
|
-
/** Default clock skew tolerance: 5 minutes */
|
|
604
|
-
const DEFAULT_CLOCK_SKEW_MS = 300 * 1e3;
|
|
605
820
|
/**
|
|
606
821
|
* Validates SAML assertion timestamp conditions (NotBefore/NotOnOrAfter).
|
|
607
822
|
* Prevents acceptance of expired or future-dated assertions.
|
|
@@ -641,6 +856,27 @@ function validateSAMLTimestamp(conditions, options = {}) {
|
|
|
641
856
|
});
|
|
642
857
|
}
|
|
643
858
|
}
|
|
859
|
+
/**
|
|
860
|
+
* Extracts the Assertion ID from a SAML response XML.
|
|
861
|
+
* Returns null if the assertion ID cannot be found.
|
|
862
|
+
*/
|
|
863
|
+
function extractAssertionId(samlContent) {
|
|
864
|
+
try {
|
|
865
|
+
const parsed = new XMLParser({
|
|
866
|
+
ignoreAttributes: false,
|
|
867
|
+
attributeNamePrefix: "@_",
|
|
868
|
+
removeNSPrefix: true
|
|
869
|
+
}).parse(samlContent);
|
|
870
|
+
const response = parsed.Response || parsed["samlp:Response"];
|
|
871
|
+
if (!response) return null;
|
|
872
|
+
const rawAssertion = response.Assertion || response["saml:Assertion"];
|
|
873
|
+
const assertion = Array.isArray(rawAssertion) ? rawAssertion[0] : rawAssertion;
|
|
874
|
+
if (!assertion) return null;
|
|
875
|
+
return assertion["@_ID"] || null;
|
|
876
|
+
} catch {
|
|
877
|
+
return null;
|
|
878
|
+
}
|
|
879
|
+
}
|
|
644
880
|
const spMetadataQuerySchema = z.object({
|
|
645
881
|
providerId: z.string(),
|
|
646
882
|
format: z.enum(["xml", "json"]).default("xml")
|
|
@@ -1248,7 +1484,7 @@ const signInSSO = (options) => {
|
|
|
1248
1484
|
});
|
|
1249
1485
|
const loginRequest = sp.createLoginRequest(idp, "redirect");
|
|
1250
1486
|
if (!loginRequest) throw new APIError("BAD_REQUEST", { message: "Invalid SAML request" });
|
|
1251
|
-
if (loginRequest.id &&
|
|
1487
|
+
if (loginRequest.id && options?.saml?.enableInResponseToValidation) {
|
|
1252
1488
|
const ttl = options?.saml?.requestTTL ?? DEFAULT_AUTHN_REQUEST_TTL_MS;
|
|
1253
1489
|
const record = {
|
|
1254
1490
|
id: loginRequest.id,
|
|
@@ -1256,8 +1492,7 @@ const signInSSO = (options) => {
|
|
|
1256
1492
|
createdAt: Date.now(),
|
|
1257
1493
|
expiresAt: Date.now() + ttl
|
|
1258
1494
|
};
|
|
1259
|
-
|
|
1260
|
-
else await ctx.context.internalAdapter.createVerificationValue({
|
|
1495
|
+
await ctx.context.internalAdapter.createVerificationValue({
|
|
1261
1496
|
identifier: `${AUTHN_REQUEST_KEY_PREFIX}${record.id}`,
|
|
1262
1497
|
value: JSON.stringify(record),
|
|
1263
1498
|
expiresAt: new Date(record.expiresAt)
|
|
@@ -1415,37 +1650,20 @@ const callbackSSO = (options) => {
|
|
|
1415
1650
|
token: tokenResponse,
|
|
1416
1651
|
provider
|
|
1417
1652
|
});
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
userInfo,
|
|
1433
|
-
token: tokenResponse,
|
|
1434
|
-
provider
|
|
1435
|
-
}) : options?.organizationProvisioning?.defaultRole || "member";
|
|
1436
|
-
await ctx.context.adapter.create({
|
|
1437
|
-
model: "member",
|
|
1438
|
-
data: {
|
|
1439
|
-
organizationId: provider.organizationId,
|
|
1440
|
-
userId: user.id,
|
|
1441
|
-
role,
|
|
1442
|
-
createdAt: /* @__PURE__ */ new Date(),
|
|
1443
|
-
updatedAt: /* @__PURE__ */ new Date()
|
|
1444
|
-
}
|
|
1445
|
-
});
|
|
1446
|
-
}
|
|
1447
|
-
}
|
|
1448
|
-
}
|
|
1653
|
+
await assignOrganizationFromProvider(ctx, {
|
|
1654
|
+
user,
|
|
1655
|
+
profile: {
|
|
1656
|
+
providerType: "oidc",
|
|
1657
|
+
providerId: provider.providerId,
|
|
1658
|
+
accountId: userInfo.id,
|
|
1659
|
+
email: userInfo.email,
|
|
1660
|
+
emailVerified: Boolean(userInfo.emailVerified),
|
|
1661
|
+
rawAttributes: userInfo
|
|
1662
|
+
},
|
|
1663
|
+
provider,
|
|
1664
|
+
token: tokenResponse,
|
|
1665
|
+
provisioningOptions: options?.organizationProvisioning
|
|
1666
|
+
});
|
|
1449
1667
|
await setSessionCookie(ctx, {
|
|
1450
1668
|
session,
|
|
1451
1669
|
user
|
|
@@ -1567,25 +1785,23 @@ const callbackSSOSAML = (options) => {
|
|
|
1567
1785
|
});
|
|
1568
1786
|
}
|
|
1569
1787
|
const { extract } = parsedResponse;
|
|
1788
|
+
validateSAMLAlgorithms(parsedResponse, options?.saml?.algorithms);
|
|
1570
1789
|
validateSAMLTimestamp(extract.conditions, {
|
|
1571
1790
|
clockSkew: options?.saml?.clockSkew,
|
|
1572
1791
|
requireTimestamps: options?.saml?.requireTimestamps,
|
|
1573
1792
|
logger: ctx.context.logger
|
|
1574
1793
|
});
|
|
1575
1794
|
const inResponseTo = extract.inResponseTo;
|
|
1576
|
-
if (options?.saml?.
|
|
1795
|
+
if (options?.saml?.enableInResponseToValidation) {
|
|
1577
1796
|
const allowIdpInitiated = options?.saml?.allowIdpInitiated !== false;
|
|
1578
1797
|
if (inResponseTo) {
|
|
1579
1798
|
let storedRequest = null;
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
if (
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
} catch {
|
|
1587
|
-
storedRequest = null;
|
|
1588
|
-
}
|
|
1799
|
+
const verification = await ctx.context.internalAdapter.findVerificationValue(`${AUTHN_REQUEST_KEY_PREFIX}${inResponseTo}`);
|
|
1800
|
+
if (verification) try {
|
|
1801
|
+
storedRequest = JSON.parse(verification.value);
|
|
1802
|
+
if (storedRequest && storedRequest.expiresAt < Date.now()) storedRequest = null;
|
|
1803
|
+
} catch {
|
|
1804
|
+
storedRequest = null;
|
|
1589
1805
|
}
|
|
1590
1806
|
if (!storedRequest) {
|
|
1591
1807
|
ctx.context.logger.error("SAML InResponseTo validation failed: unknown or expired request ID", {
|
|
@@ -1601,19 +1817,55 @@ const callbackSSOSAML = (options) => {
|
|
|
1601
1817
|
expectedProvider: storedRequest.providerId,
|
|
1602
1818
|
actualProvider: provider.providerId
|
|
1603
1819
|
});
|
|
1604
|
-
|
|
1605
|
-
else await ctx.context.internalAdapter.deleteVerificationByIdentifier(`${AUTHN_REQUEST_KEY_PREFIX}${inResponseTo}`);
|
|
1820
|
+
await ctx.context.internalAdapter.deleteVerificationByIdentifier(`${AUTHN_REQUEST_KEY_PREFIX}${inResponseTo}`);
|
|
1606
1821
|
const redirectUrl = RelayState || parsedSamlConfig.callbackUrl || ctx.context.baseURL;
|
|
1607
1822
|
throw ctx.redirect(`${redirectUrl}?error=invalid_saml_response&error_description=Provider+mismatch`);
|
|
1608
1823
|
}
|
|
1609
|
-
|
|
1610
|
-
else await ctx.context.internalAdapter.deleteVerificationByIdentifier(`${AUTHN_REQUEST_KEY_PREFIX}${inResponseTo}`);
|
|
1824
|
+
await ctx.context.internalAdapter.deleteVerificationByIdentifier(`${AUTHN_REQUEST_KEY_PREFIX}${inResponseTo}`);
|
|
1611
1825
|
} else if (!allowIdpInitiated) {
|
|
1612
1826
|
ctx.context.logger.error("SAML IdP-initiated SSO rejected: InResponseTo missing and allowIdpInitiated is false", { providerId: provider.providerId });
|
|
1613
1827
|
const redirectUrl = RelayState || parsedSamlConfig.callbackUrl || ctx.context.baseURL;
|
|
1614
1828
|
throw ctx.redirect(`${redirectUrl}?error=unsolicited_response&error_description=IdP-initiated+SSO+not+allowed`);
|
|
1615
1829
|
}
|
|
1616
1830
|
}
|
|
1831
|
+
const samlContent = parsedResponse.samlContent;
|
|
1832
|
+
const assertionId = samlContent ? extractAssertionId(samlContent) : null;
|
|
1833
|
+
if (assertionId) {
|
|
1834
|
+
const issuer = idp.entityMeta.getEntityID();
|
|
1835
|
+
const conditions = extract.conditions;
|
|
1836
|
+
const clockSkew = options?.saml?.clockSkew ?? DEFAULT_CLOCK_SKEW_MS;
|
|
1837
|
+
const expiresAt = conditions?.notOnOrAfter ? new Date(conditions.notOnOrAfter).getTime() + clockSkew : Date.now() + DEFAULT_ASSERTION_TTL_MS;
|
|
1838
|
+
const existingAssertion = await ctx.context.internalAdapter.findVerificationValue(`${USED_ASSERTION_KEY_PREFIX}${assertionId}`);
|
|
1839
|
+
let isReplay = false;
|
|
1840
|
+
if (existingAssertion) try {
|
|
1841
|
+
if (JSON.parse(existingAssertion.value).expiresAt >= Date.now()) isReplay = true;
|
|
1842
|
+
} catch (error) {
|
|
1843
|
+
ctx.context.logger.warn("Failed to parse stored assertion record", {
|
|
1844
|
+
assertionId,
|
|
1845
|
+
error
|
|
1846
|
+
});
|
|
1847
|
+
}
|
|
1848
|
+
if (isReplay) {
|
|
1849
|
+
ctx.context.logger.error("SAML assertion replay detected: assertion ID already used", {
|
|
1850
|
+
assertionId,
|
|
1851
|
+
issuer,
|
|
1852
|
+
providerId: provider.providerId
|
|
1853
|
+
});
|
|
1854
|
+
const redirectUrl = RelayState || parsedSamlConfig.callbackUrl || ctx.context.baseURL;
|
|
1855
|
+
throw ctx.redirect(`${redirectUrl}?error=replay_detected&error_description=SAML+assertion+has+already+been+used`);
|
|
1856
|
+
}
|
|
1857
|
+
await ctx.context.internalAdapter.createVerificationValue({
|
|
1858
|
+
identifier: `${USED_ASSERTION_KEY_PREFIX}${assertionId}`,
|
|
1859
|
+
value: JSON.stringify({
|
|
1860
|
+
assertionId,
|
|
1861
|
+
issuer,
|
|
1862
|
+
providerId: provider.providerId,
|
|
1863
|
+
usedAt: Date.now(),
|
|
1864
|
+
expiresAt
|
|
1865
|
+
}),
|
|
1866
|
+
expiresAt: new Date(expiresAt)
|
|
1867
|
+
});
|
|
1868
|
+
} else ctx.context.logger.warn("Could not extract assertion ID for replay protection", { providerId: provider.providerId });
|
|
1617
1869
|
const attributes = extract.attributes || {};
|
|
1618
1870
|
const mapping = parsedSamlConfig.mapping ?? {};
|
|
1619
1871
|
const userInfo = {
|
|
@@ -1632,100 +1884,49 @@ const callbackSSOSAML = (options) => {
|
|
|
1632
1884
|
});
|
|
1633
1885
|
throw new APIError("BAD_REQUEST", { message: "Unable to extract user ID or email from SAML response" });
|
|
1634
1886
|
}
|
|
1635
|
-
|
|
1636
|
-
const
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
field: "email",
|
|
1640
|
-
value: userInfo.email
|
|
1641
|
-
}]
|
|
1642
|
-
});
|
|
1643
|
-
if (existingUser) {
|
|
1644
|
-
if (!await ctx.context.adapter.findOne({
|
|
1645
|
-
model: "account",
|
|
1646
|
-
where: [
|
|
1647
|
-
{
|
|
1648
|
-
field: "userId",
|
|
1649
|
-
value: existingUser.id
|
|
1650
|
-
},
|
|
1651
|
-
{
|
|
1652
|
-
field: "providerId",
|
|
1653
|
-
value: provider.providerId
|
|
1654
|
-
},
|
|
1655
|
-
{
|
|
1656
|
-
field: "accountId",
|
|
1657
|
-
value: userInfo.id
|
|
1658
|
-
}
|
|
1659
|
-
]
|
|
1660
|
-
})) {
|
|
1661
|
-
if (!(ctx.context.options.account?.accountLinking?.trustedProviders?.includes(provider.providerId) || "domainVerified" in provider && provider.domainVerified && validateEmailDomain(userInfo.email, provider.domain))) {
|
|
1662
|
-
const redirectUrl = RelayState || parsedSamlConfig.callbackUrl || ctx.context.baseURL;
|
|
1663
|
-
throw ctx.redirect(`${redirectUrl}?error=account_not_linked`);
|
|
1664
|
-
}
|
|
1665
|
-
await ctx.context.internalAdapter.createAccount({
|
|
1666
|
-
userId: existingUser.id,
|
|
1667
|
-
providerId: provider.providerId,
|
|
1668
|
-
accountId: userInfo.id,
|
|
1669
|
-
accessToken: "",
|
|
1670
|
-
refreshToken: ""
|
|
1671
|
-
});
|
|
1672
|
-
}
|
|
1673
|
-
user = existingUser;
|
|
1674
|
-
} else {
|
|
1675
|
-
if (options?.disableImplicitSignUp) throw new APIError("UNAUTHORIZED", { message: "User not found and implicit sign up is disabled for this provider" });
|
|
1676
|
-
user = await ctx.context.internalAdapter.createUser({
|
|
1887
|
+
const isTrustedProvider = !!ctx.context.options.account?.accountLinking?.trustedProviders?.includes(provider.providerId) || "domainVerified" in provider && !!provider.domainVerified && validateEmailDomain(userInfo.email, provider.domain);
|
|
1888
|
+
const callbackUrl = RelayState || parsedSamlConfig.callbackUrl || ctx.context.baseURL;
|
|
1889
|
+
const result = await handleOAuthUserInfo(ctx, {
|
|
1890
|
+
userInfo: {
|
|
1677
1891
|
email: userInfo.email,
|
|
1678
|
-
name: userInfo.name,
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1892
|
+
name: userInfo.name || userInfo.email,
|
|
1893
|
+
id: userInfo.id,
|
|
1894
|
+
emailVerified: Boolean(userInfo.emailVerified)
|
|
1895
|
+
},
|
|
1896
|
+
account: {
|
|
1683
1897
|
providerId: provider.providerId,
|
|
1684
1898
|
accountId: userInfo.id,
|
|
1685
1899
|
accessToken: "",
|
|
1686
1900
|
refreshToken: ""
|
|
1687
|
-
}
|
|
1688
|
-
|
|
1901
|
+
},
|
|
1902
|
+
callbackURL: callbackUrl,
|
|
1903
|
+
disableSignUp: options?.disableImplicitSignUp,
|
|
1904
|
+
isTrustedProvider
|
|
1905
|
+
});
|
|
1906
|
+
if (result.error) throw ctx.redirect(`${callbackUrl}?error=${result.error.split(" ").join("_")}`);
|
|
1907
|
+
const { session, user } = result.data;
|
|
1689
1908
|
if (options?.provisionUser) await options.provisionUser({
|
|
1690
1909
|
user,
|
|
1691
1910
|
userInfo,
|
|
1692
1911
|
provider
|
|
1693
1912
|
});
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
user,
|
|
1708
|
-
userInfo,
|
|
1709
|
-
provider
|
|
1710
|
-
}) : options?.organizationProvisioning?.defaultRole || "member";
|
|
1711
|
-
await ctx.context.adapter.create({
|
|
1712
|
-
model: "member",
|
|
1713
|
-
data: {
|
|
1714
|
-
organizationId: provider.organizationId,
|
|
1715
|
-
userId: user.id,
|
|
1716
|
-
role,
|
|
1717
|
-
createdAt: /* @__PURE__ */ new Date(),
|
|
1718
|
-
updatedAt: /* @__PURE__ */ new Date()
|
|
1719
|
-
}
|
|
1720
|
-
});
|
|
1721
|
-
}
|
|
1722
|
-
}
|
|
1723
|
-
}
|
|
1913
|
+
await assignOrganizationFromProvider(ctx, {
|
|
1914
|
+
user,
|
|
1915
|
+
profile: {
|
|
1916
|
+
providerType: "saml",
|
|
1917
|
+
providerId: provider.providerId,
|
|
1918
|
+
accountId: userInfo.id,
|
|
1919
|
+
email: userInfo.email,
|
|
1920
|
+
emailVerified: Boolean(userInfo.emailVerified),
|
|
1921
|
+
rawAttributes: attributes
|
|
1922
|
+
},
|
|
1923
|
+
provider,
|
|
1924
|
+
provisioningOptions: options?.organizationProvisioning
|
|
1925
|
+
});
|
|
1724
1926
|
await setSessionCookie(ctx, {
|
|
1725
|
-
session
|
|
1927
|
+
session,
|
|
1726
1928
|
user
|
|
1727
1929
|
});
|
|
1728
|
-
const callbackUrl = RelayState || parsedSamlConfig.callbackUrl || ctx.context.baseURL;
|
|
1729
1930
|
throw ctx.redirect(callbackUrl);
|
|
1730
1931
|
});
|
|
1731
1932
|
};
|
|
@@ -1818,25 +2019,23 @@ const acsEndpoint = (options) => {
|
|
|
1818
2019
|
});
|
|
1819
2020
|
}
|
|
1820
2021
|
const { extract } = parsedResponse;
|
|
2022
|
+
validateSAMLAlgorithms(parsedResponse, options?.saml?.algorithms);
|
|
1821
2023
|
validateSAMLTimestamp(extract.conditions, {
|
|
1822
2024
|
clockSkew: options?.saml?.clockSkew,
|
|
1823
2025
|
requireTimestamps: options?.saml?.requireTimestamps,
|
|
1824
2026
|
logger: ctx.context.logger
|
|
1825
2027
|
});
|
|
1826
2028
|
const inResponseToAcs = extract.inResponseTo;
|
|
1827
|
-
if (options?.saml?.
|
|
2029
|
+
if (options?.saml?.enableInResponseToValidation) {
|
|
1828
2030
|
const allowIdpInitiated = options?.saml?.allowIdpInitiated !== false;
|
|
1829
2031
|
if (inResponseToAcs) {
|
|
1830
2032
|
let storedRequest = null;
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
if (
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
} catch {
|
|
1838
|
-
storedRequest = null;
|
|
1839
|
-
}
|
|
2033
|
+
const verification = await ctx.context.internalAdapter.findVerificationValue(`${AUTHN_REQUEST_KEY_PREFIX}${inResponseToAcs}`);
|
|
2034
|
+
if (verification) try {
|
|
2035
|
+
storedRequest = JSON.parse(verification.value);
|
|
2036
|
+
if (storedRequest && storedRequest.expiresAt < Date.now()) storedRequest = null;
|
|
2037
|
+
} catch {
|
|
2038
|
+
storedRequest = null;
|
|
1840
2039
|
}
|
|
1841
2040
|
if (!storedRequest) {
|
|
1842
2041
|
ctx.context.logger.error("SAML InResponseTo validation failed: unknown or expired request ID", {
|
|
@@ -1852,19 +2051,54 @@ const acsEndpoint = (options) => {
|
|
|
1852
2051
|
expectedProvider: storedRequest.providerId,
|
|
1853
2052
|
actualProvider: providerId
|
|
1854
2053
|
});
|
|
1855
|
-
|
|
1856
|
-
else await ctx.context.internalAdapter.deleteVerificationByIdentifier(`${AUTHN_REQUEST_KEY_PREFIX}${inResponseToAcs}`);
|
|
2054
|
+
await ctx.context.internalAdapter.deleteVerificationByIdentifier(`${AUTHN_REQUEST_KEY_PREFIX}${inResponseToAcs}`);
|
|
1857
2055
|
const redirectUrl = RelayState || parsedSamlConfig.callbackUrl || ctx.context.baseURL;
|
|
1858
2056
|
throw ctx.redirect(`${redirectUrl}?error=invalid_saml_response&error_description=Provider+mismatch`);
|
|
1859
2057
|
}
|
|
1860
|
-
|
|
1861
|
-
else await ctx.context.internalAdapter.deleteVerificationByIdentifier(`${AUTHN_REQUEST_KEY_PREFIX}${inResponseToAcs}`);
|
|
2058
|
+
await ctx.context.internalAdapter.deleteVerificationByIdentifier(`${AUTHN_REQUEST_KEY_PREFIX}${inResponseToAcs}`);
|
|
1862
2059
|
} else if (!allowIdpInitiated) {
|
|
1863
2060
|
ctx.context.logger.error("SAML IdP-initiated SSO rejected: InResponseTo missing and allowIdpInitiated is false", { providerId });
|
|
1864
2061
|
const redirectUrl = RelayState || parsedSamlConfig.callbackUrl || ctx.context.baseURL;
|
|
1865
2062
|
throw ctx.redirect(`${redirectUrl}?error=unsolicited_response&error_description=IdP-initiated+SSO+not+allowed`);
|
|
1866
2063
|
}
|
|
1867
2064
|
}
|
|
2065
|
+
const assertionIdAcs = extractAssertionId(Buffer.from(SAMLResponse, "base64").toString("utf-8"));
|
|
2066
|
+
if (assertionIdAcs) {
|
|
2067
|
+
const issuer = idp.entityMeta.getEntityID();
|
|
2068
|
+
const conditions = extract.conditions;
|
|
2069
|
+
const clockSkew = options?.saml?.clockSkew ?? DEFAULT_CLOCK_SKEW_MS;
|
|
2070
|
+
const expiresAt = conditions?.notOnOrAfter ? new Date(conditions.notOnOrAfter).getTime() + clockSkew : Date.now() + DEFAULT_ASSERTION_TTL_MS;
|
|
2071
|
+
const existingAssertion = await ctx.context.internalAdapter.findVerificationValue(`${USED_ASSERTION_KEY_PREFIX}${assertionIdAcs}`);
|
|
2072
|
+
let isReplay = false;
|
|
2073
|
+
if (existingAssertion) try {
|
|
2074
|
+
if (JSON.parse(existingAssertion.value).expiresAt >= Date.now()) isReplay = true;
|
|
2075
|
+
} catch (error) {
|
|
2076
|
+
ctx.context.logger.warn("Failed to parse stored assertion record", {
|
|
2077
|
+
assertionId: assertionIdAcs,
|
|
2078
|
+
error
|
|
2079
|
+
});
|
|
2080
|
+
}
|
|
2081
|
+
if (isReplay) {
|
|
2082
|
+
ctx.context.logger.error("SAML assertion replay detected: assertion ID already used", {
|
|
2083
|
+
assertionId: assertionIdAcs,
|
|
2084
|
+
issuer,
|
|
2085
|
+
providerId
|
|
2086
|
+
});
|
|
2087
|
+
const redirectUrl = RelayState || parsedSamlConfig.callbackUrl || ctx.context.baseURL;
|
|
2088
|
+
throw ctx.redirect(`${redirectUrl}?error=replay_detected&error_description=SAML+assertion+has+already+been+used`);
|
|
2089
|
+
}
|
|
2090
|
+
await ctx.context.internalAdapter.createVerificationValue({
|
|
2091
|
+
identifier: `${USED_ASSERTION_KEY_PREFIX}${assertionIdAcs}`,
|
|
2092
|
+
value: JSON.stringify({
|
|
2093
|
+
assertionId: assertionIdAcs,
|
|
2094
|
+
issuer,
|
|
2095
|
+
providerId,
|
|
2096
|
+
usedAt: Date.now(),
|
|
2097
|
+
expiresAt
|
|
2098
|
+
}),
|
|
2099
|
+
expiresAt: new Date(expiresAt)
|
|
2100
|
+
});
|
|
2101
|
+
} else ctx.context.logger.warn("Could not extract assertion ID for replay protection", { providerId });
|
|
1868
2102
|
const attributes = extract.attributes || {};
|
|
1869
2103
|
const mapping = parsedSamlConfig.mapping ?? {};
|
|
1870
2104
|
const userInfo = {
|
|
@@ -1883,99 +2117,49 @@ const acsEndpoint = (options) => {
|
|
|
1883
2117
|
});
|
|
1884
2118
|
throw new APIError("BAD_REQUEST", { message: "Unable to extract user ID or email from SAML response" });
|
|
1885
2119
|
}
|
|
1886
|
-
|
|
1887
|
-
const
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
field: "email",
|
|
1891
|
-
value: userInfo.email
|
|
1892
|
-
}]
|
|
1893
|
-
});
|
|
1894
|
-
if (existingUser) {
|
|
1895
|
-
if (!await ctx.context.adapter.findOne({
|
|
1896
|
-
model: "account",
|
|
1897
|
-
where: [
|
|
1898
|
-
{
|
|
1899
|
-
field: "userId",
|
|
1900
|
-
value: existingUser.id
|
|
1901
|
-
},
|
|
1902
|
-
{
|
|
1903
|
-
field: "providerId",
|
|
1904
|
-
value: provider.providerId
|
|
1905
|
-
},
|
|
1906
|
-
{
|
|
1907
|
-
field: "accountId",
|
|
1908
|
-
value: userInfo.id
|
|
1909
|
-
}
|
|
1910
|
-
]
|
|
1911
|
-
})) {
|
|
1912
|
-
if (!(ctx.context.options.account?.accountLinking?.trustedProviders?.includes(provider.providerId) || "domainVerified" in provider && provider.domainVerified && validateEmailDomain(userInfo.email, provider.domain))) throw ctx.redirect(`${parsedSamlConfig.callbackUrl}?error=account_not_found`);
|
|
1913
|
-
await ctx.context.internalAdapter.createAccount({
|
|
1914
|
-
userId: existingUser.id,
|
|
1915
|
-
providerId: provider.providerId,
|
|
1916
|
-
accountId: userInfo.id,
|
|
1917
|
-
accessToken: "",
|
|
1918
|
-
refreshToken: ""
|
|
1919
|
-
});
|
|
1920
|
-
}
|
|
1921
|
-
user = existingUser;
|
|
1922
|
-
} else {
|
|
1923
|
-
user = await ctx.context.internalAdapter.createUser({
|
|
2120
|
+
const isTrustedProvider = !!ctx.context.options.account?.accountLinking?.trustedProviders?.includes(provider.providerId) || "domainVerified" in provider && !!provider.domainVerified && validateEmailDomain(userInfo.email, provider.domain);
|
|
2121
|
+
const callbackUrl = RelayState || parsedSamlConfig.callbackUrl || ctx.context.baseURL;
|
|
2122
|
+
const result = await handleOAuthUserInfo(ctx, {
|
|
2123
|
+
userInfo: {
|
|
1924
2124
|
email: userInfo.email,
|
|
1925
|
-
name: userInfo.name,
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
2125
|
+
name: userInfo.name || userInfo.email,
|
|
2126
|
+
id: userInfo.id,
|
|
2127
|
+
emailVerified: Boolean(userInfo.emailVerified)
|
|
2128
|
+
},
|
|
2129
|
+
account: {
|
|
1930
2130
|
providerId: provider.providerId,
|
|
1931
2131
|
accountId: userInfo.id,
|
|
1932
2132
|
accessToken: "",
|
|
1933
|
-
refreshToken: ""
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
}
|
|
2133
|
+
refreshToken: ""
|
|
2134
|
+
},
|
|
2135
|
+
callbackURL: callbackUrl,
|
|
2136
|
+
disableSignUp: options?.disableImplicitSignUp,
|
|
2137
|
+
isTrustedProvider
|
|
2138
|
+
});
|
|
2139
|
+
if (result.error) throw ctx.redirect(`${callbackUrl}?error=${result.error.split(" ").join("_")}`);
|
|
2140
|
+
const { session, user } = result.data;
|
|
1939
2141
|
if (options?.provisionUser) await options.provisionUser({
|
|
1940
2142
|
user,
|
|
1941
2143
|
userInfo,
|
|
1942
2144
|
provider
|
|
1943
2145
|
});
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
user,
|
|
1958
|
-
userInfo,
|
|
1959
|
-
provider
|
|
1960
|
-
}) : options?.organizationProvisioning?.defaultRole || "member";
|
|
1961
|
-
await ctx.context.adapter.create({
|
|
1962
|
-
model: "member",
|
|
1963
|
-
data: {
|
|
1964
|
-
organizationId: provider.organizationId,
|
|
1965
|
-
userId: user.id,
|
|
1966
|
-
role,
|
|
1967
|
-
createdAt: /* @__PURE__ */ new Date(),
|
|
1968
|
-
updatedAt: /* @__PURE__ */ new Date()
|
|
1969
|
-
}
|
|
1970
|
-
});
|
|
1971
|
-
}
|
|
1972
|
-
}
|
|
1973
|
-
}
|
|
2146
|
+
await assignOrganizationFromProvider(ctx, {
|
|
2147
|
+
user,
|
|
2148
|
+
profile: {
|
|
2149
|
+
providerType: "saml",
|
|
2150
|
+
providerId: provider.providerId,
|
|
2151
|
+
accountId: userInfo.id,
|
|
2152
|
+
email: userInfo.email,
|
|
2153
|
+
emailVerified: Boolean(userInfo.emailVerified),
|
|
2154
|
+
rawAttributes: attributes
|
|
2155
|
+
},
|
|
2156
|
+
provider,
|
|
2157
|
+
provisioningOptions: options?.organizationProvisioning
|
|
2158
|
+
});
|
|
1974
2159
|
await setSessionCookie(ctx, {
|
|
1975
|
-
session
|
|
2160
|
+
session,
|
|
1976
2161
|
user
|
|
1977
2162
|
});
|
|
1978
|
-
const callbackUrl = RelayState || parsedSamlConfig.callbackUrl || ctx.context.baseURL;
|
|
1979
2163
|
throw ctx.redirect(callbackUrl);
|
|
1980
2164
|
});
|
|
1981
2165
|
};
|
|
@@ -2009,6 +2193,20 @@ function sso(options) {
|
|
|
2009
2193
|
return {
|
|
2010
2194
|
id: "sso",
|
|
2011
2195
|
endpoints,
|
|
2196
|
+
hooks: { after: [{
|
|
2197
|
+
matcher(context) {
|
|
2198
|
+
return context.path?.startsWith("/callback/") ?? false;
|
|
2199
|
+
},
|
|
2200
|
+
handler: createAuthMiddleware(async (ctx) => {
|
|
2201
|
+
const newSession = ctx.context.newSession;
|
|
2202
|
+
if (!newSession?.user) return;
|
|
2203
|
+
if (!ctx.context.options.plugins?.find((plugin) => plugin.id === "organization")) return;
|
|
2204
|
+
await assignOrganizationByDomain(ctx, {
|
|
2205
|
+
user: newSession.user,
|
|
2206
|
+
provisioningOptions: options?.organizationProvisioning
|
|
2207
|
+
});
|
|
2208
|
+
})
|
|
2209
|
+
}] },
|
|
2012
2210
|
schema: { ssoProvider: {
|
|
2013
2211
|
modelName: options?.modelName ?? "ssoProvider",
|
|
2014
2212
|
fields: {
|
|
@@ -2061,4 +2259,4 @@ function sso(options) {
|
|
|
2061
2259
|
}
|
|
2062
2260
|
|
|
2063
2261
|
//#endregion
|
|
2064
|
-
export {
|
|
2262
|
+
export { DataEncryptionAlgorithm, DigestAlgorithm, DiscoveryError, KeyEncryptionAlgorithm, REQUIRED_DISCOVERY_FIELDS, SignatureAlgorithm, computeDiscoveryUrl, discoverOIDCConfig, fetchDiscoveryDocument, needsRuntimeDiscovery, normalizeDiscoveryUrls, normalizeUrl, selectTokenEndpointAuthMethod, sso, validateDiscoveryDocument, validateDiscoveryUrl, validateSAMLTimestamp };
|