@hammadj/better-auth-sso 1.5.0-beta.9
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 +116 -0
- package/LICENSE.md +20 -0
- package/dist/client.d.mts +10 -0
- package/dist/client.mjs +15 -0
- package/dist/client.mjs.map +1 -0
- package/dist/index.d.mts +738 -0
- package/dist/index.mjs +2953 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +87 -0
- package/src/client.ts +29 -0
- package/src/constants.ts +58 -0
- package/src/domain-verification.test.ts +551 -0
- package/src/index.ts +265 -0
- package/src/linking/index.ts +2 -0
- package/src/linking/org-assignment.test.ts +325 -0
- package/src/linking/org-assignment.ts +176 -0
- package/src/linking/types.ts +10 -0
- package/src/oidc/discovery.test.ts +1157 -0
- package/src/oidc/discovery.ts +494 -0
- package/src/oidc/errors.ts +92 -0
- package/src/oidc/index.ts +31 -0
- package/src/oidc/types.ts +219 -0
- package/src/oidc.test.ts +688 -0
- package/src/providers.test.ts +1326 -0
- package/src/routes/domain-verification.ts +275 -0
- package/src/routes/providers.ts +565 -0
- package/src/routes/schemas.ts +96 -0
- package/src/routes/sso.ts +2750 -0
- package/src/saml/algorithms.test.ts +449 -0
- package/src/saml/algorithms.ts +338 -0
- package/src/saml/assertions.test.ts +239 -0
- package/src/saml/assertions.ts +62 -0
- package/src/saml/index.ts +13 -0
- package/src/saml/parser.ts +56 -0
- package/src/saml-state.ts +78 -0
- package/src/saml.test.ts +4319 -0
- package/src/types.ts +365 -0
- package/src/utils.test.ts +103 -0
- package/src/utils.ts +81 -0
- package/tsconfig.json +14 -0
- package/tsdown.config.ts +9 -0
- package/vitest.config.ts +3 -0
package/src/saml.test.ts
ADDED
|
@@ -0,0 +1,4319 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import type { createServer } from "node:http";
|
|
3
|
+
import { betterFetch } from "@better-fetch/fetch";
|
|
4
|
+
import { betterAuth } from "better-auth";
|
|
5
|
+
import { memoryAdapter } from "better-auth/adapters/memory";
|
|
6
|
+
import { APIError } from "better-auth/api";
|
|
7
|
+
import { createAuthClient } from "better-auth/client";
|
|
8
|
+
import { parseSetCookieHeader, setCookieToHeader } from "better-auth/cookies";
|
|
9
|
+
import { bearer } from "better-auth/plugins";
|
|
10
|
+
import { getTestInstance } from "better-auth/test";
|
|
11
|
+
import bodyParser from "body-parser";
|
|
12
|
+
import type {
|
|
13
|
+
Application as ExpressApp,
|
|
14
|
+
Request as ExpressRequest,
|
|
15
|
+
Response as ExpressResponse,
|
|
16
|
+
} from "express";
|
|
17
|
+
import express from "express";
|
|
18
|
+
import * as saml from "samlify";
|
|
19
|
+
import {
|
|
20
|
+
afterAll,
|
|
21
|
+
afterEach,
|
|
22
|
+
beforeAll,
|
|
23
|
+
beforeEach,
|
|
24
|
+
describe,
|
|
25
|
+
expect,
|
|
26
|
+
it,
|
|
27
|
+
vi,
|
|
28
|
+
} from "vitest";
|
|
29
|
+
import { sso, validateSAMLTimestamp } from ".";
|
|
30
|
+
import { ssoClient } from "./client";
|
|
31
|
+
import { DEFAULT_CLOCK_SKEW_MS } from "./constants";
|
|
32
|
+
|
|
33
|
+
const spMetadata = `
|
|
34
|
+
<md:EntityDescriptor xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata" entityID="http://localhost:3001/api/sso/saml2/sp/metadata">
|
|
35
|
+
<md:SPSSODescriptor AuthnRequestsSigned="false" WantAssertionsSigned="false" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
|
|
36
|
+
<md:KeyDescriptor use="signing">
|
|
37
|
+
<ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
|
|
38
|
+
<ds:X509Data>
|
|
39
|
+
<ds:X509Certificate>MIIE3jCCAsYCCQDE5FzoAkixzzANBgkqhkiG9w0BAQsFADAxMQswCQYDVQQGEwJVUzEQMA4GA1UECAwHRmxvcmlkYTEQMA4GA1UEBwwHT3JsYW5kbzAeFw0yMzExMTkxMjUyMTVaFw0zMzExMTYxMjUyMTVaMDExCzAJBgNVBAYTAlVTMRAwDgYDVQQIDAdGbG9yaWRhMRAwDgYDVQQHDAdPcmxhbmRvMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA2ELJsLZs4yBH7a2U5pA7xw+Oiut7b/ROKh2BqSTKRbEG4xy7WwljT02Mh7GTjLvswtZSUObWFO5v14HNORa3+J9JT2DH+9F+FJ770HX8a3cKYBNQt3xP4IeUyjI3QWzrGtkYPwSZ74tDpAUtuqPAxtoCaZXFDtX6lvCJDqiPnfxRZrKkepYWINSwu4DRpg6KoiPWRCYTsEcCzImInzlACdM97jpG1gLGA6a4dmjalQbRtvC56N0Z56gIhYq2F5JdzB2a10pqoIY8ggXZGIJS9I++8mmdTj6So5pPxLwnCYUhwDew1/DMbi9xIwYozs9pEtHCTn1l34jldDwTziVAxGQZO7QUuoMl997zqcPS7pVWRnfz5odKuytLvQDA0lRVfzOxtqbM3qVhoLT2iDmnuEtlZzgfbt4WEuT2538qxZJkFRpZQIrTj3ybqmWAv36Cp49dfeMwaqjhfX7/mVfbsPMSC653DSZBB+n+Uz0FC3QhH+vIdNhXNAQ5tBseHUR6pXiMnLtI/WVbMvpvFwK2faFTcx1oaP/Qk6yCq66tJvPbnatT9qGF8rdBJmAk9aBdQTI+hAh5mDtDweCrgVL+Tm/+Q85hSl4HGzH/LhLVS478tZVX+o+0yorZ35LCW3e4v8iX+1VEGSdg2ooOWtbSSXK2cYZr8ilyUQp0KueenR0CAwEAATANBgkqhkiG9w0BAQsFAAOCAgEAsonAahruWuHlYbDNQVD0ryhL/b+ttKKqVeT87XYDkvVhlSSSVAKcCwK/UU6z8Ty9dODUkd93Qsbof8fGMlXeYCtDHMRanvWLtk4wVkAMyNkDYHzJ1FbO7v44ZBbqNzSLy2kosbRELlcz+P3/42xumlDqAw/k13tWUdlLDxb0pd8R5yBev6HkIdJBIWtKmUuI+e8F/yTNf5kY7HO1p0NeKdVeZw4Ydw33+BwVxVNmhIxzdP5ZFQv0XRFWhCMo/6RLEepCvWUp/T1WRFqgwAdURaQrvvfpjO/Ls+neht1SWDeP8RRgsDrXIc3gZfaD8q4liIDTZ6HsFi7FmLbZatU8jJ4pCstxQLCvmix+1zF6Fwa9V5OApSTbVqBOsDZbJxeAoSzy5Wx28wufAZT4Kc/OaViXPV5o/ordPs4EYKgd/eNFCgIsZYXe75rYXqnieAIfJEGddsLBpqlgLkwvf5KVS4QNqqX+2YubP63y+3sICq2ScdhO3LZs3nlqQ/SgMiJnCBbDUDZ9GGgJNJVVytcSz5IDQHeflrq/zTt1c4q1DO3CS7mimAnTCjetERRQ3mgY/2hRiuCDFj3Cy7QMjFs3vBsbWrjNWlqyveFmHDRkq34Om7eA2jl3LZ5u7vSm0/ylp/vtoysMjwEmw/0NA3hZPTG3OJxcvFcXBsz0SiFcd1U=</ds:X509Certificate>
|
|
40
|
+
</ds:X509Data>
|
|
41
|
+
</ds:KeyInfo>
|
|
42
|
+
</md:KeyDescriptor>
|
|
43
|
+
<md:KeyDescriptor use="encryption">
|
|
44
|
+
<ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
|
|
45
|
+
<ds:X509Data>
|
|
46
|
+
<ds:X509Certificate>MIIE3jCCAsYCCQDE5FzoAkixzzANBgkqhkiG9w0BAQsFADAxMQswCQYDVQQGEwJVUzEQMA4GA1UECAwHRmxvcmlkYTEQMA4GA1UEBwwHT3JsYW5kbzAeFw0yMzExMTkxMjUyMTVaFw0zMzExMTYxMjUyMTVaMDExCzAJBgNVBAYTAlVTMRAwDgYDVQQIDAdGbG9yaWRhMRAwDgYDVQQHDAdPcmxhbmRvMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA2ELJsLZs4yBH7a2U5pA7xw+Oiut7b/ROKh2BqSTKRbEG4xy7WwljT02Mh7GTjLvswtZSUObWFO5v14HNORa3+J9JT2DH+9F+FJ770HX8a3cKYBNQt3xP4IeUyjI3QWzrGtkYPwSZ74tDpAUtuqPAxtoCaZXFDtX6lvCJDqiPnfxRZrKkepYWINSwu4DRpg6KoiPWRCYTsEcCzImInzlACdM97jpG1gLGA6a4dmjalQbRtvC56N0Z56gIhYq2F5JdzB2a10pqoIY8ggXZGIJS9I++8mmdTj6So5pPxLwnCYUhwDew1/DMbi9xIwYozs9pEtHCTn1l34jldDwTziVAxGQZO7QUuoMl997zqcPS7pVWRnfz5odKuytLvQDA0lRVfzOxtqbM3qVhoLT2iDmnuEtlZzgfbt4WEuT2538qxZJkFRpZQIrTj3ybqmWAv36Cp49dfeMwaqjhfX7/mVfbsPMSC653DSZBB+n+Uz0FC3QhH+vIdNhXNAQ5tBseHUR6pXiMnLtI/WVbMvpvFwK2faFTcx1oaP/Qk6yCq66tJvPbnatT9qGF8rdBJmAk9aBdQTI+hAh5mDtDweCrgVL+Tm/+Q85hSl4HGzH/LhLVS478tZVX+o+0yorZ35LCW3e4v8iX+1VEGSdg2ooOWtbSSXK2cYZr8ilyUQp0KueenR0CAwEAATANBgkqhkiG9w0BAQsFAAOCAgEAsonAahruWuHlYbDNQVD0ryhL/b+ttKKqVeT87XYDkvVhlSSSVAKcCwK/UU6z8Ty9dODUkd93Qsbof8fGMlXeYCtDHMRanvWLtk4wVkAMyNkDYHzJ1FbO7v44ZBbqNzSLy2kosbRELlcz+P3/42xumlDqAw/k13tWUdlLDxb0pd8R5yBev6HkIdJBIWtKmUuI+e8F/yTNf5kY7HO1p0NeKdVeZw4Ydw33+BwVxVNmhIxzdP5ZFQv0XRFWhCMo/6RLEepCvWUp/T1WRFqgwAdURaQrvvfpjO/Ls+neht1SWDeP8RRgsDrXIc3gZfaD8q4liIDTZ6HsFi7FmLbZatU8jJ4pCstxQLCvmix+1zF6Fwa9V5OApSTbVqBOsDZbJxeAoSzy5Wx28wufAZT4Kc/OaViXPV5o/ordPs4EYKgd/eNFCgIsZYXe75rYXqnieAIfJEGddsLBpqlgLkwvf5KVS4QNqqX+2YubP63y+3sICq2ScdhO3LZs3nlqQ/SgMiJnCBbDUDZ9GGgJNJVVytcSz5IDQHeflrq/zTt1c4q1DO3CS7mimAnTCjetERRQ3mgY/2hRiuCDFj3Cy7QMjFs3vBsbWrjNWlqyveFmHDRkq34Om7eA2jl3LZ5u7vSm0/ylp/vtoysMjwEmw/0NA3hZPTG3OJxcvFcXBsz0SiFcd1U=</ds:X509Certificate>
|
|
47
|
+
</ds:X509Data>
|
|
48
|
+
</ds:KeyInfo>
|
|
49
|
+
</md:KeyDescriptor>
|
|
50
|
+
<md:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="http://localhost:3001/api/sso/saml2/sp/sls"/>
|
|
51
|
+
<md:NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress</md:NameIDFormat>
|
|
52
|
+
<md:AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="http://localhost:3001/api/sso/saml2/sp/acs" index="1"/>
|
|
53
|
+
<md:AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="http://localhost:3001/api/sso/saml2/sp/acs" index="1"/>
|
|
54
|
+
</md:SPSSODescriptor>
|
|
55
|
+
<md:Organization>
|
|
56
|
+
<md:OrganizationName xml:lang="en-US">Organization Name</md:OrganizationName>
|
|
57
|
+
<md:OrganizationDisplayName xml:lang="en-US">Organization DisplayName</md:OrganizationDisplayName>
|
|
58
|
+
<md:OrganizationURL xml:lang="en-US">http://localhost:3001/</md:OrganizationURL>
|
|
59
|
+
</md:Organization>
|
|
60
|
+
<md:ContactPerson contactType="technical">
|
|
61
|
+
<md:GivenName>Technical Contact Name</md:GivenName>
|
|
62
|
+
<md:EmailAddress>technical_contact@gmail.com</md:EmailAddress>
|
|
63
|
+
</md:ContactPerson>
|
|
64
|
+
<md:ContactPerson contactType="support">
|
|
65
|
+
<md:GivenName>Support Contact Name</md:GivenName>
|
|
66
|
+
<md:EmailAddress>support_contact@gmail.com</md:EmailAddress>
|
|
67
|
+
</md:ContactPerson>
|
|
68
|
+
</md:EntityDescriptor>
|
|
69
|
+
`;
|
|
70
|
+
const idpMetadata = `
|
|
71
|
+
<md:EntityDescriptor xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata" entityID="http://localhost:8081/api/sso/saml2/idp/metadata">
|
|
72
|
+
<md:IDPSSODescriptor WantAuthnRequestsSigned="false" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
|
|
73
|
+
<md:KeyDescriptor use="signing">
|
|
74
|
+
<ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
|
|
75
|
+
<ds:X509Data>
|
|
76
|
+
<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>
|
|
77
|
+
</ds:X509Data>
|
|
78
|
+
</ds:KeyInfo>
|
|
79
|
+
</md:KeyDescriptor>
|
|
80
|
+
<md:KeyDescriptor use="encryption">
|
|
81
|
+
<ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
|
|
82
|
+
<ds:X509Data>
|
|
83
|
+
<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>
|
|
84
|
+
</ds:X509Data>
|
|
85
|
+
</ds:KeyInfo>
|
|
86
|
+
</md:KeyDescriptor>
|
|
87
|
+
<md:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="http://localhost:8081/api/sso/saml2/idp/slo"/>
|
|
88
|
+
<md:NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress</md:NameIDFormat>
|
|
89
|
+
<md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="http://localhost:8081/api/sso/saml2/idp/redirect"/>
|
|
90
|
+
<md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="http://localhost:8081/api/sso/saml2/idp/post"/>
|
|
91
|
+
</md:IDPSSODescriptor>
|
|
92
|
+
<md:Organization>
|
|
93
|
+
<md:OrganizationName xml:lang="en-US">Your Organization Name</md:OrganizationName>
|
|
94
|
+
<md:OrganizationDisplayName xml:lang="en-US">Your Organization DisplayName</md:OrganizationDisplayName>
|
|
95
|
+
<md:OrganizationURL xml:lang="en-US">http://localhost:8081</md:OrganizationURL>
|
|
96
|
+
</md:Organization>
|
|
97
|
+
<md:ContactPerson contactType="technical">
|
|
98
|
+
<md:GivenName>Technical Contact Name</md:GivenName>
|
|
99
|
+
<md:EmailAddress>technical_contact@gmail.com</md:EmailAddress>
|
|
100
|
+
</md:ContactPerson>
|
|
101
|
+
<md:ContactPerson contactType="support">
|
|
102
|
+
<md:GivenName>Support Contact Name</md:GivenName>
|
|
103
|
+
<md:EmailAddress>support_contact@gmail.com</md:EmailAddress>
|
|
104
|
+
</md:ContactPerson>
|
|
105
|
+
</md:EntityDescriptor>
|
|
106
|
+
`;
|
|
107
|
+
const idPk = `
|
|
108
|
+
-----BEGIN RSA PRIVATE KEY-----
|
|
109
|
+
MIIJKgIBAAKCAgEA+YIi6C8hA+NSB7dEcQf5OseCtL8wCohnnD8nnUTUdaMor9zm
|
|
110
|
+
JLhuY4ExNsHD2XO5A9RIOXerYdAVh8v6L2jUuZpIJzleoypDGlluLAjEpy52lstL
|
|
111
|
+
Hn7TytzuJjXmaTOXvWpo9oF52byqyvHKpyf27FFEL18bbVzVVZjh1oP7d3NryeF7
|
|
112
|
+
lH6YesCCm649veyC87gBUTbwFcXktLRJcHqb2eVm3+x3NTDkQKk6cP5qfHEYx0c2
|
|
113
|
+
AgPcujjOIYFbNYL0yUvFhqDJrHQGR2jp7/byyb1t4wy0EHnDoXmCCZ3FoHBEsyRS
|
|
114
|
+
JJl2naK2AhH1ocMAXVaYZJ5ni6+ATKSQXJQGGhY2AyyCqTHF2DuZP9q9wYEj5cOK
|
|
115
|
+
Km8W0eYHg9OZsimLWf8DVLZLtdfDY4SHyRbpVP5Hw3Kda2GJL84AX6q/NMIDabOK
|
|
116
|
+
EM1a9Zp6YB7EkwIVzLmW2kwciUZj6nnMr8kKkUHESDdKEyyTRss6FH9EZB+gyieP
|
|
117
|
+
u6g4ZauGYZX7KPThTL/cXjjXyKyaOR7U3xWX4nZF2aoq9masKjZTTjHyuQ7GKt+8
|
|
118
|
+
dhpIKooOJ6i6SE18sX50KS9h7GqBa5XLbfzi/mam2uzU+IXmJPRl/KpQEviKbL+k
|
|
119
|
+
9h0gDW2oeeaEkt7r3KwzroNmau/hmDCyktSClCocF/sCzMZjMEiUPWYBcH0CAwEA
|
|
120
|
+
AQKCAgABJVzdriG7r9aXnHre/gdiArqR8/LXiYrYR935tfA33hj4vc38yzAOmvBL
|
|
121
|
+
7RXmMMbfwqDWSrtpxpfiuMgcYaHgfFnqfDP4EeCfBVwhLaUhk3AN/z8IE9MLMnqR
|
|
122
|
+
iFvXjdobj5qNz0hs/JXYOsYQgHl82l6yzQAGP4/nRb17y71i7g/HrJZxtyciITI4
|
|
123
|
+
XtN/xM9RKT4wTk1J/E+xmMZhkt6WYJxZWO+vOdtChMR08mYwziAsAiK4XaYs4Mfp
|
|
124
|
+
lXuCwmg3aHauyJxEg3/n4g55AKxaytjvWwaUsMp6OmGjg6r9sqZOIFOUQXQvAylM
|
|
125
|
+
1yJGrOuagiRPCf81wAeZ0oOrOS7R+4fF4Ypa+V7Cp6Ty3VPcw8BFpXJ6fRtf92kh
|
|
126
|
+
ix00DnFEK/TdndyBpFKdmf8f2SSFBLrPlmTfjdMAvShE5yFpeWyXQjftI5q/0d3U
|
|
127
|
+
Ug0MBby66yT/TZtTKVPdK6bG3fYvzgKCpZGrKgn+umq4XR+gh9S0ptmwNF5mzJy4
|
|
128
|
+
mol5CkazGPlOSwlBc4oKeepcqZ0TKCJwonub90CJeH8IKoyRsswShRl6YTRza1SB
|
|
129
|
+
Fx4Gis5xcaNp7eXnLBDgKV/1bhCUSvQ886r+Xo4nfhk9n8WrtaQFC4tFID1e8TAM
|
|
130
|
+
jYxZIBpCHOZHX/+BpC3FyqD4RbI12iudyz4KwS5Ps/wlIpVMQQKCAQEA/70X3Fz2
|
|
131
|
+
SJyPP9UdiiqLot1ppbagQGjG20yFnfRDhNY+q2U8N77yJUXWvE7YQ6OUTOaPuJX2
|
|
132
|
+
X7vulTSQ0YyFYp0B5G4QiFtvPOpBvn7OxrFKBKxwbOU7L2rAuXWYEIRuKuwBRMFU
|
|
133
|
+
oaar8gkKlnsUtUxrLM827gmL13i3GX2bmm6NhhGCKbSCoD51+UUGo7Ix5ZLznKmX
|
|
134
|
+
G1mq4IxtJe8vLk/9RT9CzRV7VO61EgEh7Iji7g4cDIiZV+B9gG8YMlTOcALPpgud
|
|
135
|
+
nF7SEvDuMH3dgOj+iSO9piJ53okU59Mk4Nyka3p3v6RABMcDYO1/wkbE83+Oobrx
|
|
136
|
+
RiRQHtBgo1r9cQKCAQEA+cNpxVCi/BkatlzKentRabnQjfxrEQdIdc9xqzr5k2xK
|
|
137
|
+
w9n+XGzeNT+HKI/S1KkfvJTQC0j9WBQ3uupf8Zg6/mNF84YCXpun3JXpvzc+4ya3
|
|
138
|
+
i1AXtdul/JYU5qhMrJI+I1WXrWAls5zbIs23iz1Fq530Mb7FUQ5jmO0p123AmMGG
|
|
139
|
+
hSTJDqvKDMpQXdUYQMqrSL/aNh8u7wpw2S052uj2bdbdgq1FboLzbwWTOsVYs3aS
|
|
140
|
+
HABb95263Cf3OdRr4lyN6khFMLhQPUhYnn6l2ob0kIZ7V2f8fxKvJoTTDTxWpUgF
|
|
141
|
+
FrdHigaDo09WYkIukj+YdSZY/ZEAu7lyMmY0l8HNzQKCAQEA7HE3jlWknp2hE7NG
|
|
142
|
+
DGgpkfqDouKmZuZ4dGjbYJ5ljntGldCTTDcOSce4MYH0ERU8F51TY6XCk+B9RRXE
|
|
143
|
+
jvkMmY/wH/Ji9q8SuY8cGbPEGY/wj0Ge8A9AGSbp6I4AecT21lg9FARq6sneT3hs
|
|
144
|
+
gZRqIPT2YgdzEcFhuWWyY67uHmn4DuxBG634147oI/7dlJs75rVm5oElY/QTOGic
|
|
145
|
+
wWXSiU8LKurCKDqkPHI2lt7VLougw9fntu7UV5sGbahJBr/B3W277hjvL5O7Rifb
|
|
146
|
+
EJpOINFKBCE3RlK5ujWjTnK4te1JVtVzwYtqZQBa71KlvEkR7s8QYBcm22LXcKXX
|
|
147
|
+
szB9AQKCAQEAwUua8DoX6UMEiV4G1gPaXhiQb1KLCgK48XQ6ZGqf/JgyxKBRWvZm
|
|
148
|
+
go9H6vxkDnFVPn1tBU7XwvLirqX02uUVwwrReEaeTtnob68V2AbJhMLSCd9Sekwj
|
|
149
|
+
ifgc9OYLcQM9U9tKJ8PhacBbV/QduIUTBl6YPmeGDdU0/4WMfE1UYORlV2XAtLn/
|
|
150
|
+
BScOS5A/1OUE6qiQGJLJn/ZUn7+ApwrkrN09UYUH1x9BhwqphzJ0E3AQY9tjUZ+g
|
|
151
|
+
ngHQM9FSLT20Fz0XTz1V3BfBfehGM3l+jNuHWX4Ay9eJ9iWVsQihhgjW512w4AFq
|
|
152
|
+
n1knYaQWptjRBNlIxfUSvDYpSxgOW+SBgQKCAQEA7ikfNUZDmhyShcmIl9Hgcral
|
|
153
|
+
o2M/ggUVwWd9AaJD+Y/WcGoR0DPGt8UGLGTBNBwbyTgHdDzsWA+02r5r+5ArhhnP
|
|
154
|
+
iWQ1soQI9FpZIUCyzAjTQpfpzo5dGqpQbW9LuHJOEbDyY2wG+lFhIm4JJBJ/vws1
|
|
155
|
+
yt9Y170VbPXmDdLevDLmlFOILdMJWWl3hrtlU3KEogqWKDOXciYtG5Ji0+512BqH
|
|
156
|
+
yY9+uVNb1eu6MLU5R5U9GdvOFZZjShIhOlpZVR1K21dg5frBCWBZ0pvu4fZf2FAV
|
|
157
|
+
lX6+ORENSjqJsQWTaeiMoAPOj8QxQuOwUCajbVkrCZV6D49E0D9XxmZcuKCAXg==
|
|
158
|
+
-----END RSA PRIVATE KEY-----
|
|
159
|
+
|
|
160
|
+
`;
|
|
161
|
+
const spPrivateKey = `
|
|
162
|
+
-----BEGIN RSA PRIVATE KEY-----
|
|
163
|
+
Proc-Type: 4,ENCRYPTED
|
|
164
|
+
DEK-Info: DES-EDE3-CBC,9C86371F0420A091
|
|
165
|
+
|
|
166
|
+
77TqgiK/IYRgO5w3ZMyV81/gk0zN5wPTGWxoztNFjQKXCySFnrL30kUqlGituBxX
|
|
167
|
+
VgxwXbkoYMrd5MoDZKL5EJuf0H59hq92O0+3uwJA8QyZjOm4brQcjXKmIrkvihgs
|
|
168
|
+
FvpaJiGzp6kS/O7vFBDNTQWr9yY9Y3FBPcmOUWufpRp4Q5nhpSlqnMmIqZyWQUL/
|
|
169
|
+
YJSJETtzJVsk38lCqIxxDT3LtbGySahj0jRuRqspAZQeLTpnJqzNMC4vnJew9luC
|
|
170
|
+
R+UffrX7gVsnwOhNtyRzYaMsLnbRfXT8Jqx2gRHg36GxkOVgyU7e62nk9CzeC0WA
|
|
171
|
+
kHHCNVqqivRx9/EC0mQkkRgRzo3BZWp0o671sUsGTy57JhktiGfTnWMrl7ZfhAza
|
|
172
|
+
SZnjyTwuI1bTQipIkNI3aJBTP/o/gNUE1sj5D5FZlFdpq5ks2Vxww3GNx1FRrvWd
|
|
173
|
+
98z5CNt78ZR0ihLmdz/EakEBKBUteQu/5zPLUlwmGuou4wPuEHG2BsjGzb/d5Zfc
|
|
174
|
+
ElIjUV+yrMmGHvBfPyPnDUrCUyLn18S1NZiCMCdN5PqCybjhk8oMPYZhWBqp8Ymr
|
|
175
|
+
yHIC7BCnTJhIvgQZR6M68NwVv0aBBgH/I/DB0jADo6/B5Eajwus9i6zSv8QIbqhw
|
|
176
|
+
fusKtI04vxc91aP0GWRr0J/O4mkxXYNPfa3a/I7sGTXGl0k0CygckE3fLXRy/WEk
|
|
177
|
+
ikZt4UHqg5ZQ8vc5NSAM5f5Yx/72CU1I6ehFtxHsyE5yndpZXWp2X2S4l31e8fLs
|
|
178
|
+
ddOoybroJgbyLrh7JT3Yac3XOEsKATWIvqU+hNYq6KwqLWev9jInHVgjzfyOKbmF
|
|
179
|
+
hkrzDDHaKULYZuTsUq5mLc1SzSu98lXYfXp1WE4XsH0X0VicPzf8ZH4Kutuig0VG
|
|
180
|
+
5Kg9HB/Cin65VMm0ffEiTraO6johIlwFGRrtAs38ONKgsPCQUv7ee9SEGOHViNZq
|
|
181
|
+
NpWPr1KOzbI4wEB1ueKoZuEQ0a+tzfJgszJrM48bM82J6iEjN/PSOTsdTKJq9e47
|
|
182
|
+
dlUp+tqQsvGkbBOIOt5OOpkr8Z+8qbEd21ojF9Q0p0T4WMThRP6YBRKvt8mmFwRs
|
|
183
|
+
DjEhMiPa4L70Eqldfu2lWdI6ietfHrK97WXwQO1gF73LOnA+EdMXNxr1iLd0Tdke
|
|
184
|
+
z6fUSw3hKZL+I7nX6O40+KgkhXVSZOsRz5CEvo2iChIUrYGEGDl94K/ofqGu71Y+
|
|
185
|
+
G8KBvbha6EC7xcUrTYP5Gek5wsrw7cGgDZJjMsyXYFBZjQO1N6g9fncLmc5pB5Ix
|
|
186
|
+
W3gLfQS/My4daWNTvrYOgfA08J4M4ZWd0v5TglxOSV78psG4J4slppDySNFB2d/3
|
|
187
|
+
7JiwWVm5SMk0StLWwb2azmTvBoinnrZJzPnPlOytxvE5uGJ/i0WAik7C99YgVJkS
|
|
188
|
+
9hO3FJGasrOnHeiOvMZEdRuIVspKz9iMFx7hWHpVHTTyjwceEpaiEkhmqLM9QkKh
|
|
189
|
+
kCZqeWyVsKBIc0sse+CKNK8ik9eTeUlCklGMV1Q4kKjR6uuHUOLyjk/xhqslV4TS
|
|
190
|
+
jnnjCjsK5YzTa4hmbHhPZIW262KoFV9TqxYKkhP5ab7AXRSakrdrY2cwACWN4AMT
|
|
191
|
+
-----END RSA PRIVATE KEY-----
|
|
192
|
+
`;
|
|
193
|
+
const idpPrivateKey = `
|
|
194
|
+
-----BEGIN RSA PRIVATE KEY-----
|
|
195
|
+
Proc-Type: 4,ENCRYPTED
|
|
196
|
+
DEK-Info: DES-EDE3-CBC,116B0EBB2F2F0A9D
|
|
197
|
+
|
|
198
|
+
HMmUsJPVPTsq1e06yrrskfinY21OOHosfRzibLueBg9ByFFZ7+/oW/DKy1GcDeBc
|
|
199
|
+
ycL+3gylIoGUYuZ+DPC11ArjdxFqLFnHJb96rwy5h4sTP0lE+qHy+06AwsowUgp3
|
|
200
|
+
pdD2unPFeydpu5h/dqgoDzkGSucz0Ty/spHXNBvns0vJO18B7XlzXUtfH5aHco22
|
|
201
|
+
DyVY6FrJwMts9E4Rzs9JsxJJ7mi/6+Qsc0rOr8/6KKsRo1sKD6cvQIQ05dEvGrE9
|
|
202
|
+
/2fubHkRTl+zBqOVyQvC6iUtocwxlMP4KfmyYrD1wlQAnP/+smq2G+xf7uGc4X4P
|
|
203
|
+
8q0jEy2P9n5ASlwZ3XCS9hZgp8VRAcXWOYjzzNouQp3NEP9d5D3wN4aFKa/JW6pk
|
|
204
|
+
a6VwraEweuyJqvZ7nnam1emW0ge0z7hJabR0+j0PnUxFIwkI5jO3HI5UiuUzuQFe
|
|
205
|
+
2bTLA3XnJ7QD08ZKom0rmApbFrmm9BWBRTmt46NlQDy49VODPY4gFuQ/mpaFjaBy
|
|
206
|
+
fSNJaOSS/MDuAdPabNEh3l+yCGKtHIbPVIms76PxYf6o0VVxW96/Q25hrvyOJCxn
|
|
207
|
+
dVQyyJbQ1jGenu4ViDNrW9ZQfw4aJCPpY7lUQd09BGz2NMKgkrSl8bKSan4lvlF3
|
|
208
|
+
ok8BjfIw+pIrTyesPU5tF0YudDxwi8fbIG70iwrpsSt2wVIMa+Nz2lwFT1dV8be7
|
|
209
|
+
NARkkkhLWJYAsxsyVfdl+ucNSqhvo8xLITuG8CZnzKf0T2HMKnMNegFx/ipfM7ff
|
|
210
|
+
Mx5CjayN5Oy99MWsagYEutUGzCGPAuVpqYpJuuYa3lWbFk2XWihWkAiUwgRqIluE
|
|
211
|
+
M6LpO8l3LVXVjN1+6bK1GZpbfLay+E6vy4W38XMuXZSNpyhy6e+XggTPH2xbbwoi
|
|
212
|
+
OcAzcojhMaxVGpxm/aXyRxg9zBdrQjtqM/aCN91ri55bvOKxELVi+D/VcZKpd2CR
|
|
213
|
+
X/vWcqoGaK/6+vlPWMZSHCJkPa4KBT0aUcnEdeFWx2nmrwdrHvETzCYLAzVBSECV
|
|
214
|
+
ZoYH0xTkFr/RI2AOAzx701LSuYbnPoCq+w7TXtjPaooZdYVVgrYuI+j4JOlseFS7
|
|
215
|
+
1c9iRiJVPBfnpUNIZdHLw19+k81IJ/FmumiuDhfLS5pwQmtuXkO3DWZDa3UPlV8e
|
|
216
|
+
6dmZeP1XGwRLL9VpOKx7NCqZM+CdEt87CXpFFWXdw8tL+3K/2r8w4lHIzBKaVPSS
|
|
217
|
+
5uFqXc1vzfP6Qeov31IjeLPE1pWTHNqRPdmvt9Scq9tKS3o18wmLBxOVinOE0cxQ
|
|
218
|
+
oddzPd0z5NxNYVayqZORwDdVv6CVXKnrvBSnOFFslZqv1G8/diE5BXxeaAPEMcZE
|
|
219
|
+
3lD7MzdoEHK5oL2MXofLWZbNtMkOZLaLqY80zKT1UG3Gs8U44d44aLXO1dBL0HGX
|
|
220
|
+
dNfNUaH+IGZf2ccS6OR1RhwIazDZ8qk0XeUwQV588adwC3FUvscVA3eHZa95z4kX
|
|
221
|
+
xvHg+ylzRtKRfpSPzB2IVwgV9/rsOg0OmvwhV8+5IQpdcFr+hf2Bn6AVn6H9aX8A
|
|
222
|
+
JjycN6KMcHaFa0EUqagGm9tsQLmf/MGCj8sy9am1IbRmFCz5lB5A7P/YLPM2Csjg
|
|
223
|
+
-----END RSA PRIVATE KEY-----`;
|
|
224
|
+
const certificate = `
|
|
225
|
+
-----BEGIN CERTIFICATE-----
|
|
226
|
+
MIIDlzCCAn+gAwIBAgIJAO1ymQc33+bWMA0GCSqGSIb3DQEBCwUAMGIxCzAJBgNV
|
|
227
|
+
BAYTAkhLMRMwEQYDVQQIDApTb21lLVN0YXRlMRowGAYDVQQKDBFJZGVudGl0eSBQ
|
|
228
|
+
cm92aWRlcjEUMBIGA1UECwwLRGV2ZWxvcG1lbnQxDDAKBgNVBAMMA0lEUDAeFw0x
|
|
229
|
+
NTA3MDUxODAyMjdaFw0xODA3MDQxODAyMjdaMGIxCzAJBgNVBAYTAkhLMRMwEQYD
|
|
230
|
+
VQQIDApTb21lLVN0YXRlMRowGAYDVQQKDBFJZGVudGl0eSBQcm92aWRlcjEUMBIG
|
|
231
|
+
A1UECwwLRGV2ZWxvcG1lbnQxDDAKBgNVBAMMA0lEUDCCASIwDQYJKoZIhvcNAQEB
|
|
232
|
+
BQADggEPADCCAQoCggEBAODZsWhCe+yG0PalQPTUoD7yko5MTWMCRxJ8hSm2k7mG
|
|
233
|
+
3Eg/Y2v0EBdCmTw7iDCevRqUmbmFnq7MROyV4eriJzh0KabAdZf7/k6koghst3ZU
|
|
234
|
+
tWOwzshyxkBtWDwGmBpQGTGsKxJ8M1js3aSqNRXBT4OBWM9w2Glt1+8ty30RhYv3
|
|
235
|
+
pSF+/HHLH7Ac+vLSIAlokaFW34RWTcJ/8rADuRWlXih4GfnIu0W/ncm5nTSaJiRA
|
|
236
|
+
vr3dGDRO/khiXoJdbbOj7dHPULxVGbH9IbPK76TCwLbF7ikIMsPovVbTrpyL6vsb
|
|
237
|
+
VUKeEl/5GKppTwp9DLAOeoSYpCYkkDkYKu9TRQjF02MCAwEAAaNQME4wHQYDVR0O
|
|
238
|
+
BBYEFP2ut2AQdy6D1dwdwK740IHmbh38MB8GA1UdIwQYMBaAFP2ut2AQdy6D1dwd
|
|
239
|
+
wK740IHmbh38MAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBANMZUoPN
|
|
240
|
+
mHzgja2PYkbvBYMHmpvUkVoiuvQ9cJPlqGTB2CRfG68BNNs/Clz8P7cIrAdkhCUw
|
|
241
|
+
i1rSBhDuslGFNrSaIpv6B10FpBuKwef3G7YrPWFNEN6khY7aHNWSTHqKgs1DrGef
|
|
242
|
+
2B9hvkrnHWbQVSVXrBFKe1wTCqcgGcOpYoSK7L8C6iX6uIA/uZYnVQ4NgBrizJ0a
|
|
243
|
+
zkjdegz3hwO/gt4malEURy8D85/AAVt6PAzhpb9VJUGxSXr/EfntVUEz3L2gUFWW
|
|
244
|
+
k1CnZFyz0rIOEt/zPmeAY8BLyd/Tjxm4Y+gwNazKq5y9AJS+m858b/nM4QdCnUE4
|
|
245
|
+
yyoWAJDUHiAmvFA=
|
|
246
|
+
-----END CERTIFICATE-----
|
|
247
|
+
`;
|
|
248
|
+
const idpEncryptionKey = `
|
|
249
|
+
-----BEGIN RSA PRIVATE KEY-----
|
|
250
|
+
Proc-Type: 4,ENCRYPTED
|
|
251
|
+
DEK-Info: DES-EDE3-CBC,860FDB9F3BE14699
|
|
252
|
+
|
|
253
|
+
bMpTdWaAEqNciUFQhHYNv1F9N12aqOQd6cFbMozfRnNR19HW6QIPDmEOPSSCaaRy
|
|
254
|
+
QCnJhbpcSnaz9pvI7EzeJzdykDmR8Boos+0NSK9qIX0buBO55mfPr7hjx7bLFEVl
|
|
255
|
+
kkHk+k9F1rLyjyAGJrVoTNoWjyuMOFUCWR7ZxoYticwM/sL+Rbhn1FsfdkdfhFW0
|
|
256
|
+
08OHTouRK33Aifx0A3MWxR0ILvw49E6urtbbIrskEzKzfWQug8gY1TJhI3sbsMsI
|
|
257
|
+
1bS5Vg88TvilFFBGn0Yv6GEJjgOrsrKDGKtYGhuBfK4fd4rwnQKKvC6gTKeNXIfV
|
|
258
|
+
7Qm1R20LUJXC8zv35pdKoVk+NdS/MGNXJRFgO3Kkp01aVf3n1oo2+AllS02AYyWt
|
|
259
|
+
1svHecsRwbibXip8gSQsOtDdpqQrEDyqZlFHXEw/IcJE9vQWEJmpHD5GFhbKtttp
|
|
260
|
+
E0B3ZtNl6YcyUz0rSf9zjuMx/wReWdRb6H2WoIqoRS7vAUONDRPt7wvfjtLlDRVi
|
|
261
|
+
bc2RTN8yce/57lGnA1n8bxPV5+9VxCJOEipV3io/nrj+uNO8i/0rUpkKdZy8wy2C
|
|
262
|
+
Rksoxq4TxwegONz1HQcJVpJu0iBdu7B+BXVjxQQScvMQlOTbua8k+YdaCeZAb83j
|
|
263
|
+
JVX89/PFy+Xj7eGyzzBTqz7dV0Xkxq9mpiMYUCoyNL5Iq1jD9Xb5TzVW1Gbh8zCZ
|
|
264
|
+
YXjcZEQKeartaBC4/fRWyxqK3gJRX4SJkl4gYMQrPS2pbTzVCO+WLxSwIh3dOZpo
|
|
265
|
+
eErXLSrylIv9cE2Xrs0McXAR+hfGrqgtILBWwgbh2NhmUiFfLwUTUxU51eu7QZ2T
|
|
266
|
+
V1VFBX0QTmn2kM0JLSSC96mDUzbs6qfURUaXbuffF5cqdUjXgtzZj5SFEbIv4UFS
|
|
267
|
+
0DAS+6i/jTGSz7aAp/uofOxhYkCqK/s2Cex2jQbDpcKXKiWzPdULOCjAh3fdCAp0
|
|
268
|
+
3ua3fdAI7H8PslSDiPFrcY78OxZaWXzazEiun77WKbzrMloLMP5dpCPlUCOqxbZ0
|
|
269
|
+
ykSuo0M7p/UPY34yi3AMHS9grvQQ1DykMPoqKKEheI6nUGcQ1AFcdr307ILWRsPO
|
|
270
|
+
T6gHOLXZaR4+UEeYfkTKsjrMUhozx7JIyuLgTXA9TWC+tZ9WZpbJ7i3bpQ+RNwX2
|
|
271
|
+
AxQSwc9ZOcNxg8YCbGlJgJHnRVhA202kNT5ORplcRKqaOaO9LK7491gaaShjaspg
|
|
272
|
+
4THDnH+HHFORmbgwyO9P74wuw+n6tI40Ia3qzRLVz6sJBQMtLEN+cvNoNi3KYkNj
|
|
273
|
+
GJM1iWfSz6PjrEGxbzQZKoFPPiZrVRnVfPhBNyT2OZj+TJii9CaukhmkkA2/AJmS
|
|
274
|
+
5XoO3GNIaqOGYV9HLyh1++cn3NhjgFYe/Q3ORCTIg2Ltd8Qr6mYe0LcONQFgiv4c
|
|
275
|
+
AUOZtOq05fJDXE74R1JjYHPaQF6uZEbTF98jN9QZIfCEvDdv1nC83MvSwATi0j5S
|
|
276
|
+
LvdU/MSPaZ0VKzPc4JPwv72dveEPME6QyswKx9izioJVrQJr36YtmrhDlKR1WBny
|
|
277
|
+
ISbutnQPUN5fsaIsgKDIV3T7n6519t6brobcW5bdigmf5ebFeZJ16/lYy6V77UM5
|
|
278
|
+
-----END RSA PRIVATE KEY-----
|
|
279
|
+
`;
|
|
280
|
+
const spEncryptionKey = `
|
|
281
|
+
-----BEGIN RSA PRIVATE KEY-----
|
|
282
|
+
Proc-Type: 4,ENCRYPTED
|
|
283
|
+
DEK-Info: DES-EDE3-CBC,860FDB9F3BE14699
|
|
284
|
+
|
|
285
|
+
bMpTdWaAEqNciUFQhHYNv1F9N12aqOQd6cFbMozfRnNR19HW6QIPDmEOPSSCaaRy
|
|
286
|
+
QCnJhbpcSnaz9pvI7EzeJzdykDmR8Boos+0NSK9qIX0buBO55mfPr7hjx7bLFEVl
|
|
287
|
+
kkHk+k9F1rLyjyAGJrVoTNoWjyuMOFUCWR7ZxoYticwM/sL+Rbhn1FsfdkdfhFW0
|
|
288
|
+
08OHTouRK33Aifx0A3MWxR0ILvw49E6urtbbIrskEzKzfWQug8gY1TJhI3sbsMsI
|
|
289
|
+
1bS5Vg88TvilFFBGn0Yv6GEJjgOrsrKDGKtYGhuBfK4fd4rwnQKKvC6gTKeNXIfV
|
|
290
|
+
7Qm1R20LUJXC8zv35pdKoVk+NdS/MGNXJRFgO3Kkp01aVf3n1oo2+AllS02AYyWt
|
|
291
|
+
1svHecsRwbibXip8gSQsOtDdpqQrEDyqZlFHXEw/IcJE9vQWEJmpHD5GFhbKtttp
|
|
292
|
+
E0B3ZtNl6YcyUz0rSf9zjuMx/wReWdRb6H2WoIqoRS7vAUONDRPt7wvfjtLlDRVi
|
|
293
|
+
bc2RTN8yce/57lGnA1n8bxPV5+9VxCJOEipV3io/nrj+uNO8i/0rUpkKdZy8wy2C
|
|
294
|
+
Rksoxq4TxwegONz1HQcJVpJu0iBdu7B+BXVjxQQScvMQlOTbua8k+YdaCeZAb83j
|
|
295
|
+
JVX89/PFy+Xj7eGyzzBTqz7dV0Xkxq9mpiMYUCoyNL5Iq1jD9Xb5TzVW1Gbh8zCZ
|
|
296
|
+
YXjcZEQKeartaBC4/fRWyxqK3gJRX4SJkl4gYMQrPS2pbTzVCO+WLxSwIh3dOZpo
|
|
297
|
+
eErXLSrylIv9cE2Xrs0McXAR+hfGrqgtILBWwgbh2NhmUiFfLwUTUxU51eu7QZ2T
|
|
298
|
+
V1VFBX0QTmn2kM0JLSSC96mDUzbs6qfURUaXbuffF5cqdUjXgtzZj5SFEbIv4UFS
|
|
299
|
+
0DAS+6i/jTGSz7aAp/uofOxhYkCqK/s2Cex2jQbDpcKXKiWzPdULOCjAh3fdCAp0
|
|
300
|
+
3ua3fdAI7H8PslSDiPFrcY78OxZaWXzazEiun77WKbzrMloLMP5dpCPlUCOqxbZ0
|
|
301
|
+
ykSuo0M7p/UPY34yi3AMHS9grvQQ1DykMPoqKKEheI6nUGcQ1AFcdr307ILWRsPO
|
|
302
|
+
T6gHOLXZaR4+UEeYfkTKsjrMUhozx7JIyuLgTXA9TWC+tZ9WZpbJ7i3bpQ+RNwX2
|
|
303
|
+
AxQSwc9ZOcNxg8YCbGlJgJHnRVhA202kNT5ORplcRKqaOaO9LK7491gaaShjaspg
|
|
304
|
+
4THDnH+HHFORmbgwyO9P74wuw+n6tI40Ia3qzRLVz6sJBQMtLEN+cvNoNi3KYkNj
|
|
305
|
+
GJM1iWfSz6PjrEGxbzQZKoFPPiZrVRnVfPhBNyT2OZj+TJii9CaukhmkkA2/AJmS
|
|
306
|
+
5XoO3GNIaqOGYV9HLyh1++cn3NhjgFYe/Q3ORCTIg2Ltd8Qr6mYe0LcONQFgiv4c
|
|
307
|
+
AUOZtOq05fJDXE74R1JjYHPaQF6uZEbTF98jN9QZIfCEvDdv1nC83MvSwATi0j5S
|
|
308
|
+
LvdU/MSPaZ0VKzPc4JPwv72dveEPME6QyswKx9izioJVrQJr36YtmrhDlKR1WBny
|
|
309
|
+
ISbutnQPUN5fsaIsgKDIV3T7n6519t6brobcW5bdigmf5ebFeZJ16/lYy6V77UM5
|
|
310
|
+
-----END RSA PRIVATE KEY-----
|
|
311
|
+
`;
|
|
312
|
+
const generateRequestID = () => {
|
|
313
|
+
return "_" + randomUUID();
|
|
314
|
+
};
|
|
315
|
+
const createTemplateCallback =
|
|
316
|
+
(idp: any, sp: any, email: string) => (template: any) => {
|
|
317
|
+
const assertionConsumerServiceUrl =
|
|
318
|
+
sp.entityMeta.getAssertionConsumerService(
|
|
319
|
+
saml.Constants.wording.binding.post,
|
|
320
|
+
);
|
|
321
|
+
|
|
322
|
+
const nameIDFormat = idp.entitySetting.nameIDFormat;
|
|
323
|
+
const selectedNameIDFormat = Array.isArray(nameIDFormat)
|
|
324
|
+
? nameIDFormat[0]
|
|
325
|
+
: nameIDFormat;
|
|
326
|
+
|
|
327
|
+
const id = generateRequestID();
|
|
328
|
+
const now = new Date();
|
|
329
|
+
const fiveMinutesLater = new Date(now.getTime() + 5 * 60 * 1000);
|
|
330
|
+
const tagValues = {
|
|
331
|
+
ID: id,
|
|
332
|
+
AssertionID: generateRequestID(),
|
|
333
|
+
Destination: assertionConsumerServiceUrl,
|
|
334
|
+
Audience: sp.entityMeta.getEntityID(),
|
|
335
|
+
EntityID: sp.entityMeta.getEntityID(),
|
|
336
|
+
SubjectRecipient: assertionConsumerServiceUrl,
|
|
337
|
+
Issuer: idp.entityMeta.getEntityID(),
|
|
338
|
+
IssueInstant: now.toISOString(),
|
|
339
|
+
AssertionConsumerServiceURL: assertionConsumerServiceUrl,
|
|
340
|
+
StatusCode: "urn:oasis:names:tc:SAML:2.0:status:Success",
|
|
341
|
+
ConditionsNotBefore: now.toISOString(),
|
|
342
|
+
ConditionsNotOnOrAfter: fiveMinutesLater.toISOString(),
|
|
343
|
+
SubjectConfirmationDataNotOnOrAfter: fiveMinutesLater.toISOString(),
|
|
344
|
+
NameIDFormat: selectedNameIDFormat,
|
|
345
|
+
NameID: email,
|
|
346
|
+
InResponseTo: "null",
|
|
347
|
+
AuthnStatement: "",
|
|
348
|
+
attrFirstName: "Test",
|
|
349
|
+
attrLastName: "User",
|
|
350
|
+
attrEmail: "test@email.com",
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
return {
|
|
354
|
+
id,
|
|
355
|
+
context: saml.SamlLib.replaceTagsByValue(template, tagValues),
|
|
356
|
+
};
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
const createMockSAMLIdP = (port: number) => {
|
|
360
|
+
const app: ExpressApp = express();
|
|
361
|
+
let server: ReturnType<typeof createServer> | undefined;
|
|
362
|
+
|
|
363
|
+
app.use(bodyParser.urlencoded({ extended: true }));
|
|
364
|
+
app.use(bodyParser.json());
|
|
365
|
+
|
|
366
|
+
const idp = saml.IdentityProvider({
|
|
367
|
+
metadata: idpMetadata,
|
|
368
|
+
privateKey: idPk,
|
|
369
|
+
isAssertionEncrypted: false,
|
|
370
|
+
privateKeyPass: "jXmKf9By6ruLnUdRo90G",
|
|
371
|
+
loginResponseTemplate: {
|
|
372
|
+
context:
|
|
373
|
+
'<samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="{ID}" Version="2.0" IssueInstant="{IssueInstant}" Destination="{Destination}" InResponseTo="{InResponseTo}"><saml:Issuer>{Issuer}</saml:Issuer><samlp:Status><samlp:StatusCode Value="{StatusCode}"/></samlp:Status><saml:Assertion ID="{AssertionID}" Version="2.0" IssueInstant="{IssueInstant}" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"><saml:Issuer>{Issuer}</saml:Issuer><saml:Subject><saml:NameID Format="{NameIDFormat}">{NameID}</saml:NameID><saml:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer"><saml:SubjectConfirmationData NotOnOrAfter="{SubjectConfirmationDataNotOnOrAfter}" Recipient="{SubjectRecipient}" InResponseTo="{InResponseTo}"/></saml:SubjectConfirmation></saml:Subject><saml:Conditions NotBefore="{ConditionsNotBefore}" NotOnOrAfter="{ConditionsNotOnOrAfter}"><saml:AudienceRestriction><saml:Audience>{Audience}</saml:Audience></saml:AudienceRestriction></saml:Conditions>{AttributeStatement}</saml:Assertion></samlp:Response>',
|
|
374
|
+
attributes: [
|
|
375
|
+
{
|
|
376
|
+
name: "firstName",
|
|
377
|
+
valueTag: "firstName",
|
|
378
|
+
nameFormat: "urn:oasis:names:tc:SAML:2.0:attrname-format:basic",
|
|
379
|
+
valueXsiType: "xs:string",
|
|
380
|
+
},
|
|
381
|
+
{
|
|
382
|
+
name: "lastName",
|
|
383
|
+
valueTag: "lastName",
|
|
384
|
+
nameFormat: "urn:oasis:names:tc:SAML:2.0:attrname-format:basic",
|
|
385
|
+
valueXsiType: "xs:string",
|
|
386
|
+
},
|
|
387
|
+
{
|
|
388
|
+
name: "email",
|
|
389
|
+
valueTag: "email",
|
|
390
|
+
nameFormat: "urn:oasis:names:tc:SAML:2.0:attrname-format:basic",
|
|
391
|
+
valueXsiType: "xs:string",
|
|
392
|
+
},
|
|
393
|
+
],
|
|
394
|
+
},
|
|
395
|
+
});
|
|
396
|
+
const sp = saml.ServiceProvider({
|
|
397
|
+
metadata: spMetadata,
|
|
398
|
+
});
|
|
399
|
+
app.get(
|
|
400
|
+
"/api/sso/saml2/idp/post",
|
|
401
|
+
async (req: ExpressRequest, res: ExpressResponse) => {
|
|
402
|
+
const emailCase = req.query.emailCase as string;
|
|
403
|
+
const emailValue =
|
|
404
|
+
emailCase === "mixed" ? "TestUser@Example.com" : "test@email.com";
|
|
405
|
+
const user = {
|
|
406
|
+
email: emailValue,
|
|
407
|
+
emailAddress: emailValue,
|
|
408
|
+
famName: "hello world",
|
|
409
|
+
};
|
|
410
|
+
const { context, entityEndpoint } = await idp.createLoginResponse(
|
|
411
|
+
sp,
|
|
412
|
+
{} as any,
|
|
413
|
+
saml.Constants.wording.binding.post,
|
|
414
|
+
user,
|
|
415
|
+
createTemplateCallback(idp, sp, user.emailAddress),
|
|
416
|
+
);
|
|
417
|
+
res.status(200).send({ samlResponse: context, entityEndpoint });
|
|
418
|
+
},
|
|
419
|
+
);
|
|
420
|
+
app.get(
|
|
421
|
+
"/api/sso/saml2/idp/redirect",
|
|
422
|
+
async (req: ExpressRequest, res: ExpressResponse) => {
|
|
423
|
+
const emailCase = req.query.emailCase as string;
|
|
424
|
+
const emailValue =
|
|
425
|
+
emailCase === "mixed" ? "TestUser@Example.com" : "test@email.com";
|
|
426
|
+
const user = {
|
|
427
|
+
email: emailValue,
|
|
428
|
+
emailAddress: emailValue,
|
|
429
|
+
famName: "hello world",
|
|
430
|
+
};
|
|
431
|
+
const { context, entityEndpoint } = await idp.createLoginResponse(
|
|
432
|
+
sp,
|
|
433
|
+
{} as any,
|
|
434
|
+
saml.Constants.wording.binding.post,
|
|
435
|
+
user,
|
|
436
|
+
createTemplateCallback(idp, sp, user.emailAddress),
|
|
437
|
+
);
|
|
438
|
+
res.status(200).send({ samlResponse: context, entityEndpoint });
|
|
439
|
+
},
|
|
440
|
+
);
|
|
441
|
+
app.post("/api/sso/saml2/sp/acs", async (req: any, res: any) => {
|
|
442
|
+
try {
|
|
443
|
+
const parseResult = await sp.parseLoginResponse(
|
|
444
|
+
idp,
|
|
445
|
+
saml.Constants.wording.binding.post,
|
|
446
|
+
req,
|
|
447
|
+
);
|
|
448
|
+
const { extract } = parseResult;
|
|
449
|
+
const { attributes } = extract;
|
|
450
|
+
const relayState = req.body.RelayState;
|
|
451
|
+
if (relayState) {
|
|
452
|
+
return res.status(200).send({ relayState, attributes });
|
|
453
|
+
} else {
|
|
454
|
+
return res
|
|
455
|
+
.status(200)
|
|
456
|
+
.send({ extract, message: "RelayState is missing." });
|
|
457
|
+
}
|
|
458
|
+
} catch (error) {
|
|
459
|
+
console.error("Error handling SAML ACS endpoint:", error);
|
|
460
|
+
res.status(500).send({ error: "Failed to process SAML response." });
|
|
461
|
+
}
|
|
462
|
+
});
|
|
463
|
+
app.post(
|
|
464
|
+
"/api/sso/saml2/callback/:providerId",
|
|
465
|
+
async (req: ExpressRequest, res: ExpressResponse) => {
|
|
466
|
+
const { SAMLResponse, RelayState } = req.body;
|
|
467
|
+
try {
|
|
468
|
+
await sp.parseLoginResponse(idp, saml.Constants.wording.binding.post, {
|
|
469
|
+
body: { SAMLResponse },
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
res.redirect(302, RelayState || "http://localhost:3000/dashboard");
|
|
473
|
+
} catch (error) {
|
|
474
|
+
console.error("Error processing SAML callback:", error);
|
|
475
|
+
res.status(500).send({ error: "Failed to process SAML response" });
|
|
476
|
+
}
|
|
477
|
+
},
|
|
478
|
+
);
|
|
479
|
+
app.get(
|
|
480
|
+
"/api/sso/saml2/idp/metadata",
|
|
481
|
+
(req: ExpressRequest, res: ExpressResponse) => {
|
|
482
|
+
res.type("application/xml");
|
|
483
|
+
res.send(idpMetadata);
|
|
484
|
+
},
|
|
485
|
+
);
|
|
486
|
+
const start = () =>
|
|
487
|
+
new Promise<void>((resolve) => {
|
|
488
|
+
app.use(bodyParser.urlencoded({ extended: true }));
|
|
489
|
+
server = app.listen(port, () => {
|
|
490
|
+
console.log(`Mock SAML IdP running on port ${port}`);
|
|
491
|
+
resolve();
|
|
492
|
+
});
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
const stop = () =>
|
|
496
|
+
new Promise<void>((resolve, reject) => {
|
|
497
|
+
app.use(bodyParser.urlencoded({ extended: true }));
|
|
498
|
+
server?.close((err) => {
|
|
499
|
+
if (err) reject(err);
|
|
500
|
+
else resolve();
|
|
501
|
+
});
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
const metadataUrl = `http://localhost:${port}/idp/metadata`;
|
|
505
|
+
|
|
506
|
+
return { start, stop, metadataUrl };
|
|
507
|
+
};
|
|
508
|
+
|
|
509
|
+
// Shared mock SAML IdP for all tests
|
|
510
|
+
const sharedMockIdP = createMockSAMLIdP(8081);
|
|
511
|
+
|
|
512
|
+
beforeAll(async () => {
|
|
513
|
+
await sharedMockIdP.start();
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
afterAll(async () => {
|
|
517
|
+
await sharedMockIdP.stop();
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
describe("SAML SSO with defaultSSO array", async () => {
|
|
521
|
+
const data = {
|
|
522
|
+
user: [],
|
|
523
|
+
session: [],
|
|
524
|
+
verification: [],
|
|
525
|
+
account: [],
|
|
526
|
+
ssoProvider: [],
|
|
527
|
+
};
|
|
528
|
+
|
|
529
|
+
const memory = memoryAdapter(data);
|
|
530
|
+
|
|
531
|
+
const ssoOptions = {
|
|
532
|
+
defaultSSO: [
|
|
533
|
+
{
|
|
534
|
+
domain: "localhost:8081",
|
|
535
|
+
providerId: "default-saml",
|
|
536
|
+
samlConfig: {
|
|
537
|
+
issuer: "http://localhost:8081",
|
|
538
|
+
entryPoint: "http://localhost:8081/api/sso/saml2/idp/post",
|
|
539
|
+
cert: certificate,
|
|
540
|
+
callbackUrl: "http://localhost:8081/dashboard",
|
|
541
|
+
wantAssertionsSigned: false,
|
|
542
|
+
signatureAlgorithm: "sha256",
|
|
543
|
+
digestAlgorithm: "sha256",
|
|
544
|
+
idpMetadata: {
|
|
545
|
+
metadata: idpMetadata,
|
|
546
|
+
},
|
|
547
|
+
spMetadata: {
|
|
548
|
+
metadata: spMetadata,
|
|
549
|
+
},
|
|
550
|
+
identifierFormat:
|
|
551
|
+
"urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress",
|
|
552
|
+
},
|
|
553
|
+
},
|
|
554
|
+
],
|
|
555
|
+
provisionUser: vi
|
|
556
|
+
.fn()
|
|
557
|
+
.mockImplementation(async ({ user, userInfo, token, provider }) => {
|
|
558
|
+
return {
|
|
559
|
+
id: "provisioned-user-id",
|
|
560
|
+
email: userInfo.email,
|
|
561
|
+
name: userInfo.name,
|
|
562
|
+
attributes: userInfo.attributes,
|
|
563
|
+
};
|
|
564
|
+
}),
|
|
565
|
+
};
|
|
566
|
+
|
|
567
|
+
const auth = betterAuth({
|
|
568
|
+
database: memory,
|
|
569
|
+
baseURL: "http://localhost:3000",
|
|
570
|
+
emailAndPassword: {
|
|
571
|
+
enabled: true,
|
|
572
|
+
},
|
|
573
|
+
plugins: [sso(ssoOptions)],
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
it("should use default SAML SSO provider from array when no provider found in database", async () => {
|
|
577
|
+
const signInResponse = await auth.api.signInSSO({
|
|
578
|
+
body: {
|
|
579
|
+
providerId: "default-saml",
|
|
580
|
+
callbackURL: "http://localhost:3000/dashboard",
|
|
581
|
+
},
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
expect(signInResponse).toEqual({
|
|
585
|
+
url: expect.stringContaining("http://localhost:8081"),
|
|
586
|
+
redirect: true,
|
|
587
|
+
});
|
|
588
|
+
});
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
describe("SAML SSO with signed AuthnRequests", async () => {
|
|
592
|
+
// IdP metadata with WantAuthnRequestsSigned="true" for testing signed requests
|
|
593
|
+
const idpMetadataWithSignedRequests = `
|
|
594
|
+
<md:EntityDescriptor xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata" entityID="http://localhost:8081/api/sso/saml2/idp/metadata">
|
|
595
|
+
<md:IDPSSODescriptor WantAuthnRequestsSigned="true" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
|
|
596
|
+
<md:KeyDescriptor use="signing">
|
|
597
|
+
<ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
|
|
598
|
+
<ds:X509Data>
|
|
599
|
+
<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>
|
|
600
|
+
</ds:X509Data>
|
|
601
|
+
</ds:KeyInfo>
|
|
602
|
+
</md:KeyDescriptor>
|
|
603
|
+
<md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="http://localhost:8081/api/sso/saml2/idp/redirect"/>
|
|
604
|
+
<md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="http://localhost:8081/api/sso/saml2/idp/post"/>
|
|
605
|
+
</md:IDPSSODescriptor>
|
|
606
|
+
</md:EntityDescriptor>
|
|
607
|
+
`;
|
|
608
|
+
|
|
609
|
+
const data = {
|
|
610
|
+
user: [],
|
|
611
|
+
session: [],
|
|
612
|
+
verification: [],
|
|
613
|
+
account: [],
|
|
614
|
+
ssoProvider: [],
|
|
615
|
+
};
|
|
616
|
+
|
|
617
|
+
const memory = memoryAdapter(data);
|
|
618
|
+
|
|
619
|
+
const ssoOptions = {
|
|
620
|
+
defaultSSO: [
|
|
621
|
+
{
|
|
622
|
+
domain: "localhost:8081",
|
|
623
|
+
providerId: "signed-saml",
|
|
624
|
+
samlConfig: {
|
|
625
|
+
issuer: "http://localhost:8081",
|
|
626
|
+
entryPoint: "http://localhost:8081/api/sso/saml2/idp/post",
|
|
627
|
+
cert: certificate,
|
|
628
|
+
callbackUrl: "http://localhost:8081/dashboard",
|
|
629
|
+
wantAssertionsSigned: false,
|
|
630
|
+
authnRequestsSigned: true,
|
|
631
|
+
signatureAlgorithm: "sha256",
|
|
632
|
+
digestAlgorithm: "sha256",
|
|
633
|
+
privateKey: idPk,
|
|
634
|
+
spMetadata: {
|
|
635
|
+
privateKey: idPk,
|
|
636
|
+
},
|
|
637
|
+
idpMetadata: {
|
|
638
|
+
metadata: idpMetadataWithSignedRequests,
|
|
639
|
+
},
|
|
640
|
+
identifierFormat:
|
|
641
|
+
"urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress",
|
|
642
|
+
},
|
|
643
|
+
},
|
|
644
|
+
],
|
|
645
|
+
};
|
|
646
|
+
|
|
647
|
+
const auth = betterAuth({
|
|
648
|
+
database: memory,
|
|
649
|
+
baseURL: "http://localhost:3000",
|
|
650
|
+
emailAndPassword: {
|
|
651
|
+
enabled: true,
|
|
652
|
+
},
|
|
653
|
+
plugins: [sso(ssoOptions)],
|
|
654
|
+
});
|
|
655
|
+
|
|
656
|
+
it("should generate signed AuthnRequest when authnRequestsSigned is true", async () => {
|
|
657
|
+
const signInResponse = await auth.api.signInSSO({
|
|
658
|
+
body: {
|
|
659
|
+
providerId: "signed-saml",
|
|
660
|
+
callbackURL: "http://localhost:3000/dashboard",
|
|
661
|
+
},
|
|
662
|
+
});
|
|
663
|
+
|
|
664
|
+
expect(signInResponse).toEqual({
|
|
665
|
+
url: expect.stringContaining("http://localhost:8081"),
|
|
666
|
+
redirect: true,
|
|
667
|
+
});
|
|
668
|
+
// When authnRequestsSigned is true and privateKey is provided,
|
|
669
|
+
// samlify adds Signature and SigAlg parameters to the redirect URL
|
|
670
|
+
expect(signInResponse.url).toContain("Signature=");
|
|
671
|
+
expect(signInResponse.url).toContain("SigAlg=");
|
|
672
|
+
});
|
|
673
|
+
});
|
|
674
|
+
|
|
675
|
+
describe("SAML SSO without signed AuthnRequests", async () => {
|
|
676
|
+
const data = {
|
|
677
|
+
user: [],
|
|
678
|
+
session: [],
|
|
679
|
+
verification: [],
|
|
680
|
+
account: [],
|
|
681
|
+
ssoProvider: [],
|
|
682
|
+
};
|
|
683
|
+
|
|
684
|
+
const memory = memoryAdapter(data);
|
|
685
|
+
|
|
686
|
+
const ssoOptions = {
|
|
687
|
+
defaultSSO: [
|
|
688
|
+
{
|
|
689
|
+
domain: "localhost:8082",
|
|
690
|
+
providerId: "unsigned-saml",
|
|
691
|
+
samlConfig: {
|
|
692
|
+
issuer: "http://localhost:8082",
|
|
693
|
+
entryPoint: "http://localhost:8081/api/sso/saml2/idp/post",
|
|
694
|
+
cert: certificate,
|
|
695
|
+
callbackUrl: "http://localhost:8082/dashboard",
|
|
696
|
+
wantAssertionsSigned: false,
|
|
697
|
+
authnRequestsSigned: false,
|
|
698
|
+
signatureAlgorithm: "sha256",
|
|
699
|
+
digestAlgorithm: "sha256",
|
|
700
|
+
idpMetadata: {
|
|
701
|
+
metadata: idpMetadata,
|
|
702
|
+
},
|
|
703
|
+
spMetadata: {
|
|
704
|
+
metadata: spMetadata,
|
|
705
|
+
},
|
|
706
|
+
identifierFormat:
|
|
707
|
+
"urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress",
|
|
708
|
+
},
|
|
709
|
+
},
|
|
710
|
+
],
|
|
711
|
+
};
|
|
712
|
+
|
|
713
|
+
const auth = betterAuth({
|
|
714
|
+
database: memory,
|
|
715
|
+
baseURL: "http://localhost:3000",
|
|
716
|
+
emailAndPassword: {
|
|
717
|
+
enabled: true,
|
|
718
|
+
},
|
|
719
|
+
plugins: [sso(ssoOptions)],
|
|
720
|
+
});
|
|
721
|
+
|
|
722
|
+
it("should NOT include Signature in URL when authnRequestsSigned is false", async () => {
|
|
723
|
+
const signInResponse = await auth.api.signInSSO({
|
|
724
|
+
body: {
|
|
725
|
+
providerId: "unsigned-saml",
|
|
726
|
+
callbackURL: "http://localhost:3000/dashboard",
|
|
727
|
+
},
|
|
728
|
+
});
|
|
729
|
+
|
|
730
|
+
expect(signInResponse).toEqual({
|
|
731
|
+
url: expect.stringContaining("http://localhost:8081"),
|
|
732
|
+
redirect: true,
|
|
733
|
+
});
|
|
734
|
+
// When authnRequestsSigned is false (default), no Signature should be in the URL
|
|
735
|
+
expect(signInResponse.url).not.toContain("Signature=");
|
|
736
|
+
expect(signInResponse.url).not.toContain("SigAlg=");
|
|
737
|
+
});
|
|
738
|
+
});
|
|
739
|
+
|
|
740
|
+
describe("SAML SSO", async () => {
|
|
741
|
+
const data = {
|
|
742
|
+
user: [],
|
|
743
|
+
session: [],
|
|
744
|
+
verification: [],
|
|
745
|
+
account: [],
|
|
746
|
+
ssoProvider: [],
|
|
747
|
+
};
|
|
748
|
+
|
|
749
|
+
const memory = memoryAdapter(data);
|
|
750
|
+
|
|
751
|
+
const ssoOptions = {
|
|
752
|
+
provisionUser: vi
|
|
753
|
+
.fn()
|
|
754
|
+
.mockImplementation(async ({ user, userInfo, token, provider }) => {
|
|
755
|
+
return {
|
|
756
|
+
id: "provisioned-user-id",
|
|
757
|
+
email: userInfo.email,
|
|
758
|
+
name: userInfo.name,
|
|
759
|
+
attributes: userInfo.attributes,
|
|
760
|
+
};
|
|
761
|
+
}),
|
|
762
|
+
};
|
|
763
|
+
|
|
764
|
+
const auth = betterAuth({
|
|
765
|
+
database: memory,
|
|
766
|
+
baseURL: "http://localhost:3000",
|
|
767
|
+
emailAndPassword: {
|
|
768
|
+
enabled: true,
|
|
769
|
+
},
|
|
770
|
+
plugins: [sso(ssoOptions)],
|
|
771
|
+
});
|
|
772
|
+
|
|
773
|
+
const authClient = createAuthClient({
|
|
774
|
+
baseURL: "http://localhost:3000",
|
|
775
|
+
plugins: [bearer(), ssoClient()],
|
|
776
|
+
fetchOptions: {
|
|
777
|
+
customFetchImpl: async (url, init) => {
|
|
778
|
+
return auth.handler(new Request(url, init));
|
|
779
|
+
},
|
|
780
|
+
},
|
|
781
|
+
});
|
|
782
|
+
|
|
783
|
+
const testUser = {
|
|
784
|
+
email: "test@email.com",
|
|
785
|
+
password: "password",
|
|
786
|
+
name: "Test User",
|
|
787
|
+
};
|
|
788
|
+
|
|
789
|
+
beforeAll(async () => {
|
|
790
|
+
await authClient.signUp.email({
|
|
791
|
+
email: testUser.email,
|
|
792
|
+
password: testUser.password,
|
|
793
|
+
name: testUser.name,
|
|
794
|
+
});
|
|
795
|
+
});
|
|
796
|
+
|
|
797
|
+
beforeEach(() => {
|
|
798
|
+
data.user = [];
|
|
799
|
+
data.session = [];
|
|
800
|
+
data.verification = [];
|
|
801
|
+
data.account = [];
|
|
802
|
+
data.ssoProvider = [];
|
|
803
|
+
|
|
804
|
+
vi.clearAllMocks();
|
|
805
|
+
});
|
|
806
|
+
|
|
807
|
+
async function getAuthHeaders() {
|
|
808
|
+
const headers = new Headers();
|
|
809
|
+
await authClient.signUp.email({
|
|
810
|
+
email: testUser.email,
|
|
811
|
+
password: testUser.password,
|
|
812
|
+
name: testUser.name,
|
|
813
|
+
});
|
|
814
|
+
await authClient.signIn.email(testUser, {
|
|
815
|
+
throw: true,
|
|
816
|
+
onSuccess: setCookieToHeader(headers),
|
|
817
|
+
});
|
|
818
|
+
return headers;
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
it("should register a new SAML provider", async () => {
|
|
822
|
+
const headers = await getAuthHeaders();
|
|
823
|
+
await authClient.signIn.email(testUser, {
|
|
824
|
+
throw: true,
|
|
825
|
+
onSuccess: setCookieToHeader(headers),
|
|
826
|
+
});
|
|
827
|
+
|
|
828
|
+
const provider = await auth.api.registerSSOProvider({
|
|
829
|
+
body: {
|
|
830
|
+
providerId: "saml-provider-1",
|
|
831
|
+
issuer: "http://localhost:8081",
|
|
832
|
+
domain: "http://localhost:8081",
|
|
833
|
+
samlConfig: {
|
|
834
|
+
entryPoint: sharedMockIdP.metadataUrl,
|
|
835
|
+
cert: certificate,
|
|
836
|
+
callbackUrl: "http://localhost:8081/api/sso/saml2/callback",
|
|
837
|
+
wantAssertionsSigned: false,
|
|
838
|
+
signatureAlgorithm: "sha256",
|
|
839
|
+
digestAlgorithm: "sha256",
|
|
840
|
+
idpMetadata: {
|
|
841
|
+
metadata: idpMetadata,
|
|
842
|
+
privateKey: idpPrivateKey,
|
|
843
|
+
privateKeyPass: "q9ALNhGT5EhfcRmp8Pg7e9zTQeP2x1bW",
|
|
844
|
+
isAssertionEncrypted: true,
|
|
845
|
+
encPrivateKey: idpEncryptionKey,
|
|
846
|
+
encPrivateKeyPass: "g7hGcRmp8PxT5QeP2q9Ehf1bWe9zTALN",
|
|
847
|
+
},
|
|
848
|
+
spMetadata: {
|
|
849
|
+
metadata: spMetadata,
|
|
850
|
+
binding: "post",
|
|
851
|
+
privateKey: spPrivateKey,
|
|
852
|
+
privateKeyPass: "VHOSp5RUiBcrsjrcAuXFwU1NKCkGA8px",
|
|
853
|
+
isAssertionEncrypted: true,
|
|
854
|
+
encPrivateKey: spEncryptionKey,
|
|
855
|
+
encPrivateKeyPass: "BXFNKpxrsjrCkGA8cAu5wUVHOSpci1RU",
|
|
856
|
+
},
|
|
857
|
+
identifierFormat:
|
|
858
|
+
"urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress",
|
|
859
|
+
},
|
|
860
|
+
},
|
|
861
|
+
headers,
|
|
862
|
+
});
|
|
863
|
+
expect(provider).toMatchObject({
|
|
864
|
+
id: expect.any(String),
|
|
865
|
+
issuer: "http://localhost:8081",
|
|
866
|
+
samlConfig: {
|
|
867
|
+
entryPoint: sharedMockIdP.metadataUrl,
|
|
868
|
+
cert: expect.any(String),
|
|
869
|
+
callbackUrl: "http://localhost:8081/api/sso/saml2/callback",
|
|
870
|
+
wantAssertionsSigned: false,
|
|
871
|
+
signatureAlgorithm: "sha256",
|
|
872
|
+
digestAlgorithm: "sha256",
|
|
873
|
+
identifierFormat:
|
|
874
|
+
"urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress",
|
|
875
|
+
},
|
|
876
|
+
});
|
|
877
|
+
});
|
|
878
|
+
it("Should fetch sp metadata", async () => {
|
|
879
|
+
const headers = await getAuthHeaders();
|
|
880
|
+
await authClient.signIn.email(testUser, {
|
|
881
|
+
throw: true,
|
|
882
|
+
onSuccess: setCookieToHeader(headers),
|
|
883
|
+
});
|
|
884
|
+
const provider = await auth.api.registerSSOProvider({
|
|
885
|
+
body: {
|
|
886
|
+
providerId: "saml-provider-1",
|
|
887
|
+
issuer: "http://localhost:8081",
|
|
888
|
+
domain: "http://localhost:8081",
|
|
889
|
+
samlConfig: {
|
|
890
|
+
entryPoint: sharedMockIdP.metadataUrl,
|
|
891
|
+
cert: certificate,
|
|
892
|
+
callbackUrl: "http://localhost:8081/api/sso/saml2/sp/acs",
|
|
893
|
+
wantAssertionsSigned: false,
|
|
894
|
+
signatureAlgorithm: "sha256",
|
|
895
|
+
digestAlgorithm: "sha256",
|
|
896
|
+
idpMetadata: {
|
|
897
|
+
metadata: idpMetadata,
|
|
898
|
+
privateKey: idpPrivateKey,
|
|
899
|
+
privateKeyPass: "q9ALNhGT5EhfcRmp8Pg7e9zTQeP2x1bW",
|
|
900
|
+
isAssertionEncrypted: true,
|
|
901
|
+
encPrivateKey: idpEncryptionKey,
|
|
902
|
+
encPrivateKeyPass: "g7hGcRmp8PxT5QeP2q9Ehf1bWe9zTALN",
|
|
903
|
+
},
|
|
904
|
+
spMetadata: {
|
|
905
|
+
metadata: spMetadata,
|
|
906
|
+
binding: "post",
|
|
907
|
+
privateKey: spPrivateKey,
|
|
908
|
+
privateKeyPass: "VHOSp5RUiBcrsjrcAuXFwU1NKCkGA8px",
|
|
909
|
+
isAssertionEncrypted: true,
|
|
910
|
+
encPrivateKey: spEncryptionKey,
|
|
911
|
+
encPrivateKeyPass: "BXFNKpxrsjrCkGA8cAu5wUVHOSpci1RU",
|
|
912
|
+
},
|
|
913
|
+
identifierFormat:
|
|
914
|
+
"urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress",
|
|
915
|
+
},
|
|
916
|
+
},
|
|
917
|
+
headers,
|
|
918
|
+
});
|
|
919
|
+
|
|
920
|
+
const spMetadataRes = await auth.api.spMetadata({
|
|
921
|
+
query: {
|
|
922
|
+
providerId: provider.providerId,
|
|
923
|
+
},
|
|
924
|
+
});
|
|
925
|
+
const spMetadataResResValue = await spMetadataRes.text();
|
|
926
|
+
expect(spMetadataRes.status).toBe(200);
|
|
927
|
+
expect(spMetadataResResValue).toBe(spMetadata);
|
|
928
|
+
});
|
|
929
|
+
it("Should fetch sp metadata", async () => {
|
|
930
|
+
const headers = await getAuthHeaders();
|
|
931
|
+
await authClient.signIn.email(testUser, {
|
|
932
|
+
throw: true,
|
|
933
|
+
onSuccess: setCookieToHeader(headers),
|
|
934
|
+
});
|
|
935
|
+
const issuer = "http://localhost:8081";
|
|
936
|
+
const provider = await auth.api.registerSSOProvider({
|
|
937
|
+
body: {
|
|
938
|
+
providerId: "saml-provider-1",
|
|
939
|
+
issuer: issuer,
|
|
940
|
+
domain: issuer,
|
|
941
|
+
samlConfig: {
|
|
942
|
+
entryPoint: sharedMockIdP.metadataUrl,
|
|
943
|
+
cert: certificate,
|
|
944
|
+
callbackUrl: `${issuer}/api/sso/saml2/sp/acs`,
|
|
945
|
+
wantAssertionsSigned: false,
|
|
946
|
+
signatureAlgorithm: "sha256",
|
|
947
|
+
digestAlgorithm: "sha256",
|
|
948
|
+
idpMetadata: {
|
|
949
|
+
metadata: idpMetadata,
|
|
950
|
+
privateKey: idpPrivateKey,
|
|
951
|
+
privateKeyPass: "q9ALNhGT5EhfcRmp8Pg7e9zTQeP2x1bW",
|
|
952
|
+
isAssertionEncrypted: true,
|
|
953
|
+
encPrivateKey: idpEncryptionKey,
|
|
954
|
+
encPrivateKeyPass: "g7hGcRmp8PxT5QeP2q9Ehf1bWe9zTALN",
|
|
955
|
+
},
|
|
956
|
+
spMetadata: {
|
|
957
|
+
binding: "post",
|
|
958
|
+
privateKey: spPrivateKey,
|
|
959
|
+
privateKeyPass: "VHOSp5RUiBcrsjrcAuXFwU1NKCkGA8px",
|
|
960
|
+
isAssertionEncrypted: true,
|
|
961
|
+
encPrivateKey: spEncryptionKey,
|
|
962
|
+
encPrivateKeyPass: "BXFNKpxrsjrCkGA8cAu5wUVHOSpci1RU",
|
|
963
|
+
},
|
|
964
|
+
identifierFormat:
|
|
965
|
+
"urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress",
|
|
966
|
+
},
|
|
967
|
+
},
|
|
968
|
+
headers,
|
|
969
|
+
});
|
|
970
|
+
|
|
971
|
+
const spMetadataRes = await auth.api.spMetadata({
|
|
972
|
+
query: {
|
|
973
|
+
providerId: provider.providerId,
|
|
974
|
+
},
|
|
975
|
+
});
|
|
976
|
+
const spMetadataResResValue = await spMetadataRes.text();
|
|
977
|
+
expect(spMetadataRes.status).toBe(200);
|
|
978
|
+
expect(spMetadataResResValue).toBeDefined();
|
|
979
|
+
expect(spMetadataResResValue).toContain(
|
|
980
|
+
"urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress",
|
|
981
|
+
);
|
|
982
|
+
expect(spMetadataResResValue).toContain(
|
|
983
|
+
"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST",
|
|
984
|
+
);
|
|
985
|
+
expect(spMetadataResResValue).toContain(
|
|
986
|
+
`<EntityDescriptor entityID="${issuer}"`,
|
|
987
|
+
);
|
|
988
|
+
expect(spMetadataResResValue).toContain(
|
|
989
|
+
`Location="${issuer}/api/sso/saml2/sp/acs"`,
|
|
990
|
+
);
|
|
991
|
+
});
|
|
992
|
+
it("should initiate SAML login and handle response", async () => {
|
|
993
|
+
const headers = await getAuthHeaders();
|
|
994
|
+
await authClient.signIn.email(testUser, {
|
|
995
|
+
throw: true,
|
|
996
|
+
onSuccess: setCookieToHeader(headers),
|
|
997
|
+
});
|
|
998
|
+
await auth.api.registerSSOProvider({
|
|
999
|
+
body: {
|
|
1000
|
+
providerId: "saml-provider-1",
|
|
1001
|
+
issuer: "http://localhost:8081",
|
|
1002
|
+
domain: "http://localhost:8081",
|
|
1003
|
+
samlConfig: {
|
|
1004
|
+
entryPoint: "http://localhost:8081/api/sso/saml2/idp/post",
|
|
1005
|
+
cert: certificate,
|
|
1006
|
+
callbackUrl: "http://localhost:8081/dashboard",
|
|
1007
|
+
wantAssertionsSigned: false,
|
|
1008
|
+
signatureAlgorithm: "sha256",
|
|
1009
|
+
digestAlgorithm: "sha256",
|
|
1010
|
+
idpMetadata: {
|
|
1011
|
+
metadata: idpMetadata,
|
|
1012
|
+
privateKey: idpPrivateKey,
|
|
1013
|
+
privateKeyPass: "q9ALNhGT5EhfcRmp8Pg7e9zTQeP2x1bW",
|
|
1014
|
+
isAssertionEncrypted: true,
|
|
1015
|
+
encPrivateKey: idpEncryptionKey,
|
|
1016
|
+
encPrivateKeyPass: "g7hGcRmp8PxT5QeP2q9Ehf1bWe9zTALN",
|
|
1017
|
+
},
|
|
1018
|
+
spMetadata: {
|
|
1019
|
+
metadata: spMetadata,
|
|
1020
|
+
binding: "post",
|
|
1021
|
+
privateKey: spPrivateKey,
|
|
1022
|
+
privateKeyPass: "VHOSp5RUiBcrsjrcAuXFwU1NKCkGA8px",
|
|
1023
|
+
isAssertionEncrypted: true,
|
|
1024
|
+
encPrivateKey: spEncryptionKey,
|
|
1025
|
+
encPrivateKeyPass: "BXFNKpxrsjrCkGA8cAu5wUVHOSpci1RU",
|
|
1026
|
+
},
|
|
1027
|
+
identifierFormat:
|
|
1028
|
+
"urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress",
|
|
1029
|
+
},
|
|
1030
|
+
},
|
|
1031
|
+
headers,
|
|
1032
|
+
});
|
|
1033
|
+
|
|
1034
|
+
const signInResponse = await auth.api.signInSSO({
|
|
1035
|
+
body: {
|
|
1036
|
+
providerId: "saml-provider-1",
|
|
1037
|
+
callbackURL: "http://localhost:3000/dashboard",
|
|
1038
|
+
},
|
|
1039
|
+
});
|
|
1040
|
+
|
|
1041
|
+
expect(signInResponse).toEqual({
|
|
1042
|
+
url: expect.stringContaining("http://localhost:8081"),
|
|
1043
|
+
redirect: true,
|
|
1044
|
+
});
|
|
1045
|
+
let samlResponse: any;
|
|
1046
|
+
await betterFetch(signInResponse?.url as string, {
|
|
1047
|
+
onSuccess: async (context) => {
|
|
1048
|
+
samlResponse = await context.data;
|
|
1049
|
+
},
|
|
1050
|
+
});
|
|
1051
|
+
let redirectLocation = "";
|
|
1052
|
+
await betterFetch(
|
|
1053
|
+
"http://localhost:8081/api/sso/saml2/callback/saml-provider-1",
|
|
1054
|
+
{
|
|
1055
|
+
method: "POST",
|
|
1056
|
+
redirect: "manual",
|
|
1057
|
+
headers: {
|
|
1058
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
1059
|
+
},
|
|
1060
|
+
body: new URLSearchParams({
|
|
1061
|
+
SAMLResponse: samlResponse.samlResponse,
|
|
1062
|
+
}),
|
|
1063
|
+
onError: (context) => {
|
|
1064
|
+
expect(context.response.status).toBe(302);
|
|
1065
|
+
redirectLocation = context.response.headers.get("location") || "";
|
|
1066
|
+
},
|
|
1067
|
+
},
|
|
1068
|
+
);
|
|
1069
|
+
expect(redirectLocation).toBe("http://localhost:3000/dashboard");
|
|
1070
|
+
});
|
|
1071
|
+
|
|
1072
|
+
it("should not allow creating a provider if limit is set to 0", async () => {
|
|
1073
|
+
const { auth, signInWithTestUser } = await getTestInstance({
|
|
1074
|
+
plugins: [sso({ providersLimit: 0 })],
|
|
1075
|
+
});
|
|
1076
|
+
const { headers } = await signInWithTestUser();
|
|
1077
|
+
await expect(
|
|
1078
|
+
auth.api.registerSSOProvider({
|
|
1079
|
+
body: {
|
|
1080
|
+
providerId: "saml-provider-1",
|
|
1081
|
+
issuer: "http://localhost:8081",
|
|
1082
|
+
domain: "http://localhost:8081",
|
|
1083
|
+
samlConfig: {
|
|
1084
|
+
entryPoint: sharedMockIdP.metadataUrl,
|
|
1085
|
+
cert: certificate,
|
|
1086
|
+
callbackUrl: "http://localhost:8081/api/sso/saml2/callback",
|
|
1087
|
+
wantAssertionsSigned: false,
|
|
1088
|
+
signatureAlgorithm: "sha256",
|
|
1089
|
+
digestAlgorithm: "sha256",
|
|
1090
|
+
spMetadata: {
|
|
1091
|
+
metadata: spMetadata,
|
|
1092
|
+
},
|
|
1093
|
+
},
|
|
1094
|
+
},
|
|
1095
|
+
headers,
|
|
1096
|
+
}),
|
|
1097
|
+
).rejects.toMatchObject({
|
|
1098
|
+
status: "FORBIDDEN",
|
|
1099
|
+
body: { message: "SSO provider registration is disabled" },
|
|
1100
|
+
});
|
|
1101
|
+
});
|
|
1102
|
+
|
|
1103
|
+
it("should not allow creating a provider if limit is reached", async () => {
|
|
1104
|
+
const { auth, signInWithTestUser } = await getTestInstance({
|
|
1105
|
+
plugins: [sso({ providersLimit: 1 })],
|
|
1106
|
+
});
|
|
1107
|
+
const { headers } = await signInWithTestUser();
|
|
1108
|
+
|
|
1109
|
+
await auth.api.registerSSOProvider({
|
|
1110
|
+
body: {
|
|
1111
|
+
providerId: "saml-provider-1",
|
|
1112
|
+
issuer: "http://localhost:8081",
|
|
1113
|
+
domain: "http://localhost:8081",
|
|
1114
|
+
samlConfig: {
|
|
1115
|
+
entryPoint: sharedMockIdP.metadataUrl,
|
|
1116
|
+
cert: certificate,
|
|
1117
|
+
callbackUrl: "http://localhost:8081/api/sso/saml2/callback",
|
|
1118
|
+
wantAssertionsSigned: false,
|
|
1119
|
+
signatureAlgorithm: "sha256",
|
|
1120
|
+
digestAlgorithm: "sha256",
|
|
1121
|
+
spMetadata: {
|
|
1122
|
+
metadata: spMetadata,
|
|
1123
|
+
},
|
|
1124
|
+
},
|
|
1125
|
+
},
|
|
1126
|
+
headers,
|
|
1127
|
+
});
|
|
1128
|
+
|
|
1129
|
+
await expect(
|
|
1130
|
+
auth.api.registerSSOProvider({
|
|
1131
|
+
body: {
|
|
1132
|
+
providerId: "saml-provider-2",
|
|
1133
|
+
issuer: "http://localhost:8081",
|
|
1134
|
+
domain: "http://localhost:8081",
|
|
1135
|
+
samlConfig: {
|
|
1136
|
+
entryPoint: sharedMockIdP.metadataUrl,
|
|
1137
|
+
cert: certificate,
|
|
1138
|
+
callbackUrl: "http://localhost:8081/api/sso/saml2/callback",
|
|
1139
|
+
wantAssertionsSigned: false,
|
|
1140
|
+
signatureAlgorithm: "sha256",
|
|
1141
|
+
digestAlgorithm: "sha256",
|
|
1142
|
+
spMetadata: {
|
|
1143
|
+
metadata: spMetadata,
|
|
1144
|
+
},
|
|
1145
|
+
},
|
|
1146
|
+
},
|
|
1147
|
+
headers,
|
|
1148
|
+
}),
|
|
1149
|
+
).rejects.toMatchObject({
|
|
1150
|
+
status: "FORBIDDEN",
|
|
1151
|
+
body: {
|
|
1152
|
+
message: "You have reached the maximum number of SSO providers",
|
|
1153
|
+
},
|
|
1154
|
+
});
|
|
1155
|
+
});
|
|
1156
|
+
|
|
1157
|
+
it("should not allow creating a provider if limit from function is reached", async () => {
|
|
1158
|
+
const { auth, signInWithTestUser } = await getTestInstance({
|
|
1159
|
+
plugins: [
|
|
1160
|
+
sso({
|
|
1161
|
+
providersLimit: async (user) => {
|
|
1162
|
+
return user.email === "pro@example.com" ? 2 : 1;
|
|
1163
|
+
},
|
|
1164
|
+
}),
|
|
1165
|
+
],
|
|
1166
|
+
});
|
|
1167
|
+
const { headers } = await signInWithTestUser();
|
|
1168
|
+
|
|
1169
|
+
await auth.api.registerSSOProvider({
|
|
1170
|
+
body: {
|
|
1171
|
+
providerId: "saml-provider-1",
|
|
1172
|
+
issuer: "http://localhost:8081",
|
|
1173
|
+
domain: "http://localhost:8081",
|
|
1174
|
+
samlConfig: {
|
|
1175
|
+
entryPoint: sharedMockIdP.metadataUrl,
|
|
1176
|
+
cert: certificate,
|
|
1177
|
+
callbackUrl: "http://localhost:8081/api/sso/saml2/callback",
|
|
1178
|
+
wantAssertionsSigned: false,
|
|
1179
|
+
signatureAlgorithm: "sha256",
|
|
1180
|
+
digestAlgorithm: "sha256",
|
|
1181
|
+
spMetadata: {
|
|
1182
|
+
metadata: spMetadata,
|
|
1183
|
+
},
|
|
1184
|
+
},
|
|
1185
|
+
},
|
|
1186
|
+
headers,
|
|
1187
|
+
});
|
|
1188
|
+
|
|
1189
|
+
await expect(
|
|
1190
|
+
auth.api.registerSSOProvider({
|
|
1191
|
+
body: {
|
|
1192
|
+
providerId: "saml-provider-2",
|
|
1193
|
+
issuer: "http://localhost:8081",
|
|
1194
|
+
domain: "http://localhost:8081",
|
|
1195
|
+
samlConfig: {
|
|
1196
|
+
entryPoint: sharedMockIdP.metadataUrl,
|
|
1197
|
+
cert: certificate,
|
|
1198
|
+
callbackUrl: "http://localhost:8081/api/sso/saml2/callback",
|
|
1199
|
+
wantAssertionsSigned: false,
|
|
1200
|
+
signatureAlgorithm: "sha256",
|
|
1201
|
+
digestAlgorithm: "sha256",
|
|
1202
|
+
spMetadata: {
|
|
1203
|
+
metadata: spMetadata,
|
|
1204
|
+
},
|
|
1205
|
+
},
|
|
1206
|
+
},
|
|
1207
|
+
headers,
|
|
1208
|
+
}),
|
|
1209
|
+
).rejects.toMatchObject({
|
|
1210
|
+
status: "FORBIDDEN",
|
|
1211
|
+
body: {
|
|
1212
|
+
message: "You have reached the maximum number of SSO providers",
|
|
1213
|
+
},
|
|
1214
|
+
});
|
|
1215
|
+
});
|
|
1216
|
+
|
|
1217
|
+
it("should not allow creating a provider with duplicate providerId", async () => {
|
|
1218
|
+
const headers = await getAuthHeaders();
|
|
1219
|
+
await authClient.signIn.email(testUser, {
|
|
1220
|
+
throw: true,
|
|
1221
|
+
onSuccess: setCookieToHeader(headers),
|
|
1222
|
+
});
|
|
1223
|
+
|
|
1224
|
+
await auth.api.registerSSOProvider({
|
|
1225
|
+
body: {
|
|
1226
|
+
providerId: "duplicate-provider",
|
|
1227
|
+
issuer: "http://localhost:8081",
|
|
1228
|
+
domain: "http://localhost:8081",
|
|
1229
|
+
samlConfig: {
|
|
1230
|
+
entryPoint: sharedMockIdP.metadataUrl,
|
|
1231
|
+
cert: certificate,
|
|
1232
|
+
callbackUrl: "http://localhost:8081/api/sso/saml2/callback",
|
|
1233
|
+
spMetadata: {
|
|
1234
|
+
metadata: spMetadata,
|
|
1235
|
+
},
|
|
1236
|
+
},
|
|
1237
|
+
},
|
|
1238
|
+
headers,
|
|
1239
|
+
});
|
|
1240
|
+
|
|
1241
|
+
await expect(
|
|
1242
|
+
auth.api.registerSSOProvider({
|
|
1243
|
+
body: {
|
|
1244
|
+
providerId: "duplicate-provider",
|
|
1245
|
+
issuer: "http://localhost:8082",
|
|
1246
|
+
domain: "http://localhost:8082",
|
|
1247
|
+
samlConfig: {
|
|
1248
|
+
entryPoint: sharedMockIdP.metadataUrl,
|
|
1249
|
+
cert: certificate,
|
|
1250
|
+
callbackUrl: "http://localhost:8082/api/sso/saml2/callback",
|
|
1251
|
+
spMetadata: {
|
|
1252
|
+
metadata: spMetadata,
|
|
1253
|
+
},
|
|
1254
|
+
},
|
|
1255
|
+
},
|
|
1256
|
+
headers,
|
|
1257
|
+
}),
|
|
1258
|
+
).rejects.toMatchObject({
|
|
1259
|
+
status: "UNPROCESSABLE_ENTITY",
|
|
1260
|
+
body: {
|
|
1261
|
+
message: "SSO provider with this providerId already exists",
|
|
1262
|
+
},
|
|
1263
|
+
});
|
|
1264
|
+
});
|
|
1265
|
+
|
|
1266
|
+
it("should initiate SAML login and validate RelayState", async () => {
|
|
1267
|
+
const { auth, signInWithTestUser } = await getTestInstance({
|
|
1268
|
+
plugins: [sso()],
|
|
1269
|
+
});
|
|
1270
|
+
|
|
1271
|
+
const { headers } = await signInWithTestUser();
|
|
1272
|
+
await auth.api.registerSSOProvider({
|
|
1273
|
+
body: {
|
|
1274
|
+
providerId: "saml-provider-1",
|
|
1275
|
+
issuer: "http://localhost:8081",
|
|
1276
|
+
domain: "http://localhost:8081",
|
|
1277
|
+
samlConfig: {
|
|
1278
|
+
entryPoint: "http://localhost:8081/api/sso/saml2/idp/post",
|
|
1279
|
+
cert: certificate,
|
|
1280
|
+
callbackUrl: "http://localhost:3000/dashboard",
|
|
1281
|
+
wantAssertionsSigned: false,
|
|
1282
|
+
signatureAlgorithm: "sha256",
|
|
1283
|
+
digestAlgorithm: "sha256",
|
|
1284
|
+
idpMetadata: {
|
|
1285
|
+
metadata: idpMetadata,
|
|
1286
|
+
},
|
|
1287
|
+
spMetadata: {
|
|
1288
|
+
metadata: spMetadata,
|
|
1289
|
+
},
|
|
1290
|
+
identifierFormat:
|
|
1291
|
+
"urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress",
|
|
1292
|
+
},
|
|
1293
|
+
},
|
|
1294
|
+
headers,
|
|
1295
|
+
});
|
|
1296
|
+
|
|
1297
|
+
const response = await auth.api.signInSSO({
|
|
1298
|
+
body: {
|
|
1299
|
+
providerId: "saml-provider-1",
|
|
1300
|
+
callbackURL: "http://localhost:3000/dashboard",
|
|
1301
|
+
},
|
|
1302
|
+
returnHeaders: true,
|
|
1303
|
+
});
|
|
1304
|
+
|
|
1305
|
+
const signInResponse = response.response;
|
|
1306
|
+
expect(signInResponse).toEqual({
|
|
1307
|
+
url: expect.stringContaining("http://localhost:8081"),
|
|
1308
|
+
redirect: true,
|
|
1309
|
+
});
|
|
1310
|
+
|
|
1311
|
+
let samlResponse: any;
|
|
1312
|
+
await betterFetch(signInResponse?.url, {
|
|
1313
|
+
onSuccess: async (context) => {
|
|
1314
|
+
samlResponse = await context.data;
|
|
1315
|
+
},
|
|
1316
|
+
});
|
|
1317
|
+
|
|
1318
|
+
const samlRedirectUrl = new URL(signInResponse?.url);
|
|
1319
|
+
const callbackResponse = await auth.api.callbackSSOSAML({
|
|
1320
|
+
method: "POST",
|
|
1321
|
+
body: {
|
|
1322
|
+
SAMLResponse: samlResponse.samlResponse,
|
|
1323
|
+
RelayState: samlRedirectUrl.searchParams.get("RelayState") ?? "",
|
|
1324
|
+
},
|
|
1325
|
+
headers: {
|
|
1326
|
+
Cookie: response.headers.get("set-cookie") ?? "",
|
|
1327
|
+
},
|
|
1328
|
+
params: {
|
|
1329
|
+
providerId: "saml-provider-1",
|
|
1330
|
+
},
|
|
1331
|
+
asResponse: true,
|
|
1332
|
+
});
|
|
1333
|
+
|
|
1334
|
+
expect(callbackResponse.headers.get("location")).toContain("dashboard");
|
|
1335
|
+
});
|
|
1336
|
+
|
|
1337
|
+
it("should initiate SAML login and fallback to callbackUrl on invalid RelayState", async () => {
|
|
1338
|
+
const { auth, signInWithTestUser } = await getTestInstance({
|
|
1339
|
+
plugins: [sso()],
|
|
1340
|
+
});
|
|
1341
|
+
|
|
1342
|
+
const { headers } = await signInWithTestUser();
|
|
1343
|
+
await auth.api.registerSSOProvider({
|
|
1344
|
+
body: {
|
|
1345
|
+
providerId: "saml-provider-1",
|
|
1346
|
+
issuer: "http://localhost:8081",
|
|
1347
|
+
domain: "http://localhost:8081",
|
|
1348
|
+
samlConfig: {
|
|
1349
|
+
entryPoint: "http://localhost:8081/api/sso/saml2/idp/post",
|
|
1350
|
+
cert: certificate,
|
|
1351
|
+
callbackUrl: "http://localhost:3000/dashboard",
|
|
1352
|
+
wantAssertionsSigned: false,
|
|
1353
|
+
signatureAlgorithm: "sha256",
|
|
1354
|
+
digestAlgorithm: "sha256",
|
|
1355
|
+
idpMetadata: {
|
|
1356
|
+
metadata: idpMetadata,
|
|
1357
|
+
},
|
|
1358
|
+
spMetadata: {
|
|
1359
|
+
metadata: spMetadata,
|
|
1360
|
+
},
|
|
1361
|
+
identifierFormat:
|
|
1362
|
+
"urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress",
|
|
1363
|
+
},
|
|
1364
|
+
},
|
|
1365
|
+
headers,
|
|
1366
|
+
});
|
|
1367
|
+
|
|
1368
|
+
const response = await auth.api.signInSSO({
|
|
1369
|
+
body: {
|
|
1370
|
+
providerId: "saml-provider-1",
|
|
1371
|
+
callbackURL: "http://localhost:3000/dashboard",
|
|
1372
|
+
},
|
|
1373
|
+
returnHeaders: true,
|
|
1374
|
+
});
|
|
1375
|
+
|
|
1376
|
+
const signInResponse = response.response;
|
|
1377
|
+
expect(signInResponse).toEqual({
|
|
1378
|
+
url: expect.stringContaining("http://localhost:8081"),
|
|
1379
|
+
redirect: true,
|
|
1380
|
+
});
|
|
1381
|
+
|
|
1382
|
+
let samlResponse: any;
|
|
1383
|
+
await betterFetch(signInResponse?.url, {
|
|
1384
|
+
onSuccess: async (context) => {
|
|
1385
|
+
samlResponse = await context.data;
|
|
1386
|
+
},
|
|
1387
|
+
});
|
|
1388
|
+
|
|
1389
|
+
const callbackResponse = await auth.api.callbackSSOSAML({
|
|
1390
|
+
method: "POST",
|
|
1391
|
+
body: {
|
|
1392
|
+
SAMLResponse: samlResponse.samlResponse,
|
|
1393
|
+
RelayState: "not-the-right-relay-state",
|
|
1394
|
+
},
|
|
1395
|
+
headers: {
|
|
1396
|
+
Cookie: response.headers.get("set-cookie") ?? "",
|
|
1397
|
+
},
|
|
1398
|
+
params: {
|
|
1399
|
+
providerId: "saml-provider-1",
|
|
1400
|
+
},
|
|
1401
|
+
asResponse: true,
|
|
1402
|
+
});
|
|
1403
|
+
|
|
1404
|
+
expect(callbackResponse.status).toBe(302);
|
|
1405
|
+
expect(callbackResponse.headers.get("location")).toBe(
|
|
1406
|
+
"http://localhost:3000/dashboard",
|
|
1407
|
+
);
|
|
1408
|
+
});
|
|
1409
|
+
|
|
1410
|
+
it("should initiate SAML login and signup user when disableImplicitSignUp is true but requestSignup is explicitly enabled", async () => {
|
|
1411
|
+
const { auth, signInWithTestUser } = await getTestInstance({
|
|
1412
|
+
plugins: [sso({ disableImplicitSignUp: true })],
|
|
1413
|
+
});
|
|
1414
|
+
|
|
1415
|
+
const { headers } = await signInWithTestUser();
|
|
1416
|
+
await auth.api.registerSSOProvider({
|
|
1417
|
+
body: {
|
|
1418
|
+
providerId: "saml-provider-1",
|
|
1419
|
+
issuer: "http://localhost:8081",
|
|
1420
|
+
domain: "http://localhost:8081",
|
|
1421
|
+
samlConfig: {
|
|
1422
|
+
entryPoint: "http://localhost:8081/api/sso/saml2/idp/post",
|
|
1423
|
+
cert: certificate,
|
|
1424
|
+
callbackUrl: "http://localhost:3000/dashboard",
|
|
1425
|
+
wantAssertionsSigned: false,
|
|
1426
|
+
signatureAlgorithm: "sha256",
|
|
1427
|
+
digestAlgorithm: "sha256",
|
|
1428
|
+
idpMetadata: {
|
|
1429
|
+
metadata: idpMetadata,
|
|
1430
|
+
},
|
|
1431
|
+
spMetadata: {
|
|
1432
|
+
metadata: spMetadata,
|
|
1433
|
+
},
|
|
1434
|
+
identifierFormat:
|
|
1435
|
+
"urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress",
|
|
1436
|
+
},
|
|
1437
|
+
},
|
|
1438
|
+
headers,
|
|
1439
|
+
});
|
|
1440
|
+
|
|
1441
|
+
const response = await auth.api.signInSSO({
|
|
1442
|
+
body: {
|
|
1443
|
+
providerId: "saml-provider-1",
|
|
1444
|
+
callbackURL: "http://localhost:3000/dashboard",
|
|
1445
|
+
requestSignUp: true,
|
|
1446
|
+
},
|
|
1447
|
+
returnHeaders: true,
|
|
1448
|
+
});
|
|
1449
|
+
|
|
1450
|
+
const signInResponse = response.response;
|
|
1451
|
+
expect(signInResponse).toEqual({
|
|
1452
|
+
url: expect.stringContaining("http://localhost:8081"),
|
|
1453
|
+
redirect: true,
|
|
1454
|
+
});
|
|
1455
|
+
|
|
1456
|
+
let samlResponse: any;
|
|
1457
|
+
await betterFetch(signInResponse?.url, {
|
|
1458
|
+
onSuccess: async (context) => {
|
|
1459
|
+
samlResponse = await context.data;
|
|
1460
|
+
},
|
|
1461
|
+
});
|
|
1462
|
+
|
|
1463
|
+
const samlRedirectUrl = new URL(signInResponse?.url);
|
|
1464
|
+
const callbackResponse = await auth.api.callbackSSOSAML({
|
|
1465
|
+
method: "POST",
|
|
1466
|
+
body: {
|
|
1467
|
+
SAMLResponse: samlResponse.samlResponse,
|
|
1468
|
+
RelayState: samlRedirectUrl.searchParams.get("RelayState") ?? "",
|
|
1469
|
+
},
|
|
1470
|
+
headers: {
|
|
1471
|
+
Cookie: response.headers.get("set-cookie") ?? "",
|
|
1472
|
+
},
|
|
1473
|
+
params: {
|
|
1474
|
+
providerId: "saml-provider-1",
|
|
1475
|
+
},
|
|
1476
|
+
asResponse: true,
|
|
1477
|
+
});
|
|
1478
|
+
|
|
1479
|
+
expect(callbackResponse.headers.get("location")).toContain("dashboard");
|
|
1480
|
+
});
|
|
1481
|
+
|
|
1482
|
+
it("should reject SAML sign-in when disableImplicitSignUp is true and user doesn't exist", async () => {
|
|
1483
|
+
const { auth: authWithDisabledSignUp, signInWithTestUser } =
|
|
1484
|
+
await getTestInstance({
|
|
1485
|
+
plugins: [sso({ disableImplicitSignUp: true })],
|
|
1486
|
+
});
|
|
1487
|
+
|
|
1488
|
+
const { headers } = await signInWithTestUser();
|
|
1489
|
+
|
|
1490
|
+
// Register SAML provider
|
|
1491
|
+
await authWithDisabledSignUp.api.registerSSOProvider({
|
|
1492
|
+
body: {
|
|
1493
|
+
providerId: "saml-test-provider",
|
|
1494
|
+
issuer: "http://localhost:8081",
|
|
1495
|
+
domain: "http://localhost:8081",
|
|
1496
|
+
samlConfig: {
|
|
1497
|
+
entryPoint: "http://localhost:8081/api/sso/saml2/idp/post",
|
|
1498
|
+
cert: certificate,
|
|
1499
|
+
callbackUrl: "http://localhost:3000/dashboard",
|
|
1500
|
+
wantAssertionsSigned: false,
|
|
1501
|
+
signatureAlgorithm: "sha256",
|
|
1502
|
+
digestAlgorithm: "sha256",
|
|
1503
|
+
idpMetadata: {
|
|
1504
|
+
metadata: idpMetadata,
|
|
1505
|
+
},
|
|
1506
|
+
spMetadata: {
|
|
1507
|
+
metadata: spMetadata,
|
|
1508
|
+
},
|
|
1509
|
+
identifierFormat:
|
|
1510
|
+
"urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress",
|
|
1511
|
+
},
|
|
1512
|
+
},
|
|
1513
|
+
headers: headers,
|
|
1514
|
+
});
|
|
1515
|
+
|
|
1516
|
+
// Identity Provider-initiated: Get SAML response directly from IdP
|
|
1517
|
+
// The mock IdP will return test@email.com, which doesn't exist in the DB
|
|
1518
|
+
let samlResponse: any;
|
|
1519
|
+
await betterFetch("http://localhost:8081/api/sso/saml2/idp/post", {
|
|
1520
|
+
onSuccess: async (context) => {
|
|
1521
|
+
samlResponse = await context.data;
|
|
1522
|
+
},
|
|
1523
|
+
});
|
|
1524
|
+
|
|
1525
|
+
const response = await authWithDisabledSignUp.handler(
|
|
1526
|
+
new Request(
|
|
1527
|
+
"http://localhost:3000/api/auth/sso/saml2/callback/saml-test-provider",
|
|
1528
|
+
{
|
|
1529
|
+
method: "POST",
|
|
1530
|
+
headers: {
|
|
1531
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
1532
|
+
},
|
|
1533
|
+
body: new URLSearchParams({
|
|
1534
|
+
SAMLResponse: samlResponse.samlResponse,
|
|
1535
|
+
RelayState: "http://localhost:3000/dashboard",
|
|
1536
|
+
}),
|
|
1537
|
+
},
|
|
1538
|
+
),
|
|
1539
|
+
);
|
|
1540
|
+
|
|
1541
|
+
expect(response.status).toBe(302);
|
|
1542
|
+
const redirectLocation = response.headers.get("location") || "";
|
|
1543
|
+
expect(redirectLocation).toContain("error=signup_disabled");
|
|
1544
|
+
});
|
|
1545
|
+
|
|
1546
|
+
it("should reject SAML ACS (IdP-initiated) when disableImplicitSignUp is true and user doesn't exist", async () => {
|
|
1547
|
+
const { auth: authWithDisabledSignUp, signInWithTestUser } =
|
|
1548
|
+
await getTestInstance({
|
|
1549
|
+
plugins: [sso({ disableImplicitSignUp: true })],
|
|
1550
|
+
});
|
|
1551
|
+
|
|
1552
|
+
const { headers } = await signInWithTestUser();
|
|
1553
|
+
|
|
1554
|
+
await authWithDisabledSignUp.api.registerSSOProvider({
|
|
1555
|
+
body: {
|
|
1556
|
+
providerId: "saml-acs-test-provider",
|
|
1557
|
+
issuer: "http://localhost:8081",
|
|
1558
|
+
domain: "http://localhost:8081",
|
|
1559
|
+
samlConfig: {
|
|
1560
|
+
entryPoint: "http://localhost:8081/api/sso/saml2/idp/post",
|
|
1561
|
+
cert: certificate,
|
|
1562
|
+
callbackUrl: "http://localhost:3000/dashboard",
|
|
1563
|
+
wantAssertionsSigned: false,
|
|
1564
|
+
signatureAlgorithm: "sha256",
|
|
1565
|
+
digestAlgorithm: "sha256",
|
|
1566
|
+
idpMetadata: {
|
|
1567
|
+
metadata: idpMetadata,
|
|
1568
|
+
},
|
|
1569
|
+
spMetadata: {
|
|
1570
|
+
metadata: spMetadata,
|
|
1571
|
+
},
|
|
1572
|
+
identifierFormat:
|
|
1573
|
+
"urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress",
|
|
1574
|
+
},
|
|
1575
|
+
},
|
|
1576
|
+
headers: headers,
|
|
1577
|
+
});
|
|
1578
|
+
|
|
1579
|
+
let samlResponse: any;
|
|
1580
|
+
await betterFetch("http://localhost:8081/api/sso/saml2/idp/post", {
|
|
1581
|
+
onSuccess: async (context) => {
|
|
1582
|
+
samlResponse = await context.data;
|
|
1583
|
+
},
|
|
1584
|
+
});
|
|
1585
|
+
|
|
1586
|
+
const response = await authWithDisabledSignUp.handler(
|
|
1587
|
+
new Request(
|
|
1588
|
+
"http://localhost:3000/api/auth/sso/saml2/sp/acs/saml-acs-test-provider",
|
|
1589
|
+
{
|
|
1590
|
+
method: "POST",
|
|
1591
|
+
headers: {
|
|
1592
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
1593
|
+
},
|
|
1594
|
+
body: new URLSearchParams({
|
|
1595
|
+
SAMLResponse: samlResponse.samlResponse,
|
|
1596
|
+
RelayState: "http://localhost:3000/dashboard",
|
|
1597
|
+
}),
|
|
1598
|
+
},
|
|
1599
|
+
),
|
|
1600
|
+
);
|
|
1601
|
+
|
|
1602
|
+
expect(response.status).toBe(302);
|
|
1603
|
+
const redirectLocation = response.headers.get("location") || "";
|
|
1604
|
+
expect(redirectLocation).toContain("error=signup_disabled");
|
|
1605
|
+
});
|
|
1606
|
+
|
|
1607
|
+
it("should deny account linking when provider is not trusted and domain is not verified", async () => {
|
|
1608
|
+
const { auth: authUntrusted, signInWithTestUser } = await getTestInstance({
|
|
1609
|
+
account: {
|
|
1610
|
+
accountLinking: {
|
|
1611
|
+
enabled: true,
|
|
1612
|
+
trustedProviders: [],
|
|
1613
|
+
},
|
|
1614
|
+
},
|
|
1615
|
+
plugins: [sso()],
|
|
1616
|
+
});
|
|
1617
|
+
|
|
1618
|
+
const { headers } = await signInWithTestUser();
|
|
1619
|
+
|
|
1620
|
+
await authUntrusted.api.registerSSOProvider({
|
|
1621
|
+
body: {
|
|
1622
|
+
providerId: "untrusted-saml-provider",
|
|
1623
|
+
issuer: "http://localhost:8081",
|
|
1624
|
+
domain: "http://localhost:8081",
|
|
1625
|
+
samlConfig: {
|
|
1626
|
+
entryPoint: "http://localhost:8081/api/sso/saml2/idp/post",
|
|
1627
|
+
cert: certificate,
|
|
1628
|
+
callbackUrl: "http://localhost:3000/dashboard",
|
|
1629
|
+
wantAssertionsSigned: false,
|
|
1630
|
+
signatureAlgorithm: "sha256",
|
|
1631
|
+
digestAlgorithm: "sha256",
|
|
1632
|
+
idpMetadata: {
|
|
1633
|
+
metadata: idpMetadata,
|
|
1634
|
+
},
|
|
1635
|
+
spMetadata: {
|
|
1636
|
+
metadata: spMetadata,
|
|
1637
|
+
},
|
|
1638
|
+
identifierFormat:
|
|
1639
|
+
"urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress",
|
|
1640
|
+
},
|
|
1641
|
+
},
|
|
1642
|
+
headers,
|
|
1643
|
+
});
|
|
1644
|
+
|
|
1645
|
+
const ctx = await authUntrusted.$context;
|
|
1646
|
+
await ctx.adapter.create({
|
|
1647
|
+
model: "user",
|
|
1648
|
+
data: {
|
|
1649
|
+
id: "existing-user-id",
|
|
1650
|
+
email: "test@email.com",
|
|
1651
|
+
name: "Existing User",
|
|
1652
|
+
emailVerified: true,
|
|
1653
|
+
createdAt: new Date(),
|
|
1654
|
+
updatedAt: new Date(),
|
|
1655
|
+
},
|
|
1656
|
+
});
|
|
1657
|
+
|
|
1658
|
+
let samlResponse: any;
|
|
1659
|
+
await betterFetch("http://localhost:8081/api/sso/saml2/idp/post", {
|
|
1660
|
+
onSuccess: async (context) => {
|
|
1661
|
+
samlResponse = await context.data;
|
|
1662
|
+
},
|
|
1663
|
+
});
|
|
1664
|
+
|
|
1665
|
+
const response = await authUntrusted.handler(
|
|
1666
|
+
new Request(
|
|
1667
|
+
"http://localhost:3000/api/auth/sso/saml2/callback/untrusted-saml-provider",
|
|
1668
|
+
{
|
|
1669
|
+
method: "POST",
|
|
1670
|
+
headers: {
|
|
1671
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
1672
|
+
},
|
|
1673
|
+
body: new URLSearchParams({
|
|
1674
|
+
SAMLResponse: samlResponse.samlResponse,
|
|
1675
|
+
}),
|
|
1676
|
+
},
|
|
1677
|
+
),
|
|
1678
|
+
);
|
|
1679
|
+
|
|
1680
|
+
expect(response.status).toBe(302);
|
|
1681
|
+
const redirectLocation = response.headers.get("location") || "";
|
|
1682
|
+
expect(redirectLocation).toContain("error=account_not_linked");
|
|
1683
|
+
});
|
|
1684
|
+
|
|
1685
|
+
it("should allow account linking when provider is in trustedProviders", async () => {
|
|
1686
|
+
const { auth: authWithTrusted, signInWithTestUser } = await getTestInstance(
|
|
1687
|
+
{
|
|
1688
|
+
account: {
|
|
1689
|
+
accountLinking: {
|
|
1690
|
+
enabled: true,
|
|
1691
|
+
trustedProviders: ["trusted-saml-provider"],
|
|
1692
|
+
},
|
|
1693
|
+
},
|
|
1694
|
+
plugins: [sso()],
|
|
1695
|
+
},
|
|
1696
|
+
);
|
|
1697
|
+
|
|
1698
|
+
const { headers } = await signInWithTestUser();
|
|
1699
|
+
|
|
1700
|
+
await authWithTrusted.api.registerSSOProvider({
|
|
1701
|
+
body: {
|
|
1702
|
+
providerId: "trusted-saml-provider",
|
|
1703
|
+
issuer: "http://localhost:8081",
|
|
1704
|
+
domain: "http://localhost:8081",
|
|
1705
|
+
samlConfig: {
|
|
1706
|
+
entryPoint: "http://localhost:8081/api/sso/saml2/idp/post",
|
|
1707
|
+
cert: certificate,
|
|
1708
|
+
callbackUrl: "http://localhost:3000/dashboard",
|
|
1709
|
+
wantAssertionsSigned: false,
|
|
1710
|
+
signatureAlgorithm: "sha256",
|
|
1711
|
+
digestAlgorithm: "sha256",
|
|
1712
|
+
idpMetadata: {
|
|
1713
|
+
metadata: idpMetadata,
|
|
1714
|
+
},
|
|
1715
|
+
spMetadata: {
|
|
1716
|
+
metadata: spMetadata,
|
|
1717
|
+
},
|
|
1718
|
+
identifierFormat:
|
|
1719
|
+
"urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress",
|
|
1720
|
+
},
|
|
1721
|
+
},
|
|
1722
|
+
headers,
|
|
1723
|
+
});
|
|
1724
|
+
|
|
1725
|
+
const ctx = await authWithTrusted.$context;
|
|
1726
|
+
await ctx.adapter.create({
|
|
1727
|
+
model: "user",
|
|
1728
|
+
data: {
|
|
1729
|
+
id: "existing-user-id-2",
|
|
1730
|
+
email: "test@email.com",
|
|
1731
|
+
name: "Existing User",
|
|
1732
|
+
emailVerified: true,
|
|
1733
|
+
createdAt: new Date(),
|
|
1734
|
+
updatedAt: new Date(),
|
|
1735
|
+
},
|
|
1736
|
+
});
|
|
1737
|
+
|
|
1738
|
+
let samlResponse: any;
|
|
1739
|
+
await betterFetch("http://localhost:8081/api/sso/saml2/idp/post", {
|
|
1740
|
+
onSuccess: async (context) => {
|
|
1741
|
+
samlResponse = await context.data;
|
|
1742
|
+
},
|
|
1743
|
+
});
|
|
1744
|
+
|
|
1745
|
+
const response = await authWithTrusted.handler(
|
|
1746
|
+
new Request(
|
|
1747
|
+
"http://localhost:3000/api/auth/sso/saml2/callback/trusted-saml-provider",
|
|
1748
|
+
{
|
|
1749
|
+
method: "POST",
|
|
1750
|
+
headers: {
|
|
1751
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
1752
|
+
},
|
|
1753
|
+
body: new URLSearchParams({
|
|
1754
|
+
SAMLResponse: samlResponse.samlResponse,
|
|
1755
|
+
}),
|
|
1756
|
+
},
|
|
1757
|
+
),
|
|
1758
|
+
);
|
|
1759
|
+
|
|
1760
|
+
expect(response.status).toBe(302);
|
|
1761
|
+
const redirectLocation = response.headers.get("location") || "";
|
|
1762
|
+
expect(redirectLocation).not.toContain("error");
|
|
1763
|
+
expect(redirectLocation).toContain("dashboard");
|
|
1764
|
+
});
|
|
1765
|
+
|
|
1766
|
+
it("should reject unsolicited SAML response when allowIdpInitiated is false", async () => {
|
|
1767
|
+
const { auth, signInWithTestUser } = await getTestInstance({
|
|
1768
|
+
plugins: [
|
|
1769
|
+
sso({
|
|
1770
|
+
saml: {
|
|
1771
|
+
enableInResponseToValidation: true,
|
|
1772
|
+
allowIdpInitiated: false,
|
|
1773
|
+
},
|
|
1774
|
+
}),
|
|
1775
|
+
],
|
|
1776
|
+
});
|
|
1777
|
+
|
|
1778
|
+
const { headers } = await signInWithTestUser();
|
|
1779
|
+
|
|
1780
|
+
await auth.api.registerSSOProvider({
|
|
1781
|
+
body: {
|
|
1782
|
+
providerId: "strict-saml-provider",
|
|
1783
|
+
issuer: "http://localhost:8081",
|
|
1784
|
+
domain: "http://localhost:8081",
|
|
1785
|
+
samlConfig: {
|
|
1786
|
+
entryPoint: "http://localhost:8081/api/sso/saml2/idp/post",
|
|
1787
|
+
cert: certificate,
|
|
1788
|
+
callbackUrl: "http://localhost:3000/dashboard",
|
|
1789
|
+
wantAssertionsSigned: false,
|
|
1790
|
+
signatureAlgorithm: "sha256",
|
|
1791
|
+
digestAlgorithm: "sha256",
|
|
1792
|
+
idpMetadata: {
|
|
1793
|
+
metadata: idpMetadata,
|
|
1794
|
+
},
|
|
1795
|
+
spMetadata: {
|
|
1796
|
+
metadata: spMetadata,
|
|
1797
|
+
},
|
|
1798
|
+
identifierFormat:
|
|
1799
|
+
"urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress",
|
|
1800
|
+
},
|
|
1801
|
+
},
|
|
1802
|
+
headers,
|
|
1803
|
+
});
|
|
1804
|
+
|
|
1805
|
+
let samlResponse: any;
|
|
1806
|
+
await betterFetch("http://localhost:8081/api/sso/saml2/idp/post", {
|
|
1807
|
+
onSuccess: async (context) => {
|
|
1808
|
+
samlResponse = await context.data;
|
|
1809
|
+
},
|
|
1810
|
+
});
|
|
1811
|
+
|
|
1812
|
+
const response = await auth.handler(
|
|
1813
|
+
new Request(
|
|
1814
|
+
"http://localhost:3000/api/auth/sso/saml2/callback/strict-saml-provider",
|
|
1815
|
+
{
|
|
1816
|
+
method: "POST",
|
|
1817
|
+
headers: {
|
|
1818
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
1819
|
+
},
|
|
1820
|
+
body: new URLSearchParams({
|
|
1821
|
+
SAMLResponse: samlResponse.samlResponse,
|
|
1822
|
+
}),
|
|
1823
|
+
},
|
|
1824
|
+
),
|
|
1825
|
+
);
|
|
1826
|
+
|
|
1827
|
+
expect(response.status).toBe(302);
|
|
1828
|
+
const redirectLocation = response.headers.get("location") || "";
|
|
1829
|
+
expect(redirectLocation).toContain("error=unsolicited_response");
|
|
1830
|
+
});
|
|
1831
|
+
|
|
1832
|
+
it("should allow unsolicited SAML response when allowIdpInitiated is true (default)", async () => {
|
|
1833
|
+
const { auth, signInWithTestUser } = await getTestInstance({
|
|
1834
|
+
plugins: [
|
|
1835
|
+
sso({
|
|
1836
|
+
saml: {
|
|
1837
|
+
enableInResponseToValidation: true,
|
|
1838
|
+
allowIdpInitiated: true,
|
|
1839
|
+
},
|
|
1840
|
+
}),
|
|
1841
|
+
],
|
|
1842
|
+
});
|
|
1843
|
+
|
|
1844
|
+
const { headers } = await signInWithTestUser();
|
|
1845
|
+
|
|
1846
|
+
await auth.api.registerSSOProvider({
|
|
1847
|
+
body: {
|
|
1848
|
+
providerId: "permissive-saml-provider",
|
|
1849
|
+
issuer: "http://localhost:8081",
|
|
1850
|
+
domain: "http://localhost:8081",
|
|
1851
|
+
samlConfig: {
|
|
1852
|
+
entryPoint: "http://localhost:8081/api/sso/saml2/idp/post",
|
|
1853
|
+
cert: certificate,
|
|
1854
|
+
callbackUrl: "http://localhost:3000/dashboard",
|
|
1855
|
+
wantAssertionsSigned: false,
|
|
1856
|
+
signatureAlgorithm: "sha256",
|
|
1857
|
+
digestAlgorithm: "sha256",
|
|
1858
|
+
idpMetadata: {
|
|
1859
|
+
metadata: idpMetadata,
|
|
1860
|
+
},
|
|
1861
|
+
spMetadata: {
|
|
1862
|
+
metadata: spMetadata,
|
|
1863
|
+
},
|
|
1864
|
+
identifierFormat:
|
|
1865
|
+
"urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress",
|
|
1866
|
+
},
|
|
1867
|
+
},
|
|
1868
|
+
headers,
|
|
1869
|
+
});
|
|
1870
|
+
|
|
1871
|
+
let samlResponse: any;
|
|
1872
|
+
await betterFetch("http://localhost:8081/api/sso/saml2/idp/post", {
|
|
1873
|
+
onSuccess: async (context) => {
|
|
1874
|
+
samlResponse = await context.data;
|
|
1875
|
+
},
|
|
1876
|
+
});
|
|
1877
|
+
|
|
1878
|
+
const response = await auth.handler(
|
|
1879
|
+
new Request(
|
|
1880
|
+
"http://localhost:3000/api/auth/sso/saml2/callback/permissive-saml-provider",
|
|
1881
|
+
{
|
|
1882
|
+
method: "POST",
|
|
1883
|
+
headers: {
|
|
1884
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
1885
|
+
},
|
|
1886
|
+
body: new URLSearchParams({
|
|
1887
|
+
SAMLResponse: samlResponse.samlResponse,
|
|
1888
|
+
}),
|
|
1889
|
+
},
|
|
1890
|
+
),
|
|
1891
|
+
);
|
|
1892
|
+
|
|
1893
|
+
expect(response.status).toBe(302);
|
|
1894
|
+
const redirectLocation = response.headers.get("location") || "";
|
|
1895
|
+
expect(redirectLocation).not.toContain("error=unsolicited_response");
|
|
1896
|
+
});
|
|
1897
|
+
|
|
1898
|
+
it("should skip InResponseTo validation when not explicitly enabled (backward compatibility)", async () => {
|
|
1899
|
+
const { auth, signInWithTestUser } = await getTestInstance({
|
|
1900
|
+
plugins: [sso()],
|
|
1901
|
+
});
|
|
1902
|
+
|
|
1903
|
+
const { headers } = await signInWithTestUser();
|
|
1904
|
+
|
|
1905
|
+
await auth.api.registerSSOProvider({
|
|
1906
|
+
body: {
|
|
1907
|
+
providerId: "legacy-saml-provider",
|
|
1908
|
+
issuer: "http://localhost:8081",
|
|
1909
|
+
domain: "http://localhost:8081",
|
|
1910
|
+
samlConfig: {
|
|
1911
|
+
entryPoint: "http://localhost:8081/api/sso/saml2/idp/post",
|
|
1912
|
+
cert: certificate,
|
|
1913
|
+
callbackUrl: "http://localhost:3000/dashboard",
|
|
1914
|
+
wantAssertionsSigned: false,
|
|
1915
|
+
signatureAlgorithm: "sha256",
|
|
1916
|
+
digestAlgorithm: "sha256",
|
|
1917
|
+
idpMetadata: {
|
|
1918
|
+
metadata: idpMetadata,
|
|
1919
|
+
},
|
|
1920
|
+
spMetadata: {
|
|
1921
|
+
metadata: spMetadata,
|
|
1922
|
+
},
|
|
1923
|
+
identifierFormat:
|
|
1924
|
+
"urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress",
|
|
1925
|
+
},
|
|
1926
|
+
},
|
|
1927
|
+
headers,
|
|
1928
|
+
});
|
|
1929
|
+
|
|
1930
|
+
let samlResponse: any;
|
|
1931
|
+
await betterFetch("http://localhost:8081/api/sso/saml2/idp/post", {
|
|
1932
|
+
onSuccess: async (context) => {
|
|
1933
|
+
samlResponse = await context.data;
|
|
1934
|
+
},
|
|
1935
|
+
});
|
|
1936
|
+
|
|
1937
|
+
const response = await auth.handler(
|
|
1938
|
+
new Request(
|
|
1939
|
+
"http://localhost:3000/api/auth/sso/saml2/callback/legacy-saml-provider",
|
|
1940
|
+
{
|
|
1941
|
+
method: "POST",
|
|
1942
|
+
headers: {
|
|
1943
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
1944
|
+
},
|
|
1945
|
+
body: new URLSearchParams({
|
|
1946
|
+
SAMLResponse: samlResponse.samlResponse,
|
|
1947
|
+
}),
|
|
1948
|
+
},
|
|
1949
|
+
),
|
|
1950
|
+
);
|
|
1951
|
+
|
|
1952
|
+
expect(response.status).toBe(302);
|
|
1953
|
+
const redirectLocation = response.headers.get("location") || "";
|
|
1954
|
+
expect(redirectLocation).not.toContain("error=");
|
|
1955
|
+
});
|
|
1956
|
+
|
|
1957
|
+
it("should use verification table for InResponseTo validation", async () => {
|
|
1958
|
+
const { auth, signInWithTestUser } = await getTestInstance({
|
|
1959
|
+
plugins: [
|
|
1960
|
+
sso({
|
|
1961
|
+
saml: {
|
|
1962
|
+
enableInResponseToValidation: true,
|
|
1963
|
+
allowIdpInitiated: false,
|
|
1964
|
+
},
|
|
1965
|
+
}),
|
|
1966
|
+
],
|
|
1967
|
+
});
|
|
1968
|
+
|
|
1969
|
+
const { headers } = await signInWithTestUser();
|
|
1970
|
+
|
|
1971
|
+
await auth.api.registerSSOProvider({
|
|
1972
|
+
body: {
|
|
1973
|
+
providerId: "db-fallback-provider",
|
|
1974
|
+
issuer: "http://localhost:8081",
|
|
1975
|
+
domain: "http://localhost:8081",
|
|
1976
|
+
samlConfig: {
|
|
1977
|
+
entryPoint: "http://localhost:8081/api/sso/saml2/idp/post",
|
|
1978
|
+
cert: certificate,
|
|
1979
|
+
callbackUrl: "http://localhost:3000/dashboard",
|
|
1980
|
+
wantAssertionsSigned: false,
|
|
1981
|
+
signatureAlgorithm: "sha256",
|
|
1982
|
+
digestAlgorithm: "sha256",
|
|
1983
|
+
idpMetadata: {
|
|
1984
|
+
metadata: idpMetadata,
|
|
1985
|
+
},
|
|
1986
|
+
spMetadata: {
|
|
1987
|
+
metadata: spMetadata,
|
|
1988
|
+
},
|
|
1989
|
+
identifierFormat:
|
|
1990
|
+
"urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress",
|
|
1991
|
+
},
|
|
1992
|
+
},
|
|
1993
|
+
headers,
|
|
1994
|
+
});
|
|
1995
|
+
|
|
1996
|
+
// Try to use an unsolicited response - should be rejected since allowIdpInitiated is false
|
|
1997
|
+
// This proves the validation is working via the verification table fallback
|
|
1998
|
+
let samlResponse: any;
|
|
1999
|
+
await betterFetch("http://localhost:8081/api/sso/saml2/idp/post", {
|
|
2000
|
+
onSuccess: async (context) => {
|
|
2001
|
+
samlResponse = await context.data;
|
|
2002
|
+
},
|
|
2003
|
+
});
|
|
2004
|
+
|
|
2005
|
+
const response = await auth.handler(
|
|
2006
|
+
new Request(
|
|
2007
|
+
"http://localhost:3000/api/auth/sso/saml2/callback/db-fallback-provider",
|
|
2008
|
+
{
|
|
2009
|
+
method: "POST",
|
|
2010
|
+
headers: {
|
|
2011
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
2012
|
+
},
|
|
2013
|
+
body: new URLSearchParams({
|
|
2014
|
+
SAMLResponse: samlResponse.samlResponse,
|
|
2015
|
+
}),
|
|
2016
|
+
},
|
|
2017
|
+
),
|
|
2018
|
+
);
|
|
2019
|
+
|
|
2020
|
+
// Should reject unsolicited response, proving validation is active
|
|
2021
|
+
expect(response.status).toBe(302);
|
|
2022
|
+
const redirectLocation = response.headers.get("location") || "";
|
|
2023
|
+
expect(redirectLocation).toContain("error=unsolicited_response");
|
|
2024
|
+
});
|
|
2025
|
+
});
|
|
2026
|
+
|
|
2027
|
+
describe("SAML SSO with custom fields", () => {
|
|
2028
|
+
const ssoOptions = {
|
|
2029
|
+
modelName: "sso_provider",
|
|
2030
|
+
fields: {
|
|
2031
|
+
issuer: "the_issuer",
|
|
2032
|
+
oidcConfig: "oidc_config",
|
|
2033
|
+
samlConfig: "saml_config",
|
|
2034
|
+
userId: "user_id",
|
|
2035
|
+
providerId: "provider_id",
|
|
2036
|
+
organizationId: "organization_id",
|
|
2037
|
+
domain: "the_domain",
|
|
2038
|
+
},
|
|
2039
|
+
};
|
|
2040
|
+
|
|
2041
|
+
const data = {
|
|
2042
|
+
user: [],
|
|
2043
|
+
session: [],
|
|
2044
|
+
verification: [],
|
|
2045
|
+
account: [],
|
|
2046
|
+
sso_provider: [],
|
|
2047
|
+
};
|
|
2048
|
+
|
|
2049
|
+
const memory = memoryAdapter(data);
|
|
2050
|
+
|
|
2051
|
+
const auth = betterAuth({
|
|
2052
|
+
database: memory,
|
|
2053
|
+
baseURL: "http://localhost:3000",
|
|
2054
|
+
emailAndPassword: {
|
|
2055
|
+
enabled: true,
|
|
2056
|
+
},
|
|
2057
|
+
plugins: [sso(ssoOptions)],
|
|
2058
|
+
});
|
|
2059
|
+
|
|
2060
|
+
const authClient = createAuthClient({
|
|
2061
|
+
baseURL: "http://localhost:3000",
|
|
2062
|
+
plugins: [bearer(), ssoClient()],
|
|
2063
|
+
fetchOptions: {
|
|
2064
|
+
customFetchImpl: async (url, init) => {
|
|
2065
|
+
return auth.handler(new Request(url, init));
|
|
2066
|
+
},
|
|
2067
|
+
},
|
|
2068
|
+
});
|
|
2069
|
+
|
|
2070
|
+
const testUser = {
|
|
2071
|
+
email: "test@email.com",
|
|
2072
|
+
password: "password",
|
|
2073
|
+
name: "Test User",
|
|
2074
|
+
};
|
|
2075
|
+
|
|
2076
|
+
beforeAll(async () => {
|
|
2077
|
+
await authClient.signUp.email({
|
|
2078
|
+
email: testUser.email,
|
|
2079
|
+
password: testUser.password,
|
|
2080
|
+
name: testUser.name,
|
|
2081
|
+
});
|
|
2082
|
+
});
|
|
2083
|
+
|
|
2084
|
+
beforeEach(() => {
|
|
2085
|
+
data.user = [];
|
|
2086
|
+
data.session = [];
|
|
2087
|
+
data.verification = [];
|
|
2088
|
+
data.account = [];
|
|
2089
|
+
data.sso_provider = [];
|
|
2090
|
+
|
|
2091
|
+
vi.clearAllMocks();
|
|
2092
|
+
});
|
|
2093
|
+
|
|
2094
|
+
async function getAuthHeaders() {
|
|
2095
|
+
const headers = new Headers();
|
|
2096
|
+
await authClient.signUp.email({
|
|
2097
|
+
email: testUser.email,
|
|
2098
|
+
password: testUser.password,
|
|
2099
|
+
name: testUser.name,
|
|
2100
|
+
});
|
|
2101
|
+
await authClient.signIn.email(testUser, {
|
|
2102
|
+
throw: true,
|
|
2103
|
+
onSuccess: setCookieToHeader(headers),
|
|
2104
|
+
});
|
|
2105
|
+
return headers;
|
|
2106
|
+
}
|
|
2107
|
+
|
|
2108
|
+
it("should register a new SAML provider", async () => {
|
|
2109
|
+
const headers = await getAuthHeaders();
|
|
2110
|
+
|
|
2111
|
+
const provider = await auth.api.registerSSOProvider({
|
|
2112
|
+
body: {
|
|
2113
|
+
providerId: "saml-provider-1",
|
|
2114
|
+
issuer: "http://localhost:8081",
|
|
2115
|
+
domain: "http://localhost:8081",
|
|
2116
|
+
samlConfig: {
|
|
2117
|
+
entryPoint: sharedMockIdP.metadataUrl,
|
|
2118
|
+
cert: certificate,
|
|
2119
|
+
callbackUrl: "http://localhost:8081/api/sso/saml2/callback",
|
|
2120
|
+
wantAssertionsSigned: false,
|
|
2121
|
+
signatureAlgorithm: "sha256",
|
|
2122
|
+
digestAlgorithm: "sha256",
|
|
2123
|
+
idpMetadata: {
|
|
2124
|
+
metadata: idpMetadata,
|
|
2125
|
+
privateKey: idpPrivateKey,
|
|
2126
|
+
privateKeyPass: "q9ALNhGT5EhfcRmp8Pg7e9zTQeP2x1bW",
|
|
2127
|
+
isAssertionEncrypted: true,
|
|
2128
|
+
encPrivateKey: idpEncryptionKey,
|
|
2129
|
+
encPrivateKeyPass: "g7hGcRmp8PxT5QeP2q9Ehf1bWe9zTALN",
|
|
2130
|
+
},
|
|
2131
|
+
spMetadata: {
|
|
2132
|
+
metadata: spMetadata,
|
|
2133
|
+
binding: "post",
|
|
2134
|
+
privateKey: spPrivateKey,
|
|
2135
|
+
privateKeyPass: "VHOSp5RUiBcrsjrcAuXFwU1NKCkGA8px",
|
|
2136
|
+
isAssertionEncrypted: true,
|
|
2137
|
+
encPrivateKey: spEncryptionKey,
|
|
2138
|
+
encPrivateKeyPass: "BXFNKpxrsjrCkGA8cAu5wUVHOSpci1RU",
|
|
2139
|
+
},
|
|
2140
|
+
identifierFormat:
|
|
2141
|
+
"urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress",
|
|
2142
|
+
},
|
|
2143
|
+
},
|
|
2144
|
+
headers,
|
|
2145
|
+
});
|
|
2146
|
+
expect(provider).toMatchObject({
|
|
2147
|
+
id: expect.any(String),
|
|
2148
|
+
issuer: "http://localhost:8081",
|
|
2149
|
+
samlConfig: {
|
|
2150
|
+
entryPoint: sharedMockIdP.metadataUrl,
|
|
2151
|
+
cert: expect.any(String),
|
|
2152
|
+
callbackUrl: "http://localhost:8081/api/sso/saml2/callback",
|
|
2153
|
+
wantAssertionsSigned: false,
|
|
2154
|
+
signatureAlgorithm: "sha256",
|
|
2155
|
+
digestAlgorithm: "sha256",
|
|
2156
|
+
identifierFormat:
|
|
2157
|
+
"urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress",
|
|
2158
|
+
},
|
|
2159
|
+
});
|
|
2160
|
+
});
|
|
2161
|
+
});
|
|
2162
|
+
|
|
2163
|
+
import { safeJsonParse } from "./utils";
|
|
2164
|
+
|
|
2165
|
+
describe("safeJsonParse", () => {
|
|
2166
|
+
it("returns object as-is when value is already an object", () => {
|
|
2167
|
+
const obj = { a: 1, nested: { b: 2 } };
|
|
2168
|
+
const result = safeJsonParse<typeof obj>(obj);
|
|
2169
|
+
expect(result).toBe(obj); // same reference
|
|
2170
|
+
expect(result).toEqual({ a: 1, nested: { b: 2 } });
|
|
2171
|
+
});
|
|
2172
|
+
|
|
2173
|
+
it("parses stringified JSON when value is a string", () => {
|
|
2174
|
+
const json = '{"a":1,"nested":{"b":2}}';
|
|
2175
|
+
const result = safeJsonParse<{ a: number; nested: { b: number } }>(json);
|
|
2176
|
+
expect(result).toEqual({ a: 1, nested: { b: 2 } });
|
|
2177
|
+
});
|
|
2178
|
+
|
|
2179
|
+
it("returns null for null input", () => {
|
|
2180
|
+
const result = safeJsonParse<{ a: number }>(null);
|
|
2181
|
+
expect(result).toBeNull();
|
|
2182
|
+
});
|
|
2183
|
+
|
|
2184
|
+
it("returns null for undefined input", () => {
|
|
2185
|
+
const result = safeJsonParse<{ a: number }>(undefined);
|
|
2186
|
+
expect(result).toBeNull();
|
|
2187
|
+
});
|
|
2188
|
+
|
|
2189
|
+
it("throws error for invalid JSON string", () => {
|
|
2190
|
+
expect(() => safeJsonParse<{ a: number }>("not valid json")).toThrow(
|
|
2191
|
+
"Failed to parse JSON",
|
|
2192
|
+
);
|
|
2193
|
+
});
|
|
2194
|
+
|
|
2195
|
+
it("handles empty object", () => {
|
|
2196
|
+
const obj = {};
|
|
2197
|
+
const result = safeJsonParse<typeof obj>(obj);
|
|
2198
|
+
expect(result).toBe(obj);
|
|
2199
|
+
});
|
|
2200
|
+
|
|
2201
|
+
it("handles empty string JSON", () => {
|
|
2202
|
+
const result = safeJsonParse<Record<string, never>>("{}");
|
|
2203
|
+
expect(result).toEqual({});
|
|
2204
|
+
});
|
|
2205
|
+
});
|
|
2206
|
+
|
|
2207
|
+
describe("SSO Provider Config Parsing", () => {
|
|
2208
|
+
it("returns parsed SAML config and avoids [object Object] in response", async () => {
|
|
2209
|
+
const data = {
|
|
2210
|
+
user: [] as any[],
|
|
2211
|
+
session: [] as any[],
|
|
2212
|
+
verification: [] as any[],
|
|
2213
|
+
account: [] as any[],
|
|
2214
|
+
ssoProvider: [] as any[],
|
|
2215
|
+
};
|
|
2216
|
+
|
|
2217
|
+
const memory = memoryAdapter(data);
|
|
2218
|
+
|
|
2219
|
+
const auth = betterAuth({
|
|
2220
|
+
database: memory,
|
|
2221
|
+
baseURL: "http://localhost:3000",
|
|
2222
|
+
emailAndPassword: { enabled: true },
|
|
2223
|
+
plugins: [sso()],
|
|
2224
|
+
});
|
|
2225
|
+
|
|
2226
|
+
const authClient = createAuthClient({
|
|
2227
|
+
baseURL: "http://localhost:3000",
|
|
2228
|
+
plugins: [bearer(), ssoClient()],
|
|
2229
|
+
fetchOptions: {
|
|
2230
|
+
customFetchImpl: async (url, init) =>
|
|
2231
|
+
auth.handler(new Request(url, init)),
|
|
2232
|
+
},
|
|
2233
|
+
});
|
|
2234
|
+
|
|
2235
|
+
const headers = new Headers();
|
|
2236
|
+
await authClient.signUp.email({
|
|
2237
|
+
email: "test@example.com",
|
|
2238
|
+
password: "password123",
|
|
2239
|
+
name: "Test User",
|
|
2240
|
+
});
|
|
2241
|
+
await authClient.signIn.email(
|
|
2242
|
+
{ email: "test@example.com", password: "password123" },
|
|
2243
|
+
{ onSuccess: setCookieToHeader(headers) },
|
|
2244
|
+
);
|
|
2245
|
+
|
|
2246
|
+
const provider = await auth.api.registerSSOProvider({
|
|
2247
|
+
body: {
|
|
2248
|
+
providerId: "saml-config-provider",
|
|
2249
|
+
issuer: "http://localhost:8081",
|
|
2250
|
+
domain: "example.com",
|
|
2251
|
+
samlConfig: {
|
|
2252
|
+
entryPoint: "http://localhost:8081/sso",
|
|
2253
|
+
cert: "test-cert",
|
|
2254
|
+
callbackUrl: "http://localhost:3000/callback",
|
|
2255
|
+
spMetadata: {
|
|
2256
|
+
entityID: "test-entity",
|
|
2257
|
+
},
|
|
2258
|
+
},
|
|
2259
|
+
},
|
|
2260
|
+
headers,
|
|
2261
|
+
});
|
|
2262
|
+
|
|
2263
|
+
expect(provider.samlConfig).toBeDefined();
|
|
2264
|
+
expect(typeof provider.samlConfig).toBe("object");
|
|
2265
|
+
expect(provider.samlConfig?.entryPoint).toBe("http://localhost:8081/sso");
|
|
2266
|
+
expect(provider.samlConfig?.cert).toBe("test-cert");
|
|
2267
|
+
|
|
2268
|
+
const serialized = JSON.stringify(provider.samlConfig);
|
|
2269
|
+
expect(serialized).not.toContain("[object Object]");
|
|
2270
|
+
|
|
2271
|
+
expect(provider.samlConfig?.spMetadata?.entityID).toBe("test-entity");
|
|
2272
|
+
});
|
|
2273
|
+
|
|
2274
|
+
it("returns parsed OIDC config and avoids [object Object] in response", async () => {
|
|
2275
|
+
const { OAuth2Server } = await import("oauth2-mock-server");
|
|
2276
|
+
const oidcServer = new OAuth2Server();
|
|
2277
|
+
|
|
2278
|
+
await oidcServer.issuer.keys.generate("RS256");
|
|
2279
|
+
await oidcServer.start(8082, "localhost");
|
|
2280
|
+
|
|
2281
|
+
try {
|
|
2282
|
+
const data = {
|
|
2283
|
+
user: [] as any[],
|
|
2284
|
+
session: [] as any[],
|
|
2285
|
+
verification: [] as any[],
|
|
2286
|
+
account: [] as any[],
|
|
2287
|
+
ssoProvider: [] as any[],
|
|
2288
|
+
};
|
|
2289
|
+
|
|
2290
|
+
const memory = memoryAdapter(data);
|
|
2291
|
+
|
|
2292
|
+
const auth = betterAuth({
|
|
2293
|
+
database: memory,
|
|
2294
|
+
trustedOrigins: ["http://localhost:8082"],
|
|
2295
|
+
baseURL: "http://localhost:3000",
|
|
2296
|
+
emailAndPassword: { enabled: true },
|
|
2297
|
+
plugins: [sso()],
|
|
2298
|
+
});
|
|
2299
|
+
|
|
2300
|
+
const authClient = createAuthClient({
|
|
2301
|
+
baseURL: "http://localhost:3000",
|
|
2302
|
+
plugins: [bearer(), ssoClient()],
|
|
2303
|
+
fetchOptions: {
|
|
2304
|
+
customFetchImpl: async (url, init) =>
|
|
2305
|
+
auth.handler(new Request(url, init)),
|
|
2306
|
+
},
|
|
2307
|
+
});
|
|
2308
|
+
|
|
2309
|
+
const headers = new Headers();
|
|
2310
|
+
await authClient.signUp.email({
|
|
2311
|
+
email: "test@example.com",
|
|
2312
|
+
password: "password123",
|
|
2313
|
+
name: "Test User",
|
|
2314
|
+
});
|
|
2315
|
+
await authClient.signIn.email(
|
|
2316
|
+
{ email: "test@example.com", password: "password123" },
|
|
2317
|
+
{ onSuccess: setCookieToHeader(headers) },
|
|
2318
|
+
);
|
|
2319
|
+
|
|
2320
|
+
const provider = await auth.api.registerSSOProvider({
|
|
2321
|
+
body: {
|
|
2322
|
+
providerId: "oidc-config-provider",
|
|
2323
|
+
issuer: oidcServer.issuer.url!,
|
|
2324
|
+
domain: "example.com",
|
|
2325
|
+
oidcConfig: {
|
|
2326
|
+
clientId: "test-client",
|
|
2327
|
+
clientSecret: "test-secret",
|
|
2328
|
+
tokenEndpointAuthentication: "client_secret_basic",
|
|
2329
|
+
mapping: {
|
|
2330
|
+
id: "sub",
|
|
2331
|
+
email: "email",
|
|
2332
|
+
name: "name",
|
|
2333
|
+
},
|
|
2334
|
+
},
|
|
2335
|
+
},
|
|
2336
|
+
headers,
|
|
2337
|
+
});
|
|
2338
|
+
|
|
2339
|
+
expect(provider.oidcConfig).toBeDefined();
|
|
2340
|
+
expect(typeof provider.oidcConfig).toBe("object");
|
|
2341
|
+
expect(provider.oidcConfig?.clientId).toBe("test-client");
|
|
2342
|
+
expect(provider.oidcConfig?.clientSecret).toBe("test-secret");
|
|
2343
|
+
|
|
2344
|
+
const serialized = JSON.stringify(provider.oidcConfig);
|
|
2345
|
+
expect(serialized).not.toContain("[object Object]");
|
|
2346
|
+
|
|
2347
|
+
expect(provider.oidcConfig?.mapping?.id).toBe("sub");
|
|
2348
|
+
} finally {
|
|
2349
|
+
await oidcServer.stop().catch(() => {});
|
|
2350
|
+
}
|
|
2351
|
+
});
|
|
2352
|
+
});
|
|
2353
|
+
|
|
2354
|
+
describe("SAML SSO - IdP Initiated Flow", () => {
|
|
2355
|
+
it("should handle IdP-initiated flow with GET after POST redirect", async () => {
|
|
2356
|
+
const { auth, signInWithTestUser } = await getTestInstance({
|
|
2357
|
+
plugins: [sso()],
|
|
2358
|
+
});
|
|
2359
|
+
|
|
2360
|
+
const { headers } = await signInWithTestUser();
|
|
2361
|
+
|
|
2362
|
+
await auth.api.registerSSOProvider({
|
|
2363
|
+
body: {
|
|
2364
|
+
providerId: "idp-initiated-provider",
|
|
2365
|
+
issuer: "http://localhost:8081",
|
|
2366
|
+
domain: "http://localhost:8081",
|
|
2367
|
+
samlConfig: {
|
|
2368
|
+
entryPoint: sharedMockIdP.metadataUrl.replace(
|
|
2369
|
+
"/idp/metadata",
|
|
2370
|
+
"/idp/post",
|
|
2371
|
+
),
|
|
2372
|
+
cert: certificate,
|
|
2373
|
+
callbackUrl: "http://localhost:3000/dashboard",
|
|
2374
|
+
wantAssertionsSigned: false,
|
|
2375
|
+
signatureAlgorithm: "sha256",
|
|
2376
|
+
digestAlgorithm: "sha256",
|
|
2377
|
+
idpMetadata: {
|
|
2378
|
+
metadata: idpMetadata,
|
|
2379
|
+
},
|
|
2380
|
+
spMetadata: {
|
|
2381
|
+
metadata: spMetadata,
|
|
2382
|
+
},
|
|
2383
|
+
identifierFormat:
|
|
2384
|
+
"urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress",
|
|
2385
|
+
},
|
|
2386
|
+
},
|
|
2387
|
+
headers,
|
|
2388
|
+
});
|
|
2389
|
+
|
|
2390
|
+
let samlResponse:
|
|
2391
|
+
| { samlResponse: string; entityEndpoint?: string }
|
|
2392
|
+
| undefined;
|
|
2393
|
+
await betterFetch("http://localhost:8081/api/sso/saml2/idp/post", {
|
|
2394
|
+
onSuccess: async (context) => {
|
|
2395
|
+
samlResponse = context.data as {
|
|
2396
|
+
samlResponse: string;
|
|
2397
|
+
entityEndpoint?: string;
|
|
2398
|
+
};
|
|
2399
|
+
},
|
|
2400
|
+
});
|
|
2401
|
+
|
|
2402
|
+
if (!samlResponse?.samlResponse) {
|
|
2403
|
+
throw new Error("Failed to get SAML response from mock IdP");
|
|
2404
|
+
}
|
|
2405
|
+
|
|
2406
|
+
const postResponse = await auth.api.callbackSSOSAML({
|
|
2407
|
+
method: "POST",
|
|
2408
|
+
body: {
|
|
2409
|
+
SAMLResponse: samlResponse.samlResponse,
|
|
2410
|
+
RelayState: "http://localhost:3000/dashboard",
|
|
2411
|
+
},
|
|
2412
|
+
params: {
|
|
2413
|
+
providerId: "idp-initiated-provider",
|
|
2414
|
+
},
|
|
2415
|
+
asResponse: true,
|
|
2416
|
+
});
|
|
2417
|
+
|
|
2418
|
+
expect(postResponse).toBeInstanceOf(Response);
|
|
2419
|
+
expect(postResponse.status).toBe(302);
|
|
2420
|
+
const redirectLocation = postResponse.headers.get("location");
|
|
2421
|
+
expect(redirectLocation).toBe("http://localhost:3000/dashboard");
|
|
2422
|
+
|
|
2423
|
+
const cookieHeader = postResponse.headers.get("set-cookie");
|
|
2424
|
+
const getResponse = await auth.api.callbackSSOSAML({
|
|
2425
|
+
method: "GET",
|
|
2426
|
+
query: {
|
|
2427
|
+
RelayState: "http://localhost:3000/dashboard",
|
|
2428
|
+
},
|
|
2429
|
+
params: {
|
|
2430
|
+
providerId: "idp-initiated-provider",
|
|
2431
|
+
},
|
|
2432
|
+
headers: cookieHeader ? { cookie: cookieHeader } : undefined,
|
|
2433
|
+
asResponse: true,
|
|
2434
|
+
});
|
|
2435
|
+
|
|
2436
|
+
expect(getResponse).toBeInstanceOf(Response);
|
|
2437
|
+
expect(getResponse.status).toBe(302);
|
|
2438
|
+
const getRedirectLocation = getResponse.headers.get("location");
|
|
2439
|
+
expect(getRedirectLocation).toBe("http://localhost:3000/dashboard");
|
|
2440
|
+
});
|
|
2441
|
+
|
|
2442
|
+
it("should reject direct GET request without session", async () => {
|
|
2443
|
+
const { auth } = await getTestInstance({
|
|
2444
|
+
plugins: [sso()],
|
|
2445
|
+
});
|
|
2446
|
+
|
|
2447
|
+
const getResponse = await auth.api
|
|
2448
|
+
.callbackSSOSAML({
|
|
2449
|
+
method: "GET",
|
|
2450
|
+
params: {
|
|
2451
|
+
providerId: "test-provider",
|
|
2452
|
+
},
|
|
2453
|
+
asResponse: true,
|
|
2454
|
+
})
|
|
2455
|
+
.catch((e) => {
|
|
2456
|
+
if (e instanceof APIError && e.status === "FOUND") {
|
|
2457
|
+
return new Response(null, {
|
|
2458
|
+
status: e.statusCode,
|
|
2459
|
+
headers: e.headers || new Headers(),
|
|
2460
|
+
});
|
|
2461
|
+
}
|
|
2462
|
+
throw e;
|
|
2463
|
+
});
|
|
2464
|
+
|
|
2465
|
+
expect(getResponse).toBeInstanceOf(Response);
|
|
2466
|
+
expect(getResponse.status).toBe(302);
|
|
2467
|
+
const redirectLocation = getResponse.headers.get("location");
|
|
2468
|
+
expect(redirectLocation).toContain("/error");
|
|
2469
|
+
expect(redirectLocation).toContain("error=invalid_request");
|
|
2470
|
+
});
|
|
2471
|
+
|
|
2472
|
+
it("should prevent redirect loop when callbackUrl points to callback route", async () => {
|
|
2473
|
+
const { auth, signInWithTestUser } = await getTestInstance({
|
|
2474
|
+
plugins: [sso()],
|
|
2475
|
+
});
|
|
2476
|
+
|
|
2477
|
+
const { headers } = await signInWithTestUser();
|
|
2478
|
+
|
|
2479
|
+
const callbackRouteUrl =
|
|
2480
|
+
"http://localhost:3000/api/auth/sso/saml2/callback/loop-test-provider";
|
|
2481
|
+
|
|
2482
|
+
await auth.api.registerSSOProvider({
|
|
2483
|
+
body: {
|
|
2484
|
+
providerId: "loop-test-provider",
|
|
2485
|
+
issuer: "http://localhost:8081",
|
|
2486
|
+
domain: "http://localhost:8081",
|
|
2487
|
+
samlConfig: {
|
|
2488
|
+
entryPoint: sharedMockIdP.metadataUrl.replace(
|
|
2489
|
+
"/idp/metadata",
|
|
2490
|
+
"/idp/post",
|
|
2491
|
+
),
|
|
2492
|
+
cert: certificate,
|
|
2493
|
+
callbackUrl: callbackRouteUrl,
|
|
2494
|
+
wantAssertionsSigned: false,
|
|
2495
|
+
signatureAlgorithm: "sha256",
|
|
2496
|
+
digestAlgorithm: "sha256",
|
|
2497
|
+
idpMetadata: {
|
|
2498
|
+
metadata: idpMetadata,
|
|
2499
|
+
},
|
|
2500
|
+
spMetadata: {
|
|
2501
|
+
metadata: spMetadata,
|
|
2502
|
+
},
|
|
2503
|
+
identifierFormat:
|
|
2504
|
+
"urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress",
|
|
2505
|
+
},
|
|
2506
|
+
},
|
|
2507
|
+
headers,
|
|
2508
|
+
});
|
|
2509
|
+
|
|
2510
|
+
let samlResponse:
|
|
2511
|
+
| { samlResponse: string; entityEndpoint?: string }
|
|
2512
|
+
| undefined;
|
|
2513
|
+
await betterFetch("http://localhost:8081/api/sso/saml2/idp/post", {
|
|
2514
|
+
onSuccess: async (context) => {
|
|
2515
|
+
samlResponse = context.data as {
|
|
2516
|
+
samlResponse: string;
|
|
2517
|
+
entityEndpoint?: string;
|
|
2518
|
+
};
|
|
2519
|
+
},
|
|
2520
|
+
});
|
|
2521
|
+
|
|
2522
|
+
if (!samlResponse?.samlResponse) {
|
|
2523
|
+
throw new Error("Failed to get SAML response from mock IdP");
|
|
2524
|
+
}
|
|
2525
|
+
|
|
2526
|
+
const postResponse = await auth.api.callbackSSOSAML({
|
|
2527
|
+
method: "POST",
|
|
2528
|
+
body: {
|
|
2529
|
+
SAMLResponse: samlResponse.samlResponse,
|
|
2530
|
+
},
|
|
2531
|
+
params: {
|
|
2532
|
+
providerId: "loop-test-provider",
|
|
2533
|
+
},
|
|
2534
|
+
asResponse: true,
|
|
2535
|
+
});
|
|
2536
|
+
|
|
2537
|
+
expect(postResponse).toBeInstanceOf(Response);
|
|
2538
|
+
expect(postResponse.status).toBe(302);
|
|
2539
|
+
const redirectLocation = postResponse.headers.get("location");
|
|
2540
|
+
expect(redirectLocation).not.toBe(callbackRouteUrl);
|
|
2541
|
+
expect(redirectLocation).toBe("http://localhost:3000");
|
|
2542
|
+
});
|
|
2543
|
+
|
|
2544
|
+
it("should handle GET request with RelayState in query", async () => {
|
|
2545
|
+
const { auth, signInWithTestUser } = await getTestInstance({
|
|
2546
|
+
plugins: [sso()],
|
|
2547
|
+
});
|
|
2548
|
+
|
|
2549
|
+
const { headers } = await signInWithTestUser();
|
|
2550
|
+
|
|
2551
|
+
await auth.api.registerSSOProvider({
|
|
2552
|
+
body: {
|
|
2553
|
+
providerId: "relaystate-provider",
|
|
2554
|
+
issuer: "http://localhost:8081",
|
|
2555
|
+
domain: "http://localhost:8081",
|
|
2556
|
+
samlConfig: {
|
|
2557
|
+
entryPoint: sharedMockIdP.metadataUrl.replace(
|
|
2558
|
+
"/idp/metadata",
|
|
2559
|
+
"/idp/post",
|
|
2560
|
+
),
|
|
2561
|
+
cert: certificate,
|
|
2562
|
+
callbackUrl: "http://localhost:3000/dashboard",
|
|
2563
|
+
wantAssertionsSigned: false,
|
|
2564
|
+
signatureAlgorithm: "sha256",
|
|
2565
|
+
digestAlgorithm: "sha256",
|
|
2566
|
+
idpMetadata: {
|
|
2567
|
+
metadata: idpMetadata,
|
|
2568
|
+
},
|
|
2569
|
+
spMetadata: {
|
|
2570
|
+
metadata: spMetadata,
|
|
2571
|
+
},
|
|
2572
|
+
identifierFormat:
|
|
2573
|
+
"urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress",
|
|
2574
|
+
},
|
|
2575
|
+
},
|
|
2576
|
+
headers,
|
|
2577
|
+
});
|
|
2578
|
+
|
|
2579
|
+
let samlResponse:
|
|
2580
|
+
| { samlResponse: string; entityEndpoint?: string }
|
|
2581
|
+
| undefined;
|
|
2582
|
+
await betterFetch("http://localhost:8081/api/sso/saml2/idp/post", {
|
|
2583
|
+
onSuccess: async (context) => {
|
|
2584
|
+
samlResponse = context.data as {
|
|
2585
|
+
samlResponse: string;
|
|
2586
|
+
entityEndpoint?: string;
|
|
2587
|
+
};
|
|
2588
|
+
},
|
|
2589
|
+
});
|
|
2590
|
+
|
|
2591
|
+
if (!samlResponse?.samlResponse) {
|
|
2592
|
+
throw new Error("Failed to get SAML response from mock IdP");
|
|
2593
|
+
}
|
|
2594
|
+
|
|
2595
|
+
const postResponse = await auth.api.callbackSSOSAML({
|
|
2596
|
+
method: "POST",
|
|
2597
|
+
body: {
|
|
2598
|
+
SAMLResponse: samlResponse.samlResponse,
|
|
2599
|
+
RelayState: "http://localhost:3000/custom-path",
|
|
2600
|
+
},
|
|
2601
|
+
params: {
|
|
2602
|
+
providerId: "relaystate-provider",
|
|
2603
|
+
},
|
|
2604
|
+
asResponse: true,
|
|
2605
|
+
});
|
|
2606
|
+
|
|
2607
|
+
const cookieHeader = postResponse.headers.get("set-cookie");
|
|
2608
|
+
const getResponse = await auth.api.callbackSSOSAML({
|
|
2609
|
+
method: "GET",
|
|
2610
|
+
query: {
|
|
2611
|
+
RelayState: "http://localhost:3000/custom-path",
|
|
2612
|
+
},
|
|
2613
|
+
params: {
|
|
2614
|
+
providerId: "relaystate-provider",
|
|
2615
|
+
},
|
|
2616
|
+
headers: cookieHeader ? { cookie: cookieHeader } : undefined,
|
|
2617
|
+
asResponse: true,
|
|
2618
|
+
});
|
|
2619
|
+
|
|
2620
|
+
expect(getResponse).toBeInstanceOf(Response);
|
|
2621
|
+
expect(getResponse.status).toBe(302);
|
|
2622
|
+
const redirectLocation = getResponse.headers.get("location");
|
|
2623
|
+
expect(redirectLocation).toBe("http://localhost:3000/custom-path");
|
|
2624
|
+
});
|
|
2625
|
+
|
|
2626
|
+
it("should handle GET request when POST redirects to callback URL (original issue scenario)", async () => {
|
|
2627
|
+
const { auth, signInWithTestUser } = await getTestInstance({
|
|
2628
|
+
plugins: [sso()],
|
|
2629
|
+
});
|
|
2630
|
+
|
|
2631
|
+
const { headers } = await signInWithTestUser();
|
|
2632
|
+
|
|
2633
|
+
const callbackRouteUrl =
|
|
2634
|
+
"http://localhost:3000/api/auth/sso/saml2/callback/issue-6615-provider";
|
|
2635
|
+
|
|
2636
|
+
await auth.api.registerSSOProvider({
|
|
2637
|
+
body: {
|
|
2638
|
+
providerId: "issue-6615-provider",
|
|
2639
|
+
issuer: "http://localhost:8081",
|
|
2640
|
+
domain: "http://localhost:8081",
|
|
2641
|
+
samlConfig: {
|
|
2642
|
+
entryPoint: sharedMockIdP.metadataUrl.replace(
|
|
2643
|
+
"/idp/metadata",
|
|
2644
|
+
"/idp/post",
|
|
2645
|
+
),
|
|
2646
|
+
cert: certificate,
|
|
2647
|
+
callbackUrl: "http://localhost:3000/dashboard",
|
|
2648
|
+
wantAssertionsSigned: false,
|
|
2649
|
+
signatureAlgorithm: "sha256",
|
|
2650
|
+
digestAlgorithm: "sha256",
|
|
2651
|
+
idpMetadata: {
|
|
2652
|
+
metadata: idpMetadata,
|
|
2653
|
+
},
|
|
2654
|
+
spMetadata: {
|
|
2655
|
+
metadata: spMetadata,
|
|
2656
|
+
},
|
|
2657
|
+
identifierFormat:
|
|
2658
|
+
"urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress",
|
|
2659
|
+
},
|
|
2660
|
+
},
|
|
2661
|
+
headers,
|
|
2662
|
+
});
|
|
2663
|
+
|
|
2664
|
+
let samlResponse:
|
|
2665
|
+
| { samlResponse: string; entityEndpoint?: string }
|
|
2666
|
+
| undefined;
|
|
2667
|
+
await betterFetch("http://localhost:8081/api/sso/saml2/idp/post", {
|
|
2668
|
+
onSuccess: async (context) => {
|
|
2669
|
+
samlResponse = context.data as {
|
|
2670
|
+
samlResponse: string;
|
|
2671
|
+
entityEndpoint?: string;
|
|
2672
|
+
};
|
|
2673
|
+
},
|
|
2674
|
+
});
|
|
2675
|
+
|
|
2676
|
+
if (!samlResponse?.samlResponse) {
|
|
2677
|
+
throw new Error("Failed to get SAML response from mock IdP");
|
|
2678
|
+
}
|
|
2679
|
+
|
|
2680
|
+
const postResponse = await auth.api.callbackSSOSAML({
|
|
2681
|
+
method: "POST",
|
|
2682
|
+
body: {
|
|
2683
|
+
SAMLResponse: samlResponse.samlResponse,
|
|
2684
|
+
RelayState: callbackRouteUrl,
|
|
2685
|
+
},
|
|
2686
|
+
params: {
|
|
2687
|
+
providerId: "issue-6615-provider",
|
|
2688
|
+
},
|
|
2689
|
+
asResponse: true,
|
|
2690
|
+
});
|
|
2691
|
+
|
|
2692
|
+
expect(postResponse).toBeInstanceOf(Response);
|
|
2693
|
+
expect(postResponse.status).toBe(302);
|
|
2694
|
+
const postRedirectLocation = postResponse.headers.get("location");
|
|
2695
|
+
expect(postRedirectLocation).not.toBe(callbackRouteUrl);
|
|
2696
|
+
expect(postRedirectLocation).toBe("http://localhost:3000/dashboard");
|
|
2697
|
+
|
|
2698
|
+
const cookieHeader = postResponse.headers.get("set-cookie");
|
|
2699
|
+
const getResponse = await auth.api.callbackSSOSAML({
|
|
2700
|
+
method: "GET",
|
|
2701
|
+
params: {
|
|
2702
|
+
providerId: "issue-6615-provider",
|
|
2703
|
+
},
|
|
2704
|
+
headers: cookieHeader ? { cookie: cookieHeader } : undefined,
|
|
2705
|
+
asResponse: true,
|
|
2706
|
+
});
|
|
2707
|
+
|
|
2708
|
+
expect(getResponse).toBeInstanceOf(Response);
|
|
2709
|
+
expect(getResponse.status).toBe(302);
|
|
2710
|
+
const getRedirectLocation = getResponse.headers.get("location");
|
|
2711
|
+
expect(getRedirectLocation).toBe("http://localhost:3000");
|
|
2712
|
+
});
|
|
2713
|
+
|
|
2714
|
+
it("should prevent open redirect with malicious RelayState URL", async () => {
|
|
2715
|
+
const { auth, signInWithTestUser } = await getTestInstance({
|
|
2716
|
+
plugins: [sso()],
|
|
2717
|
+
});
|
|
2718
|
+
|
|
2719
|
+
const { headers } = await signInWithTestUser();
|
|
2720
|
+
|
|
2721
|
+
await auth.api.registerSSOProvider({
|
|
2722
|
+
body: {
|
|
2723
|
+
providerId: "open-redirect-test-provider",
|
|
2724
|
+
issuer: "http://localhost:8081",
|
|
2725
|
+
domain: "http://localhost:8081",
|
|
2726
|
+
samlConfig: {
|
|
2727
|
+
entryPoint: sharedMockIdP.metadataUrl.replace(
|
|
2728
|
+
"/idp/metadata",
|
|
2729
|
+
"/idp/post",
|
|
2730
|
+
),
|
|
2731
|
+
cert: certificate,
|
|
2732
|
+
callbackUrl: "http://localhost:3000/dashboard",
|
|
2733
|
+
wantAssertionsSigned: false,
|
|
2734
|
+
signatureAlgorithm: "sha256",
|
|
2735
|
+
digestAlgorithm: "sha256",
|
|
2736
|
+
idpMetadata: {
|
|
2737
|
+
metadata: idpMetadata,
|
|
2738
|
+
},
|
|
2739
|
+
spMetadata: {
|
|
2740
|
+
metadata: spMetadata,
|
|
2741
|
+
},
|
|
2742
|
+
identifierFormat:
|
|
2743
|
+
"urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress",
|
|
2744
|
+
},
|
|
2745
|
+
},
|
|
2746
|
+
headers,
|
|
2747
|
+
});
|
|
2748
|
+
|
|
2749
|
+
let samlResponse:
|
|
2750
|
+
| { samlResponse: string; entityEndpoint?: string }
|
|
2751
|
+
| undefined;
|
|
2752
|
+
await betterFetch("http://localhost:8081/api/sso/saml2/idp/post", {
|
|
2753
|
+
onSuccess: async (context) => {
|
|
2754
|
+
samlResponse = context.data as {
|
|
2755
|
+
samlResponse: string;
|
|
2756
|
+
entityEndpoint?: string;
|
|
2757
|
+
};
|
|
2758
|
+
},
|
|
2759
|
+
});
|
|
2760
|
+
|
|
2761
|
+
if (!samlResponse?.samlResponse) {
|
|
2762
|
+
throw new Error("Failed to get SAML response from mock IdP");
|
|
2763
|
+
}
|
|
2764
|
+
|
|
2765
|
+
// Test POST with malicious RelayState - raw RelayState is not trusted
|
|
2766
|
+
// Falls back to parsedSamlConfig.callbackUrl
|
|
2767
|
+
const postResponse = await auth.api.callbackSSOSAML({
|
|
2768
|
+
method: "POST",
|
|
2769
|
+
body: {
|
|
2770
|
+
SAMLResponse: samlResponse.samlResponse,
|
|
2771
|
+
RelayState: "https://evil.com/phishing",
|
|
2772
|
+
},
|
|
2773
|
+
params: {
|
|
2774
|
+
providerId: "open-redirect-test-provider",
|
|
2775
|
+
},
|
|
2776
|
+
asResponse: true,
|
|
2777
|
+
});
|
|
2778
|
+
|
|
2779
|
+
expect(postResponse).toBeInstanceOf(Response);
|
|
2780
|
+
expect(postResponse.status).toBe(302);
|
|
2781
|
+
const postRedirectLocation = postResponse.headers.get("location");
|
|
2782
|
+
// Should NOT redirect to evil.com - raw RelayState is ignored
|
|
2783
|
+
expect(postRedirectLocation).not.toContain("evil.com");
|
|
2784
|
+
// Falls back to samlConfig.callbackUrl
|
|
2785
|
+
expect(postRedirectLocation).toBe("http://localhost:3000/dashboard");
|
|
2786
|
+
});
|
|
2787
|
+
|
|
2788
|
+
it("should prevent open redirect via GET with malicious RelayState", async () => {
|
|
2789
|
+
const { auth, signInWithTestUser } = await getTestInstance({
|
|
2790
|
+
plugins: [sso()],
|
|
2791
|
+
});
|
|
2792
|
+
|
|
2793
|
+
const { headers } = await signInWithTestUser();
|
|
2794
|
+
|
|
2795
|
+
await auth.api.registerSSOProvider({
|
|
2796
|
+
body: {
|
|
2797
|
+
providerId: "open-redirect-get-provider",
|
|
2798
|
+
issuer: "http://localhost:8081",
|
|
2799
|
+
domain: "http://localhost:8081",
|
|
2800
|
+
samlConfig: {
|
|
2801
|
+
entryPoint: sharedMockIdP.metadataUrl.replace(
|
|
2802
|
+
"/idp/metadata",
|
|
2803
|
+
"/idp/post",
|
|
2804
|
+
),
|
|
2805
|
+
cert: certificate,
|
|
2806
|
+
callbackUrl: "http://localhost:3000/dashboard",
|
|
2807
|
+
wantAssertionsSigned: false,
|
|
2808
|
+
signatureAlgorithm: "sha256",
|
|
2809
|
+
digestAlgorithm: "sha256",
|
|
2810
|
+
idpMetadata: {
|
|
2811
|
+
metadata: idpMetadata,
|
|
2812
|
+
},
|
|
2813
|
+
spMetadata: {
|
|
2814
|
+
metadata: spMetadata,
|
|
2815
|
+
},
|
|
2816
|
+
identifierFormat:
|
|
2817
|
+
"urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress",
|
|
2818
|
+
},
|
|
2819
|
+
},
|
|
2820
|
+
headers,
|
|
2821
|
+
});
|
|
2822
|
+
|
|
2823
|
+
let samlResponse:
|
|
2824
|
+
| { samlResponse: string; entityEndpoint?: string }
|
|
2825
|
+
| undefined;
|
|
2826
|
+
await betterFetch("http://localhost:8081/api/sso/saml2/idp/post", {
|
|
2827
|
+
onSuccess: async (context) => {
|
|
2828
|
+
samlResponse = context.data as {
|
|
2829
|
+
samlResponse: string;
|
|
2830
|
+
entityEndpoint?: string;
|
|
2831
|
+
};
|
|
2832
|
+
},
|
|
2833
|
+
});
|
|
2834
|
+
|
|
2835
|
+
if (!samlResponse?.samlResponse) {
|
|
2836
|
+
throw new Error("Failed to get SAML response from mock IdP");
|
|
2837
|
+
}
|
|
2838
|
+
|
|
2839
|
+
// First do POST to establish session
|
|
2840
|
+
const postResponse = await auth.api.callbackSSOSAML({
|
|
2841
|
+
method: "POST",
|
|
2842
|
+
body: {
|
|
2843
|
+
SAMLResponse: samlResponse.samlResponse,
|
|
2844
|
+
},
|
|
2845
|
+
params: {
|
|
2846
|
+
providerId: "open-redirect-get-provider",
|
|
2847
|
+
},
|
|
2848
|
+
asResponse: true,
|
|
2849
|
+
});
|
|
2850
|
+
|
|
2851
|
+
const cookieHeader = postResponse.headers.get("set-cookie");
|
|
2852
|
+
|
|
2853
|
+
// Test GET with malicious RelayState in query params
|
|
2854
|
+
const getResponse = await auth.api.callbackSSOSAML({
|
|
2855
|
+
method: "GET",
|
|
2856
|
+
query: {
|
|
2857
|
+
RelayState: "https://evil.com/steal-cookies",
|
|
2858
|
+
},
|
|
2859
|
+
params: {
|
|
2860
|
+
providerId: "open-redirect-get-provider",
|
|
2861
|
+
},
|
|
2862
|
+
headers: cookieHeader ? { cookie: cookieHeader } : undefined,
|
|
2863
|
+
asResponse: true,
|
|
2864
|
+
});
|
|
2865
|
+
|
|
2866
|
+
expect(getResponse).toBeInstanceOf(Response);
|
|
2867
|
+
expect(getResponse.status).toBe(302);
|
|
2868
|
+
const getRedirectLocation = getResponse.headers.get("location");
|
|
2869
|
+
// Should NOT redirect to evil.com
|
|
2870
|
+
expect(getRedirectLocation).not.toContain("evil.com");
|
|
2871
|
+
expect(getRedirectLocation).toBe("http://localhost:3000");
|
|
2872
|
+
});
|
|
2873
|
+
|
|
2874
|
+
it("should allow relative path redirects", async () => {
|
|
2875
|
+
const { auth, signInWithTestUser } = await getTestInstance({
|
|
2876
|
+
plugins: [sso()],
|
|
2877
|
+
});
|
|
2878
|
+
|
|
2879
|
+
const { headers } = await signInWithTestUser();
|
|
2880
|
+
|
|
2881
|
+
await auth.api.registerSSOProvider({
|
|
2882
|
+
body: {
|
|
2883
|
+
providerId: "relative-path-provider",
|
|
2884
|
+
issuer: "http://localhost:8081",
|
|
2885
|
+
domain: "http://localhost:8081",
|
|
2886
|
+
samlConfig: {
|
|
2887
|
+
entryPoint: sharedMockIdP.metadataUrl.replace(
|
|
2888
|
+
"/idp/metadata",
|
|
2889
|
+
"/idp/post",
|
|
2890
|
+
),
|
|
2891
|
+
cert: certificate,
|
|
2892
|
+
callbackUrl: "http://localhost:3000/dashboard",
|
|
2893
|
+
wantAssertionsSigned: false,
|
|
2894
|
+
signatureAlgorithm: "sha256",
|
|
2895
|
+
digestAlgorithm: "sha256",
|
|
2896
|
+
idpMetadata: {
|
|
2897
|
+
metadata: idpMetadata,
|
|
2898
|
+
},
|
|
2899
|
+
spMetadata: {
|
|
2900
|
+
metadata: spMetadata,
|
|
2901
|
+
},
|
|
2902
|
+
identifierFormat:
|
|
2903
|
+
"urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress",
|
|
2904
|
+
},
|
|
2905
|
+
},
|
|
2906
|
+
headers,
|
|
2907
|
+
});
|
|
2908
|
+
|
|
2909
|
+
let samlResponse:
|
|
2910
|
+
| { samlResponse: string; entityEndpoint?: string }
|
|
2911
|
+
| undefined;
|
|
2912
|
+
await betterFetch("http://localhost:8081/api/sso/saml2/idp/post", {
|
|
2913
|
+
onSuccess: async (context) => {
|
|
2914
|
+
samlResponse = context.data as {
|
|
2915
|
+
samlResponse: string;
|
|
2916
|
+
entityEndpoint?: string;
|
|
2917
|
+
};
|
|
2918
|
+
},
|
|
2919
|
+
});
|
|
2920
|
+
|
|
2921
|
+
if (!samlResponse?.samlResponse) {
|
|
2922
|
+
throw new Error("Failed to get SAML response from mock IdP");
|
|
2923
|
+
}
|
|
2924
|
+
|
|
2925
|
+
const postResponse = await auth.api.callbackSSOSAML({
|
|
2926
|
+
method: "POST",
|
|
2927
|
+
body: {
|
|
2928
|
+
SAMLResponse: samlResponse.samlResponse,
|
|
2929
|
+
RelayState: "/dashboard/settings",
|
|
2930
|
+
},
|
|
2931
|
+
params: {
|
|
2932
|
+
providerId: "relative-path-provider",
|
|
2933
|
+
},
|
|
2934
|
+
asResponse: true,
|
|
2935
|
+
});
|
|
2936
|
+
|
|
2937
|
+
expect(postResponse).toBeInstanceOf(Response);
|
|
2938
|
+
expect(postResponse.status).toBe(302);
|
|
2939
|
+
const redirectLocation = postResponse.headers.get("location");
|
|
2940
|
+
expect(redirectLocation).toBe("http://localhost:3000/dashboard");
|
|
2941
|
+
});
|
|
2942
|
+
|
|
2943
|
+
it("should block protocol-relative URL attacks (//evil.com)", async () => {
|
|
2944
|
+
const { auth, signInWithTestUser } = await getTestInstance({
|
|
2945
|
+
plugins: [sso()],
|
|
2946
|
+
});
|
|
2947
|
+
|
|
2948
|
+
const { headers } = await signInWithTestUser();
|
|
2949
|
+
|
|
2950
|
+
await auth.api.registerSSOProvider({
|
|
2951
|
+
body: {
|
|
2952
|
+
providerId: "protocol-relative-provider",
|
|
2953
|
+
issuer: "http://localhost:8081",
|
|
2954
|
+
domain: "http://localhost:8081",
|
|
2955
|
+
samlConfig: {
|
|
2956
|
+
entryPoint: sharedMockIdP.metadataUrl.replace(
|
|
2957
|
+
"/idp/metadata",
|
|
2958
|
+
"/idp/post",
|
|
2959
|
+
),
|
|
2960
|
+
cert: certificate,
|
|
2961
|
+
callbackUrl: "http://localhost:3000/dashboard",
|
|
2962
|
+
wantAssertionsSigned: false,
|
|
2963
|
+
signatureAlgorithm: "sha256",
|
|
2964
|
+
digestAlgorithm: "sha256",
|
|
2965
|
+
idpMetadata: {
|
|
2966
|
+
metadata: idpMetadata,
|
|
2967
|
+
},
|
|
2968
|
+
spMetadata: {
|
|
2969
|
+
metadata: spMetadata,
|
|
2970
|
+
},
|
|
2971
|
+
identifierFormat:
|
|
2972
|
+
"urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress",
|
|
2973
|
+
},
|
|
2974
|
+
},
|
|
2975
|
+
headers,
|
|
2976
|
+
});
|
|
2977
|
+
|
|
2978
|
+
let samlResponse:
|
|
2979
|
+
| { samlResponse: string; entityEndpoint?: string }
|
|
2980
|
+
| undefined;
|
|
2981
|
+
await betterFetch("http://localhost:8081/api/sso/saml2/idp/post", {
|
|
2982
|
+
onSuccess: async (context) => {
|
|
2983
|
+
samlResponse = context.data as {
|
|
2984
|
+
samlResponse: string;
|
|
2985
|
+
entityEndpoint?: string;
|
|
2986
|
+
};
|
|
2987
|
+
},
|
|
2988
|
+
});
|
|
2989
|
+
|
|
2990
|
+
if (!samlResponse?.samlResponse) {
|
|
2991
|
+
throw new Error("Failed to get SAML response from mock IdP");
|
|
2992
|
+
}
|
|
2993
|
+
|
|
2994
|
+
// Test POST with protocol-relative URL - raw RelayState is not trusted
|
|
2995
|
+
// Falls back to parsedSamlConfig.callbackUrl
|
|
2996
|
+
const postResponse = await auth.api.callbackSSOSAML({
|
|
2997
|
+
method: "POST",
|
|
2998
|
+
body: {
|
|
2999
|
+
SAMLResponse: samlResponse.samlResponse,
|
|
3000
|
+
RelayState: "//evil.com/phishing",
|
|
3001
|
+
},
|
|
3002
|
+
params: {
|
|
3003
|
+
providerId: "protocol-relative-provider",
|
|
3004
|
+
},
|
|
3005
|
+
asResponse: true,
|
|
3006
|
+
});
|
|
3007
|
+
|
|
3008
|
+
expect(postResponse).toBeInstanceOf(Response);
|
|
3009
|
+
expect(postResponse.status).toBe(302);
|
|
3010
|
+
const redirectLocation = postResponse.headers.get("location");
|
|
3011
|
+
// Should NOT redirect to evil.com - raw RelayState is ignored
|
|
3012
|
+
expect(redirectLocation).not.toContain("evil.com");
|
|
3013
|
+
// Falls back to samlConfig.callbackUrl
|
|
3014
|
+
expect(redirectLocation).toBe("http://localhost:3000/dashboard");
|
|
3015
|
+
});
|
|
3016
|
+
});
|
|
3017
|
+
|
|
3018
|
+
describe("SAML SSO - Timestamp Validation", () => {
|
|
3019
|
+
describe("Valid assertions within time window", () => {
|
|
3020
|
+
it("should accept assertion with current NotBefore and future NotOnOrAfter", () => {
|
|
3021
|
+
const now = new Date();
|
|
3022
|
+
const fiveMinutesFromNow = new Date(Date.now() + 5 * 60 * 1000);
|
|
3023
|
+
expect(() =>
|
|
3024
|
+
validateSAMLTimestamp({
|
|
3025
|
+
notBefore: now.toISOString(),
|
|
3026
|
+
notOnOrAfter: fiveMinutesFromNow.toISOString(),
|
|
3027
|
+
}),
|
|
3028
|
+
).not.toThrow();
|
|
3029
|
+
});
|
|
3030
|
+
|
|
3031
|
+
it("should accept assertion within clock skew tolerance (expired 2 min ago with 5 min skew)", () => {
|
|
3032
|
+
const twoMinutesAgo = new Date(Date.now() - 2 * 60 * 1000).toISOString();
|
|
3033
|
+
expect(() =>
|
|
3034
|
+
validateSAMLTimestamp({ notOnOrAfter: twoMinutesAgo }),
|
|
3035
|
+
).not.toThrow();
|
|
3036
|
+
});
|
|
3037
|
+
|
|
3038
|
+
it("should accept assertion with NotBefore slightly in future (within clock skew)", () => {
|
|
3039
|
+
const twoMinutesFromNow = new Date(
|
|
3040
|
+
Date.now() + 2 * 60 * 1000,
|
|
3041
|
+
).toISOString();
|
|
3042
|
+
expect(() =>
|
|
3043
|
+
validateSAMLTimestamp({ notBefore: twoMinutesFromNow }),
|
|
3044
|
+
).not.toThrow();
|
|
3045
|
+
});
|
|
3046
|
+
});
|
|
3047
|
+
|
|
3048
|
+
describe("NotBefore validation (future-dated assertions)", () => {
|
|
3049
|
+
it("should reject assertion with NotBefore too far in future (beyond clock skew)", () => {
|
|
3050
|
+
const tenMinutesFromNow = new Date(
|
|
3051
|
+
Date.now() + 10 * 60 * 1000,
|
|
3052
|
+
).toISOString();
|
|
3053
|
+
expect(() =>
|
|
3054
|
+
validateSAMLTimestamp({ notBefore: tenMinutesFromNow }),
|
|
3055
|
+
).toThrow("SAML assertion is not yet valid");
|
|
3056
|
+
});
|
|
3057
|
+
|
|
3058
|
+
it("should reject with custom strict clock skew (1 second)", () => {
|
|
3059
|
+
const threeSecondsFromNow = new Date(Date.now() + 3 * 1000).toISOString();
|
|
3060
|
+
expect(() =>
|
|
3061
|
+
validateSAMLTimestamp(
|
|
3062
|
+
{ notBefore: threeSecondsFromNow },
|
|
3063
|
+
{ clockSkew: 1000 },
|
|
3064
|
+
),
|
|
3065
|
+
).toThrow("SAML assertion is not yet valid");
|
|
3066
|
+
});
|
|
3067
|
+
});
|
|
3068
|
+
|
|
3069
|
+
describe("NotOnOrAfter validation (expired assertions)", () => {
|
|
3070
|
+
it("should reject expired assertion (NotOnOrAfter in past beyond clock skew)", () => {
|
|
3071
|
+
const tenMinutesAgo = new Date(Date.now() - 10 * 60 * 1000).toISOString();
|
|
3072
|
+
expect(() =>
|
|
3073
|
+
validateSAMLTimestamp({ notOnOrAfter: tenMinutesAgo }),
|
|
3074
|
+
).toThrow("SAML assertion has expired");
|
|
3075
|
+
});
|
|
3076
|
+
|
|
3077
|
+
it("should reject with custom strict clock skew (1 second)", () => {
|
|
3078
|
+
const threeSecondsAgo = new Date(Date.now() - 3 * 1000).toISOString();
|
|
3079
|
+
expect(() =>
|
|
3080
|
+
validateSAMLTimestamp(
|
|
3081
|
+
{ notOnOrAfter: threeSecondsAgo },
|
|
3082
|
+
{ clockSkew: 1000 },
|
|
3083
|
+
),
|
|
3084
|
+
).toThrow("SAML assertion has expired");
|
|
3085
|
+
});
|
|
3086
|
+
});
|
|
3087
|
+
|
|
3088
|
+
describe("Boundary conditions (exactly at window edges)", () => {
|
|
3089
|
+
const FIXED_TIME = new Date("2024-01-15T12:00:00.000Z").getTime();
|
|
3090
|
+
|
|
3091
|
+
beforeEach(() => {
|
|
3092
|
+
vi.useFakeTimers();
|
|
3093
|
+
vi.setSystemTime(FIXED_TIME);
|
|
3094
|
+
});
|
|
3095
|
+
|
|
3096
|
+
afterEach(() => {
|
|
3097
|
+
vi.useRealTimers();
|
|
3098
|
+
});
|
|
3099
|
+
|
|
3100
|
+
it("should accept assertion expiring exactly at clock skew boundary", () => {
|
|
3101
|
+
const exactlyAtBoundary = new Date(
|
|
3102
|
+
FIXED_TIME - DEFAULT_CLOCK_SKEW_MS,
|
|
3103
|
+
).toISOString();
|
|
3104
|
+
expect(() =>
|
|
3105
|
+
validateSAMLTimestamp({ notOnOrAfter: exactlyAtBoundary }),
|
|
3106
|
+
).not.toThrow();
|
|
3107
|
+
});
|
|
3108
|
+
|
|
3109
|
+
it("should reject assertion expiring 1ms beyond clock skew boundary", () => {
|
|
3110
|
+
const justPastBoundary = new Date(
|
|
3111
|
+
FIXED_TIME - DEFAULT_CLOCK_SKEW_MS - 1,
|
|
3112
|
+
).toISOString();
|
|
3113
|
+
expect(() =>
|
|
3114
|
+
validateSAMLTimestamp({ notOnOrAfter: justPastBoundary }),
|
|
3115
|
+
).toThrow("SAML assertion has expired");
|
|
3116
|
+
});
|
|
3117
|
+
|
|
3118
|
+
it("should accept assertion with NotBefore exactly at clock skew boundary", () => {
|
|
3119
|
+
const exactlyAtBoundary = new Date(
|
|
3120
|
+
FIXED_TIME + DEFAULT_CLOCK_SKEW_MS,
|
|
3121
|
+
).toISOString();
|
|
3122
|
+
expect(() =>
|
|
3123
|
+
validateSAMLTimestamp({ notBefore: exactlyAtBoundary }),
|
|
3124
|
+
).not.toThrow();
|
|
3125
|
+
});
|
|
3126
|
+
|
|
3127
|
+
it("should reject assertion with NotBefore 1ms beyond clock skew boundary", () => {
|
|
3128
|
+
const justPastBoundary = new Date(
|
|
3129
|
+
FIXED_TIME + DEFAULT_CLOCK_SKEW_MS + 1,
|
|
3130
|
+
).toISOString();
|
|
3131
|
+
expect(() =>
|
|
3132
|
+
validateSAMLTimestamp({ notBefore: justPastBoundary }),
|
|
3133
|
+
).toThrow("SAML assertion is not yet valid");
|
|
3134
|
+
});
|
|
3135
|
+
});
|
|
3136
|
+
|
|
3137
|
+
describe("Missing timestamps behavior", () => {
|
|
3138
|
+
it("should accept missing timestamps when requireTimestamps is false (default)", () => {
|
|
3139
|
+
expect(() =>
|
|
3140
|
+
validateSAMLTimestamp(undefined, { requireTimestamps: false }),
|
|
3141
|
+
).not.toThrow();
|
|
3142
|
+
});
|
|
3143
|
+
|
|
3144
|
+
it("should accept empty conditions when requireTimestamps is false", () => {
|
|
3145
|
+
expect(() =>
|
|
3146
|
+
validateSAMLTimestamp({}, { requireTimestamps: false }),
|
|
3147
|
+
).not.toThrow();
|
|
3148
|
+
});
|
|
3149
|
+
|
|
3150
|
+
it("should reject missing timestamps when requireTimestamps is true", () => {
|
|
3151
|
+
expect(() =>
|
|
3152
|
+
validateSAMLTimestamp(undefined, { requireTimestamps: true }),
|
|
3153
|
+
).toThrow("SAML assertion missing required timestamp conditions");
|
|
3154
|
+
});
|
|
3155
|
+
|
|
3156
|
+
it("should reject empty conditions when requireTimestamps is true", () => {
|
|
3157
|
+
expect(() =>
|
|
3158
|
+
validateSAMLTimestamp({}, { requireTimestamps: true }),
|
|
3159
|
+
).toThrow("SAML assertion missing required timestamp conditions");
|
|
3160
|
+
});
|
|
3161
|
+
|
|
3162
|
+
it("should accept assertions with only NotBefore (valid)", () => {
|
|
3163
|
+
const now = new Date().toISOString();
|
|
3164
|
+
expect(() => validateSAMLTimestamp({ notBefore: now })).not.toThrow();
|
|
3165
|
+
});
|
|
3166
|
+
|
|
3167
|
+
it("should accept assertions with only NotOnOrAfter (valid, in future)", () => {
|
|
3168
|
+
const future = new Date(Date.now() + 10 * 60 * 1000).toISOString();
|
|
3169
|
+
expect(() =>
|
|
3170
|
+
validateSAMLTimestamp({ notOnOrAfter: future }),
|
|
3171
|
+
).not.toThrow();
|
|
3172
|
+
});
|
|
3173
|
+
});
|
|
3174
|
+
|
|
3175
|
+
describe("Custom clock skew configuration", () => {
|
|
3176
|
+
it("should use custom clockSkew when provided", () => {
|
|
3177
|
+
const twoSecondsAgo = new Date(Date.now() - 2 * 1000).toISOString();
|
|
3178
|
+
|
|
3179
|
+
expect(() =>
|
|
3180
|
+
validateSAMLTimestamp(
|
|
3181
|
+
{ notOnOrAfter: twoSecondsAgo },
|
|
3182
|
+
{ clockSkew: 1000 },
|
|
3183
|
+
),
|
|
3184
|
+
).toThrow("SAML assertion has expired");
|
|
3185
|
+
|
|
3186
|
+
expect(() =>
|
|
3187
|
+
validateSAMLTimestamp(
|
|
3188
|
+
{ notOnOrAfter: twoSecondsAgo },
|
|
3189
|
+
{ clockSkew: 5 * 60 * 1000 },
|
|
3190
|
+
),
|
|
3191
|
+
).not.toThrow();
|
|
3192
|
+
});
|
|
3193
|
+
|
|
3194
|
+
it("should use default 5 minute clock skew when not specified", () => {
|
|
3195
|
+
const fourMinutesAgo = new Date(Date.now() - 4 * 60 * 1000).toISOString();
|
|
3196
|
+
expect(() =>
|
|
3197
|
+
validateSAMLTimestamp({ notOnOrAfter: fourMinutesAgo }),
|
|
3198
|
+
).not.toThrow();
|
|
3199
|
+
|
|
3200
|
+
const sixMinutesAgo = new Date(Date.now() - 6 * 60 * 1000).toISOString();
|
|
3201
|
+
expect(() =>
|
|
3202
|
+
validateSAMLTimestamp({ notOnOrAfter: sixMinutesAgo }),
|
|
3203
|
+
).toThrow("SAML assertion has expired");
|
|
3204
|
+
});
|
|
3205
|
+
});
|
|
3206
|
+
|
|
3207
|
+
describe("Malformed timestamp handling", () => {
|
|
3208
|
+
it("should reject malformed NotBefore timestamp", () => {
|
|
3209
|
+
expect(() =>
|
|
3210
|
+
validateSAMLTimestamp({ notBefore: "not-a-valid-date" }),
|
|
3211
|
+
).toThrow("SAML assertion has invalid NotBefore timestamp");
|
|
3212
|
+
});
|
|
3213
|
+
|
|
3214
|
+
it("should reject malformed NotOnOrAfter timestamp", () => {
|
|
3215
|
+
expect(() =>
|
|
3216
|
+
validateSAMLTimestamp({ notOnOrAfter: "invalid-timestamp" }),
|
|
3217
|
+
).toThrow("SAML assertion has invalid NotOnOrAfter timestamp");
|
|
3218
|
+
});
|
|
3219
|
+
|
|
3220
|
+
it("should treat empty string timestamps as missing (falsy values)", () => {
|
|
3221
|
+
expect(() => validateSAMLTimestamp({ notBefore: "" })).not.toThrow();
|
|
3222
|
+
expect(() => validateSAMLTimestamp({ notOnOrAfter: "" })).not.toThrow();
|
|
3223
|
+
});
|
|
3224
|
+
|
|
3225
|
+
it("should reject garbage data in timestamps", () => {
|
|
3226
|
+
expect(() =>
|
|
3227
|
+
validateSAMLTimestamp({
|
|
3228
|
+
notBefore: "abc123xyz",
|
|
3229
|
+
notOnOrAfter: "!@#$%^&*()",
|
|
3230
|
+
}),
|
|
3231
|
+
).toThrow("SAML assertion has invalid NotBefore timestamp");
|
|
3232
|
+
});
|
|
3233
|
+
|
|
3234
|
+
it("should accept valid ISO 8601 timestamps", () => {
|
|
3235
|
+
const now = new Date();
|
|
3236
|
+
const future = new Date(Date.now() + 10 * 60 * 1000);
|
|
3237
|
+
expect(() =>
|
|
3238
|
+
validateSAMLTimestamp({
|
|
3239
|
+
notBefore: now.toISOString(),
|
|
3240
|
+
notOnOrAfter: future.toISOString(),
|
|
3241
|
+
}),
|
|
3242
|
+
).not.toThrow();
|
|
3243
|
+
});
|
|
3244
|
+
});
|
|
3245
|
+
});
|
|
3246
|
+
|
|
3247
|
+
describe("SAML ACS Origin Check Bypass", () => {
|
|
3248
|
+
describe("Positive: SAML endpoints allow external IdP origins", () => {
|
|
3249
|
+
it("should allow SAML callback POST from external IdP origin", async () => {
|
|
3250
|
+
const { auth, signInWithTestUser } = await getTestInstance({
|
|
3251
|
+
plugins: [sso()],
|
|
3252
|
+
});
|
|
3253
|
+
const { headers } = await signInWithTestUser();
|
|
3254
|
+
|
|
3255
|
+
// Register SAML provider with full config
|
|
3256
|
+
await auth.api.registerSSOProvider({
|
|
3257
|
+
body: {
|
|
3258
|
+
providerId: "origin-bypass-callback",
|
|
3259
|
+
issuer: "http://localhost:8081",
|
|
3260
|
+
domain: "origin-bypass.com",
|
|
3261
|
+
samlConfig: {
|
|
3262
|
+
entryPoint: sharedMockIdP.metadataUrl,
|
|
3263
|
+
cert: certificate,
|
|
3264
|
+
callbackUrl: "http://localhost:8081/api/auth/sso/saml2/callback",
|
|
3265
|
+
wantAssertionsSigned: false,
|
|
3266
|
+
signatureAlgorithm: "sha256",
|
|
3267
|
+
digestAlgorithm: "sha256",
|
|
3268
|
+
spMetadata: {
|
|
3269
|
+
metadata: spMetadata,
|
|
3270
|
+
},
|
|
3271
|
+
},
|
|
3272
|
+
},
|
|
3273
|
+
headers,
|
|
3274
|
+
});
|
|
3275
|
+
|
|
3276
|
+
// POST to callback with external Origin header (simulating IdP POST)
|
|
3277
|
+
// Origin check should be bypassed for SAML callback endpoints
|
|
3278
|
+
const callbackRes = await auth.handler(
|
|
3279
|
+
new Request(
|
|
3280
|
+
"http://localhost:8081/api/auth/sso/saml2/callback/origin-bypass-callback",
|
|
3281
|
+
{
|
|
3282
|
+
method: "POST",
|
|
3283
|
+
headers: {
|
|
3284
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
3285
|
+
Origin: "http://external-idp.example.com", // External IdP origin - would normally be blocked
|
|
3286
|
+
Cookie: headers.get("cookie") || "",
|
|
3287
|
+
},
|
|
3288
|
+
body: new URLSearchParams({
|
|
3289
|
+
SAMLResponse: Buffer.from("<fake-saml-response/>").toString(
|
|
3290
|
+
"base64",
|
|
3291
|
+
),
|
|
3292
|
+
RelayState: "",
|
|
3293
|
+
}).toString(),
|
|
3294
|
+
},
|
|
3295
|
+
),
|
|
3296
|
+
);
|
|
3297
|
+
|
|
3298
|
+
// Should NOT return 403 Forbidden (origin check bypassed)
|
|
3299
|
+
// May return other errors (400, 500) due to invalid SAML response, but NOT origin rejection
|
|
3300
|
+
expect(callbackRes.status).not.toBe(403);
|
|
3301
|
+
});
|
|
3302
|
+
|
|
3303
|
+
it("should allow ACS endpoint POST from external IdP origin", async () => {
|
|
3304
|
+
const { auth, signInWithTestUser } = await getTestInstance({
|
|
3305
|
+
plugins: [sso()],
|
|
3306
|
+
});
|
|
3307
|
+
const { headers } = await signInWithTestUser();
|
|
3308
|
+
|
|
3309
|
+
// Register SAML provider with full config
|
|
3310
|
+
await auth.api.registerSSOProvider({
|
|
3311
|
+
body: {
|
|
3312
|
+
providerId: "origin-bypass-acs",
|
|
3313
|
+
issuer: "http://localhost:8081",
|
|
3314
|
+
domain: "origin-bypass-acs.com",
|
|
3315
|
+
samlConfig: {
|
|
3316
|
+
entryPoint: sharedMockIdP.metadataUrl,
|
|
3317
|
+
cert: certificate,
|
|
3318
|
+
callbackUrl: "http://localhost:8081/api/auth/sso/saml2/sp/acs",
|
|
3319
|
+
wantAssertionsSigned: false,
|
|
3320
|
+
signatureAlgorithm: "sha256",
|
|
3321
|
+
digestAlgorithm: "sha256",
|
|
3322
|
+
spMetadata: {
|
|
3323
|
+
metadata: spMetadata,
|
|
3324
|
+
},
|
|
3325
|
+
},
|
|
3326
|
+
},
|
|
3327
|
+
headers,
|
|
3328
|
+
});
|
|
3329
|
+
|
|
3330
|
+
// POST to ACS with external Origin header
|
|
3331
|
+
const acsRes = await auth.handler(
|
|
3332
|
+
new Request(
|
|
3333
|
+
"http://localhost:8081/api/auth/sso/saml2/sp/acs/origin-bypass-acs",
|
|
3334
|
+
{
|
|
3335
|
+
method: "POST",
|
|
3336
|
+
headers: {
|
|
3337
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
3338
|
+
Origin: "http://idp.external.com", // External IdP origin
|
|
3339
|
+
Cookie: headers.get("cookie") || "",
|
|
3340
|
+
},
|
|
3341
|
+
body: new URLSearchParams({
|
|
3342
|
+
SAMLResponse: Buffer.from("<fake-saml-response/>").toString(
|
|
3343
|
+
"base64",
|
|
3344
|
+
),
|
|
3345
|
+
}).toString(),
|
|
3346
|
+
},
|
|
3347
|
+
),
|
|
3348
|
+
);
|
|
3349
|
+
|
|
3350
|
+
// Should NOT return 403 Forbidden
|
|
3351
|
+
expect(acsRes.status).not.toBe(403);
|
|
3352
|
+
});
|
|
3353
|
+
});
|
|
3354
|
+
|
|
3355
|
+
describe("Negative: Non-SAML endpoints remain protected", () => {
|
|
3356
|
+
it("should block POST to sign-up with untrusted origin when origin check is enabled", async () => {
|
|
3357
|
+
const { auth } = await getTestInstance({
|
|
3358
|
+
plugins: [sso()],
|
|
3359
|
+
advanced: {
|
|
3360
|
+
disableCSRFCheck: false,
|
|
3361
|
+
disableOriginCheck: false,
|
|
3362
|
+
},
|
|
3363
|
+
});
|
|
3364
|
+
|
|
3365
|
+
// Origin check applies when cookies are present and check is enabled
|
|
3366
|
+
const signUpRes = await auth.handler(
|
|
3367
|
+
new Request("http://localhost:8081/api/auth/sign-up/email", {
|
|
3368
|
+
method: "POST",
|
|
3369
|
+
headers: {
|
|
3370
|
+
"Content-Type": "application/json",
|
|
3371
|
+
Origin: "http://attacker.com",
|
|
3372
|
+
Cookie: "better-auth.session_token=fake-session",
|
|
3373
|
+
},
|
|
3374
|
+
body: JSON.stringify({
|
|
3375
|
+
email: "victim@example.com",
|
|
3376
|
+
password: "password123",
|
|
3377
|
+
name: "Victim",
|
|
3378
|
+
}),
|
|
3379
|
+
}),
|
|
3380
|
+
);
|
|
3381
|
+
|
|
3382
|
+
expect(signUpRes.status).toBe(403);
|
|
3383
|
+
});
|
|
3384
|
+
});
|
|
3385
|
+
|
|
3386
|
+
describe("Edge cases", () => {
|
|
3387
|
+
it("should allow GET requests to SAML metadata regardless of origin", async () => {
|
|
3388
|
+
const { auth } = await getTestInstance({
|
|
3389
|
+
plugins: [sso()],
|
|
3390
|
+
});
|
|
3391
|
+
|
|
3392
|
+
// GET requests always bypass origin check
|
|
3393
|
+
const metadataRes = await auth.handler(
|
|
3394
|
+
new Request("http://localhost:8081/api/auth/sso/saml2/sp/metadata", {
|
|
3395
|
+
method: "GET",
|
|
3396
|
+
headers: {
|
|
3397
|
+
Origin: "http://any-origin.com",
|
|
3398
|
+
},
|
|
3399
|
+
}),
|
|
3400
|
+
);
|
|
3401
|
+
|
|
3402
|
+
expect(metadataRes.status).not.toBe(403);
|
|
3403
|
+
});
|
|
3404
|
+
|
|
3405
|
+
it("should not redirect to malicious RelayState URLs", async () => {
|
|
3406
|
+
const { auth, signInWithTestUser } = await getTestInstance({
|
|
3407
|
+
plugins: [sso()],
|
|
3408
|
+
});
|
|
3409
|
+
const { headers } = await signInWithTestUser();
|
|
3410
|
+
|
|
3411
|
+
await auth.api.registerSSOProvider({
|
|
3412
|
+
body: {
|
|
3413
|
+
providerId: "relay-security-test",
|
|
3414
|
+
issuer: "http://localhost:8081",
|
|
3415
|
+
domain: "relay-security.com",
|
|
3416
|
+
samlConfig: {
|
|
3417
|
+
entryPoint: sharedMockIdP.metadataUrl,
|
|
3418
|
+
cert: certificate,
|
|
3419
|
+
callbackUrl: "http://localhost:8081/api/auth/sso/saml2/callback",
|
|
3420
|
+
wantAssertionsSigned: false,
|
|
3421
|
+
signatureAlgorithm: "sha256",
|
|
3422
|
+
digestAlgorithm: "sha256",
|
|
3423
|
+
spMetadata: {
|
|
3424
|
+
metadata: spMetadata,
|
|
3425
|
+
},
|
|
3426
|
+
},
|
|
3427
|
+
},
|
|
3428
|
+
headers,
|
|
3429
|
+
});
|
|
3430
|
+
|
|
3431
|
+
// Even with origin bypass, malicious RelayState should be rejected
|
|
3432
|
+
const callbackRes = await auth.handler(
|
|
3433
|
+
new Request(
|
|
3434
|
+
"http://localhost:8081/api/auth/sso/saml2/callback/relay-security-test",
|
|
3435
|
+
{
|
|
3436
|
+
method: "POST",
|
|
3437
|
+
headers: {
|
|
3438
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
3439
|
+
Origin: "http://idp.example.com",
|
|
3440
|
+
},
|
|
3441
|
+
body: new URLSearchParams({
|
|
3442
|
+
SAMLResponse: Buffer.from("<fake-saml-response/>").toString(
|
|
3443
|
+
"base64",
|
|
3444
|
+
),
|
|
3445
|
+
RelayState: "http://malicious-site.com/steal-token",
|
|
3446
|
+
}).toString(),
|
|
3447
|
+
},
|
|
3448
|
+
),
|
|
3449
|
+
);
|
|
3450
|
+
|
|
3451
|
+
// Should NOT redirect to malicious URL
|
|
3452
|
+
if (callbackRes.status === 302) {
|
|
3453
|
+
const location = callbackRes.headers.get("Location");
|
|
3454
|
+
expect(location).not.toContain("malicious-site.com");
|
|
3455
|
+
}
|
|
3456
|
+
});
|
|
3457
|
+
});
|
|
3458
|
+
});
|
|
3459
|
+
|
|
3460
|
+
describe("SAML Response Security", () => {
|
|
3461
|
+
it("should reject forged/unsigned SAML responses", async () => {
|
|
3462
|
+
const { auth, signInWithTestUser } = await getTestInstance({
|
|
3463
|
+
plugins: [sso()],
|
|
3464
|
+
});
|
|
3465
|
+
const { headers } = await signInWithTestUser();
|
|
3466
|
+
|
|
3467
|
+
await auth.api.registerSSOProvider({
|
|
3468
|
+
body: {
|
|
3469
|
+
providerId: "security-test-provider",
|
|
3470
|
+
issuer: "http://localhost:8081",
|
|
3471
|
+
domain: "security-test.com",
|
|
3472
|
+
samlConfig: {
|
|
3473
|
+
entryPoint: sharedMockIdP.metadataUrl,
|
|
3474
|
+
cert: certificate,
|
|
3475
|
+
callbackUrl: "http://localhost:8081/api/auth/sso/saml2/callback",
|
|
3476
|
+
wantAssertionsSigned: false,
|
|
3477
|
+
signatureAlgorithm: "sha256",
|
|
3478
|
+
digestAlgorithm: "sha256",
|
|
3479
|
+
spMetadata: {
|
|
3480
|
+
metadata: spMetadata,
|
|
3481
|
+
},
|
|
3482
|
+
},
|
|
3483
|
+
},
|
|
3484
|
+
headers,
|
|
3485
|
+
});
|
|
3486
|
+
|
|
3487
|
+
const forgedSAMLResponse = `
|
|
3488
|
+
<samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">
|
|
3489
|
+
<saml:Assertion>
|
|
3490
|
+
<saml:Subject>
|
|
3491
|
+
<saml2:NameID xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion">attacker@evil.com</saml2:NameID>
|
|
3492
|
+
</saml:Subject>
|
|
3493
|
+
</saml:Assertion>
|
|
3494
|
+
</samlp:Response>
|
|
3495
|
+
`;
|
|
3496
|
+
|
|
3497
|
+
const callbackRes = await auth.handler(
|
|
3498
|
+
new Request(
|
|
3499
|
+
"http://localhost:8081/api/auth/sso/saml2/callback/security-test-provider",
|
|
3500
|
+
{
|
|
3501
|
+
method: "POST",
|
|
3502
|
+
headers: {
|
|
3503
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
3504
|
+
},
|
|
3505
|
+
body: new URLSearchParams({
|
|
3506
|
+
SAMLResponse: Buffer.from(forgedSAMLResponse).toString("base64"),
|
|
3507
|
+
RelayState: "",
|
|
3508
|
+
}).toString(),
|
|
3509
|
+
},
|
|
3510
|
+
),
|
|
3511
|
+
);
|
|
3512
|
+
|
|
3513
|
+
expect(callbackRes.status).toBe(400);
|
|
3514
|
+
const body = await callbackRes.json();
|
|
3515
|
+
expect(body.message).toBe("Invalid SAML response");
|
|
3516
|
+
});
|
|
3517
|
+
|
|
3518
|
+
it("should reject SAML response with tampered nameID", async () => {
|
|
3519
|
+
const { auth, signInWithTestUser } = await getTestInstance({
|
|
3520
|
+
plugins: [sso()],
|
|
3521
|
+
});
|
|
3522
|
+
const { headers } = await signInWithTestUser();
|
|
3523
|
+
|
|
3524
|
+
await auth.api.registerSSOProvider({
|
|
3525
|
+
body: {
|
|
3526
|
+
providerId: "tamper-test-provider",
|
|
3527
|
+
issuer: "http://localhost:8081",
|
|
3528
|
+
domain: "tamper-test.com",
|
|
3529
|
+
samlConfig: {
|
|
3530
|
+
entryPoint: sharedMockIdP.metadataUrl,
|
|
3531
|
+
cert: certificate,
|
|
3532
|
+
callbackUrl: "http://localhost:8081/api/auth/sso/saml2/callback",
|
|
3533
|
+
wantAssertionsSigned: false,
|
|
3534
|
+
signatureAlgorithm: "sha256",
|
|
3535
|
+
digestAlgorithm: "sha256",
|
|
3536
|
+
spMetadata: {
|
|
3537
|
+
metadata: spMetadata,
|
|
3538
|
+
},
|
|
3539
|
+
},
|
|
3540
|
+
},
|
|
3541
|
+
headers,
|
|
3542
|
+
});
|
|
3543
|
+
|
|
3544
|
+
const tamperedResponse = `<?xml version="1.0"?>
|
|
3545
|
+
<samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol">
|
|
3546
|
+
<saml2:NameID xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion">admin@victim.com</saml2:NameID>
|
|
3547
|
+
</samlp:Response>`;
|
|
3548
|
+
|
|
3549
|
+
const callbackRes = await auth.handler(
|
|
3550
|
+
new Request(
|
|
3551
|
+
"http://localhost:8081/api/auth/sso/saml2/callback/tamper-test-provider",
|
|
3552
|
+
{
|
|
3553
|
+
method: "POST",
|
|
3554
|
+
headers: {
|
|
3555
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
3556
|
+
},
|
|
3557
|
+
body: new URLSearchParams({
|
|
3558
|
+
SAMLResponse: Buffer.from(tamperedResponse).toString("base64"),
|
|
3559
|
+
RelayState: "",
|
|
3560
|
+
}).toString(),
|
|
3561
|
+
},
|
|
3562
|
+
),
|
|
3563
|
+
);
|
|
3564
|
+
|
|
3565
|
+
expect(callbackRes.status).toBe(400);
|
|
3566
|
+
});
|
|
3567
|
+
});
|
|
3568
|
+
|
|
3569
|
+
describe("SAML SSO - Size Limit Validation", () => {
|
|
3570
|
+
it("should export default size limit constants", async () => {
|
|
3571
|
+
const { DEFAULT_MAX_SAML_RESPONSE_SIZE, DEFAULT_MAX_SAML_METADATA_SIZE } =
|
|
3572
|
+
await import("./constants");
|
|
3573
|
+
|
|
3574
|
+
expect(DEFAULT_MAX_SAML_RESPONSE_SIZE).toBe(256 * 1024);
|
|
3575
|
+
expect(DEFAULT_MAX_SAML_METADATA_SIZE).toBe(100 * 1024);
|
|
3576
|
+
});
|
|
3577
|
+
});
|
|
3578
|
+
|
|
3579
|
+
describe("SAML SSO - Assertion Replay Protection", () => {
|
|
3580
|
+
it("should reject replayed SAML assertion (same assertion submitted twice)", async () => {
|
|
3581
|
+
const { auth, signInWithTestUser } = await getTestInstance({
|
|
3582
|
+
plugins: [sso()],
|
|
3583
|
+
});
|
|
3584
|
+
|
|
3585
|
+
const { headers } = await signInWithTestUser();
|
|
3586
|
+
|
|
3587
|
+
await auth.api.registerSSOProvider({
|
|
3588
|
+
body: {
|
|
3589
|
+
providerId: "replay-test-provider",
|
|
3590
|
+
issuer: "http://localhost:8081",
|
|
3591
|
+
domain: "http://localhost:8081",
|
|
3592
|
+
samlConfig: {
|
|
3593
|
+
entryPoint: "http://localhost:8081/api/sso/saml2/idp/post",
|
|
3594
|
+
cert: certificate,
|
|
3595
|
+
callbackUrl: "http://localhost:3000/dashboard",
|
|
3596
|
+
wantAssertionsSigned: false,
|
|
3597
|
+
signatureAlgorithm: "sha256",
|
|
3598
|
+
digestAlgorithm: "sha256",
|
|
3599
|
+
idpMetadata: {
|
|
3600
|
+
metadata: idpMetadata,
|
|
3601
|
+
},
|
|
3602
|
+
spMetadata: {
|
|
3603
|
+
metadata: spMetadata,
|
|
3604
|
+
},
|
|
3605
|
+
identifierFormat:
|
|
3606
|
+
"urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress",
|
|
3607
|
+
},
|
|
3608
|
+
},
|
|
3609
|
+
headers,
|
|
3610
|
+
});
|
|
3611
|
+
|
|
3612
|
+
let samlResponse: any;
|
|
3613
|
+
await betterFetch("http://localhost:8081/api/sso/saml2/idp/post", {
|
|
3614
|
+
onSuccess: async (context) => {
|
|
3615
|
+
samlResponse = await context.data;
|
|
3616
|
+
},
|
|
3617
|
+
});
|
|
3618
|
+
|
|
3619
|
+
const firstResponse = await auth.handler(
|
|
3620
|
+
new Request(
|
|
3621
|
+
"http://localhost:3000/api/auth/sso/saml2/callback/replay-test-provider",
|
|
3622
|
+
{
|
|
3623
|
+
method: "POST",
|
|
3624
|
+
headers: {
|
|
3625
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
3626
|
+
},
|
|
3627
|
+
body: new URLSearchParams({
|
|
3628
|
+
SAMLResponse: samlResponse.samlResponse,
|
|
3629
|
+
RelayState: "http://localhost:3000/dashboard",
|
|
3630
|
+
}),
|
|
3631
|
+
},
|
|
3632
|
+
),
|
|
3633
|
+
);
|
|
3634
|
+
|
|
3635
|
+
expect(firstResponse.status).toBe(302);
|
|
3636
|
+
const firstLocation = firstResponse.headers.get("location") || "";
|
|
3637
|
+
expect(firstLocation).not.toContain("error");
|
|
3638
|
+
|
|
3639
|
+
const replayResponse = await auth.handler(
|
|
3640
|
+
new Request(
|
|
3641
|
+
"http://localhost:3000/api/auth/sso/saml2/callback/replay-test-provider",
|
|
3642
|
+
{
|
|
3643
|
+
method: "POST",
|
|
3644
|
+
headers: {
|
|
3645
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
3646
|
+
},
|
|
3647
|
+
body: new URLSearchParams({
|
|
3648
|
+
SAMLResponse: samlResponse.samlResponse,
|
|
3649
|
+
RelayState: "http://localhost:3000/dashboard",
|
|
3650
|
+
}),
|
|
3651
|
+
},
|
|
3652
|
+
),
|
|
3653
|
+
);
|
|
3654
|
+
|
|
3655
|
+
expect(replayResponse.status).toBe(302);
|
|
3656
|
+
const replayLocation = replayResponse.headers.get("location") || "";
|
|
3657
|
+
expect(replayLocation).toContain("error=replay_detected");
|
|
3658
|
+
});
|
|
3659
|
+
|
|
3660
|
+
it("should reject replayed SAML assertion on ACS endpoint", async () => {
|
|
3661
|
+
const { auth, signInWithTestUser } = await getTestInstance({
|
|
3662
|
+
plugins: [sso()],
|
|
3663
|
+
});
|
|
3664
|
+
|
|
3665
|
+
const { headers } = await signInWithTestUser();
|
|
3666
|
+
|
|
3667
|
+
await auth.api.registerSSOProvider({
|
|
3668
|
+
body: {
|
|
3669
|
+
providerId: "acs-replay-test-provider",
|
|
3670
|
+
issuer: "http://localhost:8081",
|
|
3671
|
+
domain: "http://localhost:8081",
|
|
3672
|
+
samlConfig: {
|
|
3673
|
+
entryPoint: "http://localhost:8081/api/sso/saml2/idp/post",
|
|
3674
|
+
cert: certificate,
|
|
3675
|
+
callbackUrl: "http://localhost:3000/dashboard",
|
|
3676
|
+
wantAssertionsSigned: false,
|
|
3677
|
+
signatureAlgorithm: "sha256",
|
|
3678
|
+
digestAlgorithm: "sha256",
|
|
3679
|
+
idpMetadata: {
|
|
3680
|
+
metadata: idpMetadata,
|
|
3681
|
+
},
|
|
3682
|
+
spMetadata: {
|
|
3683
|
+
metadata: spMetadata,
|
|
3684
|
+
},
|
|
3685
|
+
identifierFormat:
|
|
3686
|
+
"urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress",
|
|
3687
|
+
},
|
|
3688
|
+
},
|
|
3689
|
+
headers,
|
|
3690
|
+
});
|
|
3691
|
+
|
|
3692
|
+
let samlResponse: any;
|
|
3693
|
+
await betterFetch("http://localhost:8081/api/sso/saml2/idp/post", {
|
|
3694
|
+
onSuccess: async (context) => {
|
|
3695
|
+
samlResponse = await context.data;
|
|
3696
|
+
},
|
|
3697
|
+
});
|
|
3698
|
+
|
|
3699
|
+
const firstResponse = await auth.handler(
|
|
3700
|
+
new Request(
|
|
3701
|
+
"http://localhost:3000/api/auth/sso/saml2/sp/acs/acs-replay-test-provider",
|
|
3702
|
+
{
|
|
3703
|
+
method: "POST",
|
|
3704
|
+
headers: {
|
|
3705
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
3706
|
+
},
|
|
3707
|
+
body: new URLSearchParams({
|
|
3708
|
+
SAMLResponse: samlResponse.samlResponse,
|
|
3709
|
+
RelayState: "http://localhost:3000/dashboard",
|
|
3710
|
+
}),
|
|
3711
|
+
},
|
|
3712
|
+
),
|
|
3713
|
+
);
|
|
3714
|
+
|
|
3715
|
+
expect(firstResponse.status).toBe(302);
|
|
3716
|
+
const firstLocation = firstResponse.headers.get("location") || "";
|
|
3717
|
+
expect(firstLocation).not.toContain("error");
|
|
3718
|
+
|
|
3719
|
+
const replayResponse = await auth.handler(
|
|
3720
|
+
new Request(
|
|
3721
|
+
"http://localhost:3000/api/auth/sso/saml2/sp/acs/acs-replay-test-provider",
|
|
3722
|
+
{
|
|
3723
|
+
method: "POST",
|
|
3724
|
+
headers: {
|
|
3725
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
3726
|
+
},
|
|
3727
|
+
body: new URLSearchParams({
|
|
3728
|
+
SAMLResponse: samlResponse.samlResponse,
|
|
3729
|
+
RelayState: "http://localhost:3000/dashboard",
|
|
3730
|
+
}),
|
|
3731
|
+
},
|
|
3732
|
+
),
|
|
3733
|
+
);
|
|
3734
|
+
|
|
3735
|
+
expect(replayResponse.status).toBe(302);
|
|
3736
|
+
const replayLocation = replayResponse.headers.get("location") || "";
|
|
3737
|
+
expect(replayLocation).toContain("error=replay_detected");
|
|
3738
|
+
});
|
|
3739
|
+
|
|
3740
|
+
it("should reject cross-endpoint replay (callback → ACS)", async () => {
|
|
3741
|
+
const { auth, signInWithTestUser } = await getTestInstance({
|
|
3742
|
+
plugins: [sso()],
|
|
3743
|
+
});
|
|
3744
|
+
|
|
3745
|
+
const { headers } = await signInWithTestUser();
|
|
3746
|
+
|
|
3747
|
+
await auth.api.registerSSOProvider({
|
|
3748
|
+
body: {
|
|
3749
|
+
providerId: "cross-endpoint-provider",
|
|
3750
|
+
issuer: "http://localhost:8081",
|
|
3751
|
+
domain: "http://localhost:8081",
|
|
3752
|
+
samlConfig: {
|
|
3753
|
+
entryPoint: "http://localhost:8081/api/sso/saml2/idp/post",
|
|
3754
|
+
cert: certificate,
|
|
3755
|
+
callbackUrl: "http://localhost:3000/dashboard",
|
|
3756
|
+
wantAssertionsSigned: false,
|
|
3757
|
+
signatureAlgorithm: "sha256",
|
|
3758
|
+
digestAlgorithm: "sha256",
|
|
3759
|
+
idpMetadata: {
|
|
3760
|
+
metadata: idpMetadata,
|
|
3761
|
+
},
|
|
3762
|
+
spMetadata: {
|
|
3763
|
+
metadata: spMetadata,
|
|
3764
|
+
},
|
|
3765
|
+
identifierFormat:
|
|
3766
|
+
"urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress",
|
|
3767
|
+
},
|
|
3768
|
+
},
|
|
3769
|
+
headers,
|
|
3770
|
+
});
|
|
3771
|
+
|
|
3772
|
+
let samlResponse: any;
|
|
3773
|
+
await betterFetch("http://localhost:8081/api/sso/saml2/idp/post", {
|
|
3774
|
+
onSuccess: async (context) => {
|
|
3775
|
+
samlResponse = await context.data;
|
|
3776
|
+
},
|
|
3777
|
+
});
|
|
3778
|
+
|
|
3779
|
+
const callbackResponse = await auth.handler(
|
|
3780
|
+
new Request(
|
|
3781
|
+
"http://localhost:3000/api/auth/sso/saml2/callback/cross-endpoint-provider",
|
|
3782
|
+
{
|
|
3783
|
+
method: "POST",
|
|
3784
|
+
headers: {
|
|
3785
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
3786
|
+
},
|
|
3787
|
+
body: new URLSearchParams({
|
|
3788
|
+
SAMLResponse: samlResponse.samlResponse,
|
|
3789
|
+
RelayState: "http://localhost:3000/dashboard",
|
|
3790
|
+
}),
|
|
3791
|
+
},
|
|
3792
|
+
),
|
|
3793
|
+
);
|
|
3794
|
+
|
|
3795
|
+
expect(callbackResponse.status).toBe(302);
|
|
3796
|
+
expect(callbackResponse.headers.get("location")).not.toContain("error");
|
|
3797
|
+
|
|
3798
|
+
const acsReplayResponse = await auth.handler(
|
|
3799
|
+
new Request(
|
|
3800
|
+
"http://localhost:3000/api/auth/sso/saml2/sp/acs/cross-endpoint-provider",
|
|
3801
|
+
{
|
|
3802
|
+
method: "POST",
|
|
3803
|
+
headers: {
|
|
3804
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
3805
|
+
},
|
|
3806
|
+
body: new URLSearchParams({
|
|
3807
|
+
SAMLResponse: samlResponse.samlResponse,
|
|
3808
|
+
RelayState: "http://localhost:3000/dashboard",
|
|
3809
|
+
}),
|
|
3810
|
+
},
|
|
3811
|
+
),
|
|
3812
|
+
);
|
|
3813
|
+
|
|
3814
|
+
expect(acsReplayResponse.status).toBe(302);
|
|
3815
|
+
const acsLocation = acsReplayResponse.headers.get("location") || "";
|
|
3816
|
+
expect(acsLocation).toContain("error=replay_detected");
|
|
3817
|
+
});
|
|
3818
|
+
});
|
|
3819
|
+
|
|
3820
|
+
describe("SAML SSO - Single Assertion Validation", () => {
|
|
3821
|
+
it("should reject SAML response with multiple assertions on callback endpoint", async () => {
|
|
3822
|
+
const { auth, signInWithTestUser } = await getTestInstance({
|
|
3823
|
+
plugins: [sso()],
|
|
3824
|
+
});
|
|
3825
|
+
|
|
3826
|
+
const { headers } = await signInWithTestUser();
|
|
3827
|
+
|
|
3828
|
+
await auth.api.registerSSOProvider({
|
|
3829
|
+
body: {
|
|
3830
|
+
providerId: "multi-assertion-callback-provider",
|
|
3831
|
+
issuer: "http://localhost:8081",
|
|
3832
|
+
domain: "http://localhost:8081",
|
|
3833
|
+
samlConfig: {
|
|
3834
|
+
entryPoint: "http://localhost:8081/api/sso/saml2/idp/post",
|
|
3835
|
+
cert: certificate,
|
|
3836
|
+
callbackUrl: "http://localhost:3000/dashboard",
|
|
3837
|
+
wantAssertionsSigned: false,
|
|
3838
|
+
signatureAlgorithm: "sha256",
|
|
3839
|
+
digestAlgorithm: "sha256",
|
|
3840
|
+
idpMetadata: {
|
|
3841
|
+
metadata: idpMetadata,
|
|
3842
|
+
},
|
|
3843
|
+
spMetadata: {
|
|
3844
|
+
metadata: spMetadata,
|
|
3845
|
+
},
|
|
3846
|
+
identifierFormat:
|
|
3847
|
+
"urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress",
|
|
3848
|
+
},
|
|
3849
|
+
},
|
|
3850
|
+
headers,
|
|
3851
|
+
});
|
|
3852
|
+
|
|
3853
|
+
const multiAssertionResponse = `
|
|
3854
|
+
<saml2p:Response xmlns:saml2p="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion">
|
|
3855
|
+
<saml2:Issuer>http://localhost:8081</saml2:Issuer>
|
|
3856
|
+
<saml2p:Status>
|
|
3857
|
+
<saml2p:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/>
|
|
3858
|
+
</saml2p:Status>
|
|
3859
|
+
<saml2:Assertion ID="assertion-1">
|
|
3860
|
+
<saml2:Issuer>http://localhost:8081</saml2:Issuer>
|
|
3861
|
+
<saml2:Subject>
|
|
3862
|
+
<saml2:NameID Format="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress">legitimate@example.com</saml2:NameID>
|
|
3863
|
+
</saml2:Subject>
|
|
3864
|
+
</saml2:Assertion>
|
|
3865
|
+
<saml2:Assertion ID="assertion-2">
|
|
3866
|
+
<saml2:Issuer>http://localhost:8081</saml2:Issuer>
|
|
3867
|
+
<saml2:Subject>
|
|
3868
|
+
<saml2:NameID Format="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress">attacker@evil.com</saml2:NameID>
|
|
3869
|
+
</saml2:Subject>
|
|
3870
|
+
</saml2:Assertion>
|
|
3871
|
+
</saml2p:Response>
|
|
3872
|
+
`;
|
|
3873
|
+
|
|
3874
|
+
const encodedResponse = Buffer.from(multiAssertionResponse).toString(
|
|
3875
|
+
"base64",
|
|
3876
|
+
);
|
|
3877
|
+
|
|
3878
|
+
await expect(
|
|
3879
|
+
auth.api.callbackSSOSAML({
|
|
3880
|
+
body: {
|
|
3881
|
+
SAMLResponse: encodedResponse,
|
|
3882
|
+
RelayState: "http://localhost:3000/dashboard",
|
|
3883
|
+
},
|
|
3884
|
+
params: {
|
|
3885
|
+
providerId: "multi-assertion-callback-provider",
|
|
3886
|
+
},
|
|
3887
|
+
}),
|
|
3888
|
+
).rejects.toMatchObject({
|
|
3889
|
+
body: {
|
|
3890
|
+
code: "SAML_MULTIPLE_ASSERTIONS",
|
|
3891
|
+
},
|
|
3892
|
+
});
|
|
3893
|
+
});
|
|
3894
|
+
|
|
3895
|
+
it("should reject SAML response with multiple assertions on ACS endpoint", async () => {
|
|
3896
|
+
const { auth, signInWithTestUser } = await getTestInstance({
|
|
3897
|
+
plugins: [sso()],
|
|
3898
|
+
});
|
|
3899
|
+
|
|
3900
|
+
const { headers } = await signInWithTestUser();
|
|
3901
|
+
|
|
3902
|
+
await auth.api.registerSSOProvider({
|
|
3903
|
+
body: {
|
|
3904
|
+
providerId: "multi-assertion-acs-provider",
|
|
3905
|
+
issuer: "http://localhost:8081",
|
|
3906
|
+
domain: "http://localhost:8081",
|
|
3907
|
+
samlConfig: {
|
|
3908
|
+
entryPoint: "http://localhost:8081/api/sso/saml2/idp/post",
|
|
3909
|
+
cert: certificate,
|
|
3910
|
+
callbackUrl: "http://localhost:3000/dashboard",
|
|
3911
|
+
wantAssertionsSigned: false,
|
|
3912
|
+
signatureAlgorithm: "sha256",
|
|
3913
|
+
digestAlgorithm: "sha256",
|
|
3914
|
+
idpMetadata: {
|
|
3915
|
+
metadata: idpMetadata,
|
|
3916
|
+
},
|
|
3917
|
+
spMetadata: {
|
|
3918
|
+
metadata: spMetadata,
|
|
3919
|
+
},
|
|
3920
|
+
identifierFormat:
|
|
3921
|
+
"urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress",
|
|
3922
|
+
},
|
|
3923
|
+
},
|
|
3924
|
+
headers,
|
|
3925
|
+
});
|
|
3926
|
+
|
|
3927
|
+
const multiAssertionResponse = `
|
|
3928
|
+
<saml2p:Response xmlns:saml2p="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion">
|
|
3929
|
+
<saml2:Issuer>http://localhost:8081</saml2:Issuer>
|
|
3930
|
+
<saml2p:Status>
|
|
3931
|
+
<saml2p:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/>
|
|
3932
|
+
</saml2p:Status>
|
|
3933
|
+
<saml2:Assertion ID="assertion-1">
|
|
3934
|
+
<saml2:Issuer>http://localhost:8081</saml2:Issuer>
|
|
3935
|
+
<saml2:Subject>
|
|
3936
|
+
<saml2:NameID Format="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress">legitimate@example.com</saml2:NameID>
|
|
3937
|
+
</saml2:Subject>
|
|
3938
|
+
</saml2:Assertion>
|
|
3939
|
+
<saml2:Assertion ID="assertion-2">
|
|
3940
|
+
<saml2:Issuer>http://localhost:8081</saml2:Issuer>
|
|
3941
|
+
<saml2:Subject>
|
|
3942
|
+
<saml2:NameID Format="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress">attacker@evil.com</saml2:NameID>
|
|
3943
|
+
</saml2:Subject>
|
|
3944
|
+
</saml2:Assertion>
|
|
3945
|
+
</saml2p:Response>
|
|
3946
|
+
`;
|
|
3947
|
+
|
|
3948
|
+
const encodedResponse = Buffer.from(multiAssertionResponse).toString(
|
|
3949
|
+
"base64",
|
|
3950
|
+
);
|
|
3951
|
+
|
|
3952
|
+
const response = await auth.handler(
|
|
3953
|
+
new Request(
|
|
3954
|
+
"http://localhost:3000/api/auth/sso/saml2/sp/acs/multi-assertion-acs-provider",
|
|
3955
|
+
{
|
|
3956
|
+
method: "POST",
|
|
3957
|
+
headers: {
|
|
3958
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
3959
|
+
},
|
|
3960
|
+
body: new URLSearchParams({
|
|
3961
|
+
SAMLResponse: encodedResponse,
|
|
3962
|
+
RelayState: "http://localhost:3000/dashboard",
|
|
3963
|
+
}),
|
|
3964
|
+
},
|
|
3965
|
+
),
|
|
3966
|
+
);
|
|
3967
|
+
|
|
3968
|
+
expect(response.status).toBe(302);
|
|
3969
|
+
const location = response.headers.get("location") || "";
|
|
3970
|
+
expect(location).toContain("error=multiple_assertions");
|
|
3971
|
+
});
|
|
3972
|
+
|
|
3973
|
+
it("should reject SAML response with no assertions", async () => {
|
|
3974
|
+
const { auth, signInWithTestUser } = await getTestInstance({
|
|
3975
|
+
plugins: [sso()],
|
|
3976
|
+
});
|
|
3977
|
+
|
|
3978
|
+
const { headers } = await signInWithTestUser();
|
|
3979
|
+
|
|
3980
|
+
await auth.api.registerSSOProvider({
|
|
3981
|
+
body: {
|
|
3982
|
+
providerId: "no-assertion-provider",
|
|
3983
|
+
issuer: "http://localhost:8081",
|
|
3984
|
+
domain: "http://localhost:8081",
|
|
3985
|
+
samlConfig: {
|
|
3986
|
+
entryPoint: "http://localhost:8081/api/sso/saml2/idp/post",
|
|
3987
|
+
cert: certificate,
|
|
3988
|
+
callbackUrl: "http://localhost:3000/dashboard",
|
|
3989
|
+
wantAssertionsSigned: false,
|
|
3990
|
+
signatureAlgorithm: "sha256",
|
|
3991
|
+
digestAlgorithm: "sha256",
|
|
3992
|
+
idpMetadata: {
|
|
3993
|
+
metadata: idpMetadata,
|
|
3994
|
+
},
|
|
3995
|
+
spMetadata: {
|
|
3996
|
+
metadata: spMetadata,
|
|
3997
|
+
},
|
|
3998
|
+
identifierFormat:
|
|
3999
|
+
"urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress",
|
|
4000
|
+
},
|
|
4001
|
+
},
|
|
4002
|
+
headers,
|
|
4003
|
+
});
|
|
4004
|
+
|
|
4005
|
+
const noAssertionResponse = `
|
|
4006
|
+
<saml2p:Response xmlns:saml2p="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion">
|
|
4007
|
+
<saml2:Issuer>http://localhost:8081</saml2:Issuer>
|
|
4008
|
+
<saml2p:Status>
|
|
4009
|
+
<saml2p:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/>
|
|
4010
|
+
</saml2p:Status>
|
|
4011
|
+
</saml2p:Response>
|
|
4012
|
+
`;
|
|
4013
|
+
|
|
4014
|
+
const encodedResponse = Buffer.from(noAssertionResponse).toString("base64");
|
|
4015
|
+
|
|
4016
|
+
await expect(
|
|
4017
|
+
auth.api.callbackSSOSAML({
|
|
4018
|
+
body: {
|
|
4019
|
+
SAMLResponse: encodedResponse,
|
|
4020
|
+
RelayState: "http://localhost:3000/dashboard",
|
|
4021
|
+
},
|
|
4022
|
+
params: {
|
|
4023
|
+
providerId: "no-assertion-provider",
|
|
4024
|
+
},
|
|
4025
|
+
}),
|
|
4026
|
+
).rejects.toMatchObject({
|
|
4027
|
+
body: {
|
|
4028
|
+
code: "SAML_NO_ASSERTION",
|
|
4029
|
+
},
|
|
4030
|
+
});
|
|
4031
|
+
});
|
|
4032
|
+
|
|
4033
|
+
it("should reject SAML response with XSW-style assertion injection in Extensions", async () => {
|
|
4034
|
+
const { auth, signInWithTestUser } = await getTestInstance({
|
|
4035
|
+
plugins: [sso()],
|
|
4036
|
+
});
|
|
4037
|
+
|
|
4038
|
+
const { headers } = await signInWithTestUser();
|
|
4039
|
+
|
|
4040
|
+
await auth.api.registerSSOProvider({
|
|
4041
|
+
body: {
|
|
4042
|
+
providerId: "xsw-injection-provider",
|
|
4043
|
+
issuer: "http://localhost:8081",
|
|
4044
|
+
domain: "http://localhost:8081",
|
|
4045
|
+
samlConfig: {
|
|
4046
|
+
entryPoint: "http://localhost:8081/api/sso/saml2/idp/post",
|
|
4047
|
+
cert: certificate,
|
|
4048
|
+
callbackUrl: "http://localhost:3000/dashboard",
|
|
4049
|
+
wantAssertionsSigned: false,
|
|
4050
|
+
signatureAlgorithm: "sha256",
|
|
4051
|
+
digestAlgorithm: "sha256",
|
|
4052
|
+
idpMetadata: {
|
|
4053
|
+
metadata: idpMetadata,
|
|
4054
|
+
},
|
|
4055
|
+
spMetadata: {
|
|
4056
|
+
metadata: spMetadata,
|
|
4057
|
+
},
|
|
4058
|
+
identifierFormat:
|
|
4059
|
+
"urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress",
|
|
4060
|
+
},
|
|
4061
|
+
},
|
|
4062
|
+
headers,
|
|
4063
|
+
});
|
|
4064
|
+
|
|
4065
|
+
const xswInjectionResponse = `
|
|
4066
|
+
<saml2p:Response xmlns:saml2p="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion">
|
|
4067
|
+
<saml2:Issuer>http://localhost:8081</saml2:Issuer>
|
|
4068
|
+
<saml2p:Status>
|
|
4069
|
+
<saml2p:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/>
|
|
4070
|
+
</saml2p:Status>
|
|
4071
|
+
<saml2p:Extensions>
|
|
4072
|
+
<saml2:Assertion ID="injected-assertion">
|
|
4073
|
+
<saml2:Issuer>http://localhost:8081</saml2:Issuer>
|
|
4074
|
+
<saml2:Subject>
|
|
4075
|
+
<saml2:NameID Format="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress">attacker@evil.com</saml2:NameID>
|
|
4076
|
+
</saml2:Subject>
|
|
4077
|
+
</saml2:Assertion>
|
|
4078
|
+
</saml2p:Extensions>
|
|
4079
|
+
<saml2:Assertion ID="legitimate-assertion">
|
|
4080
|
+
<saml2:Issuer>http://localhost:8081</saml2:Issuer>
|
|
4081
|
+
<saml2:Subject>
|
|
4082
|
+
<saml2:NameID Format="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress">user@example.com</saml2:NameID>
|
|
4083
|
+
</saml2:Subject>
|
|
4084
|
+
</saml2:Assertion>
|
|
4085
|
+
</saml2p:Response>
|
|
4086
|
+
`;
|
|
4087
|
+
|
|
4088
|
+
const encodedResponse =
|
|
4089
|
+
Buffer.from(xswInjectionResponse).toString("base64");
|
|
4090
|
+
|
|
4091
|
+
await expect(
|
|
4092
|
+
auth.api.callbackSSOSAML({
|
|
4093
|
+
body: {
|
|
4094
|
+
SAMLResponse: encodedResponse,
|
|
4095
|
+
RelayState: "http://localhost:3000/dashboard",
|
|
4096
|
+
},
|
|
4097
|
+
params: {
|
|
4098
|
+
providerId: "xsw-injection-provider",
|
|
4099
|
+
},
|
|
4100
|
+
}),
|
|
4101
|
+
).rejects.toMatchObject({
|
|
4102
|
+
body: {
|
|
4103
|
+
code: "SAML_MULTIPLE_ASSERTIONS",
|
|
4104
|
+
},
|
|
4105
|
+
});
|
|
4106
|
+
});
|
|
4107
|
+
|
|
4108
|
+
it("should accept valid SAML response with exactly one assertion", async () => {
|
|
4109
|
+
const { auth, signInWithTestUser } = await getTestInstance({
|
|
4110
|
+
plugins: [sso()],
|
|
4111
|
+
});
|
|
4112
|
+
|
|
4113
|
+
const { headers } = await signInWithTestUser();
|
|
4114
|
+
|
|
4115
|
+
await auth.api.registerSSOProvider({
|
|
4116
|
+
body: {
|
|
4117
|
+
providerId: "single-assertion-provider",
|
|
4118
|
+
issuer: "http://localhost:8081",
|
|
4119
|
+
domain: "http://localhost:8081",
|
|
4120
|
+
samlConfig: {
|
|
4121
|
+
entryPoint: "http://localhost:8081/api/sso/saml2/idp/post",
|
|
4122
|
+
cert: certificate,
|
|
4123
|
+
callbackUrl: "http://localhost:3000/dashboard",
|
|
4124
|
+
wantAssertionsSigned: false,
|
|
4125
|
+
signatureAlgorithm: "sha256",
|
|
4126
|
+
digestAlgorithm: "sha256",
|
|
4127
|
+
idpMetadata: {
|
|
4128
|
+
metadata: idpMetadata,
|
|
4129
|
+
},
|
|
4130
|
+
spMetadata: {
|
|
4131
|
+
metadata: spMetadata,
|
|
4132
|
+
},
|
|
4133
|
+
identifierFormat:
|
|
4134
|
+
"urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress",
|
|
4135
|
+
},
|
|
4136
|
+
},
|
|
4137
|
+
headers,
|
|
4138
|
+
});
|
|
4139
|
+
|
|
4140
|
+
let samlResponse: any;
|
|
4141
|
+
await betterFetch("http://localhost:8081/api/sso/saml2/idp/post", {
|
|
4142
|
+
onSuccess: async (context) => {
|
|
4143
|
+
samlResponse = await context.data;
|
|
4144
|
+
},
|
|
4145
|
+
});
|
|
4146
|
+
|
|
4147
|
+
const response = await auth.handler(
|
|
4148
|
+
new Request(
|
|
4149
|
+
"http://localhost:3000/api/auth/sso/saml2/callback/single-assertion-provider",
|
|
4150
|
+
{
|
|
4151
|
+
method: "POST",
|
|
4152
|
+
headers: {
|
|
4153
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
4154
|
+
},
|
|
4155
|
+
body: new URLSearchParams({
|
|
4156
|
+
SAMLResponse: samlResponse.samlResponse,
|
|
4157
|
+
RelayState: "http://localhost:3000/dashboard",
|
|
4158
|
+
}),
|
|
4159
|
+
},
|
|
4160
|
+
),
|
|
4161
|
+
);
|
|
4162
|
+
|
|
4163
|
+
expect(response.status).toBe(302);
|
|
4164
|
+
expect(response.headers.get("location")).not.toContain("error");
|
|
4165
|
+
});
|
|
4166
|
+
|
|
4167
|
+
it("should normalize email to lowercase in SAML authentication to prevent duplicate creation", async () => {
|
|
4168
|
+
const { auth, client, signInWithTestUser, db } = await getTestInstance({
|
|
4169
|
+
plugins: [sso()],
|
|
4170
|
+
});
|
|
4171
|
+
|
|
4172
|
+
const { headers } = await signInWithTestUser();
|
|
4173
|
+
|
|
4174
|
+
await auth.api.registerSSOProvider({
|
|
4175
|
+
body: {
|
|
4176
|
+
providerId: "email-case-provider",
|
|
4177
|
+
issuer: "http://localhost:8081",
|
|
4178
|
+
domain: "example.com",
|
|
4179
|
+
samlConfig: {
|
|
4180
|
+
entryPoint: "http://localhost:8081/api/sso/saml2/idp/post",
|
|
4181
|
+
cert: certificate,
|
|
4182
|
+
callbackUrl: "http://localhost:3000/dashboard",
|
|
4183
|
+
wantAssertionsSigned: false,
|
|
4184
|
+
signatureAlgorithm: "sha256",
|
|
4185
|
+
digestAlgorithm: "sha256",
|
|
4186
|
+
idpMetadata: {
|
|
4187
|
+
metadata: idpMetadata,
|
|
4188
|
+
},
|
|
4189
|
+
spMetadata: {
|
|
4190
|
+
metadata: spMetadata,
|
|
4191
|
+
},
|
|
4192
|
+
identifierFormat:
|
|
4193
|
+
"urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress",
|
|
4194
|
+
mapping: {
|
|
4195
|
+
id: "nameID",
|
|
4196
|
+
email: "nameID",
|
|
4197
|
+
name: "displayName",
|
|
4198
|
+
},
|
|
4199
|
+
},
|
|
4200
|
+
},
|
|
4201
|
+
headers,
|
|
4202
|
+
});
|
|
4203
|
+
|
|
4204
|
+
let samlResponse1: { samlResponse: string } | undefined;
|
|
4205
|
+
await betterFetch(
|
|
4206
|
+
"http://localhost:8081/api/sso/saml2/idp/post?emailCase=mixed",
|
|
4207
|
+
{
|
|
4208
|
+
onSuccess: async (context) => {
|
|
4209
|
+
samlResponse1 = context.data as { samlResponse: string };
|
|
4210
|
+
},
|
|
4211
|
+
},
|
|
4212
|
+
);
|
|
4213
|
+
|
|
4214
|
+
expect(samlResponse1?.samlResponse).toBeDefined();
|
|
4215
|
+
|
|
4216
|
+
const firstCallbackResponse = await auth.handler(
|
|
4217
|
+
new Request(
|
|
4218
|
+
"http://localhost:3000/api/auth/sso/saml2/callback/email-case-provider",
|
|
4219
|
+
{
|
|
4220
|
+
method: "POST",
|
|
4221
|
+
headers: {
|
|
4222
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
4223
|
+
},
|
|
4224
|
+
body: new URLSearchParams({
|
|
4225
|
+
SAMLResponse: samlResponse1!.samlResponse,
|
|
4226
|
+
RelayState: "http://localhost:3000/dashboard",
|
|
4227
|
+
}),
|
|
4228
|
+
},
|
|
4229
|
+
),
|
|
4230
|
+
);
|
|
4231
|
+
|
|
4232
|
+
expect(firstCallbackResponse.status).toBe(302);
|
|
4233
|
+
expect(firstCallbackResponse.headers.get("location")).toContain(
|
|
4234
|
+
"dashboard",
|
|
4235
|
+
);
|
|
4236
|
+
expect(firstCallbackResponse.headers.get("location")).not.toContain(
|
|
4237
|
+
"error",
|
|
4238
|
+
);
|
|
4239
|
+
|
|
4240
|
+
const firstCookies = parseSetCookieHeader(
|
|
4241
|
+
firstCallbackResponse.headers.get("set-cookie") ?? "",
|
|
4242
|
+
);
|
|
4243
|
+
const firstSessionToken = firstCookies.get(
|
|
4244
|
+
"better-auth.session_token",
|
|
4245
|
+
)?.value;
|
|
4246
|
+
expect(firstSessionToken).toBeDefined();
|
|
4247
|
+
|
|
4248
|
+
const firstSession = await client.getSession({
|
|
4249
|
+
fetchOptions: {
|
|
4250
|
+
headers: {
|
|
4251
|
+
Cookie: `better-auth.session_token=${firstSessionToken}`,
|
|
4252
|
+
},
|
|
4253
|
+
},
|
|
4254
|
+
});
|
|
4255
|
+
|
|
4256
|
+
expect(firstSession.data?.user.email).toBe("testuser@example.com");
|
|
4257
|
+
const firstUserId = firstSession.data?.user.id;
|
|
4258
|
+
expect(firstUserId).toBeDefined();
|
|
4259
|
+
|
|
4260
|
+
let samlResponse2: { samlResponse: string } | undefined;
|
|
4261
|
+
await betterFetch(
|
|
4262
|
+
"http://localhost:8081/api/sso/saml2/idp/post?emailCase=mixed",
|
|
4263
|
+
{
|
|
4264
|
+
onSuccess: async (context) => {
|
|
4265
|
+
samlResponse2 = context.data as { samlResponse: string };
|
|
4266
|
+
},
|
|
4267
|
+
},
|
|
4268
|
+
);
|
|
4269
|
+
|
|
4270
|
+
const secondCallbackResponse = await auth.handler(
|
|
4271
|
+
new Request(
|
|
4272
|
+
"http://localhost:3000/api/auth/sso/saml2/callback/email-case-provider",
|
|
4273
|
+
{
|
|
4274
|
+
method: "POST",
|
|
4275
|
+
headers: {
|
|
4276
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
4277
|
+
},
|
|
4278
|
+
body: new URLSearchParams({
|
|
4279
|
+
SAMLResponse: samlResponse2!.samlResponse,
|
|
4280
|
+
RelayState: "http://localhost:3000/dashboard",
|
|
4281
|
+
}),
|
|
4282
|
+
},
|
|
4283
|
+
),
|
|
4284
|
+
);
|
|
4285
|
+
|
|
4286
|
+
expect(secondCallbackResponse.status).toBe(302);
|
|
4287
|
+
expect(secondCallbackResponse.headers.get("location")).toContain(
|
|
4288
|
+
"dashboard",
|
|
4289
|
+
);
|
|
4290
|
+
expect(secondCallbackResponse.headers.get("location")).not.toContain(
|
|
4291
|
+
"error",
|
|
4292
|
+
);
|
|
4293
|
+
|
|
4294
|
+
const secondCookies = parseSetCookieHeader(
|
|
4295
|
+
secondCallbackResponse.headers.get("set-cookie") ?? "",
|
|
4296
|
+
);
|
|
4297
|
+
const secondSessionToken = secondCookies.get(
|
|
4298
|
+
"better-auth.session_token",
|
|
4299
|
+
)?.value;
|
|
4300
|
+
expect(secondSessionToken).toBeDefined();
|
|
4301
|
+
|
|
4302
|
+
const secondSession = await client.getSession({
|
|
4303
|
+
fetchOptions: {
|
|
4304
|
+
headers: {
|
|
4305
|
+
Cookie: `better-auth.session_token=${secondSessionToken}`,
|
|
4306
|
+
},
|
|
4307
|
+
},
|
|
4308
|
+
});
|
|
4309
|
+
|
|
4310
|
+
expect(secondSession.data?.user.id).toBe(firstUserId);
|
|
4311
|
+
expect(secondSession.data?.user.email).toBe("testuser@example.com");
|
|
4312
|
+
|
|
4313
|
+
const users = (await db.findMany({ model: "user" })) as {
|
|
4314
|
+
email: string;
|
|
4315
|
+
}[];
|
|
4316
|
+
const samlUsers = users.filter((u) => u.email === "testuser@example.com");
|
|
4317
|
+
expect(samlUsers).toHaveLength(1);
|
|
4318
|
+
});
|
|
4319
|
+
});
|