@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.
- package/.turbo/turbo-build.log +8 -8
- package/dist/client.d.mts +1 -1
- package/dist/{index-DCyJckhH.d.mts → index-D-JmJR9N.d.mts} +12 -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/utils.ts +31 -0
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
|
|
2
|
-
> @better-auth/sso@1.4.6
|
|
2
|
+
> @better-auth/sso@1.4.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
|
-
[34mℹ[39m [2mdist/[22m[32m[1mclient.d.mts[22m[39m [2m 0.49 kB[22m [2m│ gzip: 0.
|
|
12
|
+
[34mℹ[39m [2mdist/[22m[32m[1mclient.d.mts[22m[39m [2m 0.49 kB[22m [2m│ gzip: 0.30 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-D-JmJR9N.d.mts[39m [2m25.42 kB[22m [2m│ gzip: 3.95 kB[22m
|
|
15
|
+
[34mℹ[39m 5 files, total: 85.98 kB
|
|
16
|
+
[32m✔[39m Build complete in [32m11585ms[39m
|
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 {
|
|
@@ -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) =>
|
|
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:
|
|
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) =>
|
|
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:
|
|
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: () =>
|
|
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) =>
|
|
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:
|
|
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) =>
|
|
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) =>
|
|
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) =>
|
|
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) =>
|
|
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-
|
|
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:
|
|
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
|
|
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.
|
|
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
|
|
68
|
+
"tsdown": "^0.17.0",
|
|
69
|
+
"better-auth": "1.4.6"
|
|
69
70
|
},
|
|
70
71
|
"peerDependencies": {
|
|
71
|
-
"better-auth": "1.4.6
|
|
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",
|