@better-auth/sso 1.5.0-beta.8 → 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.
@@ -1,5 +1,5 @@
1
1
 
2
- > @better-auth/sso@1.5.0-beta.8 build /home/runner/work/better-auth/better-auth/packages/sso
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
  ℹ tsdown v0.19.0 powered by rolldown v1.0.0-beta.59
@@ -7,10 +7,10 @@
7
7
  ℹ entry: src/index.ts, src/client.ts
8
8
  ℹ tsconfig: tsconfig.json
9
9
  ℹ Build start
10
- ℹ dist/index.mjs 104.03 kB │ gzip: 20.69 kB
10
+ ℹ dist/index.mjs 103.94 kB │ gzip: 20.68 kB
11
11
  ℹ dist/client.mjs  0.15 kB │ gzip: 0.14 kB
12
12
  ℹ dist/index.d.mts  1.67 kB │ gzip: 0.57 kB
13
13
  ℹ dist/client.d.mts  0.49 kB │ gzip: 0.29 kB
14
- ℹ dist/index-BT0wtuq1.d.mts  44.48 kB │ gzip: 9.20 kB
15
- ℹ 5 files, total: 150.82 kB
16
- ✔ Build complete in 17021ms
14
+ ℹ dist/index-78tha8qZ.d.mts  44.39 kB │ gzip: 9.18 kB
15
+ ℹ 5 files, total: 150.64 kB
16
+ ✔ Build complete in 19834ms
package/dist/client.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { t as SSOPlugin } from "./index-BT0wtuq1.mjs";
1
+ import { t as SSOPlugin } from "./index-78tha8qZ.mjs";
2
2
 
3
3
  //#region src/client.d.ts
4
4
  interface SSOClientOptions {
@@ -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-BT0wtuq1.mjs";
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 ?? "sso"
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.8",
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.1.12"
60
+ "zod": "^4.3.5"
61
61
  },
62
62
  "devDependencies": {
63
63
  "@types/body-parser": "^1.19.6",
64
- "@types/express": "^5.0.5",
65
- "better-call": "1.1.8",
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": "1.5.0-beta.8",
71
- "@better-auth/core": "1.5.0-beta.8"
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.8",
76
- "better-auth": "1.5.0-beta.8"
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: attributes[mapping.email || "email"] || extract.nameID,
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 ?? "sso",
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: attributes[mapping.email || "email"] || extract.nameID,
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 user = { emailAddress: "test@email.com", famName: "hello world" };
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 user = { emailAddress: "test@email.com", famName: "hello world" };
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
  });