@better-auth/sso 1.3.0-beta.7 → 1.3.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.3.0-beta.7 build /home/runner/work/better-auth/better-auth/packages/sso
2
+ > @better-auth/sso@1.3.0-beta.9 build /home/runner/work/better-auth/better-auth/packages/sso
3
3
  > unbuild
4
4
 
5
5
  [info] Automatically detected entries: src/index, src/client [esm] [cjs] [dts]
package/dist/index.cjs CHANGED
@@ -1015,10 +1015,10 @@ const sso = (options) => {
1015
1015
  }
1016
1016
  let session = await ctx.context.internalAdapter.createSession(user.id, ctx);
1017
1017
  await cookies.setSessionCookie(ctx, { session, user });
1018
- return ctx.json({
1019
- redirect: true,
1020
- url: RelayState || `${parsedSamlConfig.issuer}/dashboard`
1021
- });
1018
+ console.log("RelayState: ", RelayState);
1019
+ throw ctx.redirect(
1020
+ RelayState || `${parsedSamlConfig.issuer}/dashboard`
1021
+ );
1022
1022
  }
1023
1023
  )
1024
1024
  },
package/dist/index.d.cts CHANGED
@@ -552,19 +552,19 @@ declare const sso: (options?: SSOOptions) => {
552
552
  session: {
553
553
  session: Record<string, any> & {
554
554
  id: string;
555
- token: string;
556
555
  userId: string;
557
556
  expiresAt: Date;
558
557
  createdAt: Date;
559
558
  updatedAt: Date;
559
+ token: string;
560
560
  ipAddress?: string | null | undefined;
561
561
  userAgent?: string | null | undefined;
562
562
  };
563
563
  user: Record<string, any> & {
564
564
  id: string;
565
- name: string;
566
- emailVerified: boolean;
567
565
  email: string;
566
+ emailVerified: boolean;
567
+ name: string;
568
568
  createdAt: Date;
569
569
  updatedAt: Date;
570
570
  image?: string | null | undefined;
@@ -973,14 +973,8 @@ declare const sso: (options?: SSOOptions) => {
973
973
  returnHeaders?: ReturnHeaders | undefined;
974
974
  }): Promise<[AsResponse] extends [true] ? Response : [ReturnHeaders] extends [true] ? {
975
975
  headers: Headers;
976
- response: {
977
- redirect: boolean;
978
- url: string;
979
- };
980
- } : {
981
- redirect: boolean;
982
- url: string;
983
- }>;
976
+ response: never;
977
+ } : never>;
984
978
  options: {
985
979
  method: "POST";
986
980
  body: z.ZodObject<{
package/dist/index.d.mts CHANGED
@@ -552,19 +552,19 @@ declare const sso: (options?: SSOOptions) => {
552
552
  session: {
553
553
  session: Record<string, any> & {
554
554
  id: string;
555
- token: string;
556
555
  userId: string;
557
556
  expiresAt: Date;
558
557
  createdAt: Date;
559
558
  updatedAt: Date;
559
+ token: string;
560
560
  ipAddress?: string | null | undefined;
561
561
  userAgent?: string | null | undefined;
562
562
  };
563
563
  user: Record<string, any> & {
564
564
  id: string;
565
- name: string;
566
- emailVerified: boolean;
567
565
  email: string;
566
+ emailVerified: boolean;
567
+ name: string;
568
568
  createdAt: Date;
569
569
  updatedAt: Date;
570
570
  image?: string | null | undefined;
@@ -973,14 +973,8 @@ declare const sso: (options?: SSOOptions) => {
973
973
  returnHeaders?: ReturnHeaders | undefined;
974
974
  }): Promise<[AsResponse] extends [true] ? Response : [ReturnHeaders] extends [true] ? {
975
975
  headers: Headers;
976
- response: {
977
- redirect: boolean;
978
- url: string;
979
- };
980
- } : {
981
- redirect: boolean;
982
- url: string;
983
- }>;
976
+ response: never;
977
+ } : never>;
984
978
  options: {
985
979
  method: "POST";
986
980
  body: z.ZodObject<{
package/dist/index.d.ts CHANGED
@@ -552,19 +552,19 @@ declare const sso: (options?: SSOOptions) => {
552
552
  session: {
553
553
  session: Record<string, any> & {
554
554
  id: string;
555
- token: string;
556
555
  userId: string;
557
556
  expiresAt: Date;
558
557
  createdAt: Date;
559
558
  updatedAt: Date;
559
+ token: string;
560
560
  ipAddress?: string | null | undefined;
561
561
  userAgent?: string | null | undefined;
562
562
  };
563
563
  user: Record<string, any> & {
564
564
  id: string;
565
- name: string;
566
- emailVerified: boolean;
567
565
  email: string;
566
+ emailVerified: boolean;
567
+ name: string;
568
568
  createdAt: Date;
569
569
  updatedAt: Date;
570
570
  image?: string | null | undefined;
@@ -973,14 +973,8 @@ declare const sso: (options?: SSOOptions) => {
973
973
  returnHeaders?: ReturnHeaders | undefined;
974
974
  }): Promise<[AsResponse] extends [true] ? Response : [ReturnHeaders] extends [true] ? {
975
975
  headers: Headers;
976
- response: {
977
- redirect: boolean;
978
- url: string;
979
- };
980
- } : {
981
- redirect: boolean;
982
- url: string;
983
- }>;
976
+ response: never;
977
+ } : never>;
984
978
  options: {
985
979
  method: "POST";
986
980
  body: z.ZodObject<{
package/dist/index.mjs CHANGED
@@ -999,10 +999,10 @@ const sso = (options) => {
999
999
  }
1000
1000
  let session = await ctx.context.internalAdapter.createSession(user.id, ctx);
1001
1001
  await setSessionCookie(ctx, { session, user });
1002
- return ctx.json({
1003
- redirect: true,
1004
- url: RelayState || `${parsedSamlConfig.issuer}/dashboard`
1005
- });
1002
+ console.log("RelayState: ", RelayState);
1003
+ throw ctx.redirect(
1004
+ RelayState || `${parsedSamlConfig.issuer}/dashboard`
1005
+ );
1006
1006
  }
1007
1007
  )
1008
1008
  },
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@better-auth/sso",
3
3
  "author": "Bereket Engida",
4
- "version": "1.3.0-beta.7",
4
+ "version": "1.3.0-beta.9",
5
5
  "main": "dist/index.cjs",
6
6
  "license": "MIT",
7
7
  "keywords": [
@@ -47,7 +47,7 @@
47
47
  "oauth2-mock-server": "^7.2.0",
48
48
  "samlify": "^2.10.0",
49
49
  "zod": "^3.24.1",
50
- "better-auth": "^1.3.0-beta.7"
50
+ "better-auth": "^1.3.0-beta.9"
51
51
  },
52
52
  "devDependencies": {
53
53
  "@types/body-parser": "^1.19.6",
package/src/index.ts CHANGED
@@ -13,6 +13,7 @@ import {
13
13
  validateAuthorizationCode,
14
14
  validateToken,
15
15
  } from "better-auth/oauth2";
16
+
16
17
  import { createAuthEndpoint } from "better-auth/plugins";
17
18
  import { z } from "zod";
18
19
  import * as saml from "samlify";
@@ -1328,10 +1329,10 @@ export const sso = (options?: SSOOptions) => {
1328
1329
  let session: Session =
1329
1330
  await ctx.context.internalAdapter.createSession(user.id, ctx);
1330
1331
  await setSessionCookie(ctx, { session, user });
1331
- return ctx.json({
1332
- redirect: true,
1333
- url: RelayState || `${parsedSamlConfig.issuer}/dashboard`,
1334
- });
1332
+ console.log("RelayState: ", RelayState);
1333
+ throw ctx.redirect(
1334
+ RelayState || `${parsedSamlConfig.issuer}/dashboard`,
1335
+ );
1335
1336
  },
1336
1337
  ),
1337
1338
  },
package/src/saml.test.ts CHANGED
@@ -10,6 +10,7 @@ import {
10
10
  import { betterAuth } from "better-auth";
11
11
  import { memoryAdapter } from "better-auth/adapters/memory";
12
12
  import { createAuthClient } from "better-auth/client";
13
+ import { betterFetch } from "@better-fetch/fetch";
13
14
  import { setCookieToHeader } from "better-auth/cookies";
14
15
  import { bearer } from "better-auth/plugins";
15
16
  import { IdentityProvider, ServiceProvider } from "samlify";
@@ -17,6 +18,12 @@ import { sso } from ".";
17
18
  import { ssoClient } from "./client";
18
19
  import { createServer } from "http";
19
20
  import * as saml from "samlify";
21
+ import type {
22
+ Application as ExpressApp,
23
+ Request as ExpressRequest,
24
+ Response as ExpressResponse,
25
+ } from "express";
26
+ // @ts-ignore
20
27
  import express from "express";
21
28
  import bodyParser from "body-parser";
22
29
  import { randomUUID } from "crypto";
@@ -346,78 +353,80 @@ const createTemplateCallback =
346
353
  context: saml.SamlLib.replaceTagsByValue(template, tagValues),
347
354
  };
348
355
  };
349
- class MockSAMLIdP {
350
- private app: express.Application;
351
- private server: ReturnType<typeof createServer> | undefined;
352
- private port: number;
353
- private idp: ReturnType<typeof IdentityProvider>;
354
- private sp: ReturnType<typeof ServiceProvider>;
355
- constructor(port: number) {
356
- this.port = port;
357
- this.app = express();
358
- this.app.use(bodyParser.urlencoded({ extended: true }));
359
- this.app.use(bodyParser.json());
360
-
361
- this.idp = IdentityProvider({
362
- metadata: idpMetadata,
363
- privateKey: idPk,
364
- isAssertionEncrypted: false,
365
- privateKeyPass: "jXmKf9By6ruLnUdRo90G",
366
- loginResponseTemplate: {
367
- context:
368
- '<samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="{ID}" Version="2.0" IssueInstant="{IssueInstant}" Destination="{Destination}" InResponseTo="{InResponseTo}"><saml:Issuer>{Issuer}</saml:Issuer><samlp:Status><samlp:StatusCode Value="{StatusCode}"/></samlp:Status><saml:Assertion ID="{AssertionID}" Version="2.0" IssueInstant="{IssueInstant}" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"><saml:Issuer>{Issuer}</saml:Issuer><saml:Subject><saml:NameID Format="{NameIDFormat}">{NameID}</saml:NameID><saml:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer"><saml:SubjectConfirmationData NotOnOrAfter="{SubjectConfirmationDataNotOnOrAfter}" Recipient="{SubjectRecipient}" InResponseTo="{InResponseTo}"/></saml:SubjectConfirmation></saml:Subject><saml:Conditions NotBefore="{ConditionsNotBefore}" NotOnOrAfter="{ConditionsNotOnOrAfter}"><saml:AudienceRestriction><saml:Audience>{Audience}</saml:Audience></saml:AudienceRestriction></saml:Conditions>{AttributeStatement}</saml:Assertion></samlp:Response>',
369
- attributes: [
370
- {
371
- name: "firstName",
372
- valueTag: "firstName",
373
- nameFormat: "urn:oasis:names:tc:SAML:2.0:attrname-format:basic",
374
- valueXsiType: "xs:string",
375
- },
376
- {
377
- name: "lastName",
378
- valueTag: "lastName",
379
- nameFormat: "urn:oasis:names:tc:SAML:2.0:attrname-format:basic",
380
- valueXsiType: "xs:string",
381
- },
382
- {
383
- name: "email",
384
- valueTag: "email",
385
- nameFormat: "urn:oasis:names:tc:SAML:2.0:attrname-format:basic",
386
- valueXsiType: "xs:string",
387
- },
388
- ],
389
- },
390
- });
391
- this.sp = ServiceProvider({
392
- metadata: spMetadata,
393
- });
394
- this.app.get("/api/sso/saml2/idp/post", async (req, res) => {
356
+ function createMockSAMLIdP(port: number) {
357
+ const app: ExpressApp = express();
358
+ let server: ReturnType<typeof createServer> | undefined;
359
+ app.use(bodyParser.urlencoded({ extended: true }));
360
+ app.use(bodyParser.json());
361
+
362
+ const idp = IdentityProvider({
363
+ metadata: idpMetadata,
364
+ privateKey: idPk,
365
+ isAssertionEncrypted: false,
366
+ privateKeyPass: "jXmKf9By6ruLnUdRo90G",
367
+ loginResponseTemplate: {
368
+ context:
369
+ '<samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="{ID}" Version="2.0" IssueInstant="{IssueInstant}" Destination="{Destination}" InResponseTo="{InResponseTo}"><saml:Issuer>{Issuer}</saml:Issuer><samlp:Status><samlp:StatusCode Value="{StatusCode}"/></samlp:Status><saml:Assertion ID="{AssertionID}" Version="2.0" IssueInstant="{IssueInstant}" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"><saml:Issuer>{Issuer}</saml:Issuer><saml:Subject><saml:NameID Format="{NameIDFormat}">{NameID}</saml:NameID><saml:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer"><saml:SubjectConfirmationData NotOnOrAfter="{SubjectConfirmationDataNotOnOrAfter}" Recipient="{SubjectRecipient}" InResponseTo="{InResponseTo}"/></saml:SubjectConfirmation></saml:Subject><saml:Conditions NotBefore="{ConditionsNotBefore}" NotOnOrAfter="{ConditionsNotOnOrAfter}"><saml:AudienceRestriction><saml:Audience>{Audience}</saml:Audience></saml:AudienceRestriction></saml:Conditions>{AttributeStatement}</saml:Assertion></samlp:Response>',
370
+ attributes: [
371
+ {
372
+ name: "firstName",
373
+ valueTag: "firstName",
374
+ nameFormat: "urn:oasis:names:tc:SAML:2.0:attrname-format:basic",
375
+ valueXsiType: "xs:string",
376
+ },
377
+ {
378
+ name: "lastName",
379
+ valueTag: "lastName",
380
+ nameFormat: "urn:oasis:names:tc:SAML:2.0:attrname-format:basic",
381
+ valueXsiType: "xs:string",
382
+ },
383
+ {
384
+ name: "email",
385
+ valueTag: "email",
386
+ nameFormat: "urn:oasis:names:tc:SAML:2.0:attrname-format:basic",
387
+ valueXsiType: "xs:string",
388
+ },
389
+ ],
390
+ },
391
+ });
392
+ const sp = ServiceProvider({
393
+ metadata: spMetadata,
394
+ });
395
+ app.get(
396
+ "/api/sso/saml2/idp/post",
397
+ async (req: ExpressRequest, res: ExpressResponse) => {
395
398
  const user = { emailAddress: "test@email.com", famName: "hello world" };
396
- const { context, entityEndpoint } = await this.idp.createLoginResponse(
397
- this.sp,
399
+ const { context, entityEndpoint } = await idp.createLoginResponse(
400
+ sp,
398
401
  {} as any,
399
402
  saml.Constants.wording.binding.post,
400
403
  user,
401
- createTemplateCallback(this.idp, this.sp, user.emailAddress),
404
+ createTemplateCallback(idp, sp, user.emailAddress),
402
405
  );
403
406
  res.status(200).send({ samlResponse: context, entityEndpoint });
404
- });
405
- this.app.get("/api/sso/saml2/idp/redirect", async (req, res) => {
407
+ },
408
+ );
409
+ app.get(
410
+ "/api/sso/saml2/idp/redirect",
411
+ async (req: ExpressRequest, res: ExpressResponse) => {
406
412
  const user = { emailAddress: "test@email.com", famName: "hello world" };
407
- const { context, entityEndpoint } = await this.idp.createLoginResponse(
408
- this.sp,
413
+ const { context, entityEndpoint } = await idp.createLoginResponse(
414
+ sp,
409
415
  {} as any,
410
416
  saml.Constants.wording.binding.post,
411
417
  user,
412
- createTemplateCallback(this.idp, this.sp, user.emailAddress),
418
+ createTemplateCallback(idp, sp, user.emailAddress),
413
419
  );
414
420
  res.status(200).send({ samlResponse: context, entityEndpoint });
415
- });
416
- // @ts-ignore
417
- this.app.post("/api/sso/saml2/sp/acs", async (req, res) => {
421
+ },
422
+ );
423
+ // @ts-ignore
424
+ app.post(
425
+ "/api/sso/saml2/sp/acs",
426
+ async (req: ExpressRequest, res: ExpressResponse) => {
418
427
  try {
419
- const parseResult = await this.sp.parseLoginResponse(
420
- this.idp,
428
+ const parseResult = await sp.parseLoginResponse(
429
+ idp,
421
430
  saml.Constants.wording.binding.post,
422
431
  req,
423
432
  );
@@ -435,32 +444,58 @@ class MockSAMLIdP {
435
444
  console.error("Error handling SAML ACS endpoint:", error);
436
445
  res.status(500).send({ error: "Failed to process SAML response." });
437
446
  }
438
- });
439
- }
447
+ },
448
+ );
449
+ app.post(
450
+ "/api/sso/saml2/callback",
451
+ async (req: ExpressRequest, res: ExpressResponse) => {
452
+ const { SAMLResponse, RelayState } = req.body;
440
453
 
441
- start() {
442
- return new Promise<void>((resolve) => {
443
- this.app.use(bodyParser.urlencoded({ extended: true }));
444
- this.server = this.app.listen(this.port, () => {
445
- console.log(`Mock SAML IdP running on port ${this.port}`);
446
- resolve();
447
- });
448
- });
449
- }
454
+ try {
455
+ const parseResult = await sp.parseLoginResponse(
456
+ idp,
457
+ saml.Constants.wording.binding.post,
458
+ { body: { SAMLResponse } },
459
+ );
450
460
 
451
- stop() {
452
- return new Promise<void>((resolve, reject) => {
453
- this.app.use(bodyParser.urlencoded({ extended: true }));
454
- this.server?.close((err) => {
455
- if (err) reject(err);
456
- else resolve();
457
- });
458
- });
459
- }
461
+ const { attributes, nameID } = parseResult.extract;
460
462
 
461
- get metadataUrl() {
462
- return `http://localhost:${this.port}/idp/metadata`;
463
- }
463
+ res.redirect(302, RelayState || "http://localhost:3000/dashboard");
464
+ } catch (error) {
465
+ console.error("Error processing SAML callback:", error);
466
+ res.status(500).send({ error: "Failed to process SAML response" });
467
+ }
468
+ },
469
+ );
470
+ app.get(
471
+ "/api/sso/saml2/idp/metadata",
472
+ (req: ExpressRequest, res: ExpressResponse) => {
473
+ res.type("application/xml");
474
+ res.send(idpMetadata);
475
+ },
476
+ );
477
+
478
+ return {
479
+ start: () => {
480
+ return new Promise<void>((resolve) => {
481
+ server = app.listen(port, () => {
482
+ console.log(`Mock SAML IdP running on port ${port}`);
483
+ resolve();
484
+ });
485
+ });
486
+ },
487
+ stop: () => {
488
+ return new Promise<void>((resolve, reject) => {
489
+ server?.close((err) => {
490
+ if (err) reject(err);
491
+ else resolve();
492
+ });
493
+ });
494
+ },
495
+ get metadataUrl() {
496
+ return `http://localhost:${port}/idp/metadata`;
497
+ },
498
+ };
464
499
  }
465
500
 
466
501
  describe("SAML SSO", async () => {
@@ -473,7 +508,7 @@ describe("SAML SSO", async () => {
473
508
  };
474
509
 
475
510
  const memory = memoryAdapter(data);
476
- const mockIdP = new MockSAMLIdP(8081); // Different port from your main app
511
+ const mockIdP = createMockSAMLIdP(8081); // Different port from your main app
477
512
 
478
513
  const ssoOptions = {
479
514
  provisionUser: vi
@@ -580,7 +615,7 @@ describe("SAML SSO", async () => {
580
615
  encPrivateKeyPass: "g7hGcRmp8PxT5QeP2q9Ehf1bWe9zTALN",
581
616
  },
582
617
  spMetadata: {
583
- metadata: idpMetadata,
618
+ metadata: spMetadata,
584
619
  binding: "post",
585
620
  privateKey: spPrivateKey,
586
621
  privateKeyPass: "VHOSp5RUiBcrsjrcAuXFwU1NKCkGA8px",
@@ -672,9 +707,9 @@ describe("SAML SSO", async () => {
672
707
  issuer: "http://localhost:8081",
673
708
  domain: "http://localhost:8081",
674
709
  samlConfig: {
675
- entryPoint: mockIdP.metadataUrl,
710
+ entryPoint: "http://localhost:8081/api/sso/saml2/idp/post",
676
711
  cert: certificate,
677
- callbackUrl: "http://localhost:8081/api/sso/saml2/sp/acs",
712
+ callbackUrl: "http://localhost:8081/api/sso/saml2/callback",
678
713
  wantAssertionsSigned: false,
679
714
  signatureAlgorithm: "sha256",
680
715
  digestAlgorithm: "sha256",
@@ -687,9 +722,8 @@ describe("SAML SSO", async () => {
687
722
  encPrivateKeyPass: "g7hGcRmp8PxT5QeP2q9Ehf1bWe9zTALN",
688
723
  },
689
724
  spMetadata: {
690
- metadata: idpMetadata,
725
+ metadata: spMetadata,
691
726
  binding: "post",
692
- // we can do a mapping of property here
693
727
  privateKey: spPrivateKey,
694
728
  privateKeyPass: "VHOSp5RUiBcrsjrcAuXFwU1NKCkGA8px",
695
729
  isAssertionEncrypted: true,
@@ -709,25 +743,34 @@ describe("SAML SSO", async () => {
709
743
  callbackURL: "http://localhost:3000/dashboard",
710
744
  },
711
745
  });
746
+
712
747
  expect(signInResponse).toEqual({
713
748
  url: expect.stringContaining("http://localhost:8081"),
714
749
  redirect: true,
715
750
  });
716
- const loginResponse = await fetch(signInResponse?.url as string);
717
- const resultValue = await loginResponse.json();
718
- const result = await auth.api.callbackSSOSAML({
719
- body: {
720
- SAMLResponse: resultValue.samlResponse,
721
- RelayState: "http://localhost:3001/dashboard",
722
- },
723
- params: {
724
- providerId: provider.providerId,
751
+
752
+ let samlResponse: any;
753
+ await betterFetch(signInResponse?.url as string, {
754
+ onSuccess: async (context) => {
755
+ samlResponse = await context.data;
725
756
  },
726
757
  });
727
-
728
- expect(result).toEqual({
729
- redirect: true,
730
- url: "http://localhost:3001/dashboard",
758
+ let redirectLocation = "";
759
+ await betterFetch("http://localhost:8081/api/sso/saml2/callback", {
760
+ method: "POST",
761
+ redirect: "manual",
762
+ headers: {
763
+ "Content-Type": "application/x-www-form-urlencoded",
764
+ },
765
+ body: new URLSearchParams({
766
+ SAMLResponse: samlResponse.samlResponse,
767
+ RelayState: "http://localhost:3000/dashboard",
768
+ }),
769
+ onError: (context) => {
770
+ expect(context.response.status).toBe(302);
771
+ redirectLocation = context.response.headers.get("location") || "";
772
+ },
731
773
  });
774
+ expect(redirectLocation).toBe("http://localhost:3000/dashboard");
732
775
  });
733
776
  });