@better-auth/sso 1.4.0-beta.21 → 1.4.0-beta.22

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,16 +1,16 @@
1
1
 
2
- > @better-auth/sso@1.4.0-beta.21 build /home/runner/work/better-auth/better-auth/packages/sso
2
+ > @better-auth/sso@1.4.0-beta.22 build /home/runner/work/better-auth/better-auth/packages/sso
3
3
  > tsdown
4
4
 
5
- ℹ tsdown v0.16.0 powered by rolldown v1.0.0-beta.46
5
+ ℹ tsdown v0.16.5 powered by rolldown v1.0.0-beta.50
6
6
  ℹ Using tsdown config: /home/runner/work/better-auth/better-auth/packages/sso/tsdown.config.ts
7
- ℹ entry: src/client.ts, src/index.ts
7
+ ℹ entry: src/index.ts, src/client.ts
8
8
  ℹ tsconfig: tsconfig.json
9
9
  ℹ Build start
10
- ℹ dist/index.mjs 47.64 kB │ gzip: 8.57 kB
10
+ ℹ dist/index.mjs 48.56 kB │ gzip: 8.72 kB
11
11
  ℹ dist/client.mjs  0.15 kB │ gzip: 0.14 kB
12
12
  ℹ dist/client.d.mts  0.21 kB │ gzip: 0.18 kB
13
13
  ℹ dist/index.d.mts  0.18 kB │ gzip: 0.14 kB
14
- ℹ dist/index-C091fIpa.d.mts 20.75 kB │ gzip: 3.37 kB
15
- ℹ 5 files, total: 68.93 kB
16
- ✔ Build complete in 10064ms
14
+ ℹ dist/index-DOws6HlV.d.mts 21.24 kB │ gzip: 3.48 kB
15
+ ℹ 5 files, total: 70.34 kB
16
+ ✔ Build complete in 11501ms
package/dist/client.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { t as sso } from "./index-C091fIpa.mjs";
1
+ import { t as sso } from "./index-DOws6HlV.mjs";
2
2
 
3
3
  //#region src/client.d.ts
4
4
  declare const ssoClient: () => {
@@ -165,6 +165,29 @@ interface SSOOptions {
165
165
  * sign-in need to be called with with requestSignUp as true to create new users.
166
166
  */
167
167
  disableImplicitSignUp?: boolean | undefined;
168
+ /**
169
+ * The model name for the SSO provider table. Defaults to "ssoProvider".
170
+ */
171
+ modelName?: string;
172
+ /**
173
+ * Map fields
174
+ *
175
+ * @example
176
+ * ```ts
177
+ * {
178
+ * samlConfig: "saml_config"
179
+ * }
180
+ * ```
181
+ */
182
+ fields?: {
183
+ issuer?: string | undefined;
184
+ oidcConfig?: string | undefined;
185
+ samlConfig?: string | undefined;
186
+ userId?: string | undefined;
187
+ providerId?: string | undefined;
188
+ organizationId?: string | undefined;
189
+ domain?: string | undefined;
190
+ };
168
191
  /**
169
192
  * Configure the maximum number of SSO providers a user can register.
170
193
  * You can also pass a function that returns a number.
package/dist/index.d.mts CHANGED
@@ -1,2 +1,2 @@
1
- import { a as SSOProvider, i as SSOOptions, n as OIDCConfig, r as SAMLConfig, t as sso } from "./index-C091fIpa.mjs";
1
+ import { a as SSOProvider, i as SSOOptions, n as OIDCConfig, r as SAMLConfig, t as sso } from "./index-DOws6HlV.mjs";
2
2
  export { OIDCConfig, SAMLConfig, SSOOptions, SSOProvider, sso };
package/dist/index.mjs CHANGED
@@ -1,7 +1,7 @@
1
- import { createAuthorizationURL, generateState, parseState, validateAuthorizationCode, validateToken } from "better-auth";
2
1
  import { XMLValidator } from "fast-xml-parser";
3
2
  import * as saml from "samlify";
4
3
  import { BetterFetchError, betterFetch } from "@better-fetch/fetch";
4
+ import { createAuthorizationURL, generateState, parseState, validateAuthorizationCode, validateToken } from "better-auth";
5
5
  import { APIError, createAuthEndpoint, sessionMiddleware } from "better-auth/api";
6
6
  import { setSessionCookie } from "better-auth/cookies";
7
7
  import { handleOAuthUserInfo } from "better-auth/oauth2";
@@ -504,6 +504,12 @@ const signInSSO = (options) => {
504
504
  if (body.providerType === "saml" && !provider.samlConfig) throw new APIError("BAD_REQUEST", { message: "SAML provider is not configured" });
505
505
  }
506
506
  if (provider.oidcConfig && body.providerType !== "saml") {
507
+ let finalAuthUrl = provider.oidcConfig.authorizationEndpoint;
508
+ if (!finalAuthUrl && provider.oidcConfig.discoveryEndpoint) {
509
+ const discovery = await betterFetch(provider.oidcConfig.discoveryEndpoint, { method: "GET" });
510
+ if (discovery.data) finalAuthUrl = discovery.data.authorization_endpoint;
511
+ }
512
+ if (!finalAuthUrl) throw new APIError("BAD_REQUEST", { message: "Invalid OIDC configuration. Authorization URL not found." });
507
513
  const state = await generateState(ctx, void 0, false);
508
514
  const redirectURI = `${ctx.context.baseURL}/sso/callback/${provider.providerId}`;
509
515
  const authorizationURL = await createAuthorizationURL({
@@ -522,7 +528,7 @@ const signInSSO = (options) => {
522
528
  "offline_access"
523
529
  ],
524
530
  loginHint: ctx.body.loginHint || email,
525
- authorizationEndpoint: provider.oidcConfig.authorizationEndpoint
531
+ authorizationEndpoint: finalAuthUrl
526
532
  });
527
533
  return ctx.json({
528
534
  url: authorizationURL.toString(),
@@ -1183,40 +1189,50 @@ function sso(options) {
1183
1189
  callbackSSOSAML: callbackSSOSAML(options),
1184
1190
  acsEndpoint: acsEndpoint(options)
1185
1191
  },
1186
- schema: { ssoProvider: { fields: {
1187
- issuer: {
1188
- type: "string",
1189
- required: true
1190
- },
1191
- oidcConfig: {
1192
- type: "string",
1193
- required: false
1194
- },
1195
- samlConfig: {
1196
- type: "string",
1197
- required: false
1198
- },
1199
- userId: {
1200
- type: "string",
1201
- references: {
1202
- model: "user",
1203
- field: "id"
1192
+ schema: { ssoProvider: {
1193
+ modelName: options?.modelName ?? "ssoProvider",
1194
+ fields: {
1195
+ issuer: {
1196
+ type: "string",
1197
+ required: true,
1198
+ fieldName: options?.fields?.issuer ?? "issuer"
1199
+ },
1200
+ oidcConfig: {
1201
+ type: "string",
1202
+ required: false,
1203
+ fieldName: options?.fields?.oidcConfig ?? "oidcConfig"
1204
+ },
1205
+ samlConfig: {
1206
+ type: "string",
1207
+ required: false,
1208
+ fieldName: options?.fields?.samlConfig ?? "samlConfig"
1209
+ },
1210
+ userId: {
1211
+ type: "string",
1212
+ references: {
1213
+ model: "user",
1214
+ field: "id"
1215
+ },
1216
+ fieldName: options?.fields?.userId ?? "userId"
1217
+ },
1218
+ providerId: {
1219
+ type: "string",
1220
+ required: true,
1221
+ unique: true,
1222
+ fieldName: options?.fields?.providerId ?? "providerId"
1223
+ },
1224
+ organizationId: {
1225
+ type: "string",
1226
+ required: false,
1227
+ fieldName: options?.fields?.organizationId ?? "organizationId"
1228
+ },
1229
+ domain: {
1230
+ type: "string",
1231
+ required: true,
1232
+ fieldName: options?.fields?.domain ?? "domain"
1204
1233
  }
1205
- },
1206
- providerId: {
1207
- type: "string",
1208
- required: true,
1209
- unique: true
1210
- },
1211
- organizationId: {
1212
- type: "string",
1213
- required: false
1214
- },
1215
- domain: {
1216
- type: "string",
1217
- required: true
1218
1234
  }
1219
- } } }
1235
+ } }
1220
1236
  };
1221
1237
  }
1222
1238
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@better-auth/sso",
3
3
  "author": "Bereket Engida",
4
- "version": "1.4.0-beta.21",
4
+ "version": "1.4.0-beta.22",
5
5
  "type": "module",
6
6
  "main": "dist/index.mjs",
7
7
  "homepage": "https://www.better-auth.com/docs/plugins/sso",
@@ -65,10 +65,10 @@
65
65
  "express": "^5.1.0",
66
66
  "oauth2-mock-server": "^7.2.1",
67
67
  "tsdown": "^0.16.0",
68
- "better-auth": "1.4.0-beta.21"
68
+ "better-auth": "1.4.0-beta.22"
69
69
  },
70
70
  "peerDependencies": {
71
- "better-auth": "1.4.0-beta.21"
71
+ "better-auth": "1.4.0-beta.22"
72
72
  },
73
73
  "scripts": {
74
74
  "test": "vitest",
package/src/index.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { type BetterAuthPlugin } from "better-auth";
1
+ import type { BetterAuthPlugin } from "better-auth";
2
2
  import { XMLValidator } from "fast-xml-parser";
3
3
  import * as saml from "samlify";
4
4
  import {
@@ -54,18 +54,22 @@ export function sso<O extends SSOOptions>(options?: O | undefined): any {
54
54
  },
55
55
  schema: {
56
56
  ssoProvider: {
57
+ modelName: options?.modelName ?? "ssoProvider",
57
58
  fields: {
58
59
  issuer: {
59
60
  type: "string",
60
61
  required: true,
62
+ fieldName: options?.fields?.issuer ?? "issuer",
61
63
  },
62
64
  oidcConfig: {
63
65
  type: "string",
64
66
  required: false,
67
+ fieldName: options?.fields?.oidcConfig ?? "oidcConfig",
65
68
  },
66
69
  samlConfig: {
67
70
  type: "string",
68
71
  required: false,
72
+ fieldName: options?.fields?.samlConfig ?? "samlConfig",
69
73
  },
70
74
  userId: {
71
75
  type: "string",
@@ -73,19 +77,23 @@ export function sso<O extends SSOOptions>(options?: O | undefined): any {
73
77
  model: "user",
74
78
  field: "id",
75
79
  },
80
+ fieldName: options?.fields?.userId ?? "userId",
76
81
  },
77
82
  providerId: {
78
83
  type: "string",
79
84
  required: true,
80
85
  unique: true,
86
+ fieldName: options?.fields?.providerId ?? "providerId",
81
87
  },
82
88
  organizationId: {
83
89
  type: "string",
84
90
  required: false,
91
+ fieldName: options?.fields?.organizationId ?? "organizationId",
85
92
  },
86
93
  domain: {
87
94
  type: "string",
88
95
  required: true,
96
+ fieldName: options?.fields?.domain ?? "domain",
89
97
  },
90
98
  },
91
99
  },
package/src/routes/sso.ts CHANGED
@@ -1,11 +1,9 @@
1
1
  import { BetterFetchError, betterFetch } from "@better-fetch/fetch";
2
+ import type { Account, Session, User } from "better-auth";
2
3
  import {
3
- type Account,
4
4
  createAuthorizationURL,
5
5
  generateState,
6
6
  parseState,
7
- type Session,
8
- type User,
9
7
  validateAuthorizationCode,
10
8
  validateToken,
11
9
  } from "better-auth";
@@ -928,6 +926,22 @@ export const signInSSO = (options?: SSOOptions) => {
928
926
  }
929
927
 
930
928
  if (provider.oidcConfig && body.providerType !== "saml") {
929
+ let finalAuthUrl = provider.oidcConfig.authorizationEndpoint;
930
+ if (!finalAuthUrl && provider.oidcConfig.discoveryEndpoint) {
931
+ const discovery = await betterFetch<{
932
+ authorization_endpoint: string;
933
+ }>(provider.oidcConfig.discoveryEndpoint, {
934
+ method: "GET",
935
+ });
936
+ if (discovery.data) {
937
+ finalAuthUrl = discovery.data.authorization_endpoint;
938
+ }
939
+ }
940
+ if (!finalAuthUrl) {
941
+ throw new APIError("BAD_REQUEST", {
942
+ message: "Invalid OIDC configuration. Authorization URL not found.",
943
+ });
944
+ }
931
945
  const state = await generateState(ctx, undefined, false);
932
946
  const redirectURI = `${ctx.context.baseURL}/sso/callback/${provider.providerId}`;
933
947
  const authorizationURL = await createAuthorizationURL({
@@ -949,7 +963,7 @@ export const signInSSO = (options?: SSOOptions) => {
949
963
  "offline_access",
950
964
  ],
951
965
  loginHint: ctx.body.loginHint || email,
952
- authorizationEndpoint: provider.oidcConfig.authorizationEndpoint!,
966
+ authorizationEndpoint: finalAuthUrl,
953
967
  });
954
968
  return ctx.json({
955
969
  url: authorizationURL.toString(),
package/src/saml.test.ts CHANGED
@@ -13,7 +13,7 @@ import type {
13
13
  Response as ExpressResponse,
14
14
  } from "express";
15
15
  import express from "express";
16
- import { createServer } from "http";
16
+ import type { createServer } from "http";
17
17
  import * as saml from "samlify";
18
18
  import {
19
19
  afterAll,
@@ -1183,3 +1183,145 @@ describe("SAML SSO", async () => {
1183
1183
  });
1184
1184
  });
1185
1185
  });
1186
+
1187
+ describe("SAML SSO with custom fields", () => {
1188
+ const ssoOptions = {
1189
+ modelName: "sso_provider",
1190
+ fields: {
1191
+ issuer: "the_issuer",
1192
+ oidcConfig: "oidc_config",
1193
+ samlConfig: "saml_config",
1194
+ userId: "user_id",
1195
+ providerId: "provider_id",
1196
+ organizationId: "organization_id",
1197
+ domain: "the_domain",
1198
+ },
1199
+ };
1200
+
1201
+ const data = {
1202
+ user: [],
1203
+ session: [],
1204
+ verification: [],
1205
+ account: [],
1206
+ sso_provider: [],
1207
+ };
1208
+
1209
+ const memory = memoryAdapter(data);
1210
+ const mockIdP = createMockSAMLIdP(8081); // Different port from your main app
1211
+
1212
+ const auth = betterAuth({
1213
+ database: memory,
1214
+ baseURL: "http://localhost:3000",
1215
+ emailAndPassword: {
1216
+ enabled: true,
1217
+ },
1218
+ plugins: [sso(ssoOptions)],
1219
+ });
1220
+
1221
+ const authClient = createAuthClient({
1222
+ baseURL: "http://localhost:3000",
1223
+ plugins: [bearer(), ssoClient()],
1224
+ fetchOptions: {
1225
+ customFetchImpl: async (url, init) => {
1226
+ return auth.handler(new Request(url, init));
1227
+ },
1228
+ },
1229
+ });
1230
+
1231
+ const testUser = {
1232
+ email: "test@email.com",
1233
+ password: "password",
1234
+ name: "Test User",
1235
+ };
1236
+
1237
+ beforeAll(async () => {
1238
+ await mockIdP.start();
1239
+ const res = await authClient.signUp.email({
1240
+ email: testUser.email,
1241
+ password: testUser.password,
1242
+ name: testUser.name,
1243
+ });
1244
+ });
1245
+
1246
+ afterAll(async () => {
1247
+ await mockIdP.stop();
1248
+ });
1249
+
1250
+ beforeEach(() => {
1251
+ data.user = [];
1252
+ data.session = [];
1253
+ data.verification = [];
1254
+ data.account = [];
1255
+ data.sso_provider = [];
1256
+
1257
+ vi.clearAllMocks();
1258
+ });
1259
+
1260
+ async function getAuthHeaders() {
1261
+ const headers = new Headers();
1262
+ await authClient.signUp.email({
1263
+ email: testUser.email,
1264
+ password: testUser.password,
1265
+ name: testUser.name,
1266
+ });
1267
+ await authClient.signIn.email(testUser, {
1268
+ throw: true,
1269
+ onSuccess: setCookieToHeader(headers),
1270
+ });
1271
+ return headers;
1272
+ }
1273
+
1274
+ it("should register a new SAML provider", async () => {
1275
+ const headers = await getAuthHeaders();
1276
+
1277
+ const provider = await auth.api.registerSSOProvider({
1278
+ body: {
1279
+ providerId: "saml-provider-1",
1280
+ issuer: "http://localhost:8081",
1281
+ domain: "http://localhost:8081",
1282
+ samlConfig: {
1283
+ entryPoint: mockIdP.metadataUrl,
1284
+ cert: certificate,
1285
+ callbackUrl: "http://localhost:8081/api/sso/saml2/callback",
1286
+ wantAssertionsSigned: false,
1287
+ signatureAlgorithm: "sha256",
1288
+ digestAlgorithm: "sha256",
1289
+ idpMetadata: {
1290
+ metadata: idpMetadata,
1291
+ privateKey: idpPrivateKey,
1292
+ privateKeyPass: "q9ALNhGT5EhfcRmp8Pg7e9zTQeP2x1bW",
1293
+ isAssertionEncrypted: true,
1294
+ encPrivateKey: idpEncryptionKey,
1295
+ encPrivateKeyPass: "g7hGcRmp8PxT5QeP2q9Ehf1bWe9zTALN",
1296
+ },
1297
+ spMetadata: {
1298
+ metadata: spMetadata,
1299
+ binding: "post",
1300
+ privateKey: spPrivateKey,
1301
+ privateKeyPass: "VHOSp5RUiBcrsjrcAuXFwU1NKCkGA8px",
1302
+ isAssertionEncrypted: true,
1303
+ encPrivateKey: spEncryptionKey,
1304
+ encPrivateKeyPass: "BXFNKpxrsjrCkGA8cAu5wUVHOSpci1RU",
1305
+ },
1306
+ identifierFormat:
1307
+ "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress",
1308
+ },
1309
+ },
1310
+ headers,
1311
+ });
1312
+ expect(provider).toMatchObject({
1313
+ id: expect.any(String),
1314
+ issuer: "http://localhost:8081",
1315
+ samlConfig: {
1316
+ entryPoint: mockIdP.metadataUrl,
1317
+ cert: expect.any(String),
1318
+ callbackUrl: "http://localhost:8081/api/sso/saml2/callback",
1319
+ wantAssertionsSigned: false,
1320
+ signatureAlgorithm: "sha256",
1321
+ digestAlgorithm: "sha256",
1322
+ identifierFormat:
1323
+ "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress",
1324
+ },
1325
+ });
1326
+ });
1327
+ });
package/src/types.ts CHANGED
@@ -177,6 +177,29 @@ export interface SSOOptions {
177
177
  * sign-in need to be called with with requestSignUp as true to create new users.
178
178
  */
179
179
  disableImplicitSignUp?: boolean | undefined;
180
+ /**
181
+ * The model name for the SSO provider table. Defaults to "ssoProvider".
182
+ */
183
+ modelName?: string;
184
+ /**
185
+ * Map fields
186
+ *
187
+ * @example
188
+ * ```ts
189
+ * {
190
+ * samlConfig: "saml_config"
191
+ * }
192
+ * ```
193
+ */
194
+ fields?: {
195
+ issuer?: string | undefined;
196
+ oidcConfig?: string | undefined;
197
+ samlConfig?: string | undefined;
198
+ userId?: string | undefined;
199
+ providerId?: string | undefined;
200
+ organizationId?: string | undefined;
201
+ domain?: string | undefined;
202
+ };
180
203
  /**
181
204
  * Configure the maximum number of SSO providers a user can register.
182
205
  * You can also pass a function that returns a number.
@@ -0,0 +1,3 @@
1
+ import { defineProject } from "vitest/config";
2
+
3
+ export default defineProject({});