@better-auth/sso 1.4.0-beta.21 → 1.4.0-beta.23
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 +10 -10
- package/dist/client.d.mts +14 -3
- package/dist/client.mjs +1 -1
- package/dist/{index-C091fIpa.d.mts → index-xXD__4zM.d.mts} +195 -19
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +302 -51
- package/package.json +4 -4
- package/src/client.ts +20 -3
- package/src/domain-verification.test.ts +550 -0
- package/src/index.ts +66 -12
- package/src/routes/domain-verification.ts +275 -0
- package/src/routes/sso.ts +133 -19
- package/src/saml.test.ts +143 -1
- package/src/types.ts +51 -3
- package/src/utils.ts +10 -0
- package/vitest.config.ts +3 -0
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,
|
|
@@ -1183,3 +1183,145 @@ describe("SAML SSO", async () => {
|
|
|
1183
1183
|
});
|
|
1184
1184
|
});
|
|
1185
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
|
+
});
|
|
1327
|
+
});
|
package/src/types.ts
CHANGED
|
@@ -81,7 +81,7 @@ export interface SAMLConfig {
|
|
|
81
81
|
mapping?: SAMLMapping | undefined;
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
-
|
|
84
|
+
type BaseSSOProvider = {
|
|
85
85
|
issuer: string;
|
|
86
86
|
oidcConfig?: OIDCConfig | undefined;
|
|
87
87
|
samlConfig?: SAMLConfig | undefined;
|
|
@@ -91,6 +91,13 @@ export type SSOProvider = {
|
|
|
91
91
|
domain: string;
|
|
92
92
|
};
|
|
93
93
|
|
|
94
|
+
export type SSOProvider<O extends SSOOptions> =
|
|
95
|
+
O["domainVerification"] extends { enabled: true }
|
|
96
|
+
? {
|
|
97
|
+
domainVerified: boolean;
|
|
98
|
+
} & BaseSSOProvider
|
|
99
|
+
: BaseSSOProvider;
|
|
100
|
+
|
|
94
101
|
export interface SSOOptions {
|
|
95
102
|
/**
|
|
96
103
|
* custom function to provision a user when they sign in with an SSO provider.
|
|
@@ -112,7 +119,7 @@ export interface SSOOptions {
|
|
|
112
119
|
/**
|
|
113
120
|
* The SSO provider
|
|
114
121
|
*/
|
|
115
|
-
provider: SSOProvider
|
|
122
|
+
provider: SSOProvider<SSOOptions>;
|
|
116
123
|
}) => Promise<void>)
|
|
117
124
|
| undefined;
|
|
118
125
|
/**
|
|
@@ -138,7 +145,7 @@ export interface SSOOptions {
|
|
|
138
145
|
/**
|
|
139
146
|
* The SSO provider
|
|
140
147
|
*/
|
|
141
|
-
provider: SSOProvider
|
|
148
|
+
provider: SSOProvider<SSOOptions>;
|
|
142
149
|
}) => Promise<"member" | "admin">;
|
|
143
150
|
}
|
|
144
151
|
| undefined;
|
|
@@ -177,6 +184,29 @@ export interface SSOOptions {
|
|
|
177
184
|
* sign-in need to be called with with requestSignUp as true to create new users.
|
|
178
185
|
*/
|
|
179
186
|
disableImplicitSignUp?: boolean | undefined;
|
|
187
|
+
/**
|
|
188
|
+
* The model name for the SSO provider table. Defaults to "ssoProvider".
|
|
189
|
+
*/
|
|
190
|
+
modelName?: string;
|
|
191
|
+
/**
|
|
192
|
+
* Map fields
|
|
193
|
+
*
|
|
194
|
+
* @example
|
|
195
|
+
* ```ts
|
|
196
|
+
* {
|
|
197
|
+
* samlConfig: "saml_config"
|
|
198
|
+
* }
|
|
199
|
+
* ```
|
|
200
|
+
*/
|
|
201
|
+
fields?: {
|
|
202
|
+
issuer?: string | undefined;
|
|
203
|
+
oidcConfig?: string | undefined;
|
|
204
|
+
samlConfig?: string | undefined;
|
|
205
|
+
userId?: string | undefined;
|
|
206
|
+
providerId?: string | undefined;
|
|
207
|
+
organizationId?: string | undefined;
|
|
208
|
+
domain?: string | undefined;
|
|
209
|
+
};
|
|
180
210
|
/**
|
|
181
211
|
* Configure the maximum number of SSO providers a user can register.
|
|
182
212
|
* You can also pass a function that returns a number.
|
|
@@ -205,4 +235,22 @@ export interface SSOOptions {
|
|
|
205
235
|
* @default false
|
|
206
236
|
*/
|
|
207
237
|
trustEmailVerified?: boolean | undefined;
|
|
238
|
+
/**
|
|
239
|
+
* Enable domain verification on SSO providers
|
|
240
|
+
*
|
|
241
|
+
* When this option is enabled, new SSO providers will require the associated domain to be verified by the owner
|
|
242
|
+
* prior to allowing sign-ins.
|
|
243
|
+
*/
|
|
244
|
+
domainVerification?: {
|
|
245
|
+
/**
|
|
246
|
+
* Enables or disables the domain verification feature
|
|
247
|
+
*/
|
|
248
|
+
enabled?: boolean;
|
|
249
|
+
/**
|
|
250
|
+
* Prefix used to generate the domain verification token
|
|
251
|
+
*
|
|
252
|
+
* @default "better-auth-token-"
|
|
253
|
+
*/
|
|
254
|
+
tokenPrefix?: string;
|
|
255
|
+
};
|
|
208
256
|
}
|
package/src/utils.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export const validateEmailDomain = (email: string, domain: string) => {
|
|
2
|
+
const emailDomain = email.split("@")[1]?.toLowerCase();
|
|
3
|
+
const providerDomain = domain.toLowerCase();
|
|
4
|
+
if (!emailDomain || !providerDomain) {
|
|
5
|
+
return false;
|
|
6
|
+
}
|
|
7
|
+
return (
|
|
8
|
+
emailDomain === providerDomain || emailDomain.endsWith(`.${providerDomain}`)
|
|
9
|
+
);
|
|
10
|
+
};
|
package/vitest.config.ts
ADDED