@better-auth/sso 1.5.0-beta.7 → 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 +5 -5
- package/dist/client.d.mts +1 -1
- package/dist/{index-BT0wtuq1.d.mts → index-78tha8qZ.d.mts} +0 -3
- package/dist/index.d.mts +1 -1
- package/dist/index.mjs +3 -5
- package/package.json +8 -8
- package/src/oidc.test.ts +112 -0
- package/src/routes/sso.ts +7 -8
- package/src/saml.test.ts +170 -3
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
> @better-auth/sso@1.5.0-beta.
|
|
2
|
+
> @better-auth/sso@1.5.0-beta.9 build /home/runner/work/better-auth/better-auth/packages/sso
|
|
3
3
|
> tsdown
|
|
4
4
|
|
|
5
5
|
[34mℹ[39m tsdown [2mv0.19.0[22m powered by rolldown [2mv1.0.0-beta.59[22m
|
|
@@ -7,10 +7,10 @@
|
|
|
7
7
|
[34mℹ[39m entry: [34msrc/index.ts, src/client.ts[39m
|
|
8
8
|
[34mℹ[39m tsconfig: [34mtsconfig.json[39m
|
|
9
9
|
[34mℹ[39m Build start
|
|
10
|
-
[34mℹ[39m [2mdist/[22m[1mindex.mjs[22m [
|
|
10
|
+
[34mℹ[39m [2mdist/[22m[1mindex.mjs[22m [2m103.94 kB[22m [2m│ gzip: 20.68 kB[22m
|
|
11
11
|
[34mℹ[39m [2mdist/[22m[1mclient.mjs[22m [2m 0.15 kB[22m [2m│ gzip: 0.14 kB[22m
|
|
12
12
|
[34mℹ[39m [2mdist/[22m[32m[1mindex.d.mts[22m[39m [2m 1.67 kB[22m [2m│ gzip: 0.57 kB[22m
|
|
13
13
|
[34mℹ[39m [2mdist/[22m[32m[1mclient.d.mts[22m[39m [2m 0.49 kB[22m [2m│ gzip: 0.29 kB[22m
|
|
14
|
-
[34mℹ[39m [2mdist/[22m[32mindex-
|
|
15
|
-
[34mℹ[39m 5 files, total: 150.
|
|
16
|
-
[32m✔[39m Build complete in [
|
|
14
|
+
[34mℹ[39m [2mdist/[22m[32mindex-78tha8qZ.d.mts[39m [2m 44.39 kB[22m [2m│ gzip: 9.18 kB[22m
|
|
15
|
+
[34mℹ[39m 5 files, total: 150.64 kB
|
|
16
|
+
[32m✔[39m Build complete in [32m19834ms[39m
|
package/dist/client.d.mts
CHANGED
|
@@ -948,9 +948,6 @@ declare const callbackSSOSAML: (options?: SSOOptions) => better_call0.StrictEndp
|
|
|
948
948
|
}, never>;
|
|
949
949
|
declare const acsEndpoint: (options?: SSOOptions) => better_call0.StrictEndpoint<"/sso/saml2/sp/acs/:providerId", {
|
|
950
950
|
method: "POST";
|
|
951
|
-
params: z.ZodObject<{
|
|
952
|
-
providerId: z.ZodOptional<z.ZodString>;
|
|
953
|
-
}, z.core.$strip>;
|
|
954
951
|
body: z.ZodObject<{
|
|
955
952
|
SAMLResponse: z.ZodString;
|
|
956
953
|
RelayState: z.ZodOptional<z.ZodString>;
|
package/dist/index.d.mts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { A as DataEncryptionAlgorithm, C as TimestampValidationOptions, D as SSOOptions, E as SAMLConfig, M as DigestAlgorithm, N as KeyEncryptionAlgorithm, O as SSOProvider, P as SignatureAlgorithm, S as SAMLConditions, T as OIDCConfig, _ as REQUIRED_DISCOVERY_FIELDS, a as fetchDiscoveryDocument, b as DEFAULT_MAX_SAML_METADATA_SIZE, 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 validateSAMLTimestamp, x as DEFAULT_MAX_SAML_RESPONSE_SIZE, y as DEFAULT_CLOCK_SKEW_MS } from "./index-
|
|
1
|
+
import { A as DataEncryptionAlgorithm, C as TimestampValidationOptions, D as SSOOptions, E as SAMLConfig, M as DigestAlgorithm, N as KeyEncryptionAlgorithm, O as SSOProvider, P as SignatureAlgorithm, S as SAMLConditions, T as OIDCConfig, _ as REQUIRED_DISCOVERY_FIELDS, a as fetchDiscoveryDocument, b as DEFAULT_MAX_SAML_METADATA_SIZE, 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 validateSAMLTimestamp, x as DEFAULT_MAX_SAML_RESPONSE_SIZE, y as DEFAULT_CLOCK_SKEW_MS } from "./index-78tha8qZ.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
|
@@ -2108,7 +2108,7 @@ const callbackSSOSAML = (options) => {
|
|
|
2108
2108
|
const userInfo = {
|
|
2109
2109
|
...Object.fromEntries(Object.entries(mapping.extraFields || {}).map(([key, value]) => [key, attributes[value]])),
|
|
2110
2110
|
id: attributes[mapping.id || "nameID"] || extract.nameID,
|
|
2111
|
-
email: attributes[mapping.email || "email"] || extract.nameID,
|
|
2111
|
+
email: (attributes[mapping.email || "email"] || extract.nameID).toLowerCase(),
|
|
2112
2112
|
name: [attributes[mapping.firstName || "givenName"], attributes[mapping.lastName || "surname"]].filter(Boolean).join(" ") || attributes[mapping.name || "displayName"] || extract.nameID,
|
|
2113
2113
|
emailVerified: options?.trustEmailVerified && mapping.emailVerified ? attributes[mapping.emailVerified] || false : false
|
|
2114
2114
|
};
|
|
@@ -2168,7 +2168,6 @@ const callbackSSOSAML = (options) => {
|
|
|
2168
2168
|
throw ctx.redirect(safeRedirectUrl);
|
|
2169
2169
|
});
|
|
2170
2170
|
};
|
|
2171
|
-
const acsEndpointParamsSchema = z.object({ providerId: z.string().optional() });
|
|
2172
2171
|
const acsEndpointBodySchema = z.object({
|
|
2173
2172
|
SAMLResponse: z.string(),
|
|
2174
2173
|
RelayState: z.string().optional()
|
|
@@ -2176,7 +2175,6 @@ const acsEndpointBodySchema = z.object({
|
|
|
2176
2175
|
const acsEndpoint = (options) => {
|
|
2177
2176
|
return createAuthEndpoint("/sso/saml2/sp/acs/:providerId", {
|
|
2178
2177
|
method: "POST",
|
|
2179
|
-
params: acsEndpointParamsSchema,
|
|
2180
2178
|
body: acsEndpointBodySchema,
|
|
2181
2179
|
metadata: {
|
|
2182
2180
|
...HIDE_METADATA,
|
|
@@ -2208,7 +2206,7 @@ const acsEndpoint = (options) => {
|
|
|
2208
2206
|
model: "ssoProvider",
|
|
2209
2207
|
where: [{
|
|
2210
2208
|
field: "providerId",
|
|
2211
|
-
value: providerId
|
|
2209
|
+
value: providerId
|
|
2212
2210
|
}]
|
|
2213
2211
|
}).then((res) => {
|
|
2214
2212
|
if (!res) return null;
|
|
@@ -2354,7 +2352,7 @@ const acsEndpoint = (options) => {
|
|
|
2354
2352
|
const userInfo = {
|
|
2355
2353
|
...Object.fromEntries(Object.entries(mapping.extraFields || {}).map(([key, value]) => [key, attributes[value]])),
|
|
2356
2354
|
id: attributes[mapping.id || "nameID"] || extract.nameID,
|
|
2357
|
-
email: attributes[mapping.email || "email"] || extract.nameID,
|
|
2355
|
+
email: (attributes[mapping.email || "email"] || extract.nameID).toLowerCase(),
|
|
2358
2356
|
name: [attributes[mapping.firstName || "givenName"], attributes[mapping.lastName || "surname"]].filter(Boolean).join(" ") || attributes[mapping.name || "displayName"] || extract.nameID,
|
|
2359
2357
|
emailVerified: options?.trustEmailVerified && mapping.emailVerified ? attributes[mapping.emailVerified] || false : false
|
|
2360
2358
|
};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@better-auth/sso",
|
|
3
3
|
"author": "Bereket Engida",
|
|
4
|
-
"version": "1.5.0-beta.
|
|
4
|
+
"version": "1.5.0-beta.9",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.mjs",
|
|
7
7
|
"types": "dist/index.d.mts",
|
|
@@ -57,23 +57,23 @@
|
|
|
57
57
|
"fast-xml-parser": "^5.2.5",
|
|
58
58
|
"jose": "^6.1.0",
|
|
59
59
|
"samlify": "^2.10.1",
|
|
60
|
-
"zod": "^4.
|
|
60
|
+
"zod": "^4.3.5"
|
|
61
61
|
},
|
|
62
62
|
"devDependencies": {
|
|
63
63
|
"@types/body-parser": "^1.19.6",
|
|
64
|
-
"@types/express": "^5.0.
|
|
65
|
-
"better-call": "1.
|
|
64
|
+
"@types/express": "^5.0.6",
|
|
65
|
+
"better-call": "1.2.0",
|
|
66
66
|
"body-parser": "^2.2.1",
|
|
67
67
|
"express": "^5.1.0",
|
|
68
68
|
"oauth2-mock-server": "^8.2.0",
|
|
69
69
|
"tsdown": "^0.19.0",
|
|
70
|
-
"@better-auth/core": "1.5.0-beta.
|
|
71
|
-
"better-auth": "1.5.0-beta.
|
|
70
|
+
"@better-auth/core": "1.5.0-beta.9",
|
|
71
|
+
"better-auth": "1.5.0-beta.9"
|
|
72
72
|
},
|
|
73
73
|
"peerDependencies": {
|
|
74
74
|
"@better-auth/utils": "0.3.0",
|
|
75
|
-
"@better-auth/core": "1.5.0-beta.
|
|
76
|
-
"better-auth": "1.5.0-beta.
|
|
75
|
+
"@better-auth/core": "1.5.0-beta.9",
|
|
76
|
+
"better-auth": "1.5.0-beta.9"
|
|
77
77
|
},
|
|
78
78
|
"scripts": {
|
|
79
79
|
"test": "vitest",
|
package/src/oidc.test.ts
CHANGED
|
@@ -253,6 +253,118 @@ describe("SSO", async () => {
|
|
|
253
253
|
const { callbackURL } = await simulateOAuthFlow(res.url, headers);
|
|
254
254
|
expect(callbackURL).toContain("/dashboard");
|
|
255
255
|
});
|
|
256
|
+
|
|
257
|
+
it("should normalize email to lowercase in OIDC authentication", async () => {
|
|
258
|
+
const { headers } = await signInWithTestUser();
|
|
259
|
+
|
|
260
|
+
// Register a new provider for this test
|
|
261
|
+
await auth.api.registerSSOProvider({
|
|
262
|
+
body: {
|
|
263
|
+
providerId: "email-case-oidc-provider",
|
|
264
|
+
issuer: server.issuer.url!,
|
|
265
|
+
domain: "email-case-test.com",
|
|
266
|
+
oidcConfig: {
|
|
267
|
+
clientId: "email-case-test-client",
|
|
268
|
+
clientSecret: "test-client-secret",
|
|
269
|
+
discoveryEndpoint: `${server.issuer.url!}/.well-known/openid-configuration`,
|
|
270
|
+
pkce: false,
|
|
271
|
+
},
|
|
272
|
+
},
|
|
273
|
+
headers,
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
// Store original listeners and set up mixed-case email
|
|
277
|
+
const originalUserinfoListeners =
|
|
278
|
+
server.service.listeners("beforeUserinfo");
|
|
279
|
+
const originalTokenListeners =
|
|
280
|
+
server.service.listeners("beforeTokenSigning");
|
|
281
|
+
|
|
282
|
+
server.service.removeAllListeners("beforeUserinfo");
|
|
283
|
+
server.service.removeAllListeners("beforeTokenSigning");
|
|
284
|
+
|
|
285
|
+
const mixedCaseEmail = "OIDCUser@Example.COM";
|
|
286
|
+
|
|
287
|
+
server.service.on("beforeUserinfo", (userInfoResponse) => {
|
|
288
|
+
userInfoResponse.body = {
|
|
289
|
+
email: mixedCaseEmail,
|
|
290
|
+
name: "OIDC Test User",
|
|
291
|
+
sub: "oidc-email-case-test-user",
|
|
292
|
+
picture: "https://test.com/picture.png",
|
|
293
|
+
email_verified: true,
|
|
294
|
+
};
|
|
295
|
+
userInfoResponse.statusCode = 200;
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
server.service.on("beforeTokenSigning", (token) => {
|
|
299
|
+
token.payload.email = mixedCaseEmail;
|
|
300
|
+
token.payload.email_verified = true;
|
|
301
|
+
token.payload.name = "OIDC Test User";
|
|
302
|
+
token.payload.sub = "oidc-email-case-test-user";
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
try {
|
|
306
|
+
// First sign in - should create user with lowercase email
|
|
307
|
+
const signInHeaders1 = new Headers();
|
|
308
|
+
const res1 = await authClient.signIn.sso({
|
|
309
|
+
email: `user@email-case-test.com`,
|
|
310
|
+
callbackURL: "/dashboard",
|
|
311
|
+
fetchOptions: {
|
|
312
|
+
throw: true,
|
|
313
|
+
onSuccess: cookieSetter(signInHeaders1),
|
|
314
|
+
},
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
const { callbackURL: callbackURL1, headers: sessionHeaders1 } =
|
|
318
|
+
await simulateOAuthFlow(res1.url, signInHeaders1);
|
|
319
|
+
expect(callbackURL1).toContain("/dashboard");
|
|
320
|
+
|
|
321
|
+
// Get session and verify email is lowercase
|
|
322
|
+
const session1 = await authClient.getSession({
|
|
323
|
+
fetchOptions: {
|
|
324
|
+
headers: sessionHeaders1,
|
|
325
|
+
},
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
expect(session1.data?.user.email).toBe("oidcuser@example.com");
|
|
329
|
+
const firstUserId = session1.data?.user.id;
|
|
330
|
+
expect(firstUserId).toBeDefined();
|
|
331
|
+
|
|
332
|
+
// Second sign in with same mixed-case email - should find existing user
|
|
333
|
+
const signInHeaders2 = new Headers();
|
|
334
|
+
const res2 = await authClient.signIn.sso({
|
|
335
|
+
email: `user@email-case-test.com`,
|
|
336
|
+
callbackURL: "/dashboard",
|
|
337
|
+
fetchOptions: {
|
|
338
|
+
throw: true,
|
|
339
|
+
onSuccess: cookieSetter(signInHeaders2),
|
|
340
|
+
},
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
const { callbackURL: callbackURL2, headers: sessionHeaders2 } =
|
|
344
|
+
await simulateOAuthFlow(res2.url, signInHeaders2);
|
|
345
|
+
expect(callbackURL2).toContain("/dashboard");
|
|
346
|
+
|
|
347
|
+
// Verify same user is returned
|
|
348
|
+
const session2 = await authClient.getSession({
|
|
349
|
+
fetchOptions: {
|
|
350
|
+
headers: sessionHeaders2,
|
|
351
|
+
},
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
expect(session2.data?.user.id).toBe(firstUserId);
|
|
355
|
+
expect(session2.data?.user.email).toBe("oidcuser@example.com");
|
|
356
|
+
} finally {
|
|
357
|
+
// Restore original listeners
|
|
358
|
+
server.service.removeAllListeners("beforeUserinfo");
|
|
359
|
+
server.service.removeAllListeners("beforeTokenSigning");
|
|
360
|
+
for (const listener of originalUserinfoListeners) {
|
|
361
|
+
server.service.on("beforeUserinfo", listener);
|
|
362
|
+
}
|
|
363
|
+
for (const listener of originalTokenListeners) {
|
|
364
|
+
server.service.on("beforeTokenSigning", listener);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
});
|
|
256
368
|
});
|
|
257
369
|
|
|
258
370
|
describe("SSO disable implicit sign in", async () => {
|
package/src/routes/sso.ts
CHANGED
|
@@ -2148,7 +2148,9 @@ export const callbackSSOSAML = (options?: SSOOptions) => {
|
|
|
2148
2148
|
]),
|
|
2149
2149
|
),
|
|
2150
2150
|
id: attributes[mapping.id || "nameID"] || extract.nameID,
|
|
2151
|
-
email:
|
|
2151
|
+
email: (
|
|
2152
|
+
attributes[mapping.email || "email"] || extract.nameID
|
|
2153
|
+
).toLowerCase(),
|
|
2152
2154
|
name:
|
|
2153
2155
|
[
|
|
2154
2156
|
attributes[mapping.firstName || "givenName"],
|
|
@@ -2252,10 +2254,6 @@ export const callbackSSOSAML = (options?: SSOOptions) => {
|
|
|
2252
2254
|
);
|
|
2253
2255
|
};
|
|
2254
2256
|
|
|
2255
|
-
const acsEndpointParamsSchema = z.object({
|
|
2256
|
-
providerId: z.string().optional(),
|
|
2257
|
-
});
|
|
2258
|
-
|
|
2259
2257
|
const acsEndpointBodySchema = z.object({
|
|
2260
2258
|
SAMLResponse: z.string(),
|
|
2261
2259
|
RelayState: z.string().optional(),
|
|
@@ -2266,7 +2264,6 @@ export const acsEndpoint = (options?: SSOOptions) => {
|
|
|
2266
2264
|
"/sso/saml2/sp/acs/:providerId",
|
|
2267
2265
|
{
|
|
2268
2266
|
method: "POST",
|
|
2269
|
-
params: acsEndpointParamsSchema,
|
|
2270
2267
|
body: acsEndpointBodySchema,
|
|
2271
2268
|
metadata: {
|
|
2272
2269
|
...HIDE_METADATA,
|
|
@@ -2330,7 +2327,7 @@ export const acsEndpoint = (options?: SSOOptions) => {
|
|
|
2330
2327
|
where: [
|
|
2331
2328
|
{
|
|
2332
2329
|
field: "providerId",
|
|
2333
|
-
value: providerId
|
|
2330
|
+
value: providerId,
|
|
2334
2331
|
},
|
|
2335
2332
|
],
|
|
2336
2333
|
})
|
|
@@ -2613,7 +2610,9 @@ export const acsEndpoint = (options?: SSOOptions) => {
|
|
|
2613
2610
|
]),
|
|
2614
2611
|
),
|
|
2615
2612
|
id: attributes[mapping.id || "nameID"] || extract.nameID,
|
|
2616
|
-
email:
|
|
2613
|
+
email: (
|
|
2614
|
+
attributes[mapping.email || "email"] || extract.nameID
|
|
2615
|
+
).toLowerCase(),
|
|
2617
2616
|
name:
|
|
2618
2617
|
[
|
|
2619
2618
|
attributes[mapping.firstName || "givenName"],
|
package/src/saml.test.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { betterAuth } from "better-auth";
|
|
|
5
5
|
import { memoryAdapter } from "better-auth/adapters/memory";
|
|
6
6
|
import { APIError } from "better-auth/api";
|
|
7
7
|
import { createAuthClient } from "better-auth/client";
|
|
8
|
-
import { setCookieToHeader } from "better-auth/cookies";
|
|
8
|
+
import { parseSetCookieHeader, setCookieToHeader } from "better-auth/cookies";
|
|
9
9
|
import { bearer } from "better-auth/plugins";
|
|
10
10
|
import { getTestInstance } from "better-auth/test";
|
|
11
11
|
import bodyParser from "body-parser";
|
|
@@ -399,7 +399,14 @@ const createMockSAMLIdP = (port: number) => {
|
|
|
399
399
|
app.get(
|
|
400
400
|
"/api/sso/saml2/idp/post",
|
|
401
401
|
async (req: ExpressRequest, res: ExpressResponse) => {
|
|
402
|
-
const
|
|
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
|
+
};
|
|
403
410
|
const { context, entityEndpoint } = await idp.createLoginResponse(
|
|
404
411
|
sp,
|
|
405
412
|
{} as any,
|
|
@@ -413,7 +420,14 @@ const createMockSAMLIdP = (port: number) => {
|
|
|
413
420
|
app.get(
|
|
414
421
|
"/api/sso/saml2/idp/redirect",
|
|
415
422
|
async (req: ExpressRequest, res: ExpressResponse) => {
|
|
416
|
-
const
|
|
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
|
+
};
|
|
417
431
|
const { context, entityEndpoint } = await idp.createLoginResponse(
|
|
418
432
|
sp,
|
|
419
433
|
{} as any,
|
|
@@ -4000,4 +4014,157 @@ describe("SAML SSO - Single Assertion Validation", () => {
|
|
|
4000
4014
|
expect(response.status).toBe(302);
|
|
4001
4015
|
expect(response.headers.get("location")).not.toContain("error");
|
|
4002
4016
|
});
|
|
4017
|
+
|
|
4018
|
+
it("should normalize email to lowercase in SAML authentication to prevent duplicate creation", async () => {
|
|
4019
|
+
const { auth, client, signInWithTestUser, db } = await getTestInstance({
|
|
4020
|
+
plugins: [sso()],
|
|
4021
|
+
});
|
|
4022
|
+
|
|
4023
|
+
const { headers } = await signInWithTestUser();
|
|
4024
|
+
|
|
4025
|
+
await auth.api.registerSSOProvider({
|
|
4026
|
+
body: {
|
|
4027
|
+
providerId: "email-case-provider",
|
|
4028
|
+
issuer: "http://localhost:8081",
|
|
4029
|
+
domain: "example.com",
|
|
4030
|
+
samlConfig: {
|
|
4031
|
+
entryPoint: "http://localhost:8081/api/sso/saml2/idp/post",
|
|
4032
|
+
cert: certificate,
|
|
4033
|
+
callbackUrl: "http://localhost:3000/dashboard",
|
|
4034
|
+
wantAssertionsSigned: false,
|
|
4035
|
+
signatureAlgorithm: "sha256",
|
|
4036
|
+
digestAlgorithm: "sha256",
|
|
4037
|
+
idpMetadata: {
|
|
4038
|
+
metadata: idpMetadata,
|
|
4039
|
+
},
|
|
4040
|
+
spMetadata: {
|
|
4041
|
+
metadata: spMetadata,
|
|
4042
|
+
},
|
|
4043
|
+
identifierFormat:
|
|
4044
|
+
"urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress",
|
|
4045
|
+
mapping: {
|
|
4046
|
+
id: "nameID",
|
|
4047
|
+
email: "nameID",
|
|
4048
|
+
name: "displayName",
|
|
4049
|
+
},
|
|
4050
|
+
},
|
|
4051
|
+
},
|
|
4052
|
+
headers,
|
|
4053
|
+
});
|
|
4054
|
+
|
|
4055
|
+
let samlResponse1: { samlResponse: string } | undefined;
|
|
4056
|
+
await betterFetch(
|
|
4057
|
+
"http://localhost:8081/api/sso/saml2/idp/post?emailCase=mixed",
|
|
4058
|
+
{
|
|
4059
|
+
onSuccess: async (context) => {
|
|
4060
|
+
samlResponse1 = context.data as { samlResponse: string };
|
|
4061
|
+
},
|
|
4062
|
+
},
|
|
4063
|
+
);
|
|
4064
|
+
|
|
4065
|
+
expect(samlResponse1?.samlResponse).toBeDefined();
|
|
4066
|
+
|
|
4067
|
+
const firstCallbackResponse = await auth.handler(
|
|
4068
|
+
new Request(
|
|
4069
|
+
"http://localhost:3000/api/auth/sso/saml2/callback/email-case-provider",
|
|
4070
|
+
{
|
|
4071
|
+
method: "POST",
|
|
4072
|
+
headers: {
|
|
4073
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
4074
|
+
},
|
|
4075
|
+
body: new URLSearchParams({
|
|
4076
|
+
SAMLResponse: samlResponse1!.samlResponse,
|
|
4077
|
+
RelayState: "http://localhost:3000/dashboard",
|
|
4078
|
+
}),
|
|
4079
|
+
},
|
|
4080
|
+
),
|
|
4081
|
+
);
|
|
4082
|
+
|
|
4083
|
+
expect(firstCallbackResponse.status).toBe(302);
|
|
4084
|
+
expect(firstCallbackResponse.headers.get("location")).toContain(
|
|
4085
|
+
"dashboard",
|
|
4086
|
+
);
|
|
4087
|
+
expect(firstCallbackResponse.headers.get("location")).not.toContain(
|
|
4088
|
+
"error",
|
|
4089
|
+
);
|
|
4090
|
+
|
|
4091
|
+
const firstCookies = parseSetCookieHeader(
|
|
4092
|
+
firstCallbackResponse.headers.get("set-cookie") ?? "",
|
|
4093
|
+
);
|
|
4094
|
+
const firstSessionToken = firstCookies.get(
|
|
4095
|
+
"better-auth.session_token",
|
|
4096
|
+
)?.value;
|
|
4097
|
+
expect(firstSessionToken).toBeDefined();
|
|
4098
|
+
|
|
4099
|
+
const firstSession = await client.getSession({
|
|
4100
|
+
fetchOptions: {
|
|
4101
|
+
headers: {
|
|
4102
|
+
Cookie: `better-auth.session_token=${firstSessionToken}`,
|
|
4103
|
+
},
|
|
4104
|
+
},
|
|
4105
|
+
});
|
|
4106
|
+
|
|
4107
|
+
expect(firstSession.data?.user.email).toBe("testuser@example.com");
|
|
4108
|
+
const firstUserId = firstSession.data?.user.id;
|
|
4109
|
+
expect(firstUserId).toBeDefined();
|
|
4110
|
+
|
|
4111
|
+
let samlResponse2: { samlResponse: string } | undefined;
|
|
4112
|
+
await betterFetch(
|
|
4113
|
+
"http://localhost:8081/api/sso/saml2/idp/post?emailCase=mixed",
|
|
4114
|
+
{
|
|
4115
|
+
onSuccess: async (context) => {
|
|
4116
|
+
samlResponse2 = context.data as { samlResponse: string };
|
|
4117
|
+
},
|
|
4118
|
+
},
|
|
4119
|
+
);
|
|
4120
|
+
|
|
4121
|
+
const secondCallbackResponse = await auth.handler(
|
|
4122
|
+
new Request(
|
|
4123
|
+
"http://localhost:3000/api/auth/sso/saml2/callback/email-case-provider",
|
|
4124
|
+
{
|
|
4125
|
+
method: "POST",
|
|
4126
|
+
headers: {
|
|
4127
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
4128
|
+
},
|
|
4129
|
+
body: new URLSearchParams({
|
|
4130
|
+
SAMLResponse: samlResponse2!.samlResponse,
|
|
4131
|
+
RelayState: "http://localhost:3000/dashboard",
|
|
4132
|
+
}),
|
|
4133
|
+
},
|
|
4134
|
+
),
|
|
4135
|
+
);
|
|
4136
|
+
|
|
4137
|
+
expect(secondCallbackResponse.status).toBe(302);
|
|
4138
|
+
expect(secondCallbackResponse.headers.get("location")).toContain(
|
|
4139
|
+
"dashboard",
|
|
4140
|
+
);
|
|
4141
|
+
expect(secondCallbackResponse.headers.get("location")).not.toContain(
|
|
4142
|
+
"error",
|
|
4143
|
+
);
|
|
4144
|
+
|
|
4145
|
+
const secondCookies = parseSetCookieHeader(
|
|
4146
|
+
secondCallbackResponse.headers.get("set-cookie") ?? "",
|
|
4147
|
+
);
|
|
4148
|
+
const secondSessionToken = secondCookies.get(
|
|
4149
|
+
"better-auth.session_token",
|
|
4150
|
+
)?.value;
|
|
4151
|
+
expect(secondSessionToken).toBeDefined();
|
|
4152
|
+
|
|
4153
|
+
const secondSession = await client.getSession({
|
|
4154
|
+
fetchOptions: {
|
|
4155
|
+
headers: {
|
|
4156
|
+
Cookie: `better-auth.session_token=${secondSessionToken}`,
|
|
4157
|
+
},
|
|
4158
|
+
},
|
|
4159
|
+
});
|
|
4160
|
+
|
|
4161
|
+
expect(secondSession.data?.user.id).toBe(firstUserId);
|
|
4162
|
+
expect(secondSession.data?.user.email).toBe("testuser@example.com");
|
|
4163
|
+
|
|
4164
|
+
const users = (await db.findMany({ model: "user" })) as {
|
|
4165
|
+
email: string;
|
|
4166
|
+
}[];
|
|
4167
|
+
const samlUsers = users.filter((u) => u.email === "testuser@example.com");
|
|
4168
|
+
expect(samlUsers).toHaveLength(1);
|
|
4169
|
+
});
|
|
4003
4170
|
});
|