@better-auth/sso 1.4.0-beta.21 → 1.4.0-beta.23
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 +10 -10
- package/dist/client.d.mts +14 -3
- package/dist/client.mjs +1 -1
- package/dist/{index-C091fIpa.d.mts → index-xXD__4zM.d.mts} +195 -19
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +302 -51
- package/package.json +4 -4
- package/src/client.ts +20 -3
- package/src/domain-verification.test.ts +550 -0
- package/src/index.ts +66 -12
- package/src/routes/domain-verification.ts +275 -0
- package/src/routes/sso.ts +133 -19
- package/src/saml.test.ts +143 -1
- package/src/types.ts +51 -3
- package/src/utils.ts +10 -0
- package/vitest.config.ts +3 -0
package/dist/index.mjs
CHANGED
|
@@ -1,13 +1,197 @@
|
|
|
1
|
-
import { createAuthorizationURL, generateState, parseState, validateAuthorizationCode, validateToken } from "better-auth";
|
|
2
1
|
import { XMLValidator } from "fast-xml-parser";
|
|
3
2
|
import * as saml from "samlify";
|
|
4
|
-
import { BetterFetchError, betterFetch } from "@better-fetch/fetch";
|
|
5
3
|
import { APIError, createAuthEndpoint, sessionMiddleware } from "better-auth/api";
|
|
4
|
+
import { generateRandomString } from "better-auth/crypto";
|
|
5
|
+
import * as z from "zod/v4";
|
|
6
|
+
import { BetterFetchError, betterFetch } from "@better-fetch/fetch";
|
|
7
|
+
import { createAuthorizationURL, generateState, parseState, validateAuthorizationCode, validateToken } from "better-auth";
|
|
6
8
|
import { setSessionCookie } from "better-auth/cookies";
|
|
7
9
|
import { handleOAuthUserInfo } from "better-auth/oauth2";
|
|
8
10
|
import { decodeJwt } from "jose";
|
|
9
|
-
import * as z from "zod/v4";
|
|
10
11
|
|
|
12
|
+
//#region src/routes/domain-verification.ts
|
|
13
|
+
const requestDomainVerification = (options) => {
|
|
14
|
+
return createAuthEndpoint("/sso/request-domain-verification", {
|
|
15
|
+
method: "POST",
|
|
16
|
+
body: z.object({ providerId: z.string() }),
|
|
17
|
+
metadata: { openapi: {
|
|
18
|
+
summary: "Request a domain verification",
|
|
19
|
+
description: "Request a domain verification for the given SSO provider",
|
|
20
|
+
responses: {
|
|
21
|
+
"404": { description: "Provider not found" },
|
|
22
|
+
"409": { description: "Domain has already been verified" },
|
|
23
|
+
"201": { description: "Domain submitted for verification" }
|
|
24
|
+
}
|
|
25
|
+
} },
|
|
26
|
+
use: [sessionMiddleware]
|
|
27
|
+
}, async (ctx) => {
|
|
28
|
+
const body = ctx.body;
|
|
29
|
+
const provider = await ctx.context.adapter.findOne({
|
|
30
|
+
model: "ssoProvider",
|
|
31
|
+
where: [{
|
|
32
|
+
field: "providerId",
|
|
33
|
+
value: body.providerId
|
|
34
|
+
}]
|
|
35
|
+
});
|
|
36
|
+
if (!provider) throw new APIError("NOT_FOUND", {
|
|
37
|
+
message: "Provider not found",
|
|
38
|
+
code: "PROVIDER_NOT_FOUND"
|
|
39
|
+
});
|
|
40
|
+
const userId = ctx.context.session.user.id;
|
|
41
|
+
let isOrgMember = true;
|
|
42
|
+
if (provider.organizationId) isOrgMember = await ctx.context.adapter.count({
|
|
43
|
+
model: "member",
|
|
44
|
+
where: [{
|
|
45
|
+
field: "userId",
|
|
46
|
+
value: userId
|
|
47
|
+
}, {
|
|
48
|
+
field: "organizationId",
|
|
49
|
+
value: provider.organizationId
|
|
50
|
+
}]
|
|
51
|
+
}) > 0;
|
|
52
|
+
if (provider.userId !== userId || !isOrgMember) throw new APIError("FORBIDDEN", {
|
|
53
|
+
message: "User must be owner of or belong to the SSO provider organization",
|
|
54
|
+
code: "INSUFICCIENT_ACCESS"
|
|
55
|
+
});
|
|
56
|
+
if ("domainVerified" in provider && provider.domainVerified) throw new APIError("CONFLICT", {
|
|
57
|
+
message: "Domain has already been verified",
|
|
58
|
+
code: "DOMAIN_VERIFIED"
|
|
59
|
+
});
|
|
60
|
+
const activeVerification = await ctx.context.adapter.findOne({
|
|
61
|
+
model: "verification",
|
|
62
|
+
where: [{
|
|
63
|
+
field: "identifier",
|
|
64
|
+
value: options.domainVerification?.tokenPrefix ? `${options.domainVerification?.tokenPrefix}-${provider.providerId}` : `better-auth-token-${provider.providerId}`
|
|
65
|
+
}, {
|
|
66
|
+
field: "expiresAt",
|
|
67
|
+
value: /* @__PURE__ */ new Date(),
|
|
68
|
+
operator: "gt"
|
|
69
|
+
}]
|
|
70
|
+
});
|
|
71
|
+
if (activeVerification) {
|
|
72
|
+
ctx.setStatus(201);
|
|
73
|
+
return ctx.json({ domainVerificationToken: activeVerification.value });
|
|
74
|
+
}
|
|
75
|
+
const domainVerificationToken = generateRandomString(24);
|
|
76
|
+
await ctx.context.adapter.create({
|
|
77
|
+
model: "verification",
|
|
78
|
+
data: {
|
|
79
|
+
identifier: options.domainVerification?.tokenPrefix ? `${options.domainVerification?.tokenPrefix}-${provider.providerId}` : `better-auth-token-${provider.providerId}`,
|
|
80
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
81
|
+
updatedAt: /* @__PURE__ */ new Date(),
|
|
82
|
+
value: domainVerificationToken,
|
|
83
|
+
expiresAt: new Date(Date.now() + 3600 * 24 * 7 * 1e3)
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
ctx.setStatus(201);
|
|
87
|
+
return ctx.json({ domainVerificationToken });
|
|
88
|
+
});
|
|
89
|
+
};
|
|
90
|
+
const verifyDomain = (options) => {
|
|
91
|
+
return createAuthEndpoint("/sso/verify-domain", {
|
|
92
|
+
method: "POST",
|
|
93
|
+
body: z.object({ providerId: z.string() }),
|
|
94
|
+
metadata: { openapi: {
|
|
95
|
+
summary: "Verify the provider domain ownership",
|
|
96
|
+
description: "Verify the provider domain ownership via DNS records",
|
|
97
|
+
responses: {
|
|
98
|
+
"404": { description: "Provider not found" },
|
|
99
|
+
"409": { description: "Domain has already been verified or no pending verification exists" },
|
|
100
|
+
"502": { description: "Unable to verify domain ownership due to upstream validator error" },
|
|
101
|
+
"204": { description: "Domain ownership was verified" }
|
|
102
|
+
}
|
|
103
|
+
} },
|
|
104
|
+
use: [sessionMiddleware]
|
|
105
|
+
}, async (ctx) => {
|
|
106
|
+
const body = ctx.body;
|
|
107
|
+
const provider = await ctx.context.adapter.findOne({
|
|
108
|
+
model: "ssoProvider",
|
|
109
|
+
where: [{
|
|
110
|
+
field: "providerId",
|
|
111
|
+
value: body.providerId
|
|
112
|
+
}]
|
|
113
|
+
});
|
|
114
|
+
if (!provider) throw new APIError("NOT_FOUND", {
|
|
115
|
+
message: "Provider not found",
|
|
116
|
+
code: "PROVIDER_NOT_FOUND"
|
|
117
|
+
});
|
|
118
|
+
const userId = ctx.context.session.user.id;
|
|
119
|
+
let isOrgMember = true;
|
|
120
|
+
if (provider.organizationId) isOrgMember = await ctx.context.adapter.count({
|
|
121
|
+
model: "member",
|
|
122
|
+
where: [{
|
|
123
|
+
field: "userId",
|
|
124
|
+
value: userId
|
|
125
|
+
}, {
|
|
126
|
+
field: "organizationId",
|
|
127
|
+
value: provider.organizationId
|
|
128
|
+
}]
|
|
129
|
+
}) > 0;
|
|
130
|
+
if (provider.userId !== userId || !isOrgMember) throw new APIError("FORBIDDEN", {
|
|
131
|
+
message: "User must be owner of or belong to the SSO provider organization",
|
|
132
|
+
code: "INSUFICCIENT_ACCESS"
|
|
133
|
+
});
|
|
134
|
+
if ("domainVerified" in provider && provider.domainVerified) throw new APIError("CONFLICT", {
|
|
135
|
+
message: "Domain has already been verified",
|
|
136
|
+
code: "DOMAIN_VERIFIED"
|
|
137
|
+
});
|
|
138
|
+
const activeVerification = await ctx.context.adapter.findOne({
|
|
139
|
+
model: "verification",
|
|
140
|
+
where: [{
|
|
141
|
+
field: "identifier",
|
|
142
|
+
value: options.domainVerification?.tokenPrefix ? `${options.domainVerification?.tokenPrefix}-${provider.providerId}` : `better-auth-token-${provider.providerId}`
|
|
143
|
+
}, {
|
|
144
|
+
field: "expiresAt",
|
|
145
|
+
value: /* @__PURE__ */ new Date(),
|
|
146
|
+
operator: "gt"
|
|
147
|
+
}]
|
|
148
|
+
});
|
|
149
|
+
if (!activeVerification) throw new APIError("NOT_FOUND", {
|
|
150
|
+
message: "No pending domain verification exists",
|
|
151
|
+
code: "NO_PENDING_VERIFICATION"
|
|
152
|
+
});
|
|
153
|
+
let records = [];
|
|
154
|
+
let dns;
|
|
155
|
+
try {
|
|
156
|
+
dns = await import("node:dns/promises");
|
|
157
|
+
} catch (error) {
|
|
158
|
+
ctx.context.logger.error("The core node:dns module is required for the domain verification feature", error);
|
|
159
|
+
throw new APIError("INTERNAL_SERVER_ERROR", {
|
|
160
|
+
message: "Unable to verify domain ownership due to server error",
|
|
161
|
+
code: "DOMAIN_VERIFICATION_FAILED"
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
try {
|
|
165
|
+
records = (await dns.resolveTxt(new URL(provider.domain).hostname)).flat();
|
|
166
|
+
} catch (error) {
|
|
167
|
+
ctx.context.logger.warn("DNS resolution failure while validating domain ownership", error);
|
|
168
|
+
}
|
|
169
|
+
if (!records.find((record) => record.includes(`${activeVerification.identifier}=${activeVerification.value}`))) throw new APIError("BAD_GATEWAY", {
|
|
170
|
+
message: "Unable to verify domain ownership. Try again later",
|
|
171
|
+
code: "DOMAIN_VERIFICATION_FAILED"
|
|
172
|
+
});
|
|
173
|
+
await ctx.context.adapter.update({
|
|
174
|
+
model: "ssoProvider",
|
|
175
|
+
where: [{
|
|
176
|
+
field: "providerId",
|
|
177
|
+
value: provider.providerId
|
|
178
|
+
}],
|
|
179
|
+
update: { domainVerified: true }
|
|
180
|
+
});
|
|
181
|
+
ctx.setStatus(204);
|
|
182
|
+
});
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
//#endregion
|
|
186
|
+
//#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
|
|
11
195
|
//#region src/routes/sso.ts
|
|
12
196
|
/**
|
|
13
197
|
* Safely parses a value that might be a JSON string or already a parsed object
|
|
@@ -155,6 +339,14 @@ const registerSSOProvider = (options) => {
|
|
|
155
339
|
type: "string",
|
|
156
340
|
description: "The domain of the provider, used for email matching"
|
|
157
341
|
},
|
|
342
|
+
domainVerified: {
|
|
343
|
+
type: "boolean",
|
|
344
|
+
description: "A boolean indicating whether the domain has been verified or not"
|
|
345
|
+
},
|
|
346
|
+
domainVerificationToken: {
|
|
347
|
+
type: "string",
|
|
348
|
+
description: "Domain verification token. It can be used to prove ownership over the SSO domain"
|
|
349
|
+
},
|
|
158
350
|
oidcConfig: {
|
|
159
351
|
type: "object",
|
|
160
352
|
properties: {
|
|
@@ -336,6 +528,7 @@ const registerSSOProvider = (options) => {
|
|
|
336
528
|
data: {
|
|
337
529
|
issuer: body.issuer,
|
|
338
530
|
domain: body.domain,
|
|
531
|
+
domainVerified: false,
|
|
339
532
|
oidcConfig: body.oidcConfig ? JSON.stringify({
|
|
340
533
|
issuer: body.issuer,
|
|
341
534
|
clientId: body.oidcConfig.clientId,
|
|
@@ -373,11 +566,29 @@ const registerSSOProvider = (options) => {
|
|
|
373
566
|
providerId: body.providerId
|
|
374
567
|
}
|
|
375
568
|
});
|
|
569
|
+
let domainVerificationToken;
|
|
570
|
+
let domainVerified;
|
|
571
|
+
if (options?.domainVerification?.enabled) {
|
|
572
|
+
domainVerified = false;
|
|
573
|
+
domainVerificationToken = generateRandomString(24);
|
|
574
|
+
await ctx.context.adapter.create({
|
|
575
|
+
model: "verification",
|
|
576
|
+
data: {
|
|
577
|
+
identifier: options.domainVerification?.tokenPrefix ? `${options.domainVerification?.tokenPrefix}-${provider.providerId}` : `better-auth-token-${provider.providerId}`,
|
|
578
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
579
|
+
updatedAt: /* @__PURE__ */ new Date(),
|
|
580
|
+
value: domainVerificationToken,
|
|
581
|
+
expiresAt: new Date(Date.now() + 3600 * 24 * 7 * 1e3)
|
|
582
|
+
}
|
|
583
|
+
});
|
|
584
|
+
}
|
|
376
585
|
return ctx.json({
|
|
377
586
|
...provider,
|
|
378
587
|
oidcConfig: JSON.parse(provider.oidcConfig),
|
|
379
588
|
samlConfig: JSON.parse(provider.samlConfig),
|
|
380
|
-
redirectURI: `${ctx.context.baseURL}/sso/callback/${provider.providerId}
|
|
589
|
+
redirectURI: `${ctx.context.baseURL}/sso/callback/${provider.providerId}`,
|
|
590
|
+
...options?.domainVerification?.enabled ? { domainVerified } : {},
|
|
591
|
+
...options?.domainVerification?.enabled ? { domainVerificationToken } : {}
|
|
381
592
|
});
|
|
382
593
|
});
|
|
383
594
|
};
|
|
@@ -480,7 +691,8 @@ const signInSSO = (options) => {
|
|
|
480
691
|
userId: "default",
|
|
481
692
|
oidcConfig: matchingDefault.oidcConfig,
|
|
482
693
|
samlConfig: matchingDefault.samlConfig,
|
|
483
|
-
domain: matchingDefault.domain
|
|
694
|
+
domain: matchingDefault.domain,
|
|
695
|
+
...options.domainVerification?.enabled ? { domainVerified: true } : {}
|
|
484
696
|
};
|
|
485
697
|
}
|
|
486
698
|
if (!providerId && !orgId && !domain) throw new APIError("BAD_REQUEST", { message: "providerId, orgId or domain is required" });
|
|
@@ -503,7 +715,14 @@ const signInSSO = (options) => {
|
|
|
503
715
|
if (body.providerType === "oidc" && !provider.oidcConfig) throw new APIError("BAD_REQUEST", { message: "OIDC provider is not configured" });
|
|
504
716
|
if (body.providerType === "saml" && !provider.samlConfig) throw new APIError("BAD_REQUEST", { message: "SAML provider is not configured" });
|
|
505
717
|
}
|
|
718
|
+
if (options?.domainVerification?.enabled && !("domainVerified" in provider && provider.domainVerified)) throw new APIError("UNAUTHORIZED", { message: "Provider domain has not been verified" });
|
|
506
719
|
if (provider.oidcConfig && body.providerType !== "saml") {
|
|
720
|
+
let finalAuthUrl = provider.oidcConfig.authorizationEndpoint;
|
|
721
|
+
if (!finalAuthUrl && provider.oidcConfig.discoveryEndpoint) {
|
|
722
|
+
const discovery = await betterFetch(provider.oidcConfig.discoveryEndpoint, { method: "GET" });
|
|
723
|
+
if (discovery.data) finalAuthUrl = discovery.data.authorization_endpoint;
|
|
724
|
+
}
|
|
725
|
+
if (!finalAuthUrl) throw new APIError("BAD_REQUEST", { message: "Invalid OIDC configuration. Authorization URL not found." });
|
|
507
726
|
const state = await generateState(ctx, void 0, false);
|
|
508
727
|
const redirectURI = `${ctx.context.baseURL}/sso/callback/${provider.providerId}`;
|
|
509
728
|
const authorizationURL = await createAuthorizationURL({
|
|
@@ -522,7 +741,7 @@ const signInSSO = (options) => {
|
|
|
522
741
|
"offline_access"
|
|
523
742
|
],
|
|
524
743
|
loginHint: ctx.body.loginHint || email,
|
|
525
|
-
authorizationEndpoint:
|
|
744
|
+
authorizationEndpoint: finalAuthUrl
|
|
526
745
|
});
|
|
527
746
|
return ctx.json({
|
|
528
747
|
url: authorizationURL.toString(),
|
|
@@ -561,6 +780,7 @@ const callbackSSO = (options) => {
|
|
|
561
780
|
error: z.string().optional(),
|
|
562
781
|
error_description: z.string().optional()
|
|
563
782
|
}),
|
|
783
|
+
allowedMediaTypes: ["application/x-www-form-urlencoded", "application/json"],
|
|
564
784
|
metadata: {
|
|
565
785
|
isAction: false,
|
|
566
786
|
openapi: {
|
|
@@ -585,7 +805,8 @@ const callbackSSO = (options) => {
|
|
|
585
805
|
if (matchingDefault) provider = {
|
|
586
806
|
...matchingDefault,
|
|
587
807
|
issuer: matchingDefault.oidcConfig?.issuer || "",
|
|
588
|
-
userId: "default"
|
|
808
|
+
userId: "default",
|
|
809
|
+
...options.domainVerification?.enabled ? { domainVerified: true } : {}
|
|
589
810
|
};
|
|
590
811
|
}
|
|
591
812
|
if (!provider) provider = await ctx.context.adapter.findOne({
|
|
@@ -602,6 +823,7 @@ const callbackSSO = (options) => {
|
|
|
602
823
|
};
|
|
603
824
|
});
|
|
604
825
|
if (!provider) throw ctx.redirect(`${errorURL || callbackURL}/error?error=invalid_provider&error_description=provider not found`);
|
|
826
|
+
if (options?.domainVerification?.enabled && !("domainVerified" in provider && provider.domainVerified)) throw new APIError("UNAUTHORIZED", { message: "Provider domain has not been verified" });
|
|
605
827
|
let config = provider.oidcConfig;
|
|
606
828
|
if (!config) throw ctx.redirect(`${errorURL || callbackURL}/error?error=invalid_provider&error_description=provider not found`);
|
|
607
829
|
const discovery = await betterFetch(config.discoveryEndpoint);
|
|
@@ -763,7 +985,8 @@ const callbackSSOSAML = (options) => {
|
|
|
763
985
|
if (matchingDefault) provider = {
|
|
764
986
|
...matchingDefault,
|
|
765
987
|
userId: "default",
|
|
766
|
-
issuer: matchingDefault.samlConfig?.issuer || ""
|
|
988
|
+
issuer: matchingDefault.samlConfig?.issuer || "",
|
|
989
|
+
...options.domainVerification?.enabled ? { domainVerified: true } : {}
|
|
767
990
|
};
|
|
768
991
|
}
|
|
769
992
|
if (!provider) provider = await ctx.context.adapter.findOne({
|
|
@@ -780,6 +1003,7 @@ const callbackSSOSAML = (options) => {
|
|
|
780
1003
|
};
|
|
781
1004
|
});
|
|
782
1005
|
if (!provider) throw new APIError("NOT_FOUND", { message: "No provider found for the given providerId" });
|
|
1006
|
+
if (options?.domainVerification?.enabled && !("domainVerified" in provider && provider.domainVerified)) throw new APIError("UNAUTHORIZED", { message: "Provider domain has not been verified" });
|
|
783
1007
|
const parsedSamlConfig = safeJsonParse(provider.samlConfig);
|
|
784
1008
|
if (!parsedSamlConfig) throw new APIError("BAD_REQUEST", { message: "Invalid SAML configuration" });
|
|
785
1009
|
const idpData = parsedSamlConfig.idpMetadata;
|
|
@@ -979,7 +1203,8 @@ const acsEndpoint = (options) => {
|
|
|
979
1203
|
providerId: matchingDefault.providerId,
|
|
980
1204
|
userId: "default",
|
|
981
1205
|
samlConfig: matchingDefault.samlConfig,
|
|
982
|
-
domain: matchingDefault.domain
|
|
1206
|
+
domain: matchingDefault.domain,
|
|
1207
|
+
...options.domainVerification?.enabled ? { domainVerified: true } : {}
|
|
983
1208
|
};
|
|
984
1209
|
} else provider = await ctx.context.adapter.findOne({
|
|
985
1210
|
model: "ssoProvider",
|
|
@@ -995,6 +1220,7 @@ const acsEndpoint = (options) => {
|
|
|
995
1220
|
};
|
|
996
1221
|
});
|
|
997
1222
|
if (!provider?.samlConfig) throw new APIError("NOT_FOUND", { message: "No SAML provider found" });
|
|
1223
|
+
if (options?.domainVerification?.enabled && !("domainVerified" in provider && provider.domainVerified)) throw new APIError("UNAUTHORIZED", { message: "Provider domain has not been verified" });
|
|
998
1224
|
const parsedSamlConfig = provider.samlConfig;
|
|
999
1225
|
const sp = saml.ServiceProvider({
|
|
1000
1226
|
entityID: parsedSamlConfig.spMetadata?.entityID || parsedSamlConfig.issuer,
|
|
@@ -1095,7 +1321,7 @@ const acsEndpoint = (options) => {
|
|
|
1095
1321
|
}
|
|
1096
1322
|
]
|
|
1097
1323
|
})) {
|
|
1098
|
-
if (!ctx.context.options.account?.accountLinking?.trustedProviders?.includes(provider.providerId)) throw ctx.redirect(`${parsedSamlConfig.callbackUrl}?error=account_not_found`);
|
|
1324
|
+
if (!(ctx.context.options.account?.accountLinking?.trustedProviders?.includes(provider.providerId) || "domainVerified" in provider && provider.domainVerified && validateEmailDomain(userInfo.email, provider.domain))) throw ctx.redirect(`${parsedSamlConfig.callbackUrl}?error=account_not_found`);
|
|
1099
1325
|
await ctx.context.internalAdapter.createAccount({
|
|
1100
1326
|
userId: existingUser.id,
|
|
1101
1327
|
providerId: provider.providerId,
|
|
@@ -1173,50 +1399,75 @@ saml.setSchemaValidator({ async validate(xml) {
|
|
|
1173
1399
|
throw "ERR_INVALID_XML";
|
|
1174
1400
|
} });
|
|
1175
1401
|
function sso(options) {
|
|
1402
|
+
let endpoints = {
|
|
1403
|
+
spMetadata: spMetadata(),
|
|
1404
|
+
registerSSOProvider: registerSSOProvider(options),
|
|
1405
|
+
signInSSO: signInSSO(options),
|
|
1406
|
+
callbackSSO: callbackSSO(options),
|
|
1407
|
+
callbackSSOSAML: callbackSSOSAML(options),
|
|
1408
|
+
acsEndpoint: acsEndpoint(options)
|
|
1409
|
+
};
|
|
1410
|
+
if (options?.domainVerification?.enabled) {
|
|
1411
|
+
const domainVerificationEndpoints = {
|
|
1412
|
+
requestDomainVerification: requestDomainVerification(options),
|
|
1413
|
+
verifyDomain: verifyDomain(options)
|
|
1414
|
+
};
|
|
1415
|
+
endpoints = {
|
|
1416
|
+
...endpoints,
|
|
1417
|
+
...domainVerificationEndpoints
|
|
1418
|
+
};
|
|
1419
|
+
}
|
|
1176
1420
|
return {
|
|
1177
1421
|
id: "sso",
|
|
1178
|
-
endpoints
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
}
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1422
|
+
endpoints,
|
|
1423
|
+
schema: { ssoProvider: {
|
|
1424
|
+
modelName: options?.modelName ?? "ssoProvider",
|
|
1425
|
+
fields: {
|
|
1426
|
+
issuer: {
|
|
1427
|
+
type: "string",
|
|
1428
|
+
required: true,
|
|
1429
|
+
fieldName: options?.fields?.issuer ?? "issuer"
|
|
1430
|
+
},
|
|
1431
|
+
oidcConfig: {
|
|
1432
|
+
type: "string",
|
|
1433
|
+
required: false,
|
|
1434
|
+
fieldName: options?.fields?.oidcConfig ?? "oidcConfig"
|
|
1435
|
+
},
|
|
1436
|
+
samlConfig: {
|
|
1437
|
+
type: "string",
|
|
1438
|
+
required: false,
|
|
1439
|
+
fieldName: options?.fields?.samlConfig ?? "samlConfig"
|
|
1440
|
+
},
|
|
1441
|
+
userId: {
|
|
1442
|
+
type: "string",
|
|
1443
|
+
references: {
|
|
1444
|
+
model: "user",
|
|
1445
|
+
field: "id"
|
|
1446
|
+
},
|
|
1447
|
+
fieldName: options?.fields?.userId ?? "userId"
|
|
1448
|
+
},
|
|
1449
|
+
providerId: {
|
|
1450
|
+
type: "string",
|
|
1451
|
+
required: true,
|
|
1452
|
+
unique: true,
|
|
1453
|
+
fieldName: options?.fields?.providerId ?? "providerId"
|
|
1454
|
+
},
|
|
1455
|
+
organizationId: {
|
|
1456
|
+
type: "string",
|
|
1457
|
+
required: false,
|
|
1458
|
+
fieldName: options?.fields?.organizationId ?? "organizationId"
|
|
1459
|
+
},
|
|
1460
|
+
domain: {
|
|
1461
|
+
type: "string",
|
|
1462
|
+
required: true,
|
|
1463
|
+
fieldName: options?.fields?.domain ?? "domain"
|
|
1464
|
+
},
|
|
1465
|
+
...options?.domainVerification?.enabled ? { domainVerified: {
|
|
1466
|
+
type: "boolean",
|
|
1467
|
+
required: false
|
|
1468
|
+
} } : {}
|
|
1218
1469
|
}
|
|
1219
|
-
} }
|
|
1470
|
+
} }
|
|
1220
1471
|
};
|
|
1221
1472
|
}
|
|
1222
1473
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@better-auth/sso",
|
|
3
3
|
"author": "Bereket Engida",
|
|
4
|
-
"version": "1.4.0-beta.
|
|
4
|
+
"version": "1.4.0-beta.23",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.mjs",
|
|
7
7
|
"homepage": "https://www.better-auth.com/docs/plugins/sso",
|
|
@@ -60,15 +60,15 @@
|
|
|
60
60
|
"devDependencies": {
|
|
61
61
|
"@types/body-parser": "^1.19.6",
|
|
62
62
|
"@types/express": "^5.0.5",
|
|
63
|
-
"better-call": "1.0.
|
|
63
|
+
"better-call": "1.0.28",
|
|
64
64
|
"body-parser": "^2.2.0",
|
|
65
65
|
"express": "^5.1.0",
|
|
66
66
|
"oauth2-mock-server": "^7.2.1",
|
|
67
67
|
"tsdown": "^0.16.0",
|
|
68
|
-
"better-auth": "1.4.0-beta.
|
|
68
|
+
"better-auth": "1.4.0-beta.23"
|
|
69
69
|
},
|
|
70
70
|
"peerDependencies": {
|
|
71
|
-
"better-auth": "1.4.0-beta.
|
|
71
|
+
"better-auth": "1.4.0-beta.23"
|
|
72
72
|
},
|
|
73
73
|
"scripts": {
|
|
74
74
|
"test": "vitest",
|
package/src/client.ts
CHANGED
|
@@ -1,8 +1,25 @@
|
|
|
1
1
|
import type { BetterAuthClientPlugin } from "better-auth";
|
|
2
|
-
import type {
|
|
3
|
-
|
|
2
|
+
import type { SSOPlugin } from "./index";
|
|
3
|
+
|
|
4
|
+
interface SSOClientOptions {
|
|
5
|
+
domainVerification?:
|
|
6
|
+
| {
|
|
7
|
+
enabled: boolean;
|
|
8
|
+
}
|
|
9
|
+
| undefined;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const ssoClient = <CO extends SSOClientOptions>(
|
|
13
|
+
options?: CO | undefined,
|
|
14
|
+
) => {
|
|
4
15
|
return {
|
|
5
16
|
id: "sso-client",
|
|
6
|
-
$InferServerPlugin: {} as
|
|
17
|
+
$InferServerPlugin: {} as SSOPlugin<{
|
|
18
|
+
domainVerification: {
|
|
19
|
+
enabled: CO["domainVerification"] extends { enabled: true }
|
|
20
|
+
? true
|
|
21
|
+
: false;
|
|
22
|
+
};
|
|
23
|
+
}>,
|
|
7
24
|
} satisfies BetterAuthClientPlugin;
|
|
8
25
|
};
|