@hammadj/better-auth-sso 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.
Files changed (42) hide show
  1. package/.turbo/turbo-build.log +116 -0
  2. package/LICENSE.md +20 -0
  3. package/dist/client.d.mts +10 -0
  4. package/dist/client.mjs +15 -0
  5. package/dist/client.mjs.map +1 -0
  6. package/dist/index.d.mts +738 -0
  7. package/dist/index.mjs +2953 -0
  8. package/dist/index.mjs.map +1 -0
  9. package/package.json +87 -0
  10. package/src/client.ts +29 -0
  11. package/src/constants.ts +58 -0
  12. package/src/domain-verification.test.ts +551 -0
  13. package/src/index.ts +265 -0
  14. package/src/linking/index.ts +2 -0
  15. package/src/linking/org-assignment.test.ts +325 -0
  16. package/src/linking/org-assignment.ts +176 -0
  17. package/src/linking/types.ts +10 -0
  18. package/src/oidc/discovery.test.ts +1157 -0
  19. package/src/oidc/discovery.ts +494 -0
  20. package/src/oidc/errors.ts +92 -0
  21. package/src/oidc/index.ts +31 -0
  22. package/src/oidc/types.ts +219 -0
  23. package/src/oidc.test.ts +688 -0
  24. package/src/providers.test.ts +1326 -0
  25. package/src/routes/domain-verification.ts +275 -0
  26. package/src/routes/providers.ts +565 -0
  27. package/src/routes/schemas.ts +96 -0
  28. package/src/routes/sso.ts +2750 -0
  29. package/src/saml/algorithms.test.ts +449 -0
  30. package/src/saml/algorithms.ts +338 -0
  31. package/src/saml/assertions.test.ts +239 -0
  32. package/src/saml/assertions.ts +62 -0
  33. package/src/saml/index.ts +13 -0
  34. package/src/saml/parser.ts +56 -0
  35. package/src/saml-state.ts +78 -0
  36. package/src/saml.test.ts +4319 -0
  37. package/src/types.ts +365 -0
  38. package/src/utils.test.ts +103 -0
  39. package/src/utils.ts +81 -0
  40. package/tsconfig.json +14 -0
  41. package/tsdown.config.ts +9 -0
  42. package/vitest.config.ts +3 -0
@@ -0,0 +1,688 @@
1
+ import { betterFetch } from "@better-fetch/fetch";
2
+ import { createAuthClient } from "better-auth/client";
3
+ import { organization } from "better-auth/plugins";
4
+ import { getTestInstance } from "better-auth/test";
5
+ import { OAuth2Server } from "oauth2-mock-server";
6
+ import { afterAll, beforeAll, describe, expect, it } from "vitest";
7
+ import { sso } from ".";
8
+ import { ssoClient } from "./client";
9
+
10
+ const server = new OAuth2Server();
11
+
12
+ describe("SSO", async () => {
13
+ const { auth, signInWithTestUser, customFetchImpl, cookieSetter } =
14
+ await getTestInstance({
15
+ trustedOrigins: ["http://localhost:8080"],
16
+ plugins: [sso(), organization()],
17
+ });
18
+
19
+ const authClient = createAuthClient({
20
+ plugins: [ssoClient()],
21
+ baseURL: "http://localhost:3000",
22
+ fetchOptions: {
23
+ customFetchImpl,
24
+ },
25
+ });
26
+
27
+ beforeAll(async () => {
28
+ await server.issuer.keys.generate("RS256");
29
+ server.issuer.on;
30
+ await server.start(8080, "localhost");
31
+ console.log("Issuer URL:", server.issuer.url); // -> http://localhost:8080
32
+ });
33
+
34
+ afterAll(async () => {
35
+ await server.stop().catch(() => {});
36
+ });
37
+
38
+ server.service.on("beforeUserinfo", (userInfoResponse, req) => {
39
+ userInfoResponse.body = {
40
+ email: "oauth2@test.com",
41
+ name: "OAuth2 Test",
42
+ sub: "oauth2",
43
+ picture: "https://test.com/picture.png",
44
+ email_verified: true,
45
+ };
46
+ userInfoResponse.statusCode = 200;
47
+ });
48
+
49
+ server.service.on("beforeTokenSigning", (token, req) => {
50
+ token.payload.email = "sso-user@localhost:8000.com";
51
+ token.payload.email_verified = true;
52
+ token.payload.name = "Test User";
53
+ token.payload.picture = "https://test.com/picture.png";
54
+ });
55
+
56
+ async function simulateOAuthFlow(
57
+ authUrl: string,
58
+ headers: Headers,
59
+ fetchImpl?: (...args: any) => any,
60
+ ) {
61
+ let location: string | null = null;
62
+ await betterFetch(authUrl, {
63
+ method: "GET",
64
+ redirect: "manual",
65
+ onError(context) {
66
+ location = context.response.headers.get("location");
67
+ },
68
+ });
69
+
70
+ if (!location) throw new Error("No redirect location found");
71
+ const newHeaders = new Headers();
72
+ let callbackURL = "";
73
+ await betterFetch(location, {
74
+ method: "GET",
75
+ customFetchImpl: fetchImpl || customFetchImpl,
76
+ headers,
77
+ onError(context) {
78
+ callbackURL = context.response.headers.get("location") || "";
79
+ cookieSetter(newHeaders)(context);
80
+ },
81
+ });
82
+
83
+ return { callbackURL, headers: newHeaders };
84
+ }
85
+
86
+ it("should register a new SSO provider", async () => {
87
+ const { headers } = await signInWithTestUser();
88
+ const provider = await auth.api.registerSSOProvider({
89
+ body: {
90
+ issuer: server.issuer.url!,
91
+ domain: "localhost.com",
92
+ oidcConfig: {
93
+ clientId: "test",
94
+ clientSecret: "test",
95
+ authorizationEndpoint: `${server.issuer.url}/authorize`,
96
+ tokenEndpoint: `${server.issuer.url}/token`,
97
+ jwksEndpoint: `${server.issuer.url}/jwks`,
98
+ discoveryEndpoint: `${server.issuer.url}/.well-known/openid-configuration`,
99
+ mapping: {
100
+ id: "sub",
101
+ email: "email",
102
+ emailVerified: "email_verified",
103
+ name: "name",
104
+ image: "picture",
105
+ },
106
+ },
107
+ providerId: "test",
108
+ },
109
+ headers,
110
+ });
111
+ expect(provider).toMatchObject({
112
+ id: expect.any(String),
113
+ issuer: "http://localhost:8080",
114
+ oidcConfig: {
115
+ issuer: "http://localhost:8080",
116
+ clientId: "test",
117
+ clientSecret: "test",
118
+ authorizationEndpoint: "http://localhost:8080/authorize",
119
+ tokenEndpoint: "http://localhost:8080/token",
120
+ jwksEndpoint: "http://localhost:8080/jwks",
121
+ discoveryEndpoint:
122
+ "http://localhost:8080/.well-known/openid-configuration",
123
+ mapping: {
124
+ id: "sub",
125
+ email: "email",
126
+ emailVerified: "email_verified",
127
+ name: "name",
128
+ image: "picture",
129
+ },
130
+ },
131
+ userId: expect.any(String),
132
+ });
133
+ });
134
+
135
+ it("should fail to register a new SSO provider with invalid issuer", async () => {
136
+ const { headers } = await signInWithTestUser();
137
+
138
+ try {
139
+ await auth.api.registerSSOProvider({
140
+ body: {
141
+ issuer: "invalid",
142
+ domain: "localhost",
143
+ providerId: "test",
144
+ oidcConfig: {
145
+ clientId: "test",
146
+ clientSecret: "test",
147
+ },
148
+ },
149
+ headers,
150
+ });
151
+ } catch (e) {
152
+ expect(e).toMatchObject({
153
+ status: "BAD_REQUEST",
154
+ body: {
155
+ message: "Invalid issuer. Must be a valid URL",
156
+ },
157
+ });
158
+ }
159
+ });
160
+
161
+ it("should not allow creating a provider with duplicate providerId", async () => {
162
+ const { headers } = await signInWithTestUser();
163
+
164
+ await auth.api.registerSSOProvider({
165
+ body: {
166
+ issuer: server.issuer.url!,
167
+ domain: "duplicate.com",
168
+ providerId: "duplicate-oidc-provider",
169
+ oidcConfig: {
170
+ clientId: "test",
171
+ clientSecret: "test",
172
+ },
173
+ },
174
+ headers,
175
+ });
176
+
177
+ await expect(
178
+ auth.api.registerSSOProvider({
179
+ body: {
180
+ issuer: server.issuer.url!,
181
+ domain: "another-duplicate.com",
182
+ providerId: "duplicate-oidc-provider",
183
+ oidcConfig: {
184
+ clientId: "test2",
185
+ clientSecret: "test2",
186
+ },
187
+ },
188
+ headers,
189
+ }),
190
+ ).rejects.toMatchObject({
191
+ status: "UNPROCESSABLE_ENTITY",
192
+ body: {
193
+ message: "SSO provider with this providerId already exists",
194
+ },
195
+ });
196
+ });
197
+
198
+ it("should sign in with SSO provider with email matching", async () => {
199
+ const headers = new Headers();
200
+ const res = await authClient.signIn.sso({
201
+ email: "my-email@localhost.com",
202
+ callbackURL: "/dashboard",
203
+ fetchOptions: {
204
+ throw: true,
205
+ onSuccess: cookieSetter(headers),
206
+ },
207
+ });
208
+ expect(res.url).toContain("http://localhost:8080/authorize");
209
+ expect(res.url).toContain(
210
+ "redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fapi%2Fauth%2Fsso%2Fcallback%2Ftest",
211
+ );
212
+ expect(res.url).toContain("login_hint=my-email%40localhost.com");
213
+ const { callbackURL } = await simulateOAuthFlow(res.url, headers);
214
+ expect(callbackURL).toContain("/dashboard");
215
+ });
216
+
217
+ it("should sign in with SSO provider with domain", async () => {
218
+ const headers = new Headers();
219
+ const res = await authClient.signIn.sso({
220
+ email: "my-email@test.com",
221
+ domain: "localhost.com",
222
+ callbackURL: "/dashboard",
223
+ fetchOptions: {
224
+ throw: true,
225
+ onSuccess: cookieSetter(headers),
226
+ },
227
+ });
228
+ expect(res.url).toContain("http://localhost:8080/authorize");
229
+ expect(res.url).toContain(
230
+ "redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fapi%2Fauth%2Fsso%2Fcallback%2Ftest",
231
+ );
232
+ const { callbackURL } = await simulateOAuthFlow(res.url, headers);
233
+ expect(callbackURL).toContain("/dashboard");
234
+ });
235
+
236
+ it("should sign in with SSO provider with providerId", async () => {
237
+ const headers = new Headers();
238
+ const res = await authClient.signIn.sso({
239
+ providerId: "test",
240
+ loginHint: "user@example.com",
241
+ callbackURL: "/dashboard",
242
+ fetchOptions: {
243
+ throw: true,
244
+ onSuccess: cookieSetter(headers),
245
+ },
246
+ });
247
+ expect(res.url).toContain("http://localhost:8080/authorize");
248
+ expect(res.url).toContain(
249
+ "redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fapi%2Fauth%2Fsso%2Fcallback%2Ftest",
250
+ );
251
+ expect(res.url).toContain("login_hint=user%40example.com");
252
+
253
+ const { callbackURL } = await simulateOAuthFlow(res.url, headers);
254
+ expect(callbackURL).toContain("/dashboard");
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
+ });
368
+ });
369
+
370
+ describe("SSO disable implicit sign in", async () => {
371
+ const { auth, signInWithTestUser, customFetchImpl, cookieSetter } =
372
+ await getTestInstance({
373
+ trustedOrigins: ["http://localhost:8080"],
374
+ plugins: [sso({ disableImplicitSignUp: true }), organization()],
375
+ });
376
+
377
+ const authClient = createAuthClient({
378
+ plugins: [ssoClient()],
379
+ baseURL: "http://localhost:3000",
380
+ fetchOptions: {
381
+ customFetchImpl,
382
+ },
383
+ });
384
+
385
+ beforeAll(async () => {
386
+ await server.issuer.keys.generate("RS256");
387
+ server.issuer.on;
388
+ await server.start(8080, "localhost");
389
+ console.log("Issuer URL:", server.issuer.url); // -> http://localhost:8080
390
+ });
391
+
392
+ afterAll(async () => {
393
+ await server.stop();
394
+ });
395
+
396
+ server.service.on("beforeUserinfo", (userInfoResponse, req) => {
397
+ userInfoResponse.body = {
398
+ email: "oauth2@test.com",
399
+ name: "OAuth2 Test",
400
+ sub: "oauth2",
401
+ picture: "https://test.com/picture.png",
402
+ email_verified: true,
403
+ };
404
+ userInfoResponse.statusCode = 200;
405
+ });
406
+
407
+ server.service.on("beforeTokenSigning", (token, req) => {
408
+ token.payload.email = "sso-user@localhost:8000.com";
409
+ token.payload.email_verified = true;
410
+ token.payload.name = "Test User";
411
+ token.payload.picture = "https://test.com/picture.png";
412
+ });
413
+
414
+ async function simulateOAuthFlow(
415
+ authUrl: string,
416
+ headers: Headers,
417
+ fetchImpl?: (...args: any) => any,
418
+ ) {
419
+ let location: string | null = null;
420
+ await betterFetch(authUrl, {
421
+ method: "GET",
422
+ redirect: "manual",
423
+ onError(context) {
424
+ location = context.response.headers.get("location");
425
+ },
426
+ });
427
+
428
+ if (!location) throw new Error("No redirect location found");
429
+ const newHeaders = new Headers(headers);
430
+ let callbackURL = "";
431
+ await betterFetch(location, {
432
+ method: "GET",
433
+ customFetchImpl: fetchImpl || customFetchImpl,
434
+ headers,
435
+ onError(context) {
436
+ callbackURL = context.response.headers.get("location") || "";
437
+ cookieSetter(newHeaders)(context);
438
+ },
439
+ });
440
+
441
+ return { callbackURL, headers: newHeaders };
442
+ }
443
+
444
+ it("should register a new SSO provider", async () => {
445
+ const { headers } = await signInWithTestUser();
446
+ const provider = await auth.api.registerSSOProvider({
447
+ body: {
448
+ issuer: server.issuer.url!,
449
+ domain: "localhost.com",
450
+ oidcConfig: {
451
+ clientId: "test",
452
+ clientSecret: "test",
453
+ authorizationEndpoint: `${server.issuer.url}/authorize`,
454
+ tokenEndpoint: `${server.issuer.url}/token`,
455
+ jwksEndpoint: `${server.issuer.url}/jwks`,
456
+ discoveryEndpoint: `${server.issuer.url}/.well-known/openid-configuration`,
457
+ mapping: {
458
+ id: "sub",
459
+ email: "email",
460
+ emailVerified: "email_verified",
461
+ name: "name",
462
+ image: "picture",
463
+ },
464
+ },
465
+ providerId: "test",
466
+ },
467
+ headers,
468
+ });
469
+ expect(provider).toMatchObject({
470
+ id: expect.any(String),
471
+ issuer: "http://localhost:8080",
472
+ oidcConfig: {
473
+ issuer: "http://localhost:8080",
474
+ clientId: "test",
475
+ clientSecret: "test",
476
+ authorizationEndpoint: "http://localhost:8080/authorize",
477
+ tokenEndpoint: "http://localhost:8080/token",
478
+ jwksEndpoint: "http://localhost:8080/jwks",
479
+ discoveryEndpoint:
480
+ "http://localhost:8080/.well-known/openid-configuration",
481
+ mapping: {
482
+ id: "sub",
483
+ email: "email",
484
+ emailVerified: "email_verified",
485
+ name: "name",
486
+ image: "picture",
487
+ },
488
+ },
489
+ userId: expect.any(String),
490
+ });
491
+ });
492
+
493
+ it("should not create user with SSO provider when sign ups are disabled", async () => {
494
+ const headers = new Headers();
495
+ const res = await authClient.signIn.sso({
496
+ email: "my-email@localhost.com",
497
+ callbackURL: "/dashboard",
498
+ fetchOptions: {
499
+ throw: true,
500
+ onSuccess: cookieSetter(headers),
501
+ },
502
+ });
503
+ expect(res.url).toContain("http://localhost:8080/authorize");
504
+ expect(res.url).toContain(
505
+ "redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fapi%2Fauth%2Fsso%2Fcallback%2Ftest",
506
+ );
507
+ const { callbackURL } = await simulateOAuthFlow(res.url, headers);
508
+ expect(callbackURL).toContain(
509
+ "/api/auth/error/error?error=signup disabled",
510
+ );
511
+ });
512
+
513
+ it("should create user with SSO provider when sign ups are disabled but sign up is requested", async () => {
514
+ const headers = new Headers();
515
+ const res = await authClient.signIn.sso({
516
+ email: "my-email@localhost.com",
517
+ callbackURL: "/dashboard",
518
+ requestSignUp: true,
519
+ fetchOptions: {
520
+ throw: true,
521
+ onSuccess: cookieSetter(headers),
522
+ },
523
+ });
524
+ expect(res.url).toContain("http://localhost:8080/authorize");
525
+ expect(res.url).toContain(
526
+ "redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fapi%2Fauth%2Fsso%2Fcallback%2Ftest",
527
+ );
528
+ const { callbackURL } = await simulateOAuthFlow(res.url, headers);
529
+ expect(callbackURL).toContain("/dashboard");
530
+ });
531
+ });
532
+
533
+ describe("provisioning", async (ctx) => {
534
+ const { auth, signInWithTestUser, customFetchImpl, cookieSetter } =
535
+ await getTestInstance({
536
+ trustedOrigins: ["http://localhost:8080"],
537
+ plugins: [sso(), organization()],
538
+ });
539
+
540
+ const authClient = createAuthClient({
541
+ plugins: [ssoClient()],
542
+ baseURL: "http://localhost:3000",
543
+ fetchOptions: {
544
+ customFetchImpl,
545
+ },
546
+ });
547
+
548
+ beforeAll(async () => {
549
+ await server.issuer.keys.generate("RS256");
550
+ server.issuer.on;
551
+ await server.start(8080, "localhost");
552
+ console.log("Issuer URL:", server.issuer.url); // -> http://localhost:8080
553
+ });
554
+
555
+ afterAll(async () => {
556
+ await server.stop();
557
+ });
558
+ async function simulateOAuthFlow(
559
+ authUrl: string,
560
+ headers: Headers,
561
+ fetchImpl?: (...args: any) => any,
562
+ ) {
563
+ let location: string | null = null;
564
+ await betterFetch(authUrl, {
565
+ method: "GET",
566
+ redirect: "manual",
567
+ onError(context) {
568
+ location = context.response.headers.get("location");
569
+ },
570
+ });
571
+
572
+ if (!location) throw new Error("No redirect location found");
573
+
574
+ let callbackURL = "";
575
+ const newHeaders = new Headers();
576
+ await betterFetch(location, {
577
+ method: "GET",
578
+ customFetchImpl: fetchImpl || customFetchImpl,
579
+ headers,
580
+ onError(context) {
581
+ callbackURL = context.response.headers.get("location") || "";
582
+ cookieSetter(newHeaders)(context);
583
+ },
584
+ });
585
+
586
+ return callbackURL;
587
+ }
588
+
589
+ server.service.on("beforeUserinfo", (userInfoResponse, req) => {
590
+ userInfoResponse.body = {
591
+ email: "test@localhost.com",
592
+ name: "OAuth2 Test",
593
+ sub: "oauth2",
594
+ picture: "https://test.com/picture.png",
595
+ email_verified: true,
596
+ };
597
+ userInfoResponse.statusCode = 200;
598
+ });
599
+
600
+ server.service.on("beforeTokenSigning", (token, req) => {
601
+ token.payload.email = "sso-user@localhost:8000.com";
602
+ token.payload.email_verified = true;
603
+ token.payload.name = "Test User";
604
+ token.payload.picture = "https://test.com/picture.png";
605
+ });
606
+ it("should provision user", async () => {
607
+ const { headers } = await signInWithTestUser();
608
+ const organization = await auth.api.createOrganization({
609
+ body: {
610
+ name: "Localhost",
611
+ slug: "localhost",
612
+ },
613
+ headers,
614
+ });
615
+ const provider = await auth.api.registerSSOProvider({
616
+ body: {
617
+ issuer: server.issuer.url!,
618
+ domain: "localhost.com",
619
+ oidcConfig: {
620
+ clientId: "test",
621
+ clientSecret: "test",
622
+ authorizationEndpoint: `${server.issuer.url}/authorize`,
623
+ tokenEndpoint: `${server.issuer.url}/token`,
624
+ jwksEndpoint: `${server.issuer.url}/jwks`,
625
+ discoveryEndpoint: `${server.issuer.url}/.well-known/openid-configuration`,
626
+ mapping: {
627
+ id: "sub",
628
+ email: "email",
629
+ emailVerified: "email_verified",
630
+ name: "name",
631
+ image: "picture",
632
+ },
633
+ },
634
+ providerId: "test2",
635
+ organizationId: organization?.id,
636
+ },
637
+ headers,
638
+ });
639
+ expect(provider).toMatchObject({
640
+ organizationId: organization?.id,
641
+ });
642
+ const newHeaders = new Headers();
643
+ const res = await authClient.signIn.sso({
644
+ email: "my-email@localhost.com",
645
+ callbackURL: "/dashboard",
646
+ fetchOptions: {
647
+ onSuccess: cookieSetter(newHeaders),
648
+ throw: true,
649
+ },
650
+ });
651
+ expect(res.url).toContain("http://localhost:8080/authorize");
652
+ expect(res.url).toContain(
653
+ "redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fapi%2Fauth%2Fsso%2Fcallback%2Ftest",
654
+ );
655
+
656
+ const callbackURL = await simulateOAuthFlow(res.url, newHeaders);
657
+ expect(callbackURL).toContain("/dashboard");
658
+ const org = await auth.api.getFullOrganization({
659
+ query: {
660
+ organizationId: organization?.id || "",
661
+ },
662
+ headers,
663
+ });
664
+ const member = org?.members.find(
665
+ (m: any) => m.user.email === "sso-user@localhost:8000.com",
666
+ );
667
+ expect(member).toMatchObject({
668
+ role: "member",
669
+ user: {
670
+ id: expect.any(String),
671
+ name: "Test User",
672
+ email: "sso-user@localhost:8000.com",
673
+ image: "https://test.com/picture.png",
674
+ },
675
+ });
676
+ });
677
+
678
+ it("should sign in with SSO provide with org slug", async () => {
679
+ const res = await auth.api.signInSSO({
680
+ body: {
681
+ organizationSlug: "localhost",
682
+ callbackURL: "/dashboard",
683
+ },
684
+ });
685
+
686
+ expect(res.url).toContain("http://localhost:8080/authorize");
687
+ });
688
+ });