@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.
- package/.turbo/turbo-build.log +7 -7
- package/bump.config.ts +5 -0
- package/dist/client.d.mts +1 -1
- package/dist/{index-DCyJckhH.d.mts → index-CYgzSZS4.d.mts} +18 -12
- package/dist/index.d.mts +1 -1
- package/dist/index.mjs +174 -146
- package/package.json +8 -6
- package/src/oidc.test.ts +164 -0
- package/src/routes/domain-verification.ts +6 -6
- package/src/routes/sso.ts +336 -333
- package/src/saml.test.ts +346 -0
- package/src/types.ts +6 -0
- package/src/utils.ts +31 -0
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
|
|
2
|
-
> @better-auth/sso@1.4.6-beta.
|
|
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
|
-
[34mℹ[39m tsdown [2mv0.
|
|
6
|
-
[34mℹ[39m
|
|
5
|
+
[34mℹ[39m tsdown [2mv0.17.0[22m powered by rolldown [2mv1.0.0-beta.53[22m
|
|
6
|
+
[34mℹ[39m config file: [4m/home/runner/work/better-auth/better-auth/packages/sso/tsdown.config.ts[24m
|
|
7
7
|
[34mℹ[39m entry: [34msrc/index.ts, src/client.ts[39m
|
|
8
8
|
[34mℹ[39m tsconfig: [34mtsconfig.json[39m
|
|
9
9
|
[34mℹ[39m Build start
|
|
10
|
-
[34mℹ[39m [2mdist/[22m[1mindex.mjs[22m [
|
|
10
|
+
[34mℹ[39m [2mdist/[22m[1mindex.mjs[22m [2m59.70 kB[22m [2m│ gzip: 10.49 kB[22m
|
|
11
11
|
[34mℹ[39m [2mdist/[22m[1mclient.mjs[22m [2m 0.15 kB[22m [2m│ gzip: 0.14 kB[22m
|
|
12
12
|
[34mℹ[39m [2mdist/[22m[32m[1mclient.d.mts[22m[39m [2m 0.49 kB[22m [2m│ gzip: 0.29 kB[22m
|
|
13
13
|
[34mℹ[39m [2mdist/[22m[32m[1mindex.d.mts[22m[39m [2m 0.21 kB[22m [2m│ gzip: 0.15 kB[22m
|
|
14
|
-
[34mℹ[39m [2mdist/[22m[32mindex-
|
|
15
|
-
[34mℹ[39m 5 files, total:
|
|
16
|
-
[32m✔[39m Build complete in [
|
|
14
|
+
[34mℹ[39m [2mdist/[22m[32mindex-CYgzSZS4.d.mts[39m [2m25.84 kB[22m [2m│ gzip: 4.13 kB[22m
|
|
15
|
+
[34mℹ[39m 5 files, total: 86.39 kB
|
|
16
|
+
[32m✔[39m Build complete in [32m11274ms[39m
|
package/bump.config.ts
ADDED
package/dist/client.d.mts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as z from "zod/v4";
|
|
2
2
|
import { OAuth2Tokens, User } from "better-auth";
|
|
3
|
-
import * as
|
|
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) =>
|
|
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:
|
|
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) =>
|
|
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:
|
|
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: () =>
|
|
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) =>
|
|
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:
|
|
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) =>
|
|
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) =>
|
|
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) =>
|
|
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) =>
|
|
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-
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
588
|
-
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:
|
|
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:
|
|
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:
|
|
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)
|
|
1115
|
-
|
|
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:
|
|
1193
|
-
body:
|
|
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.
|
|
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.
|
|
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.
|
|
68
|
-
"better-auth": "1.4.6-beta.
|
|
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.
|
|
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",
|