@better-auth/sso 1.4.6-beta.2 → 1.4.6-beta.6

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.6-beta.2 build /home/runner/work/better-auth/better-auth/packages/sso
2
+ > @better-auth/sso@1.4.6-beta.6 build /home/runner/work/better-auth/better-auth/packages/sso
3
3
  > tsdown
4
4
 
5
- ℹ tsdown v0.16.6 powered by rolldown v1.0.0-beta.51
6
- ℹ Using tsdown config: /home/runner/work/better-auth/better-auth/packages/sso/tsdown.config.ts
5
+ ℹ tsdown v0.17.0 powered by rolldown v1.0.0-beta.53
6
+ ℹ config file: /home/runner/work/better-auth/better-auth/packages/sso/tsdown.config.ts
7
7
  ℹ entry: src/index.ts, src/client.ts
8
8
  ℹ tsconfig: tsconfig.json
9
9
  ℹ Build start
10
- ℹ dist/index.mjs 58.49 kB │ gzip: 10.33 kB
10
+ ℹ dist/index.mjs 59.70 kB │ gzip: 10.49 kB
11
11
  ℹ dist/client.mjs  0.15 kB │ gzip: 0.14 kB
12
12
  ℹ dist/client.d.mts  0.49 kB │ gzip: 0.29 kB
13
13
  ℹ dist/index.d.mts  0.21 kB │ gzip: 0.15 kB
14
- ℹ dist/index-DCyJckhH.d.mts 25.42 kB │ gzip: 3.96 kB
15
- ℹ 5 files, total: 84.77 kB
16
- ✔ Build complete in 11114ms
14
+ ℹ dist/index-CYgzSZS4.d.mts 25.84 kB │ gzip: 4.13 kB
15
+ ℹ 5 files, total: 86.39 kB
16
+ ✔ Build complete in 11274ms
package/bump.config.ts ADDED
@@ -0,0 +1,5 @@
1
+ import { defineConfig } from "bumpp";
2
+
3
+ export default defineConfig({
4
+ files: ["package.json"],
5
+ });
package/dist/client.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { t as SSOPlugin } from "./index-DCyJckhH.mjs";
1
+ import { t as SSOPlugin } from "./index-CYgzSZS4.mjs";
2
2
 
3
3
  //#region src/client.d.ts
4
4
  interface SSOClientOptions {
@@ -1,6 +1,6 @@
1
1
  import * as z from "zod/v4";
2
2
  import { OAuth2Tokens, User } from "better-auth";
3
- import * as better_call7 from "better-call";
3
+ import * as better_call0 from "better-call";
4
4
 
5
5
  //#region src/types.d.ts
6
6
  interface OIDCMapping {
@@ -216,7 +216,13 @@ interface SSOOptions {
216
216
  *
217
217
  * If you want to allow account linking for specific trusted providers, enable the `accountLinking` option in your auth config and specify those
218
218
  * providers in the `trustedProviders` list.
219
+ *
219
220
  * @default false
221
+ *
222
+ * @deprecated This option is discouraged for new projects. Relying on provider-level `email_verified` is a weaker
223
+ * trust signal compared to using `trustedProviders` in `accountLinking` or enabling `domainVerification` for SSO.
224
+ * Existing configurations will continue to work, but new integrations should use explicit trust mechanisms.
225
+ * This option may be removed in a future major version.
220
226
  */
221
227
  trustEmailVerified?: boolean | undefined;
222
228
  /**
@@ -240,7 +246,7 @@ interface SSOOptions {
240
246
  }
241
247
  //#endregion
242
248
  //#region src/routes/domain-verification.d.ts
243
- declare const requestDomainVerification: (options: SSOOptions) => better_call7.StrictEndpoint<"/sso/request-domain-verification", {
249
+ declare const requestDomainVerification: (options: SSOOptions) => better_call0.StrictEndpoint<"/sso/request-domain-verification", {
244
250
  method: "POST";
245
251
  body: z.ZodObject<{
246
252
  providerId: z.ZodString;
@@ -262,7 +268,7 @@ declare const requestDomainVerification: (options: SSOOptions) => better_call7.S
262
268
  };
263
269
  };
264
270
  };
265
- use: ((inputContext: better_call7.MiddlewareInputContext<better_call7.MiddlewareOptions>) => Promise<{
271
+ use: ((inputContext: better_call0.MiddlewareInputContext<better_call0.MiddlewareOptions>) => Promise<{
266
272
  session: {
267
273
  session: Record<string, any> & {
268
274
  id: string;
@@ -290,7 +296,7 @@ declare const requestDomainVerification: (options: SSOOptions) => better_call7.S
290
296
  }, {
291
297
  domainVerificationToken: string;
292
298
  }>;
293
- declare const verifyDomain: (options: SSOOptions) => better_call7.StrictEndpoint<"/sso/verify-domain", {
299
+ declare const verifyDomain: (options: SSOOptions) => better_call0.StrictEndpoint<"/sso/verify-domain", {
294
300
  method: "POST";
295
301
  body: z.ZodObject<{
296
302
  providerId: z.ZodString;
@@ -315,7 +321,7 @@ declare const verifyDomain: (options: SSOOptions) => better_call7.StrictEndpoint
315
321
  };
316
322
  };
317
323
  };
318
- use: ((inputContext: better_call7.MiddlewareInputContext<better_call7.MiddlewareOptions>) => Promise<{
324
+ use: ((inputContext: better_call0.MiddlewareInputContext<better_call0.MiddlewareOptions>) => Promise<{
319
325
  session: {
320
326
  session: Record<string, any> & {
321
327
  id: string;
@@ -343,7 +349,7 @@ declare const verifyDomain: (options: SSOOptions) => better_call7.StrictEndpoint
343
349
  }, void>;
344
350
  //#endregion
345
351
  //#region src/routes/sso.d.ts
346
- declare const spMetadata: () => better_call7.StrictEndpoint<"/sso/saml2/sp/metadata", {
352
+ declare const spMetadata: () => better_call0.StrictEndpoint<"/sso/saml2/sp/metadata", {
347
353
  method: "GET";
348
354
  query: z.ZodObject<{
349
355
  providerId: z.ZodString;
@@ -367,7 +373,7 @@ declare const spMetadata: () => better_call7.StrictEndpoint<"/sso/saml2/sp/metad
367
373
  } & {
368
374
  use: any[];
369
375
  }, Response>;
370
- declare const registerSSOProvider: <O extends SSOOptions>(options: O) => better_call7.StrictEndpoint<"/sso/register", {
376
+ declare const registerSSOProvider: <O extends SSOOptions>(options: O) => better_call0.StrictEndpoint<"/sso/register", {
371
377
  method: "POST";
372
378
  body: z.ZodObject<{
373
379
  providerId: z.ZodString;
@@ -445,7 +451,7 @@ declare const registerSSOProvider: <O extends SSOOptions>(options: O) => better_
445
451
  organizationId: z.ZodOptional<z.ZodString>;
446
452
  overrideUserInfo: z.ZodOptional<z.ZodDefault<z.ZodBoolean>>;
447
453
  }, z.core.$strip>;
448
- use: ((inputContext: better_call7.MiddlewareInputContext<better_call7.MiddlewareOptions>) => Promise<{
454
+ use: ((inputContext: better_call0.MiddlewareInputContext<better_call0.MiddlewareOptions>) => Promise<{
449
455
  session: {
450
456
  session: Record<string, any> & {
451
457
  id: string;
@@ -637,7 +643,7 @@ declare const registerSSOProvider: <O extends SSOOptions>(options: O) => better_
637
643
  domainVerified: boolean;
638
644
  domainVerificationToken: string;
639
645
  } & SSOProvider<O> : SSOProvider<O>>;
640
- declare const signInSSO: (options?: SSOOptions) => better_call7.StrictEndpoint<"/sign-in/sso", {
646
+ declare const signInSSO: (options?: SSOOptions) => better_call0.StrictEndpoint<"/sign-in/sso", {
641
647
  method: "POST";
642
648
  body: z.ZodObject<{
643
649
  email: z.ZodOptional<z.ZodString>;
@@ -733,7 +739,7 @@ declare const signInSSO: (options?: SSOOptions) => better_call7.StrictEndpoint<"
733
739
  url: string;
734
740
  redirect: boolean;
735
741
  }>;
736
- declare const callbackSSO: (options?: SSOOptions) => better_call7.StrictEndpoint<"/sso/callback/:providerId", {
742
+ declare const callbackSSO: (options?: SSOOptions) => better_call0.StrictEndpoint<"/sso/callback/:providerId", {
737
743
  method: "GET";
738
744
  query: z.ZodObject<{
739
745
  code: z.ZodOptional<z.ZodString>;
@@ -758,7 +764,7 @@ declare const callbackSSO: (options?: SSOOptions) => better_call7.StrictEndpoint
758
764
  } & {
759
765
  use: any[];
760
766
  }, never>;
761
- declare const callbackSSOSAML: (options?: SSOOptions) => better_call7.StrictEndpoint<"/sso/saml2/callback/:providerId", {
767
+ declare const callbackSSOSAML: (options?: SSOOptions) => better_call0.StrictEndpoint<"/sso/saml2/callback/:providerId", {
762
768
  method: "POST";
763
769
  body: z.ZodObject<{
764
770
  SAMLResponse: z.ZodString;
@@ -787,7 +793,7 @@ declare const callbackSSOSAML: (options?: SSOOptions) => better_call7.StrictEndp
787
793
  } & {
788
794
  use: any[];
789
795
  }, never>;
790
- declare const acsEndpoint: (options?: SSOOptions) => better_call7.StrictEndpoint<"/sso/saml2/sp/acs/:providerId", {
796
+ declare const acsEndpoint: (options?: SSOOptions) => better_call0.StrictEndpoint<"/sso/saml2/sp/acs/:providerId", {
791
797
  method: "POST";
792
798
  params: z.ZodObject<{
793
799
  providerId: z.ZodOptional<z.ZodString>;
package/dist/index.d.mts CHANGED
@@ -1,2 +1,2 @@
1
- import { a as SSOOptions, i as SAMLConfig, n as sso, o as SSOProvider, r as OIDCConfig, t as SSOPlugin } from "./index-DCyJckhH.mjs";
1
+ import { a as SSOOptions, i as SAMLConfig, n as sso, o as SSOProvider, r as OIDCConfig, t as SSOPlugin } from "./index-CYgzSZS4.mjs";
2
2
  export { OIDCConfig, SAMLConfig, SSOOptions, SSOPlugin, SSOProvider, sso };
package/dist/index.mjs CHANGED
@@ -10,10 +10,11 @@ import { handleOAuthUserInfo } from "better-auth/oauth2";
10
10
  import { decodeJwt } from "jose";
11
11
 
12
12
  //#region src/routes/domain-verification.ts
13
+ const domainVerificationBodySchema = z.object({ providerId: z.string() });
13
14
  const requestDomainVerification = (options) => {
14
15
  return createAuthEndpoint("/sso/request-domain-verification", {
15
16
  method: "POST",
16
- body: z.object({ providerId: z.string() }),
17
+ body: domainVerificationBodySchema,
17
18
  metadata: { openapi: {
18
19
  summary: "Request a domain verification",
19
20
  description: "Request a domain verification for the given SSO provider",
@@ -90,7 +91,7 @@ const requestDomainVerification = (options) => {
90
91
  const verifyDomain = (options) => {
91
92
  return createAuthEndpoint("/sso/verify-domain", {
92
93
  method: "POST",
93
- body: z.object({ providerId: z.string() }),
94
+ body: domainVerificationBodySchema,
94
95
  metadata: { openapi: {
95
96
  summary: "Verify the provider domain ownership",
96
97
  description: "Verify the provider domain ownership via DNS records",
@@ -184,19 +185,14 @@ const verifyDomain = (options) => {
184
185
 
185
186
  //#endregion
186
187
  //#region src/utils.ts
187
- const validateEmailDomain = (email, domain) => {
188
- const emailDomain = email.split("@")[1]?.toLowerCase();
189
- const providerDomain = domain.toLowerCase();
190
- if (!emailDomain || !providerDomain) return false;
191
- return emailDomain === providerDomain || emailDomain.endsWith(`.${providerDomain}`);
192
- };
193
-
194
- //#endregion
195
- //#region src/routes/sso.ts
196
188
  /**
197
- * Safely parses a value that might be a JSON string or already a parsed object
189
+ * Safely parses a value that might be a JSON string or already a parsed object.
198
190
  * This handles cases where ORMs like Drizzle might return already parsed objects
199
- * instead of JSON strings from TEXT/JSON columns
191
+ * instead of JSON strings from TEXT/JSON columns.
192
+ *
193
+ * @param value - The value to parse (string, object, null, or undefined)
194
+ * @returns The parsed object or null
195
+ * @throws Error if string parsing fails
200
196
  */
201
197
  function safeJsonParse(value) {
202
198
  if (!value) return null;
@@ -208,13 +204,23 @@ function safeJsonParse(value) {
208
204
  }
209
205
  return null;
210
206
  }
207
+ const validateEmailDomain = (email, domain) => {
208
+ const emailDomain = email.split("@")[1]?.toLowerCase();
209
+ const providerDomain = domain.toLowerCase();
210
+ if (!emailDomain || !providerDomain) return false;
211
+ return emailDomain === providerDomain || emailDomain.endsWith(`.${providerDomain}`);
212
+ };
213
+
214
+ //#endregion
215
+ //#region src/routes/sso.ts
216
+ const spMetadataQuerySchema = z.object({
217
+ providerId: z.string(),
218
+ format: z.enum(["xml", "json"]).default("xml")
219
+ });
211
220
  const spMetadata = () => {
212
221
  return createAuthEndpoint("/sso/saml2/sp/metadata", {
213
222
  method: "GET",
214
- query: z.object({
215
- providerId: z.string(),
216
- format: z.enum(["xml", "json"]).default("xml")
217
- }),
223
+ query: spMetadataQuerySchema,
218
224
  metadata: { openapi: {
219
225
  operationId: "getSSOServiceProviderMetadata",
220
226
  summary: "Get Service Provider metadata",
@@ -244,82 +250,83 @@ const spMetadata = () => {
244
250
  return new Response(sp.getMetadata(), { headers: { "Content-Type": "application/xml" } });
245
251
  });
246
252
  };
253
+ const ssoProviderBodySchema = z.object({
254
+ providerId: z.string({}).meta({ description: "The ID of the provider. This is used to identify the provider during login and callback" }),
255
+ issuer: z.string({}).meta({ description: "The issuer of the provider" }),
256
+ domain: z.string({}).meta({ description: "The domain of the provider. This is used for email matching" }),
257
+ oidcConfig: z.object({
258
+ clientId: z.string({}).meta({ description: "The client ID" }),
259
+ clientSecret: z.string({}).meta({ description: "The client secret" }),
260
+ authorizationEndpoint: z.string({}).meta({ description: "The authorization endpoint" }).optional(),
261
+ tokenEndpoint: z.string({}).meta({ description: "The token endpoint" }).optional(),
262
+ userInfoEndpoint: z.string({}).meta({ description: "The user info endpoint" }).optional(),
263
+ tokenEndpointAuthentication: z.enum(["client_secret_post", "client_secret_basic"]).optional(),
264
+ jwksEndpoint: z.string({}).meta({ description: "The JWKS endpoint" }).optional(),
265
+ discoveryEndpoint: z.string().optional(),
266
+ scopes: z.array(z.string(), {}).meta({ description: "The scopes to request. Defaults to ['openid', 'email', 'profile', 'offline_access']" }).optional(),
267
+ pkce: z.boolean({}).meta({ description: "Whether to use PKCE for the authorization flow" }).default(true).optional(),
268
+ mapping: z.object({
269
+ id: z.string({}).meta({ description: "Field mapping for user ID (defaults to 'sub')" }),
270
+ email: z.string({}).meta({ description: "Field mapping for email (defaults to 'email')" }),
271
+ emailVerified: z.string({}).meta({ description: "Field mapping for email verification (defaults to 'email_verified')" }).optional(),
272
+ name: z.string({}).meta({ description: "Field mapping for name (defaults to 'name')" }),
273
+ image: z.string({}).meta({ description: "Field mapping for image (defaults to 'picture')" }).optional(),
274
+ extraFields: z.record(z.string(), z.any()).optional()
275
+ }).optional()
276
+ }).optional(),
277
+ samlConfig: z.object({
278
+ entryPoint: z.string({}).meta({ description: "The entry point of the provider" }),
279
+ cert: z.string({}).meta({ description: "The certificate of the provider" }),
280
+ callbackUrl: z.string({}).meta({ description: "The callback URL of the provider" }),
281
+ audience: z.string().optional(),
282
+ idpMetadata: z.object({
283
+ metadata: z.string().optional(),
284
+ entityID: z.string().optional(),
285
+ cert: z.string().optional(),
286
+ privateKey: z.string().optional(),
287
+ privateKeyPass: z.string().optional(),
288
+ isAssertionEncrypted: z.boolean().optional(),
289
+ encPrivateKey: z.string().optional(),
290
+ encPrivateKeyPass: z.string().optional(),
291
+ singleSignOnService: z.array(z.object({
292
+ Binding: z.string().meta({ description: "The binding type for the SSO service" }),
293
+ Location: z.string().meta({ description: "The URL for the SSO service" })
294
+ })).optional().meta({ description: "Single Sign-On service configuration" })
295
+ }).optional(),
296
+ spMetadata: z.object({
297
+ metadata: z.string().optional(),
298
+ entityID: z.string().optional(),
299
+ binding: z.string().optional(),
300
+ privateKey: z.string().optional(),
301
+ privateKeyPass: z.string().optional(),
302
+ isAssertionEncrypted: z.boolean().optional(),
303
+ encPrivateKey: z.string().optional(),
304
+ encPrivateKeyPass: z.string().optional()
305
+ }),
306
+ wantAssertionsSigned: z.boolean().optional(),
307
+ signatureAlgorithm: z.string().optional(),
308
+ digestAlgorithm: z.string().optional(),
309
+ identifierFormat: z.string().optional(),
310
+ privateKey: z.string().optional(),
311
+ decryptionPvk: z.string().optional(),
312
+ additionalParams: z.record(z.string(), z.any()).optional(),
313
+ mapping: z.object({
314
+ id: z.string({}).meta({ description: "Field mapping for user ID (defaults to 'nameID')" }),
315
+ email: z.string({}).meta({ description: "Field mapping for email (defaults to 'email')" }),
316
+ emailVerified: z.string({}).meta({ description: "Field mapping for email verification" }).optional(),
317
+ name: z.string({}).meta({ description: "Field mapping for name (defaults to 'displayName')" }),
318
+ firstName: z.string({}).meta({ description: "Field mapping for first name (defaults to 'givenName')" }).optional(),
319
+ lastName: z.string({}).meta({ description: "Field mapping for last name (defaults to 'surname')" }).optional(),
320
+ extraFields: z.record(z.string(), z.any()).optional()
321
+ }).optional()
322
+ }).optional(),
323
+ organizationId: z.string({}).meta({ description: "If organization plugin is enabled, the organization id to link the provider to" }).optional(),
324
+ overrideUserInfo: z.boolean({}).meta({ description: "Override user info with the provider info. Defaults to false" }).default(false).optional()
325
+ });
247
326
  const registerSSOProvider = (options) => {
248
327
  return createAuthEndpoint("/sso/register", {
249
328
  method: "POST",
250
- body: z.object({
251
- providerId: z.string({}).meta({ description: "The ID of the provider. This is used to identify the provider during login and callback" }),
252
- issuer: z.string({}).meta({ description: "The issuer of the provider" }),
253
- domain: z.string({}).meta({ description: "The domain of the provider. This is used for email matching" }),
254
- oidcConfig: z.object({
255
- clientId: z.string({}).meta({ description: "The client ID" }),
256
- clientSecret: z.string({}).meta({ description: "The client secret" }),
257
- authorizationEndpoint: z.string({}).meta({ description: "The authorization endpoint" }).optional(),
258
- tokenEndpoint: z.string({}).meta({ description: "The token endpoint" }).optional(),
259
- userInfoEndpoint: z.string({}).meta({ description: "The user info endpoint" }).optional(),
260
- tokenEndpointAuthentication: z.enum(["client_secret_post", "client_secret_basic"]).optional(),
261
- jwksEndpoint: z.string({}).meta({ description: "The JWKS endpoint" }).optional(),
262
- discoveryEndpoint: z.string().optional(),
263
- scopes: z.array(z.string(), {}).meta({ description: "The scopes to request. Defaults to ['openid', 'email', 'profile', 'offline_access']" }).optional(),
264
- pkce: z.boolean({}).meta({ description: "Whether to use PKCE for the authorization flow" }).default(true).optional(),
265
- mapping: z.object({
266
- id: z.string({}).meta({ description: "Field mapping for user ID (defaults to 'sub')" }),
267
- email: z.string({}).meta({ description: "Field mapping for email (defaults to 'email')" }),
268
- emailVerified: z.string({}).meta({ description: "Field mapping for email verification (defaults to 'email_verified')" }).optional(),
269
- name: z.string({}).meta({ description: "Field mapping for name (defaults to 'name')" }),
270
- image: z.string({}).meta({ description: "Field mapping for image (defaults to 'picture')" }).optional(),
271
- extraFields: z.record(z.string(), z.any()).optional()
272
- }).optional()
273
- }).optional(),
274
- samlConfig: z.object({
275
- entryPoint: z.string({}).meta({ description: "The entry point of the provider" }),
276
- cert: z.string({}).meta({ description: "The certificate of the provider" }),
277
- callbackUrl: z.string({}).meta({ description: "The callback URL of the provider" }),
278
- audience: z.string().optional(),
279
- idpMetadata: z.object({
280
- metadata: z.string().optional(),
281
- entityID: z.string().optional(),
282
- cert: z.string().optional(),
283
- privateKey: z.string().optional(),
284
- privateKeyPass: z.string().optional(),
285
- isAssertionEncrypted: z.boolean().optional(),
286
- encPrivateKey: z.string().optional(),
287
- encPrivateKeyPass: z.string().optional(),
288
- singleSignOnService: z.array(z.object({
289
- Binding: z.string().meta({ description: "The binding type for the SSO service" }),
290
- Location: z.string().meta({ description: "The URL for the SSO service" })
291
- })).optional().meta({ description: "Single Sign-On service configuration" })
292
- }).optional(),
293
- spMetadata: z.object({
294
- metadata: z.string().optional(),
295
- entityID: z.string().optional(),
296
- binding: z.string().optional(),
297
- privateKey: z.string().optional(),
298
- privateKeyPass: z.string().optional(),
299
- isAssertionEncrypted: z.boolean().optional(),
300
- encPrivateKey: z.string().optional(),
301
- encPrivateKeyPass: z.string().optional()
302
- }),
303
- wantAssertionsSigned: z.boolean().optional(),
304
- signatureAlgorithm: z.string().optional(),
305
- digestAlgorithm: z.string().optional(),
306
- identifierFormat: z.string().optional(),
307
- privateKey: z.string().optional(),
308
- decryptionPvk: z.string().optional(),
309
- additionalParams: z.record(z.string(), z.any()).optional(),
310
- mapping: z.object({
311
- id: z.string({}).meta({ description: "Field mapping for user ID (defaults to 'nameID')" }),
312
- email: z.string({}).meta({ description: "Field mapping for email (defaults to 'email')" }),
313
- emailVerified: z.string({}).meta({ description: "Field mapping for email verification" }).optional(),
314
- name: z.string({}).meta({ description: "Field mapping for name (defaults to 'displayName')" }),
315
- firstName: z.string({}).meta({ description: "Field mapping for first name (defaults to 'givenName')" }).optional(),
316
- lastName: z.string({}).meta({ description: "Field mapping for last name (defaults to 'surname')" }).optional(),
317
- extraFields: z.record(z.string(), z.any()).optional()
318
- }).optional()
319
- }).optional(),
320
- organizationId: z.string({}).meta({ description: "If organization plugin is enabled, the organization id to link the provider to" }).optional(),
321
- overrideUserInfo: z.boolean({}).meta({ description: "Override user info with the provider info. Defaults to false" }).default(false).optional()
322
- }),
329
+ body: ssoProviderBodySchema,
323
330
  use: [sessionMiddleware],
324
331
  metadata: { openapi: {
325
332
  operationId: "registerSSOProvider",
@@ -584,30 +591,31 @@ const registerSSOProvider = (options) => {
584
591
  }
585
592
  return ctx.json({
586
593
  ...provider,
587
- oidcConfig: JSON.parse(provider.oidcConfig),
588
- samlConfig: JSON.parse(provider.samlConfig),
594
+ oidcConfig: safeJsonParse(provider.oidcConfig),
595
+ samlConfig: safeJsonParse(provider.samlConfig),
589
596
  redirectURI: `${ctx.context.baseURL}/sso/callback/${provider.providerId}`,
590
597
  ...options?.domainVerification?.enabled ? { domainVerified } : {},
591
598
  ...options?.domainVerification?.enabled ? { domainVerificationToken } : {}
592
599
  });
593
600
  });
594
601
  };
602
+ const signInSSOBodySchema = z.object({
603
+ email: z.string({}).meta({ description: "The email address to sign in with. This is used to identify the issuer to sign in with. It's optional if the issuer is provided" }).optional(),
604
+ organizationSlug: z.string({}).meta({ description: "The slug of the organization to sign in with" }).optional(),
605
+ providerId: z.string({}).meta({ description: "The ID of the provider to sign in with. This can be provided instead of email or issuer" }).optional(),
606
+ domain: z.string({}).meta({ description: "The domain of the provider." }).optional(),
607
+ callbackURL: z.string({}).meta({ description: "The URL to redirect to after login" }),
608
+ errorCallbackURL: z.string({}).meta({ description: "The URL to redirect to after login" }).optional(),
609
+ newUserCallbackURL: z.string({}).meta({ description: "The URL to redirect to after login if the user is new" }).optional(),
610
+ scopes: z.array(z.string(), {}).meta({ description: "Scopes to request from the provider." }).optional(),
611
+ loginHint: z.string({}).meta({ description: "Login hint to send to the identity provider (e.g., email or identifier). If supported, will be sent as 'login_hint'." }).optional(),
612
+ requestSignUp: z.boolean({}).meta({ description: "Explicitly request sign-up. Useful when disableImplicitSignUp is true for this provider" }).optional(),
613
+ providerType: z.enum(["oidc", "saml"]).optional()
614
+ });
595
615
  const signInSSO = (options) => {
596
616
  return createAuthEndpoint("/sign-in/sso", {
597
617
  method: "POST",
598
- body: z.object({
599
- email: z.string({}).meta({ description: "The email address to sign in with. This is used to identify the issuer to sign in with. It's optional if the issuer is provided" }).optional(),
600
- organizationSlug: z.string({}).meta({ description: "The slug of the organization to sign in with" }).optional(),
601
- providerId: z.string({}).meta({ description: "The ID of the provider to sign in with. This can be provided instead of email or issuer" }).optional(),
602
- domain: z.string({}).meta({ description: "The domain of the provider." }).optional(),
603
- callbackURL: z.string({}).meta({ description: "The URL to redirect to after login" }),
604
- errorCallbackURL: z.string({}).meta({ description: "The URL to redirect to after login" }).optional(),
605
- newUserCallbackURL: z.string({}).meta({ description: "The URL to redirect to after login if the user is new" }).optional(),
606
- scopes: z.array(z.string(), {}).meta({ description: "Scopes to request from the provider." }).optional(),
607
- loginHint: z.string({}).meta({ description: "Login hint to send to the identity provider (e.g., email or identifier). If supported, will be sent as 'login_hint'." }).optional(),
608
- requestSignUp: z.boolean({}).meta({ description: "Explicitly request sign-up. Useful when disableImplicitSignUp is true for this provider" }).optional(),
609
- providerType: z.enum(["oidc", "saml"]).optional()
610
- }),
618
+ body: signInSSOBodySchema,
611
619
  metadata: { openapi: {
612
620
  operationId: "signInWithSSO",
613
621
  summary: "Sign in with SSO provider",
@@ -781,15 +789,16 @@ const signInSSO = (options) => {
781
789
  throw new APIError("BAD_REQUEST", { message: "Invalid SSO provider" });
782
790
  });
783
791
  };
792
+ const callbackSSOQuerySchema = z.object({
793
+ code: z.string().optional(),
794
+ state: z.string(),
795
+ error: z.string().optional(),
796
+ error_description: z.string().optional()
797
+ });
784
798
  const callbackSSO = (options) => {
785
799
  return createAuthEndpoint("/sso/callback/:providerId", {
786
800
  method: "GET",
787
- query: z.object({
788
- code: z.string().optional(),
789
- state: z.string(),
790
- error: z.string().optional(),
791
- error_description: z.string().optional()
792
- }),
801
+ query: callbackSSOQuerySchema,
793
802
  allowedMediaTypes: ["application/x-www-form-urlencoded", "application/json"],
794
803
  metadata: {
795
804
  isAction: false,
@@ -892,6 +901,7 @@ const callbackSSO = (options) => {
892
901
  userInfo = userInfoResponse.data;
893
902
  }
894
903
  if (!userInfo.email || !userInfo.id) throw ctx.redirect(`${errorURL || callbackURL}/error?error=invalid_provider&error_description=missing_user_info`);
904
+ const isTrustedProvider = "domainVerified" in provider && provider.domainVerified === true && validateEmailDomain(userInfo.email, provider.domain);
895
905
  const linked = await handleOAuthUserInfo(ctx, {
896
906
  userInfo: {
897
907
  email: userInfo.email,
@@ -912,7 +922,8 @@ const callbackSSO = (options) => {
912
922
  },
913
923
  callbackURL,
914
924
  disableSignUp: options?.disableImplicitSignUp && !requestSignUp,
915
- overrideUserInfo: config.overrideUserInfo
925
+ overrideUserInfo: config.overrideUserInfo,
926
+ isTrustedProvider
916
927
  });
917
928
  if (linked.error) throw ctx.redirect(`${errorURL || callbackURL}/error?error=${linked.error}`);
918
929
  const { session, user } = linked.data;
@@ -966,13 +977,14 @@ const callbackSSO = (options) => {
966
977
  throw ctx.redirect(toRedirectTo);
967
978
  });
968
979
  };
980
+ const callbackSSOSAMLBodySchema = z.object({
981
+ SAMLResponse: z.string(),
982
+ RelayState: z.string().optional()
983
+ });
969
984
  const callbackSSOSAML = (options) => {
970
985
  return createAuthEndpoint("/sso/saml2/callback/:providerId", {
971
986
  method: "POST",
972
- body: z.object({
973
- SAMLResponse: z.string(),
974
- RelayState: z.string().optional()
975
- }),
987
+ body: callbackSSOSAMLBodySchema,
976
988
  metadata: {
977
989
  isAction: false,
978
990
  allowedMediaTypes: ["application/x-www-form-urlencoded", "application/json"],
@@ -1111,38 +1123,52 @@ const callbackSSOSAML = (options) => {
1111
1123
  value: userInfo.email
1112
1124
  }]
1113
1125
  });
1114
- if (existingUser) user = existingUser;
1115
- else {
1126
+ if (existingUser) {
1127
+ if (!await ctx.context.adapter.findOne({
1128
+ model: "account",
1129
+ where: [
1130
+ {
1131
+ field: "userId",
1132
+ value: existingUser.id
1133
+ },
1134
+ {
1135
+ field: "providerId",
1136
+ value: provider.providerId
1137
+ },
1138
+ {
1139
+ field: "accountId",
1140
+ value: userInfo.id
1141
+ }
1142
+ ]
1143
+ })) {
1144
+ if (!(ctx.context.options.account?.accountLinking?.trustedProviders?.includes(provider.providerId) || "domainVerified" in provider && provider.domainVerified && validateEmailDomain(userInfo.email, provider.domain))) {
1145
+ const redirectUrl = RelayState || parsedSamlConfig.callbackUrl || ctx.context.baseURL;
1146
+ throw ctx.redirect(`${redirectUrl}?error=account_not_linked`);
1147
+ }
1148
+ await ctx.context.internalAdapter.createAccount({
1149
+ userId: existingUser.id,
1150
+ providerId: provider.providerId,
1151
+ accountId: userInfo.id,
1152
+ accessToken: "",
1153
+ refreshToken: ""
1154
+ });
1155
+ }
1156
+ user = existingUser;
1157
+ } else {
1116
1158
  if (options?.disableImplicitSignUp) throw new APIError("UNAUTHORIZED", { message: "User not found and implicit sign up is disabled for this provider" });
1117
1159
  user = await ctx.context.internalAdapter.createUser({
1118
1160
  email: userInfo.email,
1119
1161
  name: userInfo.name,
1120
1162
  emailVerified: userInfo.emailVerified
1121
1163
  });
1164
+ await ctx.context.internalAdapter.createAccount({
1165
+ userId: user.id,
1166
+ providerId: provider.providerId,
1167
+ accountId: userInfo.id,
1168
+ accessToken: "",
1169
+ refreshToken: ""
1170
+ });
1122
1171
  }
1123
- if (!await ctx.context.adapter.findOne({
1124
- model: "account",
1125
- where: [
1126
- {
1127
- field: "userId",
1128
- value: user.id
1129
- },
1130
- {
1131
- field: "providerId",
1132
- value: provider.providerId
1133
- },
1134
- {
1135
- field: "accountId",
1136
- value: userInfo.id
1137
- }
1138
- ]
1139
- })) await ctx.context.internalAdapter.createAccount({
1140
- userId: user.id,
1141
- providerId: provider.providerId,
1142
- accountId: userInfo.id,
1143
- accessToken: "",
1144
- refreshToken: ""
1145
- });
1146
1172
  if (options?.provisionUser) await options.provisionUser({
1147
1173
  user,
1148
1174
  userInfo,
@@ -1186,14 +1212,16 @@ const callbackSSOSAML = (options) => {
1186
1212
  throw ctx.redirect(callbackUrl);
1187
1213
  });
1188
1214
  };
1215
+ const acsEndpointParamsSchema = z.object({ providerId: z.string().optional() });
1216
+ const acsEndpointBodySchema = z.object({
1217
+ SAMLResponse: z.string(),
1218
+ RelayState: z.string().optional()
1219
+ });
1189
1220
  const acsEndpoint = (options) => {
1190
1221
  return createAuthEndpoint("/sso/saml2/sp/acs/:providerId", {
1191
1222
  method: "POST",
1192
- params: z.object({ providerId: z.string().optional() }),
1193
- body: z.object({
1194
- SAMLResponse: z.string(),
1195
- RelayState: z.string().optional()
1196
- }),
1223
+ params: acsEndpointParamsSchema,
1224
+ body: acsEndpointBodySchema,
1197
1225
  metadata: {
1198
1226
  isAction: false,
1199
1227
  allowedMediaTypes: ["application/x-www-form-urlencoded", "application/json"],
package/package.json CHANGED
@@ -1,13 +1,14 @@
1
1
  {
2
2
  "name": "@better-auth/sso",
3
3
  "author": "Bereket Engida",
4
- "version": "1.4.6-beta.2",
4
+ "version": "1.4.6-beta.6",
5
5
  "type": "module",
6
6
  "main": "dist/index.mjs",
7
+ "types": "dist/index.d.mts",
7
8
  "homepage": "https://www.better-auth.com/docs/plugins/sso",
8
9
  "repository": {
9
10
  "type": "git",
10
- "url": "https://github.com/better-auth/better-auth",
11
+ "url": "git+https://github.com/better-auth/better-auth.git",
11
12
  "directory": "packages/sso"
12
13
  },
13
14
  "license": "MIT",
@@ -60,18 +61,19 @@
60
61
  "devDependencies": {
61
62
  "@types/body-parser": "^1.19.6",
62
63
  "@types/express": "^5.0.5",
63
- "better-call": "1.1.4",
64
+ "better-call": "1.1.5",
64
65
  "body-parser": "^2.2.1",
65
66
  "express": "^5.1.0",
66
67
  "oauth2-mock-server": "^8.2.0",
67
- "tsdown": "^0.16.0",
68
- "better-auth": "1.4.6-beta.2"
68
+ "tsdown": "^0.17.0",
69
+ "better-auth": "1.4.6-beta.6"
69
70
  },
70
71
  "peerDependencies": {
71
- "better-auth": "1.4.6-beta.2"
72
+ "better-auth": "1.4.6-beta.6"
72
73
  },
73
74
  "scripts": {
74
75
  "test": "vitest",
76
+ "coverage": "vitest run --coverage",
75
77
  "lint:package": "publint run --strict",
76
78
  "build": "tsdown",
77
79
  "dev": "tsdown --watch",