@better-auth/sso 1.4.6-beta.2 → 1.4.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 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
- ℹ dist/client.d.mts  0.49 kB │ gzip: 0.29 kB
12
+ ℹ dist/client.d.mts  0.49 kB │ gzip: 0.30 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-D-JmJR9N.d.mts 25.42 kB │ gzip: 3.95 kB
15
+ ℹ 5 files, total: 85.98 kB
16
+ ✔ Build complete in 11585ms
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-D-JmJR9N.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 {
@@ -240,7 +240,7 @@ interface SSOOptions {
240
240
  }
241
241
  //#endregion
242
242
  //#region src/routes/domain-verification.d.ts
243
- declare const requestDomainVerification: (options: SSOOptions) => better_call7.StrictEndpoint<"/sso/request-domain-verification", {
243
+ declare const requestDomainVerification: (options: SSOOptions) => better_call0.StrictEndpoint<"/sso/request-domain-verification", {
244
244
  method: "POST";
245
245
  body: z.ZodObject<{
246
246
  providerId: z.ZodString;
@@ -262,7 +262,7 @@ declare const requestDomainVerification: (options: SSOOptions) => better_call7.S
262
262
  };
263
263
  };
264
264
  };
265
- use: ((inputContext: better_call7.MiddlewareInputContext<better_call7.MiddlewareOptions>) => Promise<{
265
+ use: ((inputContext: better_call0.MiddlewareInputContext<better_call0.MiddlewareOptions>) => Promise<{
266
266
  session: {
267
267
  session: Record<string, any> & {
268
268
  id: string;
@@ -290,7 +290,7 @@ declare const requestDomainVerification: (options: SSOOptions) => better_call7.S
290
290
  }, {
291
291
  domainVerificationToken: string;
292
292
  }>;
293
- declare const verifyDomain: (options: SSOOptions) => better_call7.StrictEndpoint<"/sso/verify-domain", {
293
+ declare const verifyDomain: (options: SSOOptions) => better_call0.StrictEndpoint<"/sso/verify-domain", {
294
294
  method: "POST";
295
295
  body: z.ZodObject<{
296
296
  providerId: z.ZodString;
@@ -315,7 +315,7 @@ declare const verifyDomain: (options: SSOOptions) => better_call7.StrictEndpoint
315
315
  };
316
316
  };
317
317
  };
318
- use: ((inputContext: better_call7.MiddlewareInputContext<better_call7.MiddlewareOptions>) => Promise<{
318
+ use: ((inputContext: better_call0.MiddlewareInputContext<better_call0.MiddlewareOptions>) => Promise<{
319
319
  session: {
320
320
  session: Record<string, any> & {
321
321
  id: string;
@@ -343,7 +343,7 @@ declare const verifyDomain: (options: SSOOptions) => better_call7.StrictEndpoint
343
343
  }, void>;
344
344
  //#endregion
345
345
  //#region src/routes/sso.d.ts
346
- declare const spMetadata: () => better_call7.StrictEndpoint<"/sso/saml2/sp/metadata", {
346
+ declare const spMetadata: () => better_call0.StrictEndpoint<"/sso/saml2/sp/metadata", {
347
347
  method: "GET";
348
348
  query: z.ZodObject<{
349
349
  providerId: z.ZodString;
@@ -367,7 +367,7 @@ declare const spMetadata: () => better_call7.StrictEndpoint<"/sso/saml2/sp/metad
367
367
  } & {
368
368
  use: any[];
369
369
  }, Response>;
370
- declare const registerSSOProvider: <O extends SSOOptions>(options: O) => better_call7.StrictEndpoint<"/sso/register", {
370
+ declare const registerSSOProvider: <O extends SSOOptions>(options: O) => better_call0.StrictEndpoint<"/sso/register", {
371
371
  method: "POST";
372
372
  body: z.ZodObject<{
373
373
  providerId: z.ZodString;
@@ -445,7 +445,7 @@ declare const registerSSOProvider: <O extends SSOOptions>(options: O) => better_
445
445
  organizationId: z.ZodOptional<z.ZodString>;
446
446
  overrideUserInfo: z.ZodOptional<z.ZodDefault<z.ZodBoolean>>;
447
447
  }, z.core.$strip>;
448
- use: ((inputContext: better_call7.MiddlewareInputContext<better_call7.MiddlewareOptions>) => Promise<{
448
+ use: ((inputContext: better_call0.MiddlewareInputContext<better_call0.MiddlewareOptions>) => Promise<{
449
449
  session: {
450
450
  session: Record<string, any> & {
451
451
  id: string;
@@ -637,7 +637,7 @@ declare const registerSSOProvider: <O extends SSOOptions>(options: O) => better_
637
637
  domainVerified: boolean;
638
638
  domainVerificationToken: string;
639
639
  } & SSOProvider<O> : SSOProvider<O>>;
640
- declare const signInSSO: (options?: SSOOptions) => better_call7.StrictEndpoint<"/sign-in/sso", {
640
+ declare const signInSSO: (options?: SSOOptions) => better_call0.StrictEndpoint<"/sign-in/sso", {
641
641
  method: "POST";
642
642
  body: z.ZodObject<{
643
643
  email: z.ZodOptional<z.ZodString>;
@@ -733,7 +733,7 @@ declare const signInSSO: (options?: SSOOptions) => better_call7.StrictEndpoint<"
733
733
  url: string;
734
734
  redirect: boolean;
735
735
  }>;
736
- declare const callbackSSO: (options?: SSOOptions) => better_call7.StrictEndpoint<"/sso/callback/:providerId", {
736
+ declare const callbackSSO: (options?: SSOOptions) => better_call0.StrictEndpoint<"/sso/callback/:providerId", {
737
737
  method: "GET";
738
738
  query: z.ZodObject<{
739
739
  code: z.ZodOptional<z.ZodString>;
@@ -758,7 +758,7 @@ declare const callbackSSO: (options?: SSOOptions) => better_call7.StrictEndpoint
758
758
  } & {
759
759
  use: any[];
760
760
  }, never>;
761
- declare const callbackSSOSAML: (options?: SSOOptions) => better_call7.StrictEndpoint<"/sso/saml2/callback/:providerId", {
761
+ declare const callbackSSOSAML: (options?: SSOOptions) => better_call0.StrictEndpoint<"/sso/saml2/callback/:providerId", {
762
762
  method: "POST";
763
763
  body: z.ZodObject<{
764
764
  SAMLResponse: z.ZodString;
@@ -787,7 +787,7 @@ declare const callbackSSOSAML: (options?: SSOOptions) => better_call7.StrictEndp
787
787
  } & {
788
788
  use: any[];
789
789
  }, never>;
790
- declare const acsEndpoint: (options?: SSOOptions) => better_call7.StrictEndpoint<"/sso/saml2/sp/acs/:providerId", {
790
+ declare const acsEndpoint: (options?: SSOOptions) => better_call0.StrictEndpoint<"/sso/saml2/sp/acs/:providerId", {
791
791
  method: "POST";
792
792
  params: z.ZodObject<{
793
793
  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-D-JmJR9N.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",
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"
69
70
  },
70
71
  "peerDependencies": {
71
- "better-auth": "1.4.6-beta.2"
72
+ "better-auth": "1.4.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",