@better-auth/sso 1.4.0-beta.20 → 1.4.0-beta.22
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 +9 -10
- package/dist/client.d.mts +1 -1
- package/dist/client.mjs +0 -2
- package/dist/{index-Ba2niv2R.d.mts → index-DOws6HlV.d.mts} +30 -1
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +1238 -1
- package/package.json +3 -3
- package/src/client.ts +1 -1
- package/src/index.ts +11 -3
- package/src/routes/sso.ts +32 -4
- package/src/saml.test.ts +207 -1
- package/src/types.ts +23 -0
- package/vitest.config.ts +3 -0
- package/dist/src-BUIX9n33.mjs +0 -1215
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@better-auth/sso",
|
|
3
3
|
"author": "Bereket Engida",
|
|
4
|
-
"version": "1.4.0-beta.
|
|
4
|
+
"version": "1.4.0-beta.22",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.mjs",
|
|
7
7
|
"homepage": "https://www.better-auth.com/docs/plugins/sso",
|
|
@@ -65,10 +65,10 @@
|
|
|
65
65
|
"express": "^5.1.0",
|
|
66
66
|
"oauth2-mock-server": "^7.2.1",
|
|
67
67
|
"tsdown": "^0.16.0",
|
|
68
|
-
"better-auth": "1.4.0-beta.
|
|
68
|
+
"better-auth": "1.4.0-beta.22"
|
|
69
69
|
},
|
|
70
70
|
"peerDependencies": {
|
|
71
|
-
"better-auth": "1.4.0-beta.
|
|
71
|
+
"better-auth": "1.4.0-beta.22"
|
|
72
72
|
},
|
|
73
73
|
"scripts": {
|
|
74
74
|
"test": "vitest",
|
package/src/client.ts
CHANGED
package/src/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { BetterAuthPlugin } from "better-auth";
|
|
2
2
|
import { XMLValidator } from "fast-xml-parser";
|
|
3
3
|
import * as saml from "samlify";
|
|
4
4
|
import {
|
|
@@ -9,9 +9,9 @@ import {
|
|
|
9
9
|
signInSSO,
|
|
10
10
|
spMetadata,
|
|
11
11
|
} from "./routes/sso";
|
|
12
|
-
import type { OIDCConfig, SAMLConfig, SSOOptions } from "./types";
|
|
12
|
+
import type { OIDCConfig, SAMLConfig, SSOOptions, SSOProvider } from "./types";
|
|
13
13
|
|
|
14
|
-
export type { SAMLConfig, OIDCConfig, SSOOptions };
|
|
14
|
+
export type { SAMLConfig, OIDCConfig, SSOOptions, SSOProvider };
|
|
15
15
|
|
|
16
16
|
const fastValidator = {
|
|
17
17
|
async validate(xml: string) {
|
|
@@ -54,18 +54,22 @@ export function sso<O extends SSOOptions>(options?: O | undefined): any {
|
|
|
54
54
|
},
|
|
55
55
|
schema: {
|
|
56
56
|
ssoProvider: {
|
|
57
|
+
modelName: options?.modelName ?? "ssoProvider",
|
|
57
58
|
fields: {
|
|
58
59
|
issuer: {
|
|
59
60
|
type: "string",
|
|
60
61
|
required: true,
|
|
62
|
+
fieldName: options?.fields?.issuer ?? "issuer",
|
|
61
63
|
},
|
|
62
64
|
oidcConfig: {
|
|
63
65
|
type: "string",
|
|
64
66
|
required: false,
|
|
67
|
+
fieldName: options?.fields?.oidcConfig ?? "oidcConfig",
|
|
65
68
|
},
|
|
66
69
|
samlConfig: {
|
|
67
70
|
type: "string",
|
|
68
71
|
required: false,
|
|
72
|
+
fieldName: options?.fields?.samlConfig ?? "samlConfig",
|
|
69
73
|
},
|
|
70
74
|
userId: {
|
|
71
75
|
type: "string",
|
|
@@ -73,19 +77,23 @@ export function sso<O extends SSOOptions>(options?: O | undefined): any {
|
|
|
73
77
|
model: "user",
|
|
74
78
|
field: "id",
|
|
75
79
|
},
|
|
80
|
+
fieldName: options?.fields?.userId ?? "userId",
|
|
76
81
|
},
|
|
77
82
|
providerId: {
|
|
78
83
|
type: "string",
|
|
79
84
|
required: true,
|
|
80
85
|
unique: true,
|
|
86
|
+
fieldName: options?.fields?.providerId ?? "providerId",
|
|
81
87
|
},
|
|
82
88
|
organizationId: {
|
|
83
89
|
type: "string",
|
|
84
90
|
required: false,
|
|
91
|
+
fieldName: options?.fields?.organizationId ?? "organizationId",
|
|
85
92
|
},
|
|
86
93
|
domain: {
|
|
87
94
|
type: "string",
|
|
88
95
|
required: true,
|
|
96
|
+
fieldName: options?.fields?.domain ?? "domain",
|
|
89
97
|
},
|
|
90
98
|
},
|
|
91
99
|
},
|
package/src/routes/sso.ts
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
import { BetterFetchError, betterFetch } from "@better-fetch/fetch";
|
|
2
|
+
import type { Account, Session, User } from "better-auth";
|
|
2
3
|
import {
|
|
3
|
-
type Account,
|
|
4
4
|
createAuthorizationURL,
|
|
5
5
|
generateState,
|
|
6
6
|
parseState,
|
|
7
|
-
type Session,
|
|
8
|
-
type User,
|
|
9
7
|
validateAuthorizationCode,
|
|
10
8
|
validateToken,
|
|
11
9
|
} from "better-auth";
|
|
@@ -63,6 +61,7 @@ export const spMetadata = () => {
|
|
|
63
61
|
}),
|
|
64
62
|
metadata: {
|
|
65
63
|
openapi: {
|
|
64
|
+
operationId: "getSSOServiceProviderMetadata",
|
|
66
65
|
summary: "Get Service Provider metadata",
|
|
67
66
|
description: "Returns the SAML metadata for the Service Provider",
|
|
68
67
|
responses: {
|
|
@@ -337,6 +336,7 @@ export const registerSSOProvider = (options?: SSOOptions) => {
|
|
|
337
336
|
use: [sessionMiddleware],
|
|
338
337
|
metadata: {
|
|
339
338
|
openapi: {
|
|
339
|
+
operationId: "registerSSOProvider",
|
|
340
340
|
summary: "Register an OIDC provider",
|
|
341
341
|
description:
|
|
342
342
|
"This endpoint is used to register an OIDC provider. This is used to configure the provider and link it to an organization",
|
|
@@ -726,6 +726,7 @@ export const signInSSO = (options?: SSOOptions) => {
|
|
|
726
726
|
}),
|
|
727
727
|
metadata: {
|
|
728
728
|
openapi: {
|
|
729
|
+
operationId: "signInWithSSO",
|
|
729
730
|
summary: "Sign in with SSO provider",
|
|
730
731
|
description:
|
|
731
732
|
"This endpoint is used to sign in with an SSO provider. It redirects to the provider's authorization URL",
|
|
@@ -925,6 +926,22 @@ export const signInSSO = (options?: SSOOptions) => {
|
|
|
925
926
|
}
|
|
926
927
|
|
|
927
928
|
if (provider.oidcConfig && body.providerType !== "saml") {
|
|
929
|
+
let finalAuthUrl = provider.oidcConfig.authorizationEndpoint;
|
|
930
|
+
if (!finalAuthUrl && provider.oidcConfig.discoveryEndpoint) {
|
|
931
|
+
const discovery = await betterFetch<{
|
|
932
|
+
authorization_endpoint: string;
|
|
933
|
+
}>(provider.oidcConfig.discoveryEndpoint, {
|
|
934
|
+
method: "GET",
|
|
935
|
+
});
|
|
936
|
+
if (discovery.data) {
|
|
937
|
+
finalAuthUrl = discovery.data.authorization_endpoint;
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
if (!finalAuthUrl) {
|
|
941
|
+
throw new APIError("BAD_REQUEST", {
|
|
942
|
+
message: "Invalid OIDC configuration. Authorization URL not found.",
|
|
943
|
+
});
|
|
944
|
+
}
|
|
928
945
|
const state = await generateState(ctx, undefined, false);
|
|
929
946
|
const redirectURI = `${ctx.context.baseURL}/sso/callback/${provider.providerId}`;
|
|
930
947
|
const authorizationURL = await createAuthorizationURL({
|
|
@@ -946,7 +963,7 @@ export const signInSSO = (options?: SSOOptions) => {
|
|
|
946
963
|
"offline_access",
|
|
947
964
|
],
|
|
948
965
|
loginHint: ctx.body.loginHint || email,
|
|
949
|
-
authorizationEndpoint:
|
|
966
|
+
authorizationEndpoint: finalAuthUrl,
|
|
950
967
|
});
|
|
951
968
|
return ctx.json({
|
|
952
969
|
url: authorizationURL.toString(),
|
|
@@ -1014,6 +1031,7 @@ export const callbackSSO = (options?: SSOOptions) => {
|
|
|
1014
1031
|
metadata: {
|
|
1015
1032
|
isAction: false,
|
|
1016
1033
|
openapi: {
|
|
1034
|
+
operationId: "handleSSOCallback",
|
|
1017
1035
|
summary: "Callback URL for SSO provider",
|
|
1018
1036
|
description:
|
|
1019
1037
|
"This endpoint is used as the callback URL for SSO providers. It handles the authorization code and exchanges it for an access token",
|
|
@@ -1362,6 +1380,7 @@ export const callbackSSOSAML = (options?: SSOOptions) => {
|
|
|
1362
1380
|
metadata: {
|
|
1363
1381
|
isAction: false,
|
|
1364
1382
|
openapi: {
|
|
1383
|
+
operationId: "handleSAMLCallback",
|
|
1365
1384
|
summary: "Callback URL for SAML provider",
|
|
1366
1385
|
description:
|
|
1367
1386
|
"This endpoint is used as the callback URL for SAML providers.",
|
|
@@ -1584,6 +1603,14 @@ export const callbackSSOSAML = (options?: SSOOptions) => {
|
|
|
1584
1603
|
if (existingUser) {
|
|
1585
1604
|
user = existingUser;
|
|
1586
1605
|
} else {
|
|
1606
|
+
// if implicit sign up is disabled, we should not create a new user nor a new account.
|
|
1607
|
+
if (options?.disableImplicitSignUp) {
|
|
1608
|
+
throw new APIError("UNAUTHORIZED", {
|
|
1609
|
+
message:
|
|
1610
|
+
"User not found and implicit sign up is disabled for this provider",
|
|
1611
|
+
});
|
|
1612
|
+
}
|
|
1613
|
+
|
|
1587
1614
|
user = await ctx.context.internalAdapter.createUser({
|
|
1588
1615
|
email: userInfo.email,
|
|
1589
1616
|
name: userInfo.name,
|
|
@@ -1687,6 +1714,7 @@ export const acsEndpoint = (options?: SSOOptions) => {
|
|
|
1687
1714
|
metadata: {
|
|
1688
1715
|
isAction: false,
|
|
1689
1716
|
openapi: {
|
|
1717
|
+
operationId: "handleSAMLAssertionConsumerService",
|
|
1690
1718
|
summary: "SAML Assertion Consumer Service",
|
|
1691
1719
|
description:
|
|
1692
1720
|
"Handles SAML responses from IdP after successful authentication",
|
package/src/saml.test.ts
CHANGED
|
@@ -13,7 +13,7 @@ import type {
|
|
|
13
13
|
Response as ExpressResponse,
|
|
14
14
|
} from "express";
|
|
15
15
|
import express from "express";
|
|
16
|
-
import { createServer } from "http";
|
|
16
|
+
import type { createServer } from "http";
|
|
17
17
|
import * as saml from "samlify";
|
|
18
18
|
import {
|
|
19
19
|
afterAll,
|
|
@@ -1118,4 +1118,210 @@ describe("SAML SSO", async () => {
|
|
|
1118
1118
|
},
|
|
1119
1119
|
});
|
|
1120
1120
|
});
|
|
1121
|
+
|
|
1122
|
+
it("should reject SAML sign-in when disableImplicitSignUp is true and user doesn't exist", async () => {
|
|
1123
|
+
const { auth: authWithDisabledSignUp, signInWithTestUser } =
|
|
1124
|
+
await getTestInstance({
|
|
1125
|
+
plugins: [sso({ disableImplicitSignUp: true })],
|
|
1126
|
+
});
|
|
1127
|
+
|
|
1128
|
+
const { headers } = await signInWithTestUser();
|
|
1129
|
+
|
|
1130
|
+
// Register SAML provider
|
|
1131
|
+
await authWithDisabledSignUp.api.registerSSOProvider({
|
|
1132
|
+
body: {
|
|
1133
|
+
providerId: "saml-test-provider",
|
|
1134
|
+
issuer: "http://localhost:8081",
|
|
1135
|
+
domain: "http://localhost:8081",
|
|
1136
|
+
samlConfig: {
|
|
1137
|
+
entryPoint: "http://localhost:8081/api/sso/saml2/idp/post",
|
|
1138
|
+
cert: certificate,
|
|
1139
|
+
callbackUrl: "http://localhost:3000/dashboard",
|
|
1140
|
+
wantAssertionsSigned: false,
|
|
1141
|
+
signatureAlgorithm: "sha256",
|
|
1142
|
+
digestAlgorithm: "sha256",
|
|
1143
|
+
idpMetadata: {
|
|
1144
|
+
metadata: idpMetadata,
|
|
1145
|
+
},
|
|
1146
|
+
spMetadata: {
|
|
1147
|
+
metadata: spMetadata,
|
|
1148
|
+
},
|
|
1149
|
+
identifierFormat:
|
|
1150
|
+
"urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress",
|
|
1151
|
+
},
|
|
1152
|
+
},
|
|
1153
|
+
headers: headers,
|
|
1154
|
+
});
|
|
1155
|
+
|
|
1156
|
+
// Identity Provider-initiated: Get SAML response directly from IdP
|
|
1157
|
+
// The mock IdP will return test@email.com, which doesn't exist in the DB
|
|
1158
|
+
let samlResponse: any;
|
|
1159
|
+
await betterFetch("http://localhost:8081/api/sso/saml2/idp/post", {
|
|
1160
|
+
onSuccess: async (context) => {
|
|
1161
|
+
samlResponse = await context.data;
|
|
1162
|
+
},
|
|
1163
|
+
});
|
|
1164
|
+
|
|
1165
|
+
// Attempt to complete SAML callback - should fail because test@email.com doesn't exist
|
|
1166
|
+
// and disableImplicitSignUp is true
|
|
1167
|
+
await expect(
|
|
1168
|
+
authWithDisabledSignUp.api.callbackSSOSAML({
|
|
1169
|
+
body: {
|
|
1170
|
+
SAMLResponse: samlResponse.samlResponse,
|
|
1171
|
+
RelayState: "http://localhost:3000/dashboard",
|
|
1172
|
+
},
|
|
1173
|
+
params: {
|
|
1174
|
+
providerId: "saml-test-provider",
|
|
1175
|
+
},
|
|
1176
|
+
}),
|
|
1177
|
+
).rejects.toMatchObject({
|
|
1178
|
+
status: "UNAUTHORIZED",
|
|
1179
|
+
body: {
|
|
1180
|
+
message:
|
|
1181
|
+
"User not found and implicit sign up is disabled for this provider",
|
|
1182
|
+
},
|
|
1183
|
+
});
|
|
1184
|
+
});
|
|
1185
|
+
});
|
|
1186
|
+
|
|
1187
|
+
describe("SAML SSO with custom fields", () => {
|
|
1188
|
+
const ssoOptions = {
|
|
1189
|
+
modelName: "sso_provider",
|
|
1190
|
+
fields: {
|
|
1191
|
+
issuer: "the_issuer",
|
|
1192
|
+
oidcConfig: "oidc_config",
|
|
1193
|
+
samlConfig: "saml_config",
|
|
1194
|
+
userId: "user_id",
|
|
1195
|
+
providerId: "provider_id",
|
|
1196
|
+
organizationId: "organization_id",
|
|
1197
|
+
domain: "the_domain",
|
|
1198
|
+
},
|
|
1199
|
+
};
|
|
1200
|
+
|
|
1201
|
+
const data = {
|
|
1202
|
+
user: [],
|
|
1203
|
+
session: [],
|
|
1204
|
+
verification: [],
|
|
1205
|
+
account: [],
|
|
1206
|
+
sso_provider: [],
|
|
1207
|
+
};
|
|
1208
|
+
|
|
1209
|
+
const memory = memoryAdapter(data);
|
|
1210
|
+
const mockIdP = createMockSAMLIdP(8081); // Different port from your main app
|
|
1211
|
+
|
|
1212
|
+
const auth = betterAuth({
|
|
1213
|
+
database: memory,
|
|
1214
|
+
baseURL: "http://localhost:3000",
|
|
1215
|
+
emailAndPassword: {
|
|
1216
|
+
enabled: true,
|
|
1217
|
+
},
|
|
1218
|
+
plugins: [sso(ssoOptions)],
|
|
1219
|
+
});
|
|
1220
|
+
|
|
1221
|
+
const authClient = createAuthClient({
|
|
1222
|
+
baseURL: "http://localhost:3000",
|
|
1223
|
+
plugins: [bearer(), ssoClient()],
|
|
1224
|
+
fetchOptions: {
|
|
1225
|
+
customFetchImpl: async (url, init) => {
|
|
1226
|
+
return auth.handler(new Request(url, init));
|
|
1227
|
+
},
|
|
1228
|
+
},
|
|
1229
|
+
});
|
|
1230
|
+
|
|
1231
|
+
const testUser = {
|
|
1232
|
+
email: "test@email.com",
|
|
1233
|
+
password: "password",
|
|
1234
|
+
name: "Test User",
|
|
1235
|
+
};
|
|
1236
|
+
|
|
1237
|
+
beforeAll(async () => {
|
|
1238
|
+
await mockIdP.start();
|
|
1239
|
+
const res = await authClient.signUp.email({
|
|
1240
|
+
email: testUser.email,
|
|
1241
|
+
password: testUser.password,
|
|
1242
|
+
name: testUser.name,
|
|
1243
|
+
});
|
|
1244
|
+
});
|
|
1245
|
+
|
|
1246
|
+
afterAll(async () => {
|
|
1247
|
+
await mockIdP.stop();
|
|
1248
|
+
});
|
|
1249
|
+
|
|
1250
|
+
beforeEach(() => {
|
|
1251
|
+
data.user = [];
|
|
1252
|
+
data.session = [];
|
|
1253
|
+
data.verification = [];
|
|
1254
|
+
data.account = [];
|
|
1255
|
+
data.sso_provider = [];
|
|
1256
|
+
|
|
1257
|
+
vi.clearAllMocks();
|
|
1258
|
+
});
|
|
1259
|
+
|
|
1260
|
+
async function getAuthHeaders() {
|
|
1261
|
+
const headers = new Headers();
|
|
1262
|
+
await authClient.signUp.email({
|
|
1263
|
+
email: testUser.email,
|
|
1264
|
+
password: testUser.password,
|
|
1265
|
+
name: testUser.name,
|
|
1266
|
+
});
|
|
1267
|
+
await authClient.signIn.email(testUser, {
|
|
1268
|
+
throw: true,
|
|
1269
|
+
onSuccess: setCookieToHeader(headers),
|
|
1270
|
+
});
|
|
1271
|
+
return headers;
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
it("should register a new SAML provider", async () => {
|
|
1275
|
+
const headers = await getAuthHeaders();
|
|
1276
|
+
|
|
1277
|
+
const provider = await auth.api.registerSSOProvider({
|
|
1278
|
+
body: {
|
|
1279
|
+
providerId: "saml-provider-1",
|
|
1280
|
+
issuer: "http://localhost:8081",
|
|
1281
|
+
domain: "http://localhost:8081",
|
|
1282
|
+
samlConfig: {
|
|
1283
|
+
entryPoint: mockIdP.metadataUrl,
|
|
1284
|
+
cert: certificate,
|
|
1285
|
+
callbackUrl: "http://localhost:8081/api/sso/saml2/callback",
|
|
1286
|
+
wantAssertionsSigned: false,
|
|
1287
|
+
signatureAlgorithm: "sha256",
|
|
1288
|
+
digestAlgorithm: "sha256",
|
|
1289
|
+
idpMetadata: {
|
|
1290
|
+
metadata: idpMetadata,
|
|
1291
|
+
privateKey: idpPrivateKey,
|
|
1292
|
+
privateKeyPass: "q9ALNhGT5EhfcRmp8Pg7e9zTQeP2x1bW",
|
|
1293
|
+
isAssertionEncrypted: true,
|
|
1294
|
+
encPrivateKey: idpEncryptionKey,
|
|
1295
|
+
encPrivateKeyPass: "g7hGcRmp8PxT5QeP2q9Ehf1bWe9zTALN",
|
|
1296
|
+
},
|
|
1297
|
+
spMetadata: {
|
|
1298
|
+
metadata: spMetadata,
|
|
1299
|
+
binding: "post",
|
|
1300
|
+
privateKey: spPrivateKey,
|
|
1301
|
+
privateKeyPass: "VHOSp5RUiBcrsjrcAuXFwU1NKCkGA8px",
|
|
1302
|
+
isAssertionEncrypted: true,
|
|
1303
|
+
encPrivateKey: spEncryptionKey,
|
|
1304
|
+
encPrivateKeyPass: "BXFNKpxrsjrCkGA8cAu5wUVHOSpci1RU",
|
|
1305
|
+
},
|
|
1306
|
+
identifierFormat:
|
|
1307
|
+
"urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress",
|
|
1308
|
+
},
|
|
1309
|
+
},
|
|
1310
|
+
headers,
|
|
1311
|
+
});
|
|
1312
|
+
expect(provider).toMatchObject({
|
|
1313
|
+
id: expect.any(String),
|
|
1314
|
+
issuer: "http://localhost:8081",
|
|
1315
|
+
samlConfig: {
|
|
1316
|
+
entryPoint: mockIdP.metadataUrl,
|
|
1317
|
+
cert: expect.any(String),
|
|
1318
|
+
callbackUrl: "http://localhost:8081/api/sso/saml2/callback",
|
|
1319
|
+
wantAssertionsSigned: false,
|
|
1320
|
+
signatureAlgorithm: "sha256",
|
|
1321
|
+
digestAlgorithm: "sha256",
|
|
1322
|
+
identifierFormat:
|
|
1323
|
+
"urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress",
|
|
1324
|
+
},
|
|
1325
|
+
});
|
|
1326
|
+
});
|
|
1121
1327
|
});
|
package/src/types.ts
CHANGED
|
@@ -177,6 +177,29 @@ export interface SSOOptions {
|
|
|
177
177
|
* sign-in need to be called with with requestSignUp as true to create new users.
|
|
178
178
|
*/
|
|
179
179
|
disableImplicitSignUp?: boolean | undefined;
|
|
180
|
+
/**
|
|
181
|
+
* The model name for the SSO provider table. Defaults to "ssoProvider".
|
|
182
|
+
*/
|
|
183
|
+
modelName?: string;
|
|
184
|
+
/**
|
|
185
|
+
* Map fields
|
|
186
|
+
*
|
|
187
|
+
* @example
|
|
188
|
+
* ```ts
|
|
189
|
+
* {
|
|
190
|
+
* samlConfig: "saml_config"
|
|
191
|
+
* }
|
|
192
|
+
* ```
|
|
193
|
+
*/
|
|
194
|
+
fields?: {
|
|
195
|
+
issuer?: string | undefined;
|
|
196
|
+
oidcConfig?: string | undefined;
|
|
197
|
+
samlConfig?: string | undefined;
|
|
198
|
+
userId?: string | undefined;
|
|
199
|
+
providerId?: string | undefined;
|
|
200
|
+
organizationId?: string | undefined;
|
|
201
|
+
domain?: string | undefined;
|
|
202
|
+
};
|
|
180
203
|
/**
|
|
181
204
|
* Configure the maximum number of SSO providers a user can register.
|
|
182
205
|
* You can also pass a function that returns a number.
|
package/vitest.config.ts
ADDED