@better-auth/sso 1.3.0-beta.1

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.
@@ -0,0 +1,488 @@
1
+ import { afterAll, beforeAll, describe, expect, it } from "vitest";
2
+ import { sso } from ".";
3
+ import { OAuth2Server } from "oauth2-mock-server";
4
+ import { betterFetch } from "@better-fetch/fetch";
5
+ import { organization } from "better-auth/plugins/organization";
6
+ import { getTestInstanceMemory } from "better-auth/test";
7
+
8
+ let server = new OAuth2Server();
9
+
10
+ describe("SSO", async () => {
11
+ const { auth, signInWithTestUser, customFetchImpl } =
12
+ await getTestInstanceMemory({
13
+ plugins: [sso(), organization()],
14
+ });
15
+
16
+ beforeAll(async () => {
17
+ await server.issuer.keys.generate("RS256");
18
+ server.issuer.on;
19
+ await server.start(8080, "localhost");
20
+ console.log("Issuer URL:", server.issuer.url); // -> http://localhost:8080
21
+ });
22
+
23
+ afterAll(async () => {
24
+ await server.stop().catch(() => {});
25
+ });
26
+
27
+ server.service.on("beforeUserinfo", (userInfoResponse, req) => {
28
+ userInfoResponse.body = {
29
+ email: "oauth2@test.com",
30
+ name: "OAuth2 Test",
31
+ sub: "oauth2",
32
+ picture: "https://test.com/picture.png",
33
+ email_verified: true,
34
+ };
35
+ userInfoResponse.statusCode = 200;
36
+ });
37
+
38
+ server.service.on("beforeTokenSigning", (token, req) => {
39
+ token.payload.email = "sso-user@localhost:8000.com";
40
+ token.payload.email_verified = true;
41
+ token.payload.name = "Test User";
42
+ token.payload.picture = "https://test.com/picture.png";
43
+ });
44
+
45
+ async function simulateOAuthFlow(
46
+ authUrl: string,
47
+ headers: Headers,
48
+ fetchImpl?: (...args: any) => any,
49
+ ) {
50
+ let location: string | null = null;
51
+ await betterFetch(authUrl, {
52
+ method: "GET",
53
+ redirect: "manual",
54
+ onError(context) {
55
+ location = context.response.headers.get("location");
56
+ },
57
+ });
58
+
59
+ if (!location) throw new Error("No redirect location found");
60
+
61
+ let callbackURL = "";
62
+ await betterFetch(location, {
63
+ method: "GET",
64
+ customFetchImpl: fetchImpl || customFetchImpl,
65
+ headers,
66
+ onError(context) {
67
+ callbackURL = context.response.headers.get("location") || "";
68
+ },
69
+ });
70
+
71
+ return callbackURL;
72
+ }
73
+
74
+ it("should register a new SSO provider", async () => {
75
+ const { headers } = await signInWithTestUser();
76
+ const provider = await auth.api.registerSSOProvider({
77
+ body: {
78
+ issuer: server.issuer.url!,
79
+ domain: "localhost.com",
80
+ oidcConfig: {
81
+ clientId: "test",
82
+ clientSecret: "test",
83
+ authorizationEndpoint: `${server.issuer.url}/authorize`,
84
+ tokenEndpoint: `${server.issuer.url}/token`,
85
+ jwksEndpoint: `${server.issuer.url}/jwks`,
86
+ discoveryEndpoint: `${server.issuer.url}/.well-known/openid-configuration`,
87
+ },
88
+ mapping: {
89
+ id: "sub",
90
+ email: "email",
91
+ emailVerified: "email_verified",
92
+ name: "name",
93
+ image: "picture",
94
+ },
95
+ providerId: "test",
96
+ },
97
+ headers,
98
+ });
99
+ expect(provider).toMatchObject({
100
+ id: expect.any(String),
101
+ issuer: "http://localhost:8080",
102
+ oidcConfig: {
103
+ issuer: "http://localhost:8080",
104
+ clientId: "test",
105
+ clientSecret: "test",
106
+ authorizationEndpoint: "http://localhost:8080/authorize",
107
+ tokenEndpoint: "http://localhost:8080/token",
108
+ jwksEndpoint: "http://localhost:8080/jwks",
109
+ discoveryEndpoint:
110
+ "http://localhost:8080/.well-known/openid-configuration",
111
+ mapping: {
112
+ id: "sub",
113
+ email: "email",
114
+ emailVerified: "email_verified",
115
+ name: "name",
116
+ image: "picture",
117
+ },
118
+ },
119
+ userId: expect.any(String),
120
+ });
121
+ });
122
+
123
+ it("should fail to register a new SSO provider with invalid issuer", async () => {
124
+ const { headers } = await signInWithTestUser();
125
+
126
+ try {
127
+ await auth.api.registerSSOProvider({
128
+ body: {
129
+ issuer: "invalid",
130
+ domain: "localhost",
131
+ providerId: "test",
132
+ oidcConfig: {
133
+ clientId: "test",
134
+ clientSecret: "test",
135
+ },
136
+ },
137
+ headers,
138
+ });
139
+ } catch (e) {
140
+ expect(e).toMatchObject({
141
+ status: "BAD_REQUEST",
142
+ body: {
143
+ message: "Invalid issuer. Must be a valid URL",
144
+ },
145
+ });
146
+ }
147
+ });
148
+
149
+ it("should sign in with SSO provider with email matching", async () => {
150
+ const res = await auth.api.signInSSO({
151
+ body: {
152
+ email: "my-email@localhost.com",
153
+ callbackURL: "/dashboard",
154
+ },
155
+ });
156
+ expect(res.url).toContain("http://localhost:8080/authorize");
157
+ expect(res.url).toContain(
158
+ "redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fapi%2Fauth%2Fsso%2Fcallback%2Ftest",
159
+ );
160
+ const headers = new Headers();
161
+ const callbackURL = await simulateOAuthFlow(res.url, headers);
162
+ expect(callbackURL).toContain("/dashboard");
163
+ });
164
+
165
+ it("should sign in with SSO provider with domain", async () => {
166
+ const res = await auth.api.signInSSO({
167
+ body: {
168
+ email: "my-email@test.com",
169
+ domain: "localhost.com",
170
+ callbackURL: "/dashboard",
171
+ },
172
+ });
173
+ expect(res.url).toContain("http://localhost:8080/authorize");
174
+ expect(res.url).toContain(
175
+ "redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fapi%2Fauth%2Fsso%2Fcallback%2Ftest",
176
+ );
177
+ const headers = new Headers();
178
+ const callbackURL = await simulateOAuthFlow(res.url, headers);
179
+ expect(callbackURL).toContain("/dashboard");
180
+ });
181
+
182
+ it("should sign in with SSO provider with providerId", async () => {
183
+ const res = await auth.api.signInSSO({
184
+ body: {
185
+ providerId: "test",
186
+ callbackURL: "/dashboard",
187
+ },
188
+ });
189
+ expect(res.url).toContain("http://localhost:8080/authorize");
190
+ expect(res.url).toContain(
191
+ "redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fapi%2Fauth%2Fsso%2Fcallback%2Ftest",
192
+ );
193
+ const headers = new Headers();
194
+ const callbackURL = await simulateOAuthFlow(res.url, headers);
195
+ expect(callbackURL).toContain("/dashboard");
196
+ });
197
+ });
198
+
199
+ describe("SSO disable implicit sign in", async () => {
200
+ const { auth, signInWithTestUser, customFetchImpl } =
201
+ await getTestInstanceMemory({
202
+ plugins: [sso({ disableImplicitSignUp: true }), organization()],
203
+ });
204
+
205
+ beforeAll(async () => {
206
+ await server.issuer.keys.generate("RS256");
207
+ server.issuer.on;
208
+ await server.start(8080, "localhost");
209
+ console.log("Issuer URL:", server.issuer.url); // -> http://localhost:8080
210
+ });
211
+
212
+ afterAll(async () => {
213
+ await server.stop();
214
+ });
215
+
216
+ server.service.on("beforeUserinfo", (userInfoResponse, req) => {
217
+ userInfoResponse.body = {
218
+ email: "oauth2@test.com",
219
+ name: "OAuth2 Test",
220
+ sub: "oauth2",
221
+ picture: "https://test.com/picture.png",
222
+ email_verified: true,
223
+ };
224
+ userInfoResponse.statusCode = 200;
225
+ });
226
+
227
+ server.service.on("beforeTokenSigning", (token, req) => {
228
+ token.payload.email = "sso-user@localhost:8000.com";
229
+ token.payload.email_verified = true;
230
+ token.payload.name = "Test User";
231
+ token.payload.picture = "https://test.com/picture.png";
232
+ });
233
+
234
+ async function simulateOAuthFlow(
235
+ authUrl: string,
236
+ headers: Headers,
237
+ fetchImpl?: (...args: any) => any,
238
+ ) {
239
+ let location: string | null = null;
240
+ await betterFetch(authUrl, {
241
+ method: "GET",
242
+ redirect: "manual",
243
+ onError(context) {
244
+ location = context.response.headers.get("location");
245
+ },
246
+ });
247
+
248
+ if (!location) throw new Error("No redirect location found");
249
+
250
+ let callbackURL = "";
251
+ await betterFetch(location, {
252
+ method: "GET",
253
+ customFetchImpl: fetchImpl || customFetchImpl,
254
+ headers,
255
+ onError(context) {
256
+ callbackURL = context.response.headers.get("location") || "";
257
+ },
258
+ });
259
+
260
+ return callbackURL;
261
+ }
262
+
263
+ it("should register a new SSO provider", async () => {
264
+ const { headers } = await signInWithTestUser();
265
+ const provider = await auth.api.registerSSOProvider({
266
+ body: {
267
+ issuer: server.issuer.url!,
268
+ domain: "localhost.com",
269
+ oidcConfig: {
270
+ clientId: "test",
271
+ clientSecret: "test",
272
+ authorizationEndpoint: `${server.issuer.url}/authorize`,
273
+ tokenEndpoint: `${server.issuer.url}/token`,
274
+ jwksEndpoint: `${server.issuer.url}/jwks`,
275
+ },
276
+ mapping: {
277
+ id: "sub",
278
+ email: "email",
279
+ emailVerified: "email_verified",
280
+ name: "name",
281
+ image: "picture",
282
+ },
283
+ providerId: "test",
284
+ },
285
+ headers,
286
+ });
287
+ expect(provider).toMatchObject({
288
+ id: expect.any(String),
289
+ issuer: "http://localhost:8080",
290
+ oidcConfig: {
291
+ issuer: "http://localhost:8080",
292
+ clientId: "test",
293
+ clientSecret: "test",
294
+ authorizationEndpoint: "http://localhost:8080/authorize",
295
+ tokenEndpoint: "http://localhost:8080/token",
296
+ jwksEndpoint: "http://localhost:8080/jwks",
297
+ discoveryEndpoint:
298
+ "http://localhost:8080/.well-known/openid-configuration",
299
+ mapping: {
300
+ id: "sub",
301
+ email: "email",
302
+ emailVerified: "email_verified",
303
+ name: "name",
304
+ image: "picture",
305
+ },
306
+ },
307
+ userId: expect.any(String),
308
+ });
309
+ });
310
+
311
+ it("should not create user with SSO provider when sign ups are disabled", async () => {
312
+ const res = await auth.api.signInSSO({
313
+ body: {
314
+ email: "my-email@localhost.com",
315
+ callbackURL: "/dashboard",
316
+ },
317
+ });
318
+ expect(res.url).toContain("http://localhost:8080/authorize");
319
+ expect(res.url).toContain(
320
+ "redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fapi%2Fauth%2Fsso%2Fcallback%2Ftest",
321
+ );
322
+ const headers = new Headers();
323
+ const callbackURL = await simulateOAuthFlow(res.url, headers);
324
+ expect(callbackURL).toContain(
325
+ "/api/auth/error/error?error=signup disabled",
326
+ );
327
+ });
328
+
329
+ it("should create user with SSO provider when sign ups are disabled but sign up is requested", async () => {
330
+ const res = await auth.api.signInSSO({
331
+ body: {
332
+ email: "my-email@localhost.com",
333
+ callbackURL: "/dashboard",
334
+ requestSignUp: true,
335
+ },
336
+ });
337
+ expect(res.url).toContain("http://localhost:8080/authorize");
338
+ expect(res.url).toContain(
339
+ "redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fapi%2Fauth%2Fsso%2Fcallback%2Ftest",
340
+ );
341
+ const headers = new Headers();
342
+ const callbackURL = await simulateOAuthFlow(res.url, headers);
343
+ expect(callbackURL).toContain("/dashboard");
344
+ });
345
+ });
346
+
347
+ describe("provisioning", async (ctx) => {
348
+ const { auth, signInWithTestUser, customFetchImpl } =
349
+ await getTestInstanceMemory({
350
+ plugins: [sso(), organization()],
351
+ });
352
+
353
+ beforeAll(async () => {
354
+ await server.issuer.keys.generate("RS256");
355
+ server.issuer.on;
356
+ await server.start(8080, "localhost");
357
+ console.log("Issuer URL:", server.issuer.url); // -> http://localhost:8080
358
+ });
359
+
360
+ afterAll(async () => {
361
+ await server.stop();
362
+ });
363
+ async function simulateOAuthFlow(
364
+ authUrl: string,
365
+ headers: Headers,
366
+ fetchImpl?: (...args: any) => any,
367
+ ) {
368
+ let location: string | null = null;
369
+ await betterFetch(authUrl, {
370
+ method: "GET",
371
+ redirect: "manual",
372
+ onError(context) {
373
+ location = context.response.headers.get("location");
374
+ },
375
+ });
376
+
377
+ if (!location) throw new Error("No redirect location found");
378
+
379
+ let callbackURL = "";
380
+ await betterFetch(location, {
381
+ method: "GET",
382
+ customFetchImpl: fetchImpl || customFetchImpl,
383
+ headers,
384
+ onError(context) {
385
+ callbackURL = context.response.headers.get("location") || "";
386
+ },
387
+ });
388
+
389
+ return callbackURL;
390
+ }
391
+
392
+ server.service.on("beforeUserinfo", (userInfoResponse, req) => {
393
+ userInfoResponse.body = {
394
+ email: "test@localhost.com",
395
+ name: "OAuth2 Test",
396
+ sub: "oauth2",
397
+ picture: "https://test.com/picture.png",
398
+ email_verified: true,
399
+ };
400
+ userInfoResponse.statusCode = 200;
401
+ });
402
+
403
+ server.service.on("beforeTokenSigning", (token, req) => {
404
+ token.payload.email = "sso-user@localhost:8000.com";
405
+ token.payload.email_verified = true;
406
+ token.payload.name = "Test User";
407
+ token.payload.picture = "https://test.com/picture.png";
408
+ });
409
+ it("should provision user", async () => {
410
+ const { headers } = await signInWithTestUser();
411
+ const organization = await auth.api.createOrganization({
412
+ body: {
413
+ name: "Localhost",
414
+ slug: "localhost",
415
+ },
416
+ headers,
417
+ });
418
+ const provider = await auth.api.registerSSOProvider({
419
+ body: {
420
+ issuer: server.issuer.url!,
421
+ domain: "localhost.com",
422
+ oidcConfig: {
423
+ clientId: "test",
424
+ clientSecret: "test",
425
+ authorizationEndpoint: `${server.issuer.url}/authorize`,
426
+ tokenEndpoint: `${server.issuer.url}/token`,
427
+ jwksEndpoint: `${server.issuer.url}/jwks`,
428
+ },
429
+ mapping: {
430
+ id: "sub",
431
+ email: "email",
432
+ emailVerified: "email_verified",
433
+ name: "name",
434
+ image: "picture",
435
+ },
436
+ providerId: "test2",
437
+ organizationId: organization?.id,
438
+ },
439
+ headers,
440
+ });
441
+ expect(provider).toMatchObject({
442
+ organizationId: organization?.id,
443
+ });
444
+
445
+ const res = await auth.api.signInSSO({
446
+ body: {
447
+ email: "my-email@localhost.com",
448
+ callbackURL: "/dashboard",
449
+ },
450
+ });
451
+ expect(res.url).toContain("http://localhost:8080/authorize");
452
+ expect(res.url).toContain(
453
+ "redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fapi%2Fauth%2Fsso%2Fcallback%2Ftest",
454
+ );
455
+ const newHeaders = new Headers();
456
+ const callbackURL = await simulateOAuthFlow(res.url, newHeaders);
457
+ expect(callbackURL).toContain("/dashboard");
458
+ const org = await auth.api.getFullOrganization({
459
+ query: {
460
+ organizationId: organization?.id || "",
461
+ },
462
+ headers,
463
+ });
464
+ const member = org?.members.find(
465
+ (m: any) => m.user.email === "sso-user@localhost:8000.com",
466
+ );
467
+ expect(member).toMatchObject({
468
+ role: "member",
469
+ user: {
470
+ id: expect.any(String),
471
+ name: "Test User",
472
+ email: "sso-user@localhost:8000.com",
473
+ image: "https://test.com/picture.png",
474
+ },
475
+ });
476
+ });
477
+
478
+ it("should sign in with SSO provide with org slug", async () => {
479
+ const res = await auth.api.signInSSO({
480
+ body: {
481
+ organizationSlug: "localhost",
482
+ callbackURL: "/dashboard",
483
+ },
484
+ });
485
+
486
+ expect(res.url).toContain("http://localhost:8080/authorize");
487
+ });
488
+ });