@better-auth/sso 1.4.0-beta.2 → 1.4.0-beta.20

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
@@ -1,31 +1,31 @@
1
- import {
2
- afterAll,
3
- beforeAll,
4
- beforeEach,
5
- describe,
6
- expect,
7
- it,
8
- vi,
9
- } from "vitest";
1
+ import { betterFetch } from "@better-fetch/fetch";
10
2
  import { betterAuth } from "better-auth";
11
3
  import { memoryAdapter } from "better-auth/adapters/memory";
12
4
  import { createAuthClient } from "better-auth/client";
13
- import { betterFetch } from "@better-fetch/fetch";
14
5
  import { setCookieToHeader } from "better-auth/cookies";
15
6
  import { bearer } from "better-auth/plugins";
16
- import { sso } from ".";
17
- import { ssoClient } from "./client";
18
- import { createServer } from "http";
19
- import * as saml from "samlify";
7
+ import { getTestInstance } from "better-auth/test";
8
+ import bodyParser from "body-parser";
9
+ import { randomUUID } from "crypto";
20
10
  import type {
21
11
  Application as ExpressApp,
22
12
  Request as ExpressRequest,
23
13
  Response as ExpressResponse,
24
14
  } from "express";
25
15
  import express from "express";
26
- import bodyParser from "body-parser";
27
- import { randomUUID } from "crypto";
28
- import { getTestInstanceMemory } from "better-auth/test";
16
+ import { createServer } from "http";
17
+ import * as saml from "samlify";
18
+ import {
19
+ afterAll,
20
+ beforeAll,
21
+ beforeEach,
22
+ describe,
23
+ expect,
24
+ it,
25
+ vi,
26
+ } from "vitest";
27
+ import { sso } from ".";
28
+ import { ssoClient } from "./client";
29
29
 
30
30
  const spMetadata = `
31
31
  <md:EntityDescriptor xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata" entityID="http://localhost:3001/api/sso/saml2/sp/metadata">
@@ -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
@@ -698,7 +698,7 @@ describe("SAML SSO", async () => {
698
698
  privateKey: idpPrivateKey,
699
699
  privateKeyPass: "q9ALNhGT5EhfcRmp8Pg7e9zTQeP2x1bW",
700
700
  isAssertionEncrypted: true,
701
- encPrivateKey: idpEncyptionKey,
701
+ encPrivateKey: idpEncryptionKey,
702
702
  encPrivateKeyPass: "g7hGcRmp8PxT5QeP2q9Ehf1bWe9zTALN",
703
703
  },
704
704
  spMetadata: {
@@ -707,7 +707,7 @@ describe("SAML SSO", async () => {
707
707
  privateKey: spPrivateKey,
708
708
  privateKeyPass: "VHOSp5RUiBcrsjrcAuXFwU1NKCkGA8px",
709
709
  isAssertionEncrypted: true,
710
- encPrivateKey: spEncyptionKey,
710
+ encPrivateKey: spEncryptionKey,
711
711
  encPrivateKeyPass: "BXFNKpxrsjrCkGA8cAu5wUVHOSpci1RU",
712
712
  },
713
713
  identifierFormat:
@@ -754,7 +754,7 @@ describe("SAML SSO", async () => {
754
754
  privateKey: idpPrivateKey,
755
755
  privateKeyPass: "q9ALNhGT5EhfcRmp8Pg7e9zTQeP2x1bW",
756
756
  isAssertionEncrypted: true,
757
- encPrivateKey: idpEncyptionKey,
757
+ encPrivateKey: idpEncryptionKey,
758
758
  encPrivateKeyPass: "g7hGcRmp8PxT5QeP2q9Ehf1bWe9zTALN",
759
759
  },
760
760
  spMetadata: {
@@ -763,7 +763,7 @@ describe("SAML SSO", async () => {
763
763
  privateKey: spPrivateKey,
764
764
  privateKeyPass: "VHOSp5RUiBcrsjrcAuXFwU1NKCkGA8px",
765
765
  isAssertionEncrypted: true,
766
- encPrivateKey: spEncyptionKey,
766
+ encPrivateKey: spEncryptionKey,
767
767
  encPrivateKeyPass: "BXFNKpxrsjrCkGA8cAu5wUVHOSpci1RU",
768
768
  },
769
769
  identifierFormat:
@@ -782,6 +782,69 @@ describe("SAML SSO", async () => {
782
782
  expect(spMetadataRes.status).toBe(200);
783
783
  expect(spMetadataResResValue).toBe(spMetadata);
784
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
+ });
785
848
  it("should initiate SAML login and handle response", async () => {
786
849
  const headers = await getAuthHeaders();
787
850
  const res = await authClient.signIn.email(testUser, {
@@ -805,7 +868,7 @@ describe("SAML SSO", async () => {
805
868
  privateKey: idpPrivateKey,
806
869
  privateKeyPass: "q9ALNhGT5EhfcRmp8Pg7e9zTQeP2x1bW",
807
870
  isAssertionEncrypted: true,
808
- encPrivateKey: idpEncyptionKey,
871
+ encPrivateKey: idpEncryptionKey,
809
872
  encPrivateKeyPass: "g7hGcRmp8PxT5QeP2q9Ehf1bWe9zTALN",
810
873
  },
811
874
  spMetadata: {
@@ -814,7 +877,7 @@ describe("SAML SSO", async () => {
814
877
  privateKey: spPrivateKey,
815
878
  privateKeyPass: "VHOSp5RUiBcrsjrcAuXFwU1NKCkGA8px",
816
879
  isAssertionEncrypted: true,
817
- encPrivateKey: spEncyptionKey,
880
+ encPrivateKey: spEncryptionKey,
818
881
  encPrivateKeyPass: "BXFNKpxrsjrCkGA8cAu5wUVHOSpci1RU",
819
882
  },
820
883
  identifierFormat:
@@ -863,7 +926,7 @@ describe("SAML SSO", async () => {
863
926
  });
864
927
 
865
928
  it("should not allow creating a provider if limit is set to 0", async () => {
866
- const { auth, signInWithTestUser } = await getTestInstanceMemory({
929
+ const { auth, signInWithTestUser } = await getTestInstance({
867
930
  plugins: [sso({ providersLimit: 0 })],
868
931
  });
869
932
  const { headers } = await signInWithTestUser();
@@ -894,7 +957,7 @@ describe("SAML SSO", async () => {
894
957
  });
895
958
 
896
959
  it("should not allow creating a provider if limit is reached", async () => {
897
- const { auth, signInWithTestUser } = await getTestInstanceMemory({
960
+ const { auth, signInWithTestUser } = await getTestInstance({
898
961
  plugins: [sso({ providersLimit: 1 })],
899
962
  });
900
963
  const { headers } = await signInWithTestUser();
@@ -948,7 +1011,7 @@ describe("SAML SSO", async () => {
948
1011
  });
949
1012
 
950
1013
  it("should not allow creating a provider if limit from function is reached", async () => {
951
- const { auth, signInWithTestUser } = await getTestInstanceMemory({
1014
+ const { auth, signInWithTestUser } = await getTestInstance({
952
1015
  plugins: [
953
1016
  sso({
954
1017
  providersLimit: async (user) => {
@@ -1006,4 +1069,53 @@ describe("SAML SSO", async () => {
1006
1069
  },
1007
1070
  });
1008
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
+ });
1009
1121
  });
package/src/types.ts ADDED
@@ -0,0 +1,208 @@
1
+ import type { OAuth2Tokens, User } from "better-auth";
2
+
3
+ export interface OIDCMapping {
4
+ id?: string | undefined;
5
+ email?: string | undefined;
6
+ emailVerified?: string | undefined;
7
+ name?: string | undefined;
8
+ image?: string | undefined;
9
+ extraFields?: Record<string, string> | undefined;
10
+ }
11
+
12
+ export interface SAMLMapping {
13
+ id?: string | undefined;
14
+ email?: string | undefined;
15
+ emailVerified?: string | undefined;
16
+ name?: string | undefined;
17
+ firstName?: string | undefined;
18
+ lastName?: string | undefined;
19
+ extraFields?: Record<string, string> | undefined;
20
+ }
21
+
22
+ export interface OIDCConfig {
23
+ issuer: string;
24
+ pkce: boolean;
25
+ clientId: string;
26
+ clientSecret: string;
27
+ authorizationEndpoint?: string | undefined;
28
+ discoveryEndpoint: string;
29
+ userInfoEndpoint?: string | undefined;
30
+ scopes?: string[] | undefined;
31
+ overrideUserInfo?: boolean | undefined;
32
+ tokenEndpoint?: string | undefined;
33
+ tokenEndpointAuthentication?:
34
+ | ("client_secret_post" | "client_secret_basic")
35
+ | undefined;
36
+ jwksEndpoint?: string | undefined;
37
+ mapping?: OIDCMapping | undefined;
38
+ }
39
+
40
+ export interface SAMLConfig {
41
+ issuer: string;
42
+ entryPoint: string;
43
+ cert: string;
44
+ callbackUrl: string;
45
+ audience?: string | undefined;
46
+ idpMetadata?:
47
+ | {
48
+ metadata?: string;
49
+ entityID?: string;
50
+ entityURL?: string;
51
+ redirectURL?: string;
52
+ cert?: string;
53
+ privateKey?: string;
54
+ privateKeyPass?: string;
55
+ isAssertionEncrypted?: boolean;
56
+ encPrivateKey?: string;
57
+ encPrivateKeyPass?: string;
58
+ singleSignOnService?: Array<{
59
+ Binding: string;
60
+ Location: string;
61
+ }>;
62
+ }
63
+ | undefined;
64
+ spMetadata: {
65
+ metadata?: string | undefined;
66
+ entityID?: string | undefined;
67
+ binding?: string | undefined;
68
+ privateKey?: string | undefined;
69
+ privateKeyPass?: string | undefined;
70
+ isAssertionEncrypted?: boolean | undefined;
71
+ encPrivateKey?: string | undefined;
72
+ encPrivateKeyPass?: string | undefined;
73
+ };
74
+ wantAssertionsSigned?: boolean | undefined;
75
+ signatureAlgorithm?: string | undefined;
76
+ digestAlgorithm?: string | undefined;
77
+ identifierFormat?: string | undefined;
78
+ privateKey?: string | undefined;
79
+ decryptionPvk?: string | undefined;
80
+ additionalParams?: Record<string, any> | undefined;
81
+ mapping?: SAMLMapping | undefined;
82
+ }
83
+
84
+ export type SSOProvider = {
85
+ issuer: string;
86
+ oidcConfig?: OIDCConfig | undefined;
87
+ samlConfig?: SAMLConfig | undefined;
88
+ userId: string;
89
+ providerId: string;
90
+ organizationId?: string | undefined;
91
+ domain: string;
92
+ };
93
+
94
+ export interface SSOOptions {
95
+ /**
96
+ * custom function to provision a user when they sign in with an SSO provider.
97
+ */
98
+ provisionUser?:
99
+ | ((data: {
100
+ /**
101
+ * The user object from the database
102
+ */
103
+ user: User & Record<string, any>;
104
+ /**
105
+ * The user info object from the provider
106
+ */
107
+ userInfo: Record<string, any>;
108
+ /**
109
+ * The OAuth2 tokens from the provider
110
+ */
111
+ token?: OAuth2Tokens;
112
+ /**
113
+ * The SSO provider
114
+ */
115
+ provider: SSOProvider;
116
+ }) => Promise<void>)
117
+ | undefined;
118
+ /**
119
+ * Organization provisioning options
120
+ */
121
+ organizationProvisioning?:
122
+ | {
123
+ disabled?: boolean;
124
+ defaultRole?: "member" | "admin";
125
+ getRole?: (data: {
126
+ /**
127
+ * The user object from the database
128
+ */
129
+ user: User & Record<string, any>;
130
+ /**
131
+ * The user info object from the provider
132
+ */
133
+ userInfo: Record<string, any>;
134
+ /**
135
+ * The OAuth2 tokens from the provider
136
+ */
137
+ token?: OAuth2Tokens;
138
+ /**
139
+ * The SSO provider
140
+ */
141
+ provider: SSOProvider;
142
+ }) => Promise<"member" | "admin">;
143
+ }
144
+ | undefined;
145
+ /**
146
+ * Default SSO provider configurations for testing.
147
+ * These will take the precedence over the database providers.
148
+ */
149
+ defaultSSO?:
150
+ | Array<{
151
+ /**
152
+ * The domain to match for this default provider.
153
+ * This is only used to match incoming requests to this default provider.
154
+ */
155
+ domain: string;
156
+ /**
157
+ * The provider ID to use
158
+ */
159
+ providerId: string;
160
+ /**
161
+ * SAML configuration
162
+ */
163
+ samlConfig?: SAMLConfig;
164
+ /**
165
+ * OIDC configuration
166
+ */
167
+ oidcConfig?: OIDCConfig;
168
+ }>
169
+ | undefined;
170
+ /**
171
+ * Override user info with the provider info.
172
+ * @default false
173
+ */
174
+ defaultOverrideUserInfo?: boolean | undefined;
175
+ /**
176
+ * Disable implicit sign up for new users. When set to true for the provider,
177
+ * sign-in need to be called with with requestSignUp as true to create new users.
178
+ */
179
+ disableImplicitSignUp?: boolean | undefined;
180
+ /**
181
+ * Configure the maximum number of SSO providers a user can register.
182
+ * You can also pass a function that returns a number.
183
+ * Set to 0 to disable SSO provider registration.
184
+ *
185
+ * @example
186
+ * ```ts
187
+ * providersLimit: async (user) => {
188
+ * const plan = await getUserPlan(user);
189
+ * return plan.name === "pro" ? 10 : 1;
190
+ * }
191
+ * ```
192
+ * @default 10
193
+ */
194
+ providersLimit?:
195
+ | (number | ((user: User) => Promise<number> | number))
196
+ | undefined;
197
+ /**
198
+ * Trust the email verified flag from the provider.
199
+ *
200
+ * ⚠️ Use this with caution — it can lead to account takeover if misused. Only enable it if users **cannot freely register new providers**. You can
201
+ * prevent that by using `disabledPaths` or other safeguards to block provider registration from the client.
202
+ *
203
+ * If you want to allow account linking for specific trusted providers, enable the `accountLinking` option in your auth config and specify those
204
+ * providers in the `trustedProviders` list.
205
+ * @default false
206
+ */
207
+ trustEmailVerified?: boolean | undefined;
208
+ }
package/tsconfig.json CHANGED
@@ -1,20 +1,11 @@
1
1
  {
2
+ "extends": "../../tsconfig.base.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
+ "lib": ["esnext", "dom", "dom.iterable"]
17
5
  },
18
- "exclude": ["node_modules", "dist"],
19
- "include": ["src"]
6
+ "references": [
7
+ {
8
+ "path": "../better-auth/tsconfig.json"
9
+ }
10
+ ]
20
11
  }
@@ -0,0 +1,8 @@
1
+ import { defineConfig } from "tsdown";
2
+
3
+ export default defineConfig({
4
+ dts: { build: true, incremental: true },
5
+ format: ["esm"],
6
+ entry: ["./src/index.ts", "./src/client.ts"],
7
+ external: ["better-auth", "better-call", "@better-fetch/fetch", "stripe"],
8
+ });
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.cjs DELETED
@@ -1,10 +0,0 @@
1
- 'use strict';
2
-
3
- const ssoClient = () => {
4
- return {
5
- id: "sso-client",
6
- $InferServerPlugin: {}
7
- };
8
- };
9
-
10
- exports.ssoClient = ssoClient;
package/dist/client.d.cts DELETED
@@ -1,11 +0,0 @@
1
- import { sso } from './index.cjs';
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.d.ts DELETED
@@ -1,11 +0,0 @@
1
- import { sso } from './index.js';
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 };