@better-auth/sso 1.7.0-beta.0 → 1.7.0-beta.2
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/dist/client.d.mts
CHANGED
package/dist/client.mjs
CHANGED
|
@@ -81,16 +81,29 @@ interface OIDCConfig {
|
|
|
81
81
|
mapping?: OIDCMapping | undefined;
|
|
82
82
|
}
|
|
83
83
|
interface SAMLConfig {
|
|
84
|
+
/**
|
|
85
|
+
* SP Entity ID. Used as the `entityID` in SP metadata when
|
|
86
|
+
* `spMetadata.entityID` is not set. Also used as the expected
|
|
87
|
+
* audience for SAML assertion validation when `audience` is not set.
|
|
88
|
+
*/
|
|
84
89
|
issuer: string;
|
|
90
|
+
/**
|
|
91
|
+
* IdP SSO URL. Used as the redirect destination when
|
|
92
|
+
* `idpMetadata.metadata` is not provided. Ignored when
|
|
93
|
+
* IdP metadata XML is set (the SSO URL is extracted from the XML).
|
|
94
|
+
*/
|
|
85
95
|
entryPoint: string;
|
|
96
|
+
/**
|
|
97
|
+
* IdP signing certificate. Used to verify SAML response signatures
|
|
98
|
+
* when `idpMetadata.metadata` is not provided. Ignored when IdP
|
|
99
|
+
* metadata XML is set (the certificate is extracted from the XML).
|
|
100
|
+
* When both this and `idpMetadata.cert` are set, `idpMetadata.cert` takes precedence.
|
|
101
|
+
*/
|
|
86
102
|
cert: string;
|
|
87
|
-
callbackUrl: string;
|
|
88
103
|
audience?: string | undefined;
|
|
89
104
|
idpMetadata?: {
|
|
90
105
|
metadata?: string;
|
|
91
106
|
entityID?: string;
|
|
92
|
-
entityURL?: string;
|
|
93
|
-
redirectURL?: string;
|
|
94
107
|
cert?: string;
|
|
95
108
|
privateKey?: string;
|
|
96
109
|
privateKeyPass?: string;
|
|
@@ -106,7 +119,12 @@ interface SAMLConfig {
|
|
|
106
119
|
Location: string;
|
|
107
120
|
}>;
|
|
108
121
|
} | undefined;
|
|
109
|
-
|
|
122
|
+
/**
|
|
123
|
+
* SP metadata configuration. All fields are optional; when omitted,
|
|
124
|
+
* SP metadata is auto-generated from `issuer`, `wantAssertionsSigned`,
|
|
125
|
+
* `authnRequestsSigned`, and `identifierFormat`.
|
|
126
|
+
*/
|
|
127
|
+
spMetadata?: {
|
|
110
128
|
metadata?: string | undefined;
|
|
111
129
|
entityID?: string | undefined;
|
|
112
130
|
binding?: string | undefined;
|
|
@@ -116,14 +134,17 @@ interface SAMLConfig {
|
|
|
116
134
|
encPrivateKey?: string | undefined;
|
|
117
135
|
encPrivateKeyPass?: string | undefined;
|
|
118
136
|
};
|
|
137
|
+
/**
|
|
138
|
+
* Request signed assertions from the IdP. When true, the SP metadata
|
|
139
|
+
* advertises `WantAssertionsSigned="true"` and samlify will reject
|
|
140
|
+
* unsigned assertions.
|
|
141
|
+
*/
|
|
119
142
|
wantAssertionsSigned?: boolean | undefined;
|
|
120
143
|
authnRequestsSigned?: boolean | undefined;
|
|
121
144
|
signatureAlgorithm?: string | undefined;
|
|
122
145
|
digestAlgorithm?: string | undefined;
|
|
123
146
|
identifierFormat?: string | undefined;
|
|
124
147
|
privateKey?: string | undefined;
|
|
125
|
-
decryptionPvk?: string | undefined;
|
|
126
|
-
additionalParams?: Record<string, any> | undefined;
|
|
127
148
|
mapping?: SAMLMapping | undefined;
|
|
128
149
|
}
|
|
129
150
|
type BaseSSOProvider = {
|
|
@@ -614,7 +635,6 @@ declare const listSSOProviders: () => better_call0.StrictEndpoint<"/sso/provider
|
|
|
614
635
|
} | undefined;
|
|
615
636
|
samlConfig: {
|
|
616
637
|
entryPoint: string;
|
|
617
|
-
callbackUrl: string;
|
|
618
638
|
audience: string | undefined;
|
|
619
639
|
wantAssertionsSigned: boolean | undefined;
|
|
620
640
|
authnRequestsSigned: boolean | undefined;
|
|
@@ -699,7 +719,6 @@ declare const getSSOProvider: () => better_call0.StrictEndpoint<"/sso/get-provid
|
|
|
699
719
|
} | undefined;
|
|
700
720
|
samlConfig: {
|
|
701
721
|
entryPoint: string;
|
|
702
|
-
callbackUrl: string;
|
|
703
722
|
audience: string | undefined;
|
|
704
723
|
wantAssertionsSigned: boolean | undefined;
|
|
705
724
|
authnRequestsSigned: boolean | undefined;
|
|
@@ -775,7 +794,6 @@ declare const updateSSOProvider: (options: SSOOptions) => better_call0.StrictEnd
|
|
|
775
794
|
samlConfig: z.ZodOptional<z.ZodObject<{
|
|
776
795
|
entryPoint: z.ZodOptional<z.ZodString>;
|
|
777
796
|
cert: z.ZodOptional<z.ZodString>;
|
|
778
|
-
callbackUrl: z.ZodOptional<z.ZodString>;
|
|
779
797
|
audience: z.ZodOptional<z.ZodString>;
|
|
780
798
|
idpMetadata: z.ZodOptional<z.ZodObject<{
|
|
781
799
|
metadata: z.ZodOptional<z.ZodString>;
|
|
@@ -807,8 +825,6 @@ declare const updateSSOProvider: (options: SSOOptions) => better_call0.StrictEnd
|
|
|
807
825
|
digestAlgorithm: z.ZodOptional<z.ZodString>;
|
|
808
826
|
identifierFormat: z.ZodOptional<z.ZodString>;
|
|
809
827
|
privateKey: z.ZodOptional<z.ZodString>;
|
|
810
|
-
decryptionPvk: z.ZodOptional<z.ZodString>;
|
|
811
|
-
additionalParams: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodAny>>;
|
|
812
828
|
mapping: z.ZodOptional<z.ZodObject<{
|
|
813
829
|
id: z.ZodOptional<z.ZodString>;
|
|
814
830
|
email: z.ZodOptional<z.ZodString>;
|
|
@@ -859,7 +875,6 @@ declare const updateSSOProvider: (options: SSOOptions) => better_call0.StrictEnd
|
|
|
859
875
|
} | undefined;
|
|
860
876
|
samlConfig: {
|
|
861
877
|
entryPoint: string;
|
|
862
|
-
callbackUrl: string;
|
|
863
878
|
audience: string | undefined;
|
|
864
879
|
wantAssertionsSigned: boolean | undefined;
|
|
865
880
|
authnRequestsSigned: boolean | undefined;
|
|
@@ -932,10 +947,6 @@ declare const spMetadata: (options?: SSOOptions) => better_call0.StrictEndpoint<
|
|
|
932
947
|
method: "GET";
|
|
933
948
|
query: z.ZodObject<{
|
|
934
949
|
providerId: z.ZodString;
|
|
935
|
-
format: z.ZodDefault<z.ZodEnum<{
|
|
936
|
-
json: "json";
|
|
937
|
-
xml: "xml";
|
|
938
|
-
}>>;
|
|
939
950
|
}, z.core.$strip>;
|
|
940
951
|
metadata: {
|
|
941
952
|
openapi: {
|
|
@@ -986,7 +997,6 @@ declare const registerSSOProvider: <O extends SSOOptions>(options: O) => better_
|
|
|
986
997
|
samlConfig: z.ZodOptional<z.ZodObject<{
|
|
987
998
|
entryPoint: z.ZodString;
|
|
988
999
|
cert: z.ZodString;
|
|
989
|
-
callbackUrl: z.ZodString;
|
|
990
1000
|
audience: z.ZodOptional<z.ZodString>;
|
|
991
1001
|
idpMetadata: z.ZodOptional<z.ZodObject<{
|
|
992
1002
|
metadata: z.ZodOptional<z.ZodString>;
|
|
@@ -1002,7 +1012,7 @@ declare const registerSSOProvider: <O extends SSOOptions>(options: O) => better_
|
|
|
1002
1012
|
Location: z.ZodString;
|
|
1003
1013
|
}, z.core.$strip>>>;
|
|
1004
1014
|
}, z.core.$strip>>;
|
|
1005
|
-
spMetadata: z.ZodObject<{
|
|
1015
|
+
spMetadata: z.ZodOptional<z.ZodObject<{
|
|
1006
1016
|
metadata: z.ZodOptional<z.ZodString>;
|
|
1007
1017
|
entityID: z.ZodOptional<z.ZodString>;
|
|
1008
1018
|
binding: z.ZodOptional<z.ZodString>;
|
|
@@ -1011,15 +1021,13 @@ declare const registerSSOProvider: <O extends SSOOptions>(options: O) => better_
|
|
|
1011
1021
|
isAssertionEncrypted: z.ZodOptional<z.ZodBoolean>;
|
|
1012
1022
|
encPrivateKey: z.ZodOptional<z.ZodString>;
|
|
1013
1023
|
encPrivateKeyPass: z.ZodOptional<z.ZodString>;
|
|
1014
|
-
}, z.core.$strip
|
|
1024
|
+
}, z.core.$strip>>;
|
|
1015
1025
|
wantAssertionsSigned: z.ZodOptional<z.ZodBoolean>;
|
|
1016
1026
|
authnRequestsSigned: z.ZodOptional<z.ZodBoolean>;
|
|
1017
1027
|
signatureAlgorithm: z.ZodOptional<z.ZodString>;
|
|
1018
1028
|
digestAlgorithm: z.ZodOptional<z.ZodString>;
|
|
1019
1029
|
identifierFormat: z.ZodOptional<z.ZodString>;
|
|
1020
1030
|
privateKey: z.ZodOptional<z.ZodString>;
|
|
1021
|
-
decryptionPvk: z.ZodOptional<z.ZodString>;
|
|
1022
|
-
additionalParams: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodAny>>;
|
|
1023
1031
|
mapping: z.ZodOptional<z.ZodObject<{
|
|
1024
1032
|
id: z.ZodString;
|
|
1025
1033
|
email: z.ZodString;
|
|
@@ -1376,7 +1384,7 @@ declare const callbackSSOShared: (options?: SSOOptions) => better_call0.StrictEn
|
|
|
1376
1384
|
}, z.core.$strip>;
|
|
1377
1385
|
allowedMediaTypes: readonly ["application/x-www-form-urlencoded", "application/json"];
|
|
1378
1386
|
}, void>;
|
|
1379
|
-
declare const
|
|
1387
|
+
declare const acsEndpoint: (options?: SSOOptions) => better_call0.StrictEndpoint<"/sso/saml2/sp/acs/:providerId", {
|
|
1380
1388
|
method: ("POST" | "GET")[];
|
|
1381
1389
|
body: z.ZodOptional<z.ZodObject<{
|
|
1382
1390
|
SAMLResponse: z.ZodString;
|
|
@@ -1398,28 +1406,7 @@ declare const callbackSSOSAML: (options?: SSOOptions) => better_call0.StrictEndp
|
|
|
1398
1406
|
"400": {
|
|
1399
1407
|
description: string;
|
|
1400
1408
|
};
|
|
1401
|
-
"
|
|
1402
|
-
description: string;
|
|
1403
|
-
};
|
|
1404
|
-
};
|
|
1405
|
-
};
|
|
1406
|
-
scope: "server";
|
|
1407
|
-
};
|
|
1408
|
-
}, never>;
|
|
1409
|
-
declare const acsEndpoint: (options?: SSOOptions) => better_call0.StrictEndpoint<"/sso/saml2/sp/acs/:providerId", {
|
|
1410
|
-
method: "POST";
|
|
1411
|
-
body: z.ZodObject<{
|
|
1412
|
-
SAMLResponse: z.ZodString;
|
|
1413
|
-
RelayState: z.ZodOptional<z.ZodString>;
|
|
1414
|
-
}, z.core.$strip>;
|
|
1415
|
-
metadata: {
|
|
1416
|
-
allowedMediaTypes: string[];
|
|
1417
|
-
openapi: {
|
|
1418
|
-
operationId: string;
|
|
1419
|
-
summary: string;
|
|
1420
|
-
description: string;
|
|
1421
|
-
responses: {
|
|
1422
|
-
"302": {
|
|
1409
|
+
"404": {
|
|
1423
1410
|
description: string;
|
|
1424
1411
|
};
|
|
1425
1412
|
};
|
|
@@ -1788,7 +1775,6 @@ type SSOEndpoints<O extends SSOOptions> = {
|
|
|
1788
1775
|
signInSSO: ReturnType<typeof signInSSO>;
|
|
1789
1776
|
callbackSSO: ReturnType<typeof callbackSSO>;
|
|
1790
1777
|
callbackSSOShared: ReturnType<typeof callbackSSOShared>;
|
|
1791
|
-
callbackSSOSAML: ReturnType<typeof callbackSSOSAML>;
|
|
1792
1778
|
acsEndpoint: ReturnType<typeof acsEndpoint>;
|
|
1793
1779
|
sloEndpoint: ReturnType<typeof sloEndpoint>;
|
|
1794
1780
|
initiateSLO: ReturnType<typeof initiateSLO>;
|
package/dist/index.d.mts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { A as DataEncryptionAlgorithm, C as DEFAULT_MAX_SAML_METADATA_SIZE, D as SSOOptions, E as SAMLConfig, M as DigestAlgorithm, N as KeyEncryptionAlgorithm, O as SSOProvider, P as SignatureAlgorithm, S as DEFAULT_CLOCK_SKEW_MS, T as OIDCConfig, _ as REQUIRED_DISCOVERY_FIELDS, a as fetchDiscoveryDocument, b as TimestampValidationOptions, c as normalizeUrl, d as validateDiscoveryUrl, f as DiscoverOIDCConfigParams, g as OIDCDiscoveryDocument, h as HydratedOIDCConfig, i as discoverOIDCConfig, j as DeprecatedAlgorithmBehavior, k as AlgorithmValidationOptions, l as selectTokenEndpointAuthMethod, m as DiscoveryErrorCode, n as sso, o as needsRuntimeDiscovery, p as DiscoveryError, r as computeDiscoveryUrl, s as normalizeDiscoveryUrls, t as SSOPlugin, u as validateDiscoveryDocument, v as RequiredDiscoveryField, w as DEFAULT_MAX_SAML_RESPONSE_SIZE, x as validateSAMLTimestamp, y as SAMLConditions } from "./index-
|
|
1
|
+
import { A as DataEncryptionAlgorithm, C as DEFAULT_MAX_SAML_METADATA_SIZE, D as SSOOptions, E as SAMLConfig, M as DigestAlgorithm, N as KeyEncryptionAlgorithm, O as SSOProvider, P as SignatureAlgorithm, S as DEFAULT_CLOCK_SKEW_MS, T as OIDCConfig, _ as REQUIRED_DISCOVERY_FIELDS, a as fetchDiscoveryDocument, b as TimestampValidationOptions, c as normalizeUrl, d as validateDiscoveryUrl, f as DiscoverOIDCConfigParams, g as OIDCDiscoveryDocument, h as HydratedOIDCConfig, i as discoverOIDCConfig, j as DeprecatedAlgorithmBehavior, k as AlgorithmValidationOptions, l as selectTokenEndpointAuthMethod, m as DiscoveryErrorCode, n as sso, o as needsRuntimeDiscovery, p as DiscoveryError, r as computeDiscoveryUrl, s as normalizeDiscoveryUrls, t as SSOPlugin, u as validateDiscoveryDocument, v as RequiredDiscoveryField, w as DEFAULT_MAX_SAML_RESPONSE_SIZE, x as validateSAMLTimestamp, y as SAMLConditions } from "./index-CagV4mMx.mjs";
|
|
2
2
|
export { AlgorithmValidationOptions, DEFAULT_CLOCK_SKEW_MS, DEFAULT_MAX_SAML_METADATA_SIZE, DEFAULT_MAX_SAML_RESPONSE_SIZE, DataEncryptionAlgorithm, DeprecatedAlgorithmBehavior, DigestAlgorithm, DiscoverOIDCConfigParams, DiscoveryError, DiscoveryErrorCode, HydratedOIDCConfig, KeyEncryptionAlgorithm, OIDCConfig, OIDCDiscoveryDocument, REQUIRED_DISCOVERY_FIELDS, RequiredDiscoveryField, SAMLConditions, SAMLConfig, SSOOptions, SSOPlugin, SSOProvider, SignatureAlgorithm, TimestampValidationOptions, computeDiscoveryUrl, discoverOIDCConfig, fetchDiscoveryDocument, needsRuntimeDiscovery, normalizeDiscoveryUrls, normalizeUrl, selectTokenEndpointAuthMethod, sso, validateDiscoveryDocument, validateDiscoveryUrl, validateSAMLTimestamp };
|
package/dist/index.mjs
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import { t as PACKAGE_VERSION } from "./version-
|
|
1
|
+
import { t as PACKAGE_VERSION } from "./version-BVHIqaH7.mjs";
|
|
2
2
|
import { APIError, createAuthEndpoint, createAuthMiddleware, getSessionFromCtx, sessionMiddleware } from "better-auth/api";
|
|
3
3
|
import { XMLParser, XMLValidator } from "fast-xml-parser";
|
|
4
|
-
import * as saml from "samlify";
|
|
5
4
|
import { X509Certificate } from "node:crypto";
|
|
6
5
|
import { getHostname } from "tldts";
|
|
7
6
|
import { generateRandomString } from "better-auth/crypto";
|
|
@@ -13,6 +12,8 @@ import { deleteSessionCookie, setSessionCookie } from "better-auth/cookies";
|
|
|
13
12
|
import { handleOAuthUserInfo } from "better-auth/oauth2";
|
|
14
13
|
import { decodeJwt } from "jose";
|
|
15
14
|
import { defineErrorCodes } from "@better-auth/core/utils/error-codes";
|
|
15
|
+
import * as samlifyNamespace from "samlify";
|
|
16
|
+
import samlifyDefault from "samlify";
|
|
16
17
|
//#region src/constants.ts
|
|
17
18
|
/**
|
|
18
19
|
* SAML Constants
|
|
@@ -693,7 +694,10 @@ async function validateInResponseTo(c, ctx) {
|
|
|
693
694
|
* could be accepted.
|
|
694
695
|
*/
|
|
695
696
|
function validateAudience(c, ctx) {
|
|
696
|
-
if (!ctx.expectedAudience)
|
|
697
|
+
if (!ctx.expectedAudience) {
|
|
698
|
+
c.context.logger.warn("Could not determine SP entity ID for audience validation; skipping", { providerId: ctx.providerId });
|
|
699
|
+
return;
|
|
700
|
+
}
|
|
697
701
|
const audience = ctx.extract.audience;
|
|
698
702
|
if (!audience) {
|
|
699
703
|
c.context.logger.error("SAML assertion missing AudienceRestriction but audience is configured — rejecting", { providerId: ctx.providerId });
|
|
@@ -751,7 +755,6 @@ const oidcConfigSchema = z.object({
|
|
|
751
755
|
const samlConfigSchema = z.object({
|
|
752
756
|
entryPoint: z.string().url().optional(),
|
|
753
757
|
cert: z.string().optional(),
|
|
754
|
-
callbackUrl: z.string().url().optional(),
|
|
755
758
|
audience: z.string().optional(),
|
|
756
759
|
idpMetadata: z.object({
|
|
757
760
|
metadata: z.string().optional(),
|
|
@@ -783,8 +786,6 @@ const samlConfigSchema = z.object({
|
|
|
783
786
|
digestAlgorithm: z.string().optional(),
|
|
784
787
|
identifierFormat: z.string().optional(),
|
|
785
788
|
privateKey: z.string().optional(),
|
|
786
|
-
decryptionPvk: z.string().optional(),
|
|
787
|
-
additionalParams: z.record(z.string(), z.any()).optional(),
|
|
788
789
|
mapping: samlMappingSchema
|
|
789
790
|
});
|
|
790
791
|
const updateSSOProviderBodySchema = z.object({
|
|
@@ -861,7 +862,6 @@ function sanitizeProvider(provider, baseURL) {
|
|
|
861
862
|
} : void 0,
|
|
862
863
|
samlConfig: samlConfig ? {
|
|
863
864
|
entryPoint: samlConfig.entryPoint,
|
|
864
|
-
callbackUrl: samlConfig.callbackUrl,
|
|
865
865
|
audience: samlConfig.audience,
|
|
866
866
|
wantAssertionsSigned: samlConfig.wantAssertionsSigned,
|
|
867
867
|
authnRequestsSigned: samlConfig.authnRequestsSigned,
|
|
@@ -964,7 +964,6 @@ function mergeSAMLConfig(current, updates, issuer) {
|
|
|
964
964
|
issuer,
|
|
965
965
|
entryPoint: updates.entryPoint ?? current.entryPoint,
|
|
966
966
|
cert: updates.cert ?? current.cert,
|
|
967
|
-
callbackUrl: updates.callbackUrl ?? current.callbackUrl,
|
|
968
967
|
spMetadata: updates.spMetadata ?? current.spMetadata,
|
|
969
968
|
idpMetadata: updates.idpMetadata ?? current.idpMetadata,
|
|
970
969
|
mapping: updates.mapping ?? current.mapping,
|
|
@@ -1508,8 +1507,19 @@ async function parseRelayState(c) {
|
|
|
1508
1507
|
if (!parsedData.errorURL) parsedData.errorURL = errorURL;
|
|
1509
1508
|
return parsedData;
|
|
1510
1509
|
}
|
|
1510
|
+
const saml = typeof samlifyNamespace.SPMetadata === "function" && typeof samlifyNamespace.setSchemaValidator === "function" ? samlifyNamespace : samlifyDefault ?? samlifyNamespace;
|
|
1511
1511
|
//#endregion
|
|
1512
1512
|
//#region src/routes/helpers.ts
|
|
1513
|
+
/**
|
|
1514
|
+
* Normalizes a PEM string by trimming leading/trailing whitespace from each
|
|
1515
|
+
* line. Native `crypto.createPrivateKey` (used by samlify 2.12+) rejects PEM
|
|
1516
|
+
* blocks with leading whitespace, which is common when keys are stored in
|
|
1517
|
+
* indented config files, environment variables, or JSON.
|
|
1518
|
+
*/
|
|
1519
|
+
function normalizePem(pem) {
|
|
1520
|
+
if (!pem) return pem;
|
|
1521
|
+
return pem.split("\n").map((line) => line.trim()).join("\n");
|
|
1522
|
+
}
|
|
1513
1523
|
async function findSAMLProvider(providerId, options, adapter) {
|
|
1514
1524
|
if (options?.defaultSSO?.length) {
|
|
1515
1525
|
const match = options.defaultSSO.find((p) => p.providerId === providerId);
|
|
@@ -1536,30 +1546,35 @@ async function findSAMLProvider(providerId, options, adapter) {
|
|
|
1536
1546
|
function createSP(config, baseURL, providerId, opts) {
|
|
1537
1547
|
const spData = config.spMetadata;
|
|
1538
1548
|
const sloLocation = `${baseURL}/sso/saml2/sp/slo/${providerId}`;
|
|
1539
|
-
const acsUrl =
|
|
1540
|
-
|
|
1549
|
+
const acsUrl = `${baseURL}/sso/saml2/sp/acs/${providerId}`;
|
|
1550
|
+
let metadata = spData?.metadata;
|
|
1551
|
+
if (!metadata) metadata = saml.SPMetadata({
|
|
1541
1552
|
entityID: spData?.entityID || config.issuer,
|
|
1542
|
-
assertionConsumerService:
|
|
1553
|
+
assertionConsumerService: [{
|
|
1543
1554
|
Binding: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST",
|
|
1544
1555
|
Location: acsUrl
|
|
1545
1556
|
}],
|
|
1546
|
-
singleLogoutService: [{
|
|
1557
|
+
singleLogoutService: opts?.sloOptions ? [{
|
|
1547
1558
|
Binding: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST",
|
|
1548
1559
|
Location: sloLocation
|
|
1549
1560
|
}, {
|
|
1550
1561
|
Binding: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect",
|
|
1551
1562
|
Location: sloLocation
|
|
1552
|
-
}],
|
|
1563
|
+
}] : void 0,
|
|
1553
1564
|
wantMessageSigned: config.wantAssertionsSigned || false,
|
|
1565
|
+
authnRequestsSigned: config.authnRequestsSigned || false,
|
|
1566
|
+
nameIDFormat: config.identifierFormat ? [config.identifierFormat] : void 0
|
|
1567
|
+
}).getMetadata() || "";
|
|
1568
|
+
return saml.ServiceProvider({
|
|
1569
|
+
metadata,
|
|
1570
|
+
allowCreate: true,
|
|
1554
1571
|
wantLogoutRequestSigned: opts?.sloOptions?.wantLogoutRequestSigned ?? false,
|
|
1555
1572
|
wantLogoutResponseSigned: opts?.sloOptions?.wantLogoutResponseSigned ?? false,
|
|
1556
|
-
|
|
1557
|
-
privateKey: spData?.privateKey || config.privateKey,
|
|
1573
|
+
privateKey: normalizePem(spData?.privateKey || config.privateKey),
|
|
1558
1574
|
privateKeyPass: spData?.privateKeyPass,
|
|
1559
1575
|
isAssertionEncrypted: spData?.isAssertionEncrypted || false,
|
|
1560
|
-
encPrivateKey: spData?.encPrivateKey,
|
|
1576
|
+
encPrivateKey: normalizePem(spData?.encPrivateKey),
|
|
1561
1577
|
encPrivateKeyPass: spData?.encPrivateKeyPass,
|
|
1562
|
-
nameIDFormat: config.identifierFormat ? [config.identifierFormat] : void 0,
|
|
1563
1578
|
relayState: opts?.relayState
|
|
1564
1579
|
});
|
|
1565
1580
|
}
|
|
@@ -1567,10 +1582,10 @@ function createIdP(config) {
|
|
|
1567
1582
|
const idpData = config.idpMetadata;
|
|
1568
1583
|
if (idpData?.metadata) return saml.IdentityProvider({
|
|
1569
1584
|
metadata: idpData.metadata,
|
|
1570
|
-
privateKey: idpData.privateKey,
|
|
1585
|
+
privateKey: normalizePem(idpData.privateKey),
|
|
1571
1586
|
privateKeyPass: idpData.privateKeyPass,
|
|
1572
1587
|
isAssertionEncrypted: idpData.isAssertionEncrypted,
|
|
1573
|
-
encPrivateKey: idpData.encPrivateKey,
|
|
1588
|
+
encPrivateKey: normalizePem(idpData.encPrivateKey),
|
|
1574
1589
|
encPrivateKeyPass: idpData.encPrivateKeyPass
|
|
1575
1590
|
});
|
|
1576
1591
|
return saml.IdentityProvider({
|
|
@@ -1580,10 +1595,10 @@ function createIdP(config) {
|
|
|
1580
1595
|
Location: config.entryPoint
|
|
1581
1596
|
}],
|
|
1582
1597
|
singleLogoutService: idpData?.singleLogoutService,
|
|
1583
|
-
signingCert: idpData?.cert || config.cert,
|
|
1598
|
+
signingCert: normalizePem(idpData?.cert || config.cert),
|
|
1584
1599
|
wantAuthnRequestsSigned: config.authnRequestsSigned || false,
|
|
1585
1600
|
isAssertionEncrypted: idpData?.isAssertionEncrypted || false,
|
|
1586
|
-
encPrivateKey: idpData?.encPrivateKey,
|
|
1601
|
+
encPrivateKey: normalizePem(idpData?.encPrivateKey),
|
|
1587
1602
|
encPrivateKeyPass: idpData?.encPrivateKeyPass
|
|
1588
1603
|
});
|
|
1589
1604
|
}
|
|
@@ -1694,8 +1709,8 @@ function extractAssertionId(samlContent) {
|
|
|
1694
1709
|
/**
|
|
1695
1710
|
* Unified SAML response processing pipeline.
|
|
1696
1711
|
*
|
|
1697
|
-
*
|
|
1698
|
-
*
|
|
1712
|
+
* The `/sso/saml2/sp/acs/:providerId` endpoint delegates to this function.
|
|
1713
|
+
* It handles the full lifecycle: provider lookup,
|
|
1699
1714
|
* SP/IdP construction, response validation, session creation, and redirect
|
|
1700
1715
|
* URL computation.
|
|
1701
1716
|
*/
|
|
@@ -1718,7 +1733,7 @@ async function processSAMLResponse(ctx, params, options) {
|
|
|
1718
1733
|
if (!parsedSamlConfig) throw new APIError("BAD_REQUEST", { message: "Invalid SAML configuration" });
|
|
1719
1734
|
const sp = createSP(parsedSamlConfig, ctx.context.baseURL, providerId);
|
|
1720
1735
|
const idp = createIdP(parsedSamlConfig);
|
|
1721
|
-
const samlRedirectUrl = getSafeRedirectUrl(relayState?.callbackURL
|
|
1736
|
+
const samlRedirectUrl = getSafeRedirectUrl(relayState?.callbackURL, params.currentCallbackPath, appOrigin, (url, settings) => ctx.context.isTrustedOrigin(url, settings));
|
|
1722
1737
|
validateSingleAssertion(SAMLResponse);
|
|
1723
1738
|
let parsedResponse;
|
|
1724
1739
|
try {
|
|
@@ -1755,7 +1770,7 @@ async function processSAMLResponse(ctx, params, options) {
|
|
|
1755
1770
|
});
|
|
1756
1771
|
validateAudience(ctx, {
|
|
1757
1772
|
extract,
|
|
1758
|
-
expectedAudience: parsedSamlConfig.audience,
|
|
1773
|
+
expectedAudience: parsedSamlConfig.audience || sp.entityMeta.getEntityID(),
|
|
1759
1774
|
providerId,
|
|
1760
1775
|
redirectUrl: samlRedirectUrl
|
|
1761
1776
|
});
|
|
@@ -1815,7 +1830,7 @@ async function processSAMLResponse(ctx, params, options) {
|
|
|
1815
1830
|
throw new APIError("BAD_REQUEST", { message: "Unable to extract user ID or email from SAML response" });
|
|
1816
1831
|
}
|
|
1817
1832
|
const isTrustedProvider = ctx.context.trustedProviders.includes(providerId) || "domainVerified" in provider && !!provider.domainVerified && validateEmailDomain(userInfo.email, provider.domain);
|
|
1818
|
-
const
|
|
1833
|
+
const postAuthRedirect = relayState?.callbackURL || ctx.context.baseURL;
|
|
1819
1834
|
const result = await handleOAuthUserInfo(ctx, {
|
|
1820
1835
|
userInfo: {
|
|
1821
1836
|
email: userInfo.email,
|
|
@@ -1829,7 +1844,7 @@ async function processSAMLResponse(ctx, params, options) {
|
|
|
1829
1844
|
accessToken: "",
|
|
1830
1845
|
refreshToken: ""
|
|
1831
1846
|
},
|
|
1832
|
-
callbackURL:
|
|
1847
|
+
callbackURL: postAuthRedirect,
|
|
1833
1848
|
disableSignUp: options?.disableImplicitSignUp,
|
|
1834
1849
|
isTrustedProvider
|
|
1835
1850
|
});
|
|
@@ -1876,7 +1891,7 @@ async function processSAMLResponse(ctx, params, options) {
|
|
|
1876
1891
|
expiresAt: session.expiresAt
|
|
1877
1892
|
}).catch((e) => ctx.context.logger.warn("Failed to create SAML session lookup record", e));
|
|
1878
1893
|
}
|
|
1879
|
-
return getSafeRedirectUrl(relayState?.callbackURL
|
|
1894
|
+
return getSafeRedirectUrl(relayState?.callbackURL, currentCallbackPath, appOrigin, (url, settings) => ctx.context.isTrustedOrigin(url, settings));
|
|
1880
1895
|
}
|
|
1881
1896
|
//#endregion
|
|
1882
1897
|
//#region src/routes/sso.ts
|
|
@@ -1893,10 +1908,7 @@ function getOIDCRedirectURI(baseURL, providerId, options) {
|
|
|
1893
1908
|
}
|
|
1894
1909
|
return `${baseURL}/sso/callback/${providerId}`;
|
|
1895
1910
|
}
|
|
1896
|
-
const spMetadataQuerySchema = z.object({
|
|
1897
|
-
providerId: z.string(),
|
|
1898
|
-
format: z.enum(["xml", "json"]).default("xml")
|
|
1899
|
-
});
|
|
1911
|
+
const spMetadataQuerySchema = z.object({ providerId: z.string() });
|
|
1900
1912
|
const spMetadata = (options) => {
|
|
1901
1913
|
return createAuthEndpoint("/sso/saml2/sp/metadata", {
|
|
1902
1914
|
method: "GET",
|
|
@@ -1918,25 +1930,10 @@ const spMetadata = (options) => {
|
|
|
1918
1930
|
if (!provider) throw new APIError("NOT_FOUND", { message: "No provider found for the given providerId" });
|
|
1919
1931
|
const parsedSamlConfig = safeJsonParse(provider.samlConfig);
|
|
1920
1932
|
if (!parsedSamlConfig) throw new APIError("BAD_REQUEST", { message: "Invalid SAML configuration" });
|
|
1921
|
-
const
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
}, {
|
|
1926
|
-
Binding: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect",
|
|
1927
|
-
Location: sloLocation
|
|
1928
|
-
}] : void 0;
|
|
1929
|
-
const sp = parsedSamlConfig.spMetadata.metadata ? saml.ServiceProvider({ metadata: parsedSamlConfig.spMetadata.metadata }) : saml.SPMetadata({
|
|
1930
|
-
entityID: parsedSamlConfig.spMetadata?.entityID || parsedSamlConfig.issuer,
|
|
1931
|
-
assertionConsumerService: [{
|
|
1932
|
-
Binding: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST",
|
|
1933
|
-
Location: parsedSamlConfig.callbackUrl || `${ctx.context.baseURL}/sso/saml2/sp/acs/${ctx.query.providerId}`
|
|
1934
|
-
}],
|
|
1935
|
-
singleLogoutService,
|
|
1936
|
-
wantMessageSigned: parsedSamlConfig.wantAssertionsSigned || false,
|
|
1937
|
-
authnRequestsSigned: parsedSamlConfig.authnRequestsSigned || false,
|
|
1938
|
-
nameIDFormat: parsedSamlConfig.identifierFormat ? [parsedSamlConfig.identifierFormat] : void 0
|
|
1939
|
-
});
|
|
1933
|
+
const sp = createSP(parsedSamlConfig, ctx.context.baseURL, ctx.query.providerId, options?.saml?.enableSingleLogout ? { sloOptions: {
|
|
1934
|
+
wantLogoutRequestSigned: options?.saml?.wantLogoutRequestSigned,
|
|
1935
|
+
wantLogoutResponseSigned: options?.saml?.wantLogoutResponseSigned
|
|
1936
|
+
} } : void 0);
|
|
1940
1937
|
return new Response(sp.getMetadata(), { headers: { "Content-Type": "application/xml" } });
|
|
1941
1938
|
});
|
|
1942
1939
|
};
|
|
@@ -1974,7 +1971,6 @@ const ssoProviderBodySchema = z.object({
|
|
|
1974
1971
|
samlConfig: z.object({
|
|
1975
1972
|
entryPoint: z.string({}).meta({ description: "The entry point of the provider" }),
|
|
1976
1973
|
cert: z.string({}).meta({ description: "The certificate of the provider" }),
|
|
1977
|
-
callbackUrl: z.string({}).meta({ description: "The callback URL of the provider" }),
|
|
1978
1974
|
audience: z.string().optional(),
|
|
1979
1975
|
idpMetadata: z.object({
|
|
1980
1976
|
metadata: z.string().optional(),
|
|
@@ -1999,15 +1995,13 @@ const ssoProviderBodySchema = z.object({
|
|
|
1999
1995
|
isAssertionEncrypted: z.boolean().optional(),
|
|
2000
1996
|
encPrivateKey: z.string().optional(),
|
|
2001
1997
|
encPrivateKeyPass: z.string().optional()
|
|
2002
|
-
}),
|
|
1998
|
+
}).optional(),
|
|
2003
1999
|
wantAssertionsSigned: z.boolean().optional(),
|
|
2004
2000
|
authnRequestsSigned: z.boolean().optional(),
|
|
2005
2001
|
signatureAlgorithm: z.string().optional(),
|
|
2006
2002
|
digestAlgorithm: z.string().optional(),
|
|
2007
2003
|
identifierFormat: z.string().optional(),
|
|
2008
2004
|
privateKey: z.string().optional(),
|
|
2009
|
-
decryptionPvk: z.string().optional(),
|
|
2010
|
-
additionalParams: z.record(z.string(), z.any()).optional(),
|
|
2011
2005
|
mapping: z.object({
|
|
2012
2006
|
id: z.string({}).meta({ description: "Field mapping for user ID (defaults to 'nameID')" }),
|
|
2013
2007
|
email: z.string({}).meta({ description: "Field mapping for email (defaults to 'email')" }),
|
|
@@ -2321,7 +2315,6 @@ const registerSSOProvider = (options) => {
|
|
|
2321
2315
|
issuer: body.issuer,
|
|
2322
2316
|
entryPoint: body.samlConfig.entryPoint,
|
|
2323
2317
|
cert: body.samlConfig.cert,
|
|
2324
|
-
callbackUrl: body.samlConfig.callbackUrl,
|
|
2325
2318
|
audience: body.samlConfig.audience,
|
|
2326
2319
|
idpMetadata: body.samlConfig.idpMetadata,
|
|
2327
2320
|
spMetadata: body.samlConfig.spMetadata,
|
|
@@ -2331,8 +2324,6 @@ const registerSSOProvider = (options) => {
|
|
|
2331
2324
|
digestAlgorithm: body.samlConfig.digestAlgorithm,
|
|
2332
2325
|
identifierFormat: body.samlConfig.identifierFormat,
|
|
2333
2326
|
privateKey: body.samlConfig.privateKey,
|
|
2334
|
-
decryptionPvk: body.samlConfig.decryptionPvk,
|
|
2335
|
-
additionalParams: body.samlConfig.additionalParams,
|
|
2336
2327
|
mapping: body.samlConfig.mapping
|
|
2337
2328
|
}) : null,
|
|
2338
2329
|
organizationId: body.organizationId,
|
|
@@ -2539,46 +2530,8 @@ const signInSSO = (options) => {
|
|
|
2539
2530
|
if (!parsedSamlConfig) throw new APIError("BAD_REQUEST", { message: "Invalid SAML configuration" });
|
|
2540
2531
|
if (parsedSamlConfig.authnRequestsSigned && !parsedSamlConfig.spMetadata?.privateKey && !parsedSamlConfig.privateKey) throw new APIError("BAD_REQUEST", { message: "authnRequestsSigned is enabled but no privateKey provided in spMetadata or samlConfig" });
|
|
2541
2532
|
const { state: relayState } = await generateRelayState(ctx, void 0, false);
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
entityID: parsedSamlConfig.spMetadata?.entityID || parsedSamlConfig.issuer,
|
|
2545
|
-
assertionConsumerService: [{
|
|
2546
|
-
Binding: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST",
|
|
2547
|
-
Location: parsedSamlConfig.callbackUrl || `${ctx.context.baseURL}/sso/saml2/sp/acs/${provider.providerId}`
|
|
2548
|
-
}],
|
|
2549
|
-
wantMessageSigned: parsedSamlConfig.wantAssertionsSigned || false,
|
|
2550
|
-
authnRequestsSigned: parsedSamlConfig.authnRequestsSigned || false,
|
|
2551
|
-
nameIDFormat: parsedSamlConfig.identifierFormat ? [parsedSamlConfig.identifierFormat] : void 0
|
|
2552
|
-
}).getMetadata() || "";
|
|
2553
|
-
const sp = saml.ServiceProvider({
|
|
2554
|
-
metadata,
|
|
2555
|
-
allowCreate: true,
|
|
2556
|
-
privateKey: parsedSamlConfig.spMetadata?.privateKey || parsedSamlConfig.privateKey,
|
|
2557
|
-
privateKeyPass: parsedSamlConfig.spMetadata?.privateKeyPass,
|
|
2558
|
-
relayState
|
|
2559
|
-
});
|
|
2560
|
-
const idpData = parsedSamlConfig.idpMetadata;
|
|
2561
|
-
let idp;
|
|
2562
|
-
if (!idpData?.metadata) idp = saml.IdentityProvider({
|
|
2563
|
-
entityID: idpData?.entityID || parsedSamlConfig.issuer,
|
|
2564
|
-
singleSignOnService: idpData?.singleSignOnService || [{
|
|
2565
|
-
Binding: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect",
|
|
2566
|
-
Location: parsedSamlConfig.entryPoint
|
|
2567
|
-
}],
|
|
2568
|
-
signingCert: idpData?.cert || parsedSamlConfig.cert,
|
|
2569
|
-
wantAuthnRequestsSigned: parsedSamlConfig.authnRequestsSigned || false,
|
|
2570
|
-
isAssertionEncrypted: idpData?.isAssertionEncrypted || false,
|
|
2571
|
-
encPrivateKey: idpData?.encPrivateKey,
|
|
2572
|
-
encPrivateKeyPass: idpData?.encPrivateKeyPass
|
|
2573
|
-
});
|
|
2574
|
-
else idp = saml.IdentityProvider({
|
|
2575
|
-
metadata: idpData.metadata,
|
|
2576
|
-
privateKey: idpData.privateKey,
|
|
2577
|
-
privateKeyPass: idpData.privateKeyPass,
|
|
2578
|
-
isAssertionEncrypted: idpData.isAssertionEncrypted,
|
|
2579
|
-
encPrivateKey: idpData.encPrivateKey,
|
|
2580
|
-
encPrivateKeyPass: idpData.encPrivateKeyPass
|
|
2581
|
-
});
|
|
2533
|
+
const sp = createSP(parsedSamlConfig, ctx.context.baseURL, provider.providerId, { relayState });
|
|
2534
|
+
const idp = createIdP(parsedSamlConfig);
|
|
2582
2535
|
const loginRequest = sp.createLoginRequest(idp, "redirect");
|
|
2583
2536
|
if (!loginRequest) throw new APIError("BAD_REQUEST", { message: "Invalid SAML request" });
|
|
2584
2537
|
if (loginRequest.id && options?.saml?.enableInResponseToValidation !== false) {
|
|
@@ -2853,72 +2806,42 @@ const callbackSSOShared = (options) => {
|
|
|
2853
2806
|
return handleOIDCCallback(ctx, options, providerId, stateData);
|
|
2854
2807
|
});
|
|
2855
2808
|
};
|
|
2856
|
-
const
|
|
2809
|
+
const acsEndpointBodySchema = z.object({
|
|
2857
2810
|
SAMLResponse: z.string(),
|
|
2858
2811
|
RelayState: z.string().optional()
|
|
2859
2812
|
});
|
|
2860
|
-
const
|
|
2861
|
-
return createAuthEndpoint("/sso/saml2/
|
|
2813
|
+
const acsEndpoint = (options) => {
|
|
2814
|
+
return createAuthEndpoint("/sso/saml2/sp/acs/:providerId", {
|
|
2862
2815
|
method: ["GET", "POST"],
|
|
2863
|
-
body:
|
|
2816
|
+
body: acsEndpointBodySchema.optional(),
|
|
2864
2817
|
query: z.object({ RelayState: z.string().optional() }).optional(),
|
|
2865
2818
|
metadata: {
|
|
2866
2819
|
...HIDE_METADATA,
|
|
2867
2820
|
allowedMediaTypes: ["application/x-www-form-urlencoded", "application/json"],
|
|
2868
2821
|
openapi: {
|
|
2869
|
-
operationId: "
|
|
2870
|
-
summary: "
|
|
2871
|
-
description: "
|
|
2822
|
+
operationId: "handleSAMLAssertionConsumerService",
|
|
2823
|
+
summary: "SAML Assertion Consumer Service",
|
|
2824
|
+
description: "Handles SAML responses from IdP after successful authentication. Supports GET for post-auth redirects and POST for SAML response processing.",
|
|
2872
2825
|
responses: {
|
|
2873
|
-
"302": { description: "Redirects
|
|
2874
|
-
"400": { description: "
|
|
2875
|
-
"
|
|
2826
|
+
"302": { description: "Redirects after authentication (success or error with query params)" },
|
|
2827
|
+
"400": { description: "Missing SAMLResponse in POST body" },
|
|
2828
|
+
"404": { description: "SAML provider not found" }
|
|
2876
2829
|
}
|
|
2877
2830
|
}
|
|
2878
2831
|
}
|
|
2879
2832
|
}, async (ctx) => {
|
|
2880
2833
|
const { providerId } = ctx.params;
|
|
2834
|
+
const currentCallbackPath = `${ctx.context.baseURL}/sso/saml2/sp/acs/${providerId}`;
|
|
2881
2835
|
const appOrigin = new URL(ctx.context.baseURL).origin;
|
|
2882
|
-
const errorURL = ctx.context.options.onAPIError?.errorURL || `${appOrigin}/error`;
|
|
2883
|
-
const currentCallbackPath = `${ctx.context.baseURL}/sso/saml2/callback/${providerId}`;
|
|
2884
2836
|
if (ctx.method === "GET" && !ctx.body?.SAMLResponse) {
|
|
2885
|
-
if (!(await getSessionFromCtx(ctx))?.session)
|
|
2837
|
+
if (!(await getSessionFromCtx(ctx))?.session) {
|
|
2838
|
+
const errorURL = ctx.context.options.onAPIError?.errorURL || `${appOrigin}/error`;
|
|
2839
|
+
throw ctx.redirect(`${errorURL}?error=invalid_request`);
|
|
2840
|
+
}
|
|
2886
2841
|
const relayState = ctx.query?.RelayState;
|
|
2887
|
-
|
|
2888
|
-
throw ctx.redirect(safeRedirectUrl);
|
|
2842
|
+
throw ctx.redirect(getSafeRedirectUrl(relayState, currentCallbackPath, appOrigin, (url, settings) => ctx.context.isTrustedOrigin(url, settings)));
|
|
2889
2843
|
}
|
|
2890
2844
|
if (!ctx.body?.SAMLResponse) throw new APIError("BAD_REQUEST", { message: "SAMLResponse is required for POST requests" });
|
|
2891
|
-
const safeRedirectUrl = await processSAMLResponse(ctx, {
|
|
2892
|
-
SAMLResponse: ctx.body.SAMLResponse,
|
|
2893
|
-
RelayState: ctx.body.RelayState,
|
|
2894
|
-
providerId,
|
|
2895
|
-
currentCallbackPath
|
|
2896
|
-
}, options);
|
|
2897
|
-
throw ctx.redirect(safeRedirectUrl);
|
|
2898
|
-
});
|
|
2899
|
-
};
|
|
2900
|
-
const acsEndpointBodySchema = z.object({
|
|
2901
|
-
SAMLResponse: z.string(),
|
|
2902
|
-
RelayState: z.string().optional()
|
|
2903
|
-
});
|
|
2904
|
-
const acsEndpoint = (options) => {
|
|
2905
|
-
return createAuthEndpoint("/sso/saml2/sp/acs/:providerId", {
|
|
2906
|
-
method: "POST",
|
|
2907
|
-
body: acsEndpointBodySchema,
|
|
2908
|
-
metadata: {
|
|
2909
|
-
...HIDE_METADATA,
|
|
2910
|
-
allowedMediaTypes: ["application/x-www-form-urlencoded", "application/json"],
|
|
2911
|
-
openapi: {
|
|
2912
|
-
operationId: "handleSAMLAssertionConsumerService",
|
|
2913
|
-
summary: "SAML Assertion Consumer Service",
|
|
2914
|
-
description: "Handles SAML responses from IdP after successful authentication",
|
|
2915
|
-
responses: { "302": { description: "Redirects to the callback URL after successful authentication" } }
|
|
2916
|
-
}
|
|
2917
|
-
}
|
|
2918
|
-
}, async (ctx) => {
|
|
2919
|
-
const { providerId } = ctx.params;
|
|
2920
|
-
const currentCallbackPath = `${ctx.context.baseURL}/sso/saml2/sp/acs/${providerId}`;
|
|
2921
|
-
const appOrigin = new URL(ctx.context.baseURL).origin;
|
|
2922
2845
|
try {
|
|
2923
2846
|
const safeRedirectUrl = await processSAMLResponse(ctx, {
|
|
2924
2847
|
SAMLResponse: ctx.body.SAMLResponse,
|
|
@@ -2930,9 +2853,8 @@ const acsEndpoint = (options) => {
|
|
|
2930
2853
|
} catch (error) {
|
|
2931
2854
|
if (error instanceof Response || error && typeof error === "object" && "status" in error && error.status === 302) throw error;
|
|
2932
2855
|
if (error instanceof APIError && error.statusCode === 400) {
|
|
2933
|
-
const
|
|
2934
|
-
const
|
|
2935
|
-
const redirectUrl = getSafeRedirectUrl(ctx.body.RelayState || void 0, currentCallbackPath, appOrigin, (url, settings) => ctx.context.isTrustedOrigin(url, settings));
|
|
2856
|
+
const errorCode = (error.body?.code || "saml_error").toLowerCase();
|
|
2857
|
+
const redirectUrl = getSafeRedirectUrl(ctx.body?.RelayState || void 0, currentCallbackPath, appOrigin, (url, settings) => ctx.context.isTrustedOrigin(url, settings));
|
|
2936
2858
|
throw ctx.redirect(`${redirectUrl}${redirectUrl.includes("?") ? "&" : "?"}error=${encodeURIComponent(errorCode)}&error_description=${encodeURIComponent(error.message)}`);
|
|
2937
2859
|
}
|
|
2938
2860
|
throw error;
|
|
@@ -3107,11 +3029,7 @@ saml.setSchemaValidator({ async validate(xml) {
|
|
|
3107
3029
|
* These endpoints receive POST requests from external Identity Providers,
|
|
3108
3030
|
* which won't have a matching Origin header.
|
|
3109
3031
|
*/
|
|
3110
|
-
const SAML_SKIP_ORIGIN_CHECK_PATHS = [
|
|
3111
|
-
"/sso/saml2/callback",
|
|
3112
|
-
"/sso/saml2/sp/acs",
|
|
3113
|
-
"/sso/saml2/sp/slo"
|
|
3114
|
-
];
|
|
3032
|
+
const SAML_SKIP_ORIGIN_CHECK_PATHS = ["/sso/saml2/sp/acs", "/sso/saml2/sp/slo"];
|
|
3115
3033
|
function sso(options) {
|
|
3116
3034
|
const optionsWithStore = options;
|
|
3117
3035
|
let endpoints = {
|
|
@@ -3120,7 +3038,6 @@ function sso(options) {
|
|
|
3120
3038
|
signInSSO: signInSSO(optionsWithStore),
|
|
3121
3039
|
callbackSSO: callbackSSO(optionsWithStore),
|
|
3122
3040
|
callbackSSOShared: callbackSSOShared(optionsWithStore),
|
|
3123
|
-
callbackSSOSAML: callbackSSOSAML(optionsWithStore),
|
|
3124
3041
|
acsEndpoint: acsEndpoint(optionsWithStore),
|
|
3125
3042
|
sloEndpoint: sloEndpoint(optionsWithStore),
|
|
3126
3043
|
initiateSLO: initiateSLO(optionsWithStore),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@better-auth/sso",
|
|
3
|
-
"version": "1.7.0-beta.
|
|
3
|
+
"version": "1.7.0-beta.2",
|
|
4
4
|
"description": "SSO plugin for Better Auth",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
"dependencies": {
|
|
59
59
|
"fast-xml-parser": "^5.5.7",
|
|
60
60
|
"jose": "^6.1.3",
|
|
61
|
-
"samlify": "~2.
|
|
61
|
+
"samlify": "~2.12.0",
|
|
62
62
|
"tldts": "^6.1.0",
|
|
63
63
|
"zod": "^4.3.6"
|
|
64
64
|
},
|
|
@@ -70,15 +70,15 @@
|
|
|
70
70
|
"express": "^5.2.1",
|
|
71
71
|
"oauth2-mock-server": "^8.2.2",
|
|
72
72
|
"tsdown": "0.21.1",
|
|
73
|
-
"@better-auth/core": "1.7.0-beta.
|
|
74
|
-
"better-auth": "1.7.0-beta.
|
|
73
|
+
"@better-auth/core": "1.7.0-beta.2",
|
|
74
|
+
"better-auth": "1.7.0-beta.2"
|
|
75
75
|
},
|
|
76
76
|
"peerDependencies": {
|
|
77
77
|
"@better-auth/utils": "0.4.0",
|
|
78
78
|
"@better-fetch/fetch": "1.1.21",
|
|
79
79
|
"better-call": "1.3.5",
|
|
80
|
-
"@better-auth/core": "^1.7.0-beta.
|
|
81
|
-
"better-auth": "^1.7.0-beta.
|
|
80
|
+
"@better-auth/core": "^1.7.0-beta.2",
|
|
81
|
+
"better-auth": "^1.7.0-beta.2"
|
|
82
82
|
},
|
|
83
83
|
"scripts": {
|
|
84
84
|
"build": "tsdown",
|