@better-auth/sso 1.4.0-beta.1 → 1.4.0-beta.11

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/src/saml.test.ts CHANGED
@@ -242,7 +242,7 @@ const certificate = `
242
242
  yyoWAJDUHiAmvFA=
243
243
  -----END CERTIFICATE-----
244
244
  `;
245
- const idpEncyptionKey = `
245
+ const idpEncryptionKey = `
246
246
  -----BEGIN RSA PRIVATE KEY-----
247
247
  Proc-Type: 4,ENCRYPTED
248
248
  DEK-Info: DES-EDE3-CBC,860FDB9F3BE14699
@@ -274,7 +274,7 @@ const idpEncyptionKey = `
274
274
  ISbutnQPUN5fsaIsgKDIV3T7n6519t6brobcW5bdigmf5ebFeZJ16/lYy6V77UM5
275
275
  -----END RSA PRIVATE KEY-----
276
276
  `;
277
- const spEncyptionKey = `
277
+ const spEncryptionKey = `
278
278
  -----BEGIN RSA PRIVATE KEY-----
279
279
  Proc-Type: 4,ENCRYPTED
280
280
  DEK-Info: DES-EDE3-CBC,860FDB9F3BE14699
@@ -493,6 +493,98 @@ const createMockSAMLIdP = (port: number) => {
493
493
  return { start, stop, metadataUrl };
494
494
  };
495
495
 
496
+ describe("SAML SSO with defaultSSO array", async () => {
497
+ const data = {
498
+ user: [],
499
+ session: [],
500
+ verification: [],
501
+ account: [],
502
+ ssoProvider: [],
503
+ };
504
+
505
+ const memory = memoryAdapter(data);
506
+ const mockIdP = createMockSAMLIdP(8081); // Different port from your main app
507
+
508
+ const ssoOptions = {
509
+ defaultSSO: [
510
+ {
511
+ domain: "localhost:8081",
512
+ providerId: "default-saml",
513
+ samlConfig: {
514
+ issuer: "http://localhost:8081",
515
+ entryPoint: "http://localhost:8081/api/sso/saml2/idp/post",
516
+ cert: certificate,
517
+ callbackUrl: "http://localhost:8081/dashboard",
518
+ wantAssertionsSigned: false,
519
+ signatureAlgorithm: "sha256",
520
+ digestAlgorithm: "sha256",
521
+ idpMetadata: {
522
+ metadata: idpMetadata,
523
+ },
524
+ spMetadata: {
525
+ metadata: spMetadata,
526
+ },
527
+ identifierFormat:
528
+ "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress",
529
+ },
530
+ },
531
+ ],
532
+ provisionUser: vi
533
+ .fn()
534
+ .mockImplementation(async ({ user, userInfo, token, provider }) => {
535
+ return {
536
+ id: "provisioned-user-id",
537
+ email: userInfo.email,
538
+ name: userInfo.name,
539
+ attributes: userInfo.attributes,
540
+ };
541
+ }),
542
+ };
543
+
544
+ const auth = betterAuth({
545
+ database: memory,
546
+ baseURL: "http://localhost:3000",
547
+ emailAndPassword: {
548
+ enabled: true,
549
+ },
550
+ plugins: [sso(ssoOptions)],
551
+ });
552
+
553
+ const ctx = await auth.$context;
554
+
555
+ const authClient = createAuthClient({
556
+ baseURL: "http://localhost:3000",
557
+ plugins: [bearer(), ssoClient()],
558
+ fetchOptions: {
559
+ customFetchImpl: async (url, init) => {
560
+ return auth.handler(new Request(url, init));
561
+ },
562
+ },
563
+ });
564
+
565
+ beforeAll(async () => {
566
+ await mockIdP.start();
567
+ });
568
+
569
+ afterAll(async () => {
570
+ await mockIdP.stop();
571
+ });
572
+
573
+ it("should use default SAML SSO provider from array when no provider found in database", async () => {
574
+ const signInResponse = await auth.api.signInSSO({
575
+ body: {
576
+ providerId: "default-saml",
577
+ callbackURL: "http://localhost:3000/dashboard",
578
+ },
579
+ });
580
+
581
+ expect(signInResponse).toEqual({
582
+ url: expect.stringContaining("http://localhost:8081"),
583
+ redirect: true,
584
+ });
585
+ });
586
+ });
587
+
496
588
  describe("SAML SSO", async () => {
497
589
  const data = {
498
590
  user: [],
@@ -606,7 +698,7 @@ describe("SAML SSO", async () => {
606
698
  privateKey: idpPrivateKey,
607
699
  privateKeyPass: "q9ALNhGT5EhfcRmp8Pg7e9zTQeP2x1bW",
608
700
  isAssertionEncrypted: true,
609
- encPrivateKey: idpEncyptionKey,
701
+ encPrivateKey: idpEncryptionKey,
610
702
  encPrivateKeyPass: "g7hGcRmp8PxT5QeP2q9Ehf1bWe9zTALN",
611
703
  },
612
704
  spMetadata: {
@@ -615,7 +707,7 @@ describe("SAML SSO", async () => {
615
707
  privateKey: spPrivateKey,
616
708
  privateKeyPass: "VHOSp5RUiBcrsjrcAuXFwU1NKCkGA8px",
617
709
  isAssertionEncrypted: true,
618
- encPrivateKey: spEncyptionKey,
710
+ encPrivateKey: spEncryptionKey,
619
711
  encPrivateKeyPass: "BXFNKpxrsjrCkGA8cAu5wUVHOSpci1RU",
620
712
  },
621
713
  identifierFormat:
@@ -662,7 +754,7 @@ describe("SAML SSO", async () => {
662
754
  privateKey: idpPrivateKey,
663
755
  privateKeyPass: "q9ALNhGT5EhfcRmp8Pg7e9zTQeP2x1bW",
664
756
  isAssertionEncrypted: true,
665
- encPrivateKey: idpEncyptionKey,
757
+ encPrivateKey: idpEncryptionKey,
666
758
  encPrivateKeyPass: "g7hGcRmp8PxT5QeP2q9Ehf1bWe9zTALN",
667
759
  },
668
760
  spMetadata: {
@@ -671,7 +763,7 @@ describe("SAML SSO", async () => {
671
763
  privateKey: spPrivateKey,
672
764
  privateKeyPass: "VHOSp5RUiBcrsjrcAuXFwU1NKCkGA8px",
673
765
  isAssertionEncrypted: true,
674
- encPrivateKey: spEncyptionKey,
766
+ encPrivateKey: spEncryptionKey,
675
767
  encPrivateKeyPass: "BXFNKpxrsjrCkGA8cAu5wUVHOSpci1RU",
676
768
  },
677
769
  identifierFormat:
@@ -690,6 +782,69 @@ describe("SAML SSO", async () => {
690
782
  expect(spMetadataRes.status).toBe(200);
691
783
  expect(spMetadataResResValue).toBe(spMetadata);
692
784
  });
785
+ it("Should fetch sp metadata", async () => {
786
+ const headers = await getAuthHeaders();
787
+ await authClient.signIn.email(testUser, {
788
+ throw: true,
789
+ onSuccess: setCookieToHeader(headers),
790
+ });
791
+ const issuer = "http://localhost:8081";
792
+ const provider = await auth.api.registerSSOProvider({
793
+ body: {
794
+ providerId: "saml-provider-1",
795
+ issuer: issuer,
796
+ domain: issuer,
797
+ samlConfig: {
798
+ entryPoint: mockIdP.metadataUrl,
799
+ cert: certificate,
800
+ callbackUrl: `${issuer}/api/sso/saml2/sp/acs`,
801
+ wantAssertionsSigned: false,
802
+ signatureAlgorithm: "sha256",
803
+ digestAlgorithm: "sha256",
804
+ idpMetadata: {
805
+ metadata: idpMetadata,
806
+ privateKey: idpPrivateKey,
807
+ privateKeyPass: "q9ALNhGT5EhfcRmp8Pg7e9zTQeP2x1bW",
808
+ isAssertionEncrypted: true,
809
+ encPrivateKey: idpEncryptionKey,
810
+ encPrivateKeyPass: "g7hGcRmp8PxT5QeP2q9Ehf1bWe9zTALN",
811
+ },
812
+ spMetadata: {
813
+ binding: "post",
814
+ privateKey: spPrivateKey,
815
+ privateKeyPass: "VHOSp5RUiBcrsjrcAuXFwU1NKCkGA8px",
816
+ isAssertionEncrypted: true,
817
+ encPrivateKey: spEncryptionKey,
818
+ encPrivateKeyPass: "BXFNKpxrsjrCkGA8cAu5wUVHOSpci1RU",
819
+ },
820
+ identifierFormat:
821
+ "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress",
822
+ },
823
+ },
824
+ headers,
825
+ });
826
+
827
+ const spMetadataRes = await auth.api.spMetadata({
828
+ query: {
829
+ providerId: provider.providerId,
830
+ },
831
+ });
832
+ const spMetadataResResValue = await spMetadataRes.text();
833
+ expect(spMetadataRes.status).toBe(200);
834
+ expect(spMetadataResResValue).toBeDefined();
835
+ expect(spMetadataResResValue).toContain(
836
+ "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress",
837
+ );
838
+ expect(spMetadataResResValue).toContain(
839
+ "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST",
840
+ );
841
+ expect(spMetadataResResValue).toContain(
842
+ `<EntityDescriptor entityID="${issuer}"`,
843
+ );
844
+ expect(spMetadataResResValue).toContain(
845
+ `Location="${issuer}/api/sso/saml2/sp/acs"`,
846
+ );
847
+ });
693
848
  it("should initiate SAML login and handle response", async () => {
694
849
  const headers = await getAuthHeaders();
695
850
  const res = await authClient.signIn.email(testUser, {
@@ -713,7 +868,7 @@ describe("SAML SSO", async () => {
713
868
  privateKey: idpPrivateKey,
714
869
  privateKeyPass: "q9ALNhGT5EhfcRmp8Pg7e9zTQeP2x1bW",
715
870
  isAssertionEncrypted: true,
716
- encPrivateKey: idpEncyptionKey,
871
+ encPrivateKey: idpEncryptionKey,
717
872
  encPrivateKeyPass: "g7hGcRmp8PxT5QeP2q9Ehf1bWe9zTALN",
718
873
  },
719
874
  spMetadata: {
@@ -722,7 +877,7 @@ describe("SAML SSO", async () => {
722
877
  privateKey: spPrivateKey,
723
878
  privateKeyPass: "VHOSp5RUiBcrsjrcAuXFwU1NKCkGA8px",
724
879
  isAssertionEncrypted: true,
725
- encPrivateKey: spEncyptionKey,
880
+ encPrivateKey: spEncryptionKey,
726
881
  encPrivateKeyPass: "BXFNKpxrsjrCkGA8cAu5wUVHOSpci1RU",
727
882
  },
728
883
  identifierFormat:
@@ -914,4 +1069,53 @@ describe("SAML SSO", async () => {
914
1069
  },
915
1070
  });
916
1071
  });
1072
+
1073
+ it("should not allow creating a provider with duplicate providerId", async () => {
1074
+ const headers = await getAuthHeaders();
1075
+ await authClient.signIn.email(testUser, {
1076
+ throw: true,
1077
+ onSuccess: setCookieToHeader(headers),
1078
+ });
1079
+
1080
+ await auth.api.registerSSOProvider({
1081
+ body: {
1082
+ providerId: "duplicate-provider",
1083
+ issuer: "http://localhost:8081",
1084
+ domain: "http://localhost:8081",
1085
+ samlConfig: {
1086
+ entryPoint: mockIdP.metadataUrl,
1087
+ cert: certificate,
1088
+ callbackUrl: "http://localhost:8081/api/sso/saml2/callback",
1089
+ spMetadata: {
1090
+ metadata: spMetadata,
1091
+ },
1092
+ },
1093
+ },
1094
+ headers,
1095
+ });
1096
+
1097
+ await expect(
1098
+ auth.api.registerSSOProvider({
1099
+ body: {
1100
+ providerId: "duplicate-provider",
1101
+ issuer: "http://localhost:8082",
1102
+ domain: "http://localhost:8082",
1103
+ samlConfig: {
1104
+ entryPoint: mockIdP.metadataUrl,
1105
+ cert: certificate,
1106
+ callbackUrl: "http://localhost:8082/api/sso/saml2/callback",
1107
+ spMetadata: {
1108
+ metadata: spMetadata,
1109
+ },
1110
+ },
1111
+ },
1112
+ headers,
1113
+ }),
1114
+ ).rejects.toMatchObject({
1115
+ status: "UNPROCESSABLE_ENTITY",
1116
+ body: {
1117
+ message: "SSO provider with this providerId already exists",
1118
+ },
1119
+ });
1120
+ });
917
1121
  });
package/tsconfig.json CHANGED
@@ -1,20 +1,9 @@
1
1
  {
2
+ "extends": "../../tsconfig.json",
2
3
  "compilerOptions": {
3
- "esModuleInterop": true,
4
- "skipLibCheck": true,
5
- "target": "es2022",
6
- "allowJs": true,
7
- "resolveJsonModule": true,
8
- "module": "ESNext",
9
- "noEmit": true,
10
- "moduleResolution": "Bundler",
11
- "moduleDetection": "force",
12
- "isolatedModules": true,
13
- "verbatimModuleSyntax": true,
14
- "strict": true,
15
- "noImplicitOverride": true,
16
- "noFallthroughCasesInSwitch": true
4
+ "rootDir": "./src",
5
+ "outDir": "./dist",
6
+ "lib": ["esnext", "dom", "dom.iterable"]
17
7
  },
18
- "exclude": ["node_modules", "dist"],
19
8
  "include": ["src"]
20
9
  }
@@ -0,0 +1,8 @@
1
+ import { defineConfig } from "tsdown";
2
+
3
+ export default defineConfig({
4
+ dts: true,
5
+ format: ["esm", "cjs"],
6
+ entry: ["./src/index.ts", "./src/client.ts"],
7
+ external: ["better-auth", "better-call", "@better-fetch/fetch", "stripe"],
8
+ });
package/CHANGELOG.md DELETED
@@ -1,20 +0,0 @@
1
- # @better-auth/sso
2
-
3
- ## 1.3.4
4
-
5
- ### Patch Changes
6
-
7
- - 2bd2fa9: Added support for listing organization members with pagination, sorting, and filtering, and improved client inference for additional organization fields. Also fixed date handling in rate limits and tokens, improved Notion OAuth user extraction, and ensured session is always set in context.
8
-
9
- Organization
10
-
11
- - Added listMembers API with pagination, sorting, and filtering.
12
- - Added membersLimit param to getFullOrganization.
13
- - Improved client inference for additional fields in organization schemas.
14
- - Bug Fixes
15
- - Fixed date handling by casting DB values to Date objects before using date methods.
16
- - Fixed Notion OAuth to extract user info correctly.
17
- - Ensured session is set in context when reading from cookie cach
18
-
19
- - Updated dependencies [2bd2fa9]
20
- - better-auth@1.3.4
package/build.config.ts DELETED
@@ -1,12 +0,0 @@
1
- import { defineBuildConfig } from "unbuild";
2
-
3
- export default defineBuildConfig({
4
- declaration: true,
5
- rollup: {
6
- emitCJS: true,
7
- },
8
- outDir: "dist",
9
- clean: false,
10
- failOnWarn: false,
11
- externals: ["better-auth", "better-call", "@better-fetch/fetch", "stripe"],
12
- });
package/dist/client.d.mts DELETED
@@ -1,11 +0,0 @@
1
- import { sso } from './index.mjs';
2
- import 'better-call';
3
- import 'better-auth';
4
- import 'zod/v4';
5
-
6
- declare const ssoClient: () => {
7
- id: "sso-client";
8
- $InferServerPlugin: ReturnType<typeof sso>;
9
- };
10
-
11
- export { ssoClient };
package/dist/client.mjs DELETED
@@ -1,8 +0,0 @@
1
- const ssoClient = () => {
2
- return {
3
- id: "sso-client",
4
- $InferServerPlugin: {}
5
- };
6
- };
7
-
8
- export { ssoClient };