@better-auth/sso 1.4.18 → 1.4.19
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 +7 -7
- package/dist/client.d.mts +1 -1
- package/dist/{index-C4nbdf2g.d.mts → index-D-VInsst.d.mts} +7 -3
- package/dist/index.d.mts +1 -1
- package/dist/index.mjs +97 -62
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -3
- package/src/domain-verification.test.ts +46 -4
- package/src/linking/org-assignment.ts +2 -2
- package/src/oidc.test.ts +1 -3
- package/src/routes/domain-verification.ts +34 -12
- package/src/routes/sso.ts +131 -93
- package/src/saml-state.ts +1 -1
- package/src/saml.test.ts +392 -0
- package/src/types.ts +6 -2
package/src/saml.test.ts
CHANGED
|
@@ -574,6 +574,244 @@ describe("SAML SSO with defaultSSO array", async () => {
|
|
|
574
574
|
});
|
|
575
575
|
});
|
|
576
576
|
|
|
577
|
+
describe("SAML SSO with signed AuthnRequests", async () => {
|
|
578
|
+
// IdP metadata with WantAuthnRequestsSigned="true" for testing signed requests
|
|
579
|
+
const idpMetadataWithSignedRequests = `
|
|
580
|
+
<md:EntityDescriptor xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata" entityID="http://localhost:8081/api/sso/saml2/idp/metadata">
|
|
581
|
+
<md:IDPSSODescriptor WantAuthnRequestsSigned="true" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
|
|
582
|
+
<md:KeyDescriptor use="signing">
|
|
583
|
+
<ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
|
|
584
|
+
<ds:X509Data>
|
|
585
|
+
<ds:X509Certificate>MIIFOjCCAyICCQCqP5DN+xQZDjANBgkqhkiG9w0BAQsFADBfMQswCQYDVQQGEwJVUzEQMA4GA1UECAwHRmxvcmlkYTEQMA4GA1UEBwwHT3JsYW5kbzENMAsGA1UECgwEVGVzdDEdMBsGCSqGSIb3DQEJARYOdGVzdEBnbWFpbC5jb20wHhcNMjMxMTE5MTIzNzE3WhcNMzMxMTE2MTIzNzE3WjBfMQswCQYDVQQGEwJVUzEQMA4GA1UECAwHRmxvcmlkYTEQMA4GA1UEBwwHT3JsYW5kbzENMAsGA1UECgwEVGVzdDEdMBsGCSqGSIb3DQEJARYOdGVzdEBnbWFpbC5jb20wggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQD5giLoLyED41IHt0RxB/k6x4K0vzAKiGecPyedRNR1oyiv3OYkuG5jgTE2wcPZc7kD1Eg5d6th0BWHy/ovaNS5mkgnOV6jKkMaWW4sCMSnLnaWy0seftPK3O4mNeZpM5e9amj2gXnZvKrK8cqnJ/bsUUQvXxttXNVVmOHWg/t3c2vJ4XuUfph6wIKbrj297ILzuAFRNvAVxeS0tElwepvZ5Wbf7Hc1MORAqTpw/mp8cRjHRzYCA9y6OM4hgVs1gvTJS8WGoMmsdAZHaOnv9vLJvW3jDLQQecOheYIJncWgcESzJFIkmXadorYCEfWhwwBdVphknmeLr4BMpJBclAYaFjYDLIKpMcXYO5k/2r3BgSPlw4oqbxbR5geD05myKYtZ/wNUtku118NjhIfJFulU/kfDcp1rYYkvzgBfqr80wgNps4oQzVr1mnpgHsSTAhXMuZbaTByJRmPqecyvyQqRQcRIN0oTLJNGyzoUf0RkH6DKJ4+7qDhlq4Zhlfso9OFMv9xeONfIrJo5HtTfFZfidkXZqir2ZqwqNlNOMfK5DsYq37x2Gkgqig4nqLpITXyxfnQpL2HsaoFrlctt/OL+Zqba7NT4heYk9GX8qlAS+Ipsv6T2HSANbah55oSS3uvcrDOug2Zq7+GYMLKS1IKUKhwX+wLMxmMwSJQ9ZgFwfQIDAQABMA0GCSqGSIb3DQEBCwUAA4ICAQCkGPZdflocTSXIe5bbehsBn/IPdyb38eH2HaAvWqO2XNcDcq+6/uLc8BVK4JMa3AFS9xtBza7MOXN/lw/Ccb8uJGVNUE31+rTvsJaDtMCQkp+9aG04I1BonEHfSB0ANcTy/Gp+4hKyFCd6x35uyPO7CWX5Z8I87q9LF6Dte3/v1j7VZgDjAi9yHpBJv9Xje33AK1vF+WmEfDUOi8y2B8htVeoyS3owln3ZUbnmJdCmMp2BMRq63ymINwklEaYaNrp1L201bSqNdKZF2sNwROWyDX+WFYgufrnzPYb6HS8gYb4oEZmaG5cBM7Hs730/3BlbHKhxNTy1Io2TVCYcMQD+ieiVg5e5eGTwaPYGuVvY3NVhO8FaYBG7K2NT2hqutdCMaQpGyHEzbbbTY1afhbeMmWWqivRnVJNDv4kgBc2SE8JO82qHikIW9Om0cghC5xwTT+1JTtxxD1KeC1M1IwLzzuuMmwJSKAsv4duDqN+YRIP78J2SlrssqlsmoF8+48e7Vzr7JRT/Ya274P8RpUPNtxTR7WDmZ4tunqXjiBpz6l0uTtVXnj5UBo4HCyRjWJOGf15OCuQX03qz8tKn1IbZUf723qrmSF+cxBwHqpAywqhTSsaLjIXKnQ0UlMov7QWb0a5N07JZMdMSerbHvbXd/z9S1Ssea2+EGuTYuQur3A==</ds:X509Certificate>
|
|
586
|
+
</ds:X509Data>
|
|
587
|
+
</ds:KeyInfo>
|
|
588
|
+
</md:KeyDescriptor>
|
|
589
|
+
<md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="http://localhost:8081/api/sso/saml2/idp/redirect"/>
|
|
590
|
+
<md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="http://localhost:8081/api/sso/saml2/idp/post"/>
|
|
591
|
+
</md:IDPSSODescriptor>
|
|
592
|
+
</md:EntityDescriptor>
|
|
593
|
+
`;
|
|
594
|
+
|
|
595
|
+
const data = {
|
|
596
|
+
user: [],
|
|
597
|
+
session: [],
|
|
598
|
+
verification: [],
|
|
599
|
+
account: [],
|
|
600
|
+
ssoProvider: [],
|
|
601
|
+
};
|
|
602
|
+
|
|
603
|
+
const memory = memoryAdapter(data);
|
|
604
|
+
|
|
605
|
+
const ssoOptions = {
|
|
606
|
+
defaultSSO: [
|
|
607
|
+
{
|
|
608
|
+
domain: "localhost:8081",
|
|
609
|
+
providerId: "signed-saml",
|
|
610
|
+
samlConfig: {
|
|
611
|
+
issuer: "http://localhost:8081",
|
|
612
|
+
entryPoint: "http://localhost:8081/api/sso/saml2/idp/post",
|
|
613
|
+
cert: certificate,
|
|
614
|
+
callbackUrl: "http://localhost:8081/dashboard",
|
|
615
|
+
wantAssertionsSigned: false,
|
|
616
|
+
authnRequestsSigned: true,
|
|
617
|
+
signatureAlgorithm: "sha256",
|
|
618
|
+
digestAlgorithm: "sha256",
|
|
619
|
+
privateKey: idPk,
|
|
620
|
+
spMetadata: {
|
|
621
|
+
privateKey: idPk,
|
|
622
|
+
},
|
|
623
|
+
idpMetadata: {
|
|
624
|
+
metadata: idpMetadataWithSignedRequests,
|
|
625
|
+
},
|
|
626
|
+
identifierFormat:
|
|
627
|
+
"urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress",
|
|
628
|
+
},
|
|
629
|
+
},
|
|
630
|
+
],
|
|
631
|
+
};
|
|
632
|
+
|
|
633
|
+
const auth = betterAuth({
|
|
634
|
+
database: memory,
|
|
635
|
+
baseURL: "http://localhost:3000",
|
|
636
|
+
emailAndPassword: {
|
|
637
|
+
enabled: true,
|
|
638
|
+
},
|
|
639
|
+
plugins: [sso(ssoOptions)],
|
|
640
|
+
});
|
|
641
|
+
|
|
642
|
+
it("should generate signed AuthnRequest when authnRequestsSigned is true", async () => {
|
|
643
|
+
const signInResponse = await auth.api.signInSSO({
|
|
644
|
+
body: {
|
|
645
|
+
providerId: "signed-saml",
|
|
646
|
+
callbackURL: "http://localhost:3000/dashboard",
|
|
647
|
+
},
|
|
648
|
+
});
|
|
649
|
+
|
|
650
|
+
expect(signInResponse).toEqual({
|
|
651
|
+
url: expect.stringContaining("http://localhost:8081"),
|
|
652
|
+
redirect: true,
|
|
653
|
+
});
|
|
654
|
+
// When authnRequestsSigned is true and privateKey is provided,
|
|
655
|
+
// samlify adds Signature and SigAlg parameters to the redirect URL
|
|
656
|
+
expect(signInResponse.url).toContain("Signature=");
|
|
657
|
+
expect(signInResponse.url).toContain("SigAlg=");
|
|
658
|
+
});
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
describe("SAML SSO without signed AuthnRequests", async () => {
|
|
662
|
+
const data = {
|
|
663
|
+
user: [],
|
|
664
|
+
session: [],
|
|
665
|
+
verification: [],
|
|
666
|
+
account: [],
|
|
667
|
+
ssoProvider: [],
|
|
668
|
+
};
|
|
669
|
+
|
|
670
|
+
const memory = memoryAdapter(data);
|
|
671
|
+
|
|
672
|
+
const ssoOptions = {
|
|
673
|
+
defaultSSO: [
|
|
674
|
+
{
|
|
675
|
+
domain: "localhost:8082",
|
|
676
|
+
providerId: "unsigned-saml",
|
|
677
|
+
samlConfig: {
|
|
678
|
+
issuer: "http://localhost:8082",
|
|
679
|
+
entryPoint: "http://localhost:8081/api/sso/saml2/idp/post",
|
|
680
|
+
cert: certificate,
|
|
681
|
+
callbackUrl: "http://localhost:8082/dashboard",
|
|
682
|
+
wantAssertionsSigned: false,
|
|
683
|
+
authnRequestsSigned: false,
|
|
684
|
+
signatureAlgorithm: "sha256",
|
|
685
|
+
digestAlgorithm: "sha256",
|
|
686
|
+
idpMetadata: {
|
|
687
|
+
metadata: idpMetadata,
|
|
688
|
+
},
|
|
689
|
+
spMetadata: {
|
|
690
|
+
metadata: spMetadata,
|
|
691
|
+
},
|
|
692
|
+
identifierFormat:
|
|
693
|
+
"urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress",
|
|
694
|
+
},
|
|
695
|
+
},
|
|
696
|
+
],
|
|
697
|
+
};
|
|
698
|
+
|
|
699
|
+
const auth = betterAuth({
|
|
700
|
+
database: memory,
|
|
701
|
+
baseURL: "http://localhost:3000",
|
|
702
|
+
emailAndPassword: {
|
|
703
|
+
enabled: true,
|
|
704
|
+
},
|
|
705
|
+
plugins: [sso(ssoOptions)],
|
|
706
|
+
});
|
|
707
|
+
|
|
708
|
+
it("should NOT include Signature in URL when authnRequestsSigned is false", async () => {
|
|
709
|
+
const signInResponse = await auth.api.signInSSO({
|
|
710
|
+
body: {
|
|
711
|
+
providerId: "unsigned-saml",
|
|
712
|
+
callbackURL: "http://localhost:3000/dashboard",
|
|
713
|
+
},
|
|
714
|
+
});
|
|
715
|
+
|
|
716
|
+
expect(signInResponse).toEqual({
|
|
717
|
+
url: expect.stringContaining("http://localhost:8081"),
|
|
718
|
+
redirect: true,
|
|
719
|
+
});
|
|
720
|
+
// When authnRequestsSigned is false (default), no Signature should be in the URL
|
|
721
|
+
expect(signInResponse.url).not.toContain("Signature=");
|
|
722
|
+
expect(signInResponse.url).not.toContain("SigAlg=");
|
|
723
|
+
});
|
|
724
|
+
});
|
|
725
|
+
|
|
726
|
+
describe("SAML SSO with idpMetadata but without metadata XML (fallback to top-level config)", async () => {
|
|
727
|
+
const data = {
|
|
728
|
+
user: [],
|
|
729
|
+
session: [],
|
|
730
|
+
verification: [],
|
|
731
|
+
account: [],
|
|
732
|
+
ssoProvider: [],
|
|
733
|
+
};
|
|
734
|
+
|
|
735
|
+
const memory = memoryAdapter(data);
|
|
736
|
+
|
|
737
|
+
// This tests the fix for signInSSO where IdentityProvider was incorrectly constructed
|
|
738
|
+
// when idpMetadata is provided but without a full metadata XML.
|
|
739
|
+
// The bug was:
|
|
740
|
+
// 1. Using encryptCert instead of signingCert (samlify expects signingCert)
|
|
741
|
+
// 2. Not falling back to parsedSamlConfig.issuer when entityID is missing
|
|
742
|
+
// 3. Not falling back to parsedSamlConfig.entryPoint when singleSignOnService is missing
|
|
743
|
+
const ssoOptions = {
|
|
744
|
+
defaultSSO: [
|
|
745
|
+
{
|
|
746
|
+
domain: "localhost:8083",
|
|
747
|
+
providerId: "partial-idp-metadata-saml",
|
|
748
|
+
samlConfig: {
|
|
749
|
+
issuer: "http://localhost:8083/issuer",
|
|
750
|
+
entryPoint: "http://localhost:8081/api/sso/saml2/idp/redirect",
|
|
751
|
+
cert: certificate,
|
|
752
|
+
callbackUrl: "http://localhost:8083/dashboard",
|
|
753
|
+
wantAssertionsSigned: false,
|
|
754
|
+
authnRequestsSigned: false,
|
|
755
|
+
spMetadata: {},
|
|
756
|
+
// idpMetadata is provided but WITHOUT metadata XML - this triggers the fallback path
|
|
757
|
+
// The fix ensures signingCert is used (not encryptCert) and entryPoint/issuer fallbacks work
|
|
758
|
+
idpMetadata: {
|
|
759
|
+
// No metadata XML provided
|
|
760
|
+
// cert could be provided here, but we test fallback to top-level cert
|
|
761
|
+
entityID: "http://localhost:8081/custom-entity-id",
|
|
762
|
+
},
|
|
763
|
+
identifierFormat:
|
|
764
|
+
"urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress",
|
|
765
|
+
},
|
|
766
|
+
},
|
|
767
|
+
],
|
|
768
|
+
};
|
|
769
|
+
|
|
770
|
+
const auth = betterAuth({
|
|
771
|
+
database: memory,
|
|
772
|
+
baseURL: "http://localhost:3000",
|
|
773
|
+
emailAndPassword: {
|
|
774
|
+
enabled: true,
|
|
775
|
+
},
|
|
776
|
+
plugins: [sso(ssoOptions)],
|
|
777
|
+
});
|
|
778
|
+
|
|
779
|
+
it("should initiate SAML login using fallback entryPoint when idpMetadata has no metadata XML", async () => {
|
|
780
|
+
const signInResponse = await auth.api.signInSSO({
|
|
781
|
+
body: {
|
|
782
|
+
providerId: "partial-idp-metadata-saml",
|
|
783
|
+
callbackURL: "http://localhost:3000/dashboard",
|
|
784
|
+
},
|
|
785
|
+
});
|
|
786
|
+
|
|
787
|
+
// The URL should point to the entryPoint from top-level config (fallback)
|
|
788
|
+
expect(signInResponse).toEqual({
|
|
789
|
+
url: expect.stringContaining(
|
|
790
|
+
"http://localhost:8081/api/sso/saml2/idp/redirect",
|
|
791
|
+
),
|
|
792
|
+
redirect: true,
|
|
793
|
+
});
|
|
794
|
+
// The URL should contain a SAMLRequest parameter, proving the IdP was constructed correctly
|
|
795
|
+
// with signingCert (not encryptCert) - if encryptCert was used, samlify would fail
|
|
796
|
+
expect(signInResponse.url).toContain("SAMLRequest=");
|
|
797
|
+
});
|
|
798
|
+
|
|
799
|
+
it("should use idpMetadata.entityID when provided (not fall back to issuer)", async () => {
|
|
800
|
+
const signInResponse = await auth.api.signInSSO({
|
|
801
|
+
body: {
|
|
802
|
+
providerId: "partial-idp-metadata-saml",
|
|
803
|
+
callbackURL: "http://localhost:3000/dashboard",
|
|
804
|
+
},
|
|
805
|
+
});
|
|
806
|
+
|
|
807
|
+
// The fact that we get a valid SAMLRequest proves the IdentityProvider
|
|
808
|
+
// was constructed correctly. The entityID from idpMetadata should be used.
|
|
809
|
+
const url = new URL(signInResponse.url);
|
|
810
|
+
const samlRequest = url.searchParams.get("SAMLRequest");
|
|
811
|
+
expect(samlRequest).toBeTruthy();
|
|
812
|
+
});
|
|
813
|
+
});
|
|
814
|
+
|
|
577
815
|
describe("SAML SSO", async () => {
|
|
578
816
|
const data = {
|
|
579
817
|
user: [],
|
|
@@ -1859,6 +2097,160 @@ describe("SAML SSO", async () => {
|
|
|
1859
2097
|
const redirectLocation = response.headers.get("location") || "";
|
|
1860
2098
|
expect(redirectLocation).toContain("error=unsolicited_response");
|
|
1861
2099
|
});
|
|
2100
|
+
|
|
2101
|
+
/**
|
|
2102
|
+
* @see https://github.com/better-auth/better-auth/issues/7777
|
|
2103
|
+
*/
|
|
2104
|
+
it("should correctly parse verification-ID-based RelayState on ACS route (SP-initiated)", async () => {
|
|
2105
|
+
const { auth, signInWithTestUser } = await getTestInstance({
|
|
2106
|
+
plugins: [sso()],
|
|
2107
|
+
});
|
|
2108
|
+
|
|
2109
|
+
const { headers } = await signInWithTestUser();
|
|
2110
|
+
|
|
2111
|
+
await auth.api.registerSSOProvider({
|
|
2112
|
+
body: {
|
|
2113
|
+
providerId: "saml-acs-relay-provider",
|
|
2114
|
+
issuer: "http://localhost:8081",
|
|
2115
|
+
domain: "http://localhost:8081",
|
|
2116
|
+
samlConfig: {
|
|
2117
|
+
entryPoint: "http://localhost:8081/api/sso/saml2/idp/post",
|
|
2118
|
+
cert: certificate,
|
|
2119
|
+
callbackUrl: "http://localhost:3000/dashboard",
|
|
2120
|
+
wantAssertionsSigned: false,
|
|
2121
|
+
signatureAlgorithm: "sha256",
|
|
2122
|
+
digestAlgorithm: "sha256",
|
|
2123
|
+
idpMetadata: {
|
|
2124
|
+
metadata: idpMetadata,
|
|
2125
|
+
},
|
|
2126
|
+
spMetadata: {
|
|
2127
|
+
metadata: spMetadata,
|
|
2128
|
+
},
|
|
2129
|
+
identifierFormat:
|
|
2130
|
+
"urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress",
|
|
2131
|
+
},
|
|
2132
|
+
},
|
|
2133
|
+
headers,
|
|
2134
|
+
});
|
|
2135
|
+
|
|
2136
|
+
// SP-initiated: signInSSO returns a URL with a RelayState verification ID
|
|
2137
|
+
const signInRes = await auth.api.signInSSO({
|
|
2138
|
+
body: {
|
|
2139
|
+
providerId: "saml-acs-relay-provider",
|
|
2140
|
+
callbackURL: "http://localhost:3000/dashboard",
|
|
2141
|
+
},
|
|
2142
|
+
returnHeaders: true,
|
|
2143
|
+
});
|
|
2144
|
+
|
|
2145
|
+
const signInResponse = signInRes.response;
|
|
2146
|
+
expect(signInResponse).toEqual({
|
|
2147
|
+
url: expect.stringContaining("http://localhost:8081"),
|
|
2148
|
+
redirect: true,
|
|
2149
|
+
});
|
|
2150
|
+
|
|
2151
|
+
const samlRedirectUrl = new URL(signInResponse?.url);
|
|
2152
|
+
const relayStateParam = samlRedirectUrl.searchParams.get("RelayState");
|
|
2153
|
+
// RelayState should be a verification ID, not a raw URL
|
|
2154
|
+
expect(relayStateParam).toBeTruthy();
|
|
2155
|
+
expect(relayStateParam).not.toContain("http");
|
|
2156
|
+
|
|
2157
|
+
let samlResponse: any;
|
|
2158
|
+
await betterFetch(signInResponse?.url, {
|
|
2159
|
+
onSuccess: async (context) => {
|
|
2160
|
+
samlResponse = await context.data;
|
|
2161
|
+
},
|
|
2162
|
+
});
|
|
2163
|
+
|
|
2164
|
+
// POST to the ACS endpoint with the verification-ID-based RelayState
|
|
2165
|
+
const acsResponse = await auth.handler(
|
|
2166
|
+
new Request(
|
|
2167
|
+
"http://localhost:3000/api/auth/sso/saml2/sp/acs/saml-acs-relay-provider",
|
|
2168
|
+
{
|
|
2169
|
+
method: "POST",
|
|
2170
|
+
headers: {
|
|
2171
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
2172
|
+
Cookie: signInRes.headers.get("set-cookie") ?? "",
|
|
2173
|
+
},
|
|
2174
|
+
body: new URLSearchParams({
|
|
2175
|
+
SAMLResponse: samlResponse.samlResponse,
|
|
2176
|
+
RelayState: relayStateParam!,
|
|
2177
|
+
}),
|
|
2178
|
+
},
|
|
2179
|
+
),
|
|
2180
|
+
);
|
|
2181
|
+
|
|
2182
|
+
expect(acsResponse.status).toBe(302);
|
|
2183
|
+
const acsRedirectLocation = acsResponse.headers.get("location") || "";
|
|
2184
|
+
// Must redirect to the callbackURL from the relay state, not to the verification ID
|
|
2185
|
+
expect(acsRedirectLocation).toContain("dashboard");
|
|
2186
|
+
expect(acsRedirectLocation).not.toContain("error");
|
|
2187
|
+
});
|
|
2188
|
+
|
|
2189
|
+
/**
|
|
2190
|
+
* @see https://github.com/better-auth/better-auth/issues/7777
|
|
2191
|
+
*/
|
|
2192
|
+
it("should fallback to provider callbackUrl on ACS route when RelayState is invalid", async () => {
|
|
2193
|
+
const { auth, signInWithTestUser } = await getTestInstance({
|
|
2194
|
+
plugins: [sso()],
|
|
2195
|
+
});
|
|
2196
|
+
|
|
2197
|
+
const { headers } = await signInWithTestUser();
|
|
2198
|
+
|
|
2199
|
+
await auth.api.registerSSOProvider({
|
|
2200
|
+
body: {
|
|
2201
|
+
providerId: "saml-acs-bad-relay-provider",
|
|
2202
|
+
issuer: "http://localhost:8081",
|
|
2203
|
+
domain: "http://localhost:8081",
|
|
2204
|
+
samlConfig: {
|
|
2205
|
+
entryPoint: "http://localhost:8081/api/sso/saml2/idp/post",
|
|
2206
|
+
cert: certificate,
|
|
2207
|
+
callbackUrl: "http://localhost:3000/dashboard",
|
|
2208
|
+
wantAssertionsSigned: false,
|
|
2209
|
+
signatureAlgorithm: "sha256",
|
|
2210
|
+
digestAlgorithm: "sha256",
|
|
2211
|
+
idpMetadata: {
|
|
2212
|
+
metadata: idpMetadata,
|
|
2213
|
+
},
|
|
2214
|
+
spMetadata: {
|
|
2215
|
+
metadata: spMetadata,
|
|
2216
|
+
},
|
|
2217
|
+
identifierFormat:
|
|
2218
|
+
"urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress",
|
|
2219
|
+
},
|
|
2220
|
+
},
|
|
2221
|
+
headers,
|
|
2222
|
+
});
|
|
2223
|
+
|
|
2224
|
+
let samlResponse: any;
|
|
2225
|
+
await betterFetch("http://localhost:8081/api/sso/saml2/idp/post", {
|
|
2226
|
+
onSuccess: async (context) => {
|
|
2227
|
+
samlResponse = await context.data;
|
|
2228
|
+
},
|
|
2229
|
+
});
|
|
2230
|
+
|
|
2231
|
+
// POST with a garbage RelayState - should fallback to provider callbackUrl
|
|
2232
|
+
const acsResponse = await auth.handler(
|
|
2233
|
+
new Request(
|
|
2234
|
+
"http://localhost:3000/api/auth/sso/saml2/sp/acs/saml-acs-bad-relay-provider",
|
|
2235
|
+
{
|
|
2236
|
+
method: "POST",
|
|
2237
|
+
headers: {
|
|
2238
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
2239
|
+
},
|
|
2240
|
+
body: new URLSearchParams({
|
|
2241
|
+
SAMLResponse: samlResponse.samlResponse,
|
|
2242
|
+
RelayState: "not-a-valid-relay-state",
|
|
2243
|
+
}),
|
|
2244
|
+
},
|
|
2245
|
+
),
|
|
2246
|
+
);
|
|
2247
|
+
|
|
2248
|
+
expect(acsResponse.status).toBe(302);
|
|
2249
|
+
const location = acsResponse.headers.get("location") || "";
|
|
2250
|
+
// Should redirect to the provider's callbackUrl, not the garbage RelayState
|
|
2251
|
+
expect(location).toContain("dashboard");
|
|
2252
|
+
expect(location).not.toContain("not-a-valid-relay-state");
|
|
2253
|
+
});
|
|
1862
2254
|
});
|
|
1863
2255
|
|
|
1864
2256
|
describe("SAML SSO with custom fields", () => {
|
package/src/types.ts
CHANGED
|
@@ -73,6 +73,7 @@ export interface SAMLConfig {
|
|
|
73
73
|
encPrivateKeyPass?: string | undefined;
|
|
74
74
|
};
|
|
75
75
|
wantAssertionsSigned?: boolean | undefined;
|
|
76
|
+
authnRequestsSigned?: boolean | undefined;
|
|
76
77
|
signatureAlgorithm?: string | undefined;
|
|
77
78
|
digestAlgorithm?: string | undefined;
|
|
78
79
|
identifierFormat?: string | undefined;
|
|
@@ -252,9 +253,12 @@ export interface SSOOptions {
|
|
|
252
253
|
*/
|
|
253
254
|
enabled?: boolean;
|
|
254
255
|
/**
|
|
255
|
-
* Prefix used to generate the domain verification token
|
|
256
|
+
* Prefix used to generate the domain verification token.
|
|
257
|
+
* An underscore is automatically prepended to follow DNS
|
|
258
|
+
* infrastructure subdomain conventions (RFC 8552), so do
|
|
259
|
+
* not include a leading underscore.
|
|
256
260
|
*
|
|
257
|
-
* @default "better-auth-token
|
|
261
|
+
* @default "better-auth-token"
|
|
258
262
|
*/
|
|
259
263
|
tokenPrefix?: string;
|
|
260
264
|
};
|