@hammadj/better-auth-sso 1.5.0-beta.9
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 +116 -0
- package/LICENSE.md +20 -0
- package/dist/client.d.mts +10 -0
- package/dist/client.mjs +15 -0
- package/dist/client.mjs.map +1 -0
- package/dist/index.d.mts +738 -0
- package/dist/index.mjs +2953 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +87 -0
- package/src/client.ts +29 -0
- package/src/constants.ts +58 -0
- package/src/domain-verification.test.ts +551 -0
- package/src/index.ts +265 -0
- package/src/linking/index.ts +2 -0
- package/src/linking/org-assignment.test.ts +325 -0
- package/src/linking/org-assignment.ts +176 -0
- package/src/linking/types.ts +10 -0
- package/src/oidc/discovery.test.ts +1157 -0
- package/src/oidc/discovery.ts +494 -0
- package/src/oidc/errors.ts +92 -0
- package/src/oidc/index.ts +31 -0
- package/src/oidc/types.ts +219 -0
- package/src/oidc.test.ts +688 -0
- package/src/providers.test.ts +1326 -0
- package/src/routes/domain-verification.ts +275 -0
- package/src/routes/providers.ts +565 -0
- package/src/routes/schemas.ts +96 -0
- package/src/routes/sso.ts +2750 -0
- package/src/saml/algorithms.test.ts +449 -0
- package/src/saml/algorithms.ts +338 -0
- package/src/saml/assertions.test.ts +239 -0
- package/src/saml/assertions.ts +62 -0
- package/src/saml/index.ts +13 -0
- package/src/saml/parser.ts +56 -0
- package/src/saml-state.ts +78 -0
- package/src/saml.test.ts +4319 -0
- package/src/types.ts +365 -0
- package/src/utils.test.ts +103 -0
- package/src/utils.ts +81 -0
- package/tsconfig.json +14 -0
- package/tsdown.config.ts +9 -0
- package/vitest.config.ts +3 -0
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
import type { Verification } from "better-auth";
|
|
2
|
+
import {
|
|
3
|
+
APIError,
|
|
4
|
+
createAuthEndpoint,
|
|
5
|
+
sessionMiddleware,
|
|
6
|
+
} from "better-auth/api";
|
|
7
|
+
import { generateRandomString } from "better-auth/crypto";
|
|
8
|
+
import * as z from "zod/v4";
|
|
9
|
+
import type { SSOOptions, SSOProvider } from "../types";
|
|
10
|
+
|
|
11
|
+
const domainVerificationBodySchema = z.object({
|
|
12
|
+
providerId: z.string(),
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
export const requestDomainVerification = (options: SSOOptions) => {
|
|
16
|
+
return createAuthEndpoint(
|
|
17
|
+
"/sso/request-domain-verification",
|
|
18
|
+
{
|
|
19
|
+
method: "POST",
|
|
20
|
+
body: domainVerificationBodySchema,
|
|
21
|
+
metadata: {
|
|
22
|
+
openapi: {
|
|
23
|
+
summary: "Request a domain verification",
|
|
24
|
+
description:
|
|
25
|
+
"Request a domain verification for the given SSO provider",
|
|
26
|
+
responses: {
|
|
27
|
+
"404": {
|
|
28
|
+
description: "Provider not found",
|
|
29
|
+
},
|
|
30
|
+
"409": {
|
|
31
|
+
description: "Domain has already been verified",
|
|
32
|
+
},
|
|
33
|
+
"201": {
|
|
34
|
+
description: "Domain submitted for verification",
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
use: [sessionMiddleware],
|
|
40
|
+
},
|
|
41
|
+
async (ctx) => {
|
|
42
|
+
const body = ctx.body;
|
|
43
|
+
const provider = await ctx.context.adapter.findOne<
|
|
44
|
+
SSOProvider<SSOOptions>
|
|
45
|
+
>({
|
|
46
|
+
model: "ssoProvider",
|
|
47
|
+
where: [{ field: "providerId", value: body.providerId }],
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
if (!provider) {
|
|
51
|
+
throw new APIError("NOT_FOUND", {
|
|
52
|
+
message: "Provider not found",
|
|
53
|
+
code: "PROVIDER_NOT_FOUND",
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const userId = ctx.context.session.user.id;
|
|
58
|
+
let isOrgMember = true;
|
|
59
|
+
if (provider.organizationId) {
|
|
60
|
+
const membershipsCount = await ctx.context.adapter.count({
|
|
61
|
+
model: "member",
|
|
62
|
+
where: [
|
|
63
|
+
{ field: "userId", value: userId },
|
|
64
|
+
{ field: "organizationId", value: provider.organizationId },
|
|
65
|
+
],
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
isOrgMember = membershipsCount > 0;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (provider.userId !== userId || !isOrgMember) {
|
|
72
|
+
throw new APIError("FORBIDDEN", {
|
|
73
|
+
message:
|
|
74
|
+
"User must be owner of or belong to the SSO provider organization",
|
|
75
|
+
code: "INSUFICCIENT_ACCESS",
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if ("domainVerified" in provider && provider.domainVerified) {
|
|
80
|
+
throw new APIError("CONFLICT", {
|
|
81
|
+
message: "Domain has already been verified",
|
|
82
|
+
code: "DOMAIN_VERIFIED",
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const activeVerification =
|
|
87
|
+
await ctx.context.adapter.findOne<Verification>({
|
|
88
|
+
model: "verification",
|
|
89
|
+
where: [
|
|
90
|
+
{
|
|
91
|
+
field: "identifier",
|
|
92
|
+
value: options.domainVerification?.tokenPrefix
|
|
93
|
+
? `${options.domainVerification?.tokenPrefix}-${provider.providerId}`
|
|
94
|
+
: `better-auth-token-${provider.providerId}`,
|
|
95
|
+
},
|
|
96
|
+
{ field: "expiresAt", value: new Date(), operator: "gt" },
|
|
97
|
+
],
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
if (activeVerification) {
|
|
101
|
+
ctx.setStatus(201);
|
|
102
|
+
return ctx.json({ domainVerificationToken: activeVerification.value });
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const domainVerificationToken = generateRandomString(24);
|
|
106
|
+
await ctx.context.adapter.create<Verification>({
|
|
107
|
+
model: "verification",
|
|
108
|
+
data: {
|
|
109
|
+
identifier: options.domainVerification?.tokenPrefix
|
|
110
|
+
? `${options.domainVerification?.tokenPrefix}-${provider.providerId}`
|
|
111
|
+
: `better-auth-token-${provider.providerId}`,
|
|
112
|
+
createdAt: new Date(),
|
|
113
|
+
updatedAt: new Date(),
|
|
114
|
+
value: domainVerificationToken,
|
|
115
|
+
expiresAt: new Date(Date.now() + 3600 * 24 * 7 * 1000), // 1 week
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
ctx.setStatus(201);
|
|
120
|
+
return ctx.json({
|
|
121
|
+
domainVerificationToken,
|
|
122
|
+
});
|
|
123
|
+
},
|
|
124
|
+
);
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
export const verifyDomain = (options: SSOOptions) => {
|
|
128
|
+
return createAuthEndpoint(
|
|
129
|
+
"/sso/verify-domain",
|
|
130
|
+
{
|
|
131
|
+
method: "POST",
|
|
132
|
+
body: domainVerificationBodySchema,
|
|
133
|
+
metadata: {
|
|
134
|
+
openapi: {
|
|
135
|
+
summary: "Verify the provider domain ownership",
|
|
136
|
+
description: "Verify the provider domain ownership via DNS records",
|
|
137
|
+
responses: {
|
|
138
|
+
"404": {
|
|
139
|
+
description: "Provider not found",
|
|
140
|
+
},
|
|
141
|
+
"409": {
|
|
142
|
+
description:
|
|
143
|
+
"Domain has already been verified or no pending verification exists",
|
|
144
|
+
},
|
|
145
|
+
"502": {
|
|
146
|
+
description:
|
|
147
|
+
"Unable to verify domain ownership due to upstream validator error",
|
|
148
|
+
},
|
|
149
|
+
"204": {
|
|
150
|
+
description: "Domain ownership was verified",
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
use: [sessionMiddleware],
|
|
156
|
+
},
|
|
157
|
+
async (ctx) => {
|
|
158
|
+
const body = ctx.body;
|
|
159
|
+
const provider = await ctx.context.adapter.findOne<
|
|
160
|
+
SSOProvider<SSOOptions>
|
|
161
|
+
>({
|
|
162
|
+
model: "ssoProvider",
|
|
163
|
+
where: [{ field: "providerId", value: body.providerId }],
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
if (!provider) {
|
|
167
|
+
throw new APIError("NOT_FOUND", {
|
|
168
|
+
message: "Provider not found",
|
|
169
|
+
code: "PROVIDER_NOT_FOUND",
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const userId = ctx.context.session.user.id;
|
|
174
|
+
let isOrgMember = true;
|
|
175
|
+
if (provider.organizationId) {
|
|
176
|
+
const membershipsCount = await ctx.context.adapter.count({
|
|
177
|
+
model: "member",
|
|
178
|
+
where: [
|
|
179
|
+
{ field: "userId", value: userId },
|
|
180
|
+
{ field: "organizationId", value: provider.organizationId },
|
|
181
|
+
],
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
isOrgMember = membershipsCount > 0;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (provider.userId !== userId || !isOrgMember) {
|
|
188
|
+
throw new APIError("FORBIDDEN", {
|
|
189
|
+
message:
|
|
190
|
+
"User must be owner of or belong to the SSO provider organization",
|
|
191
|
+
code: "INSUFICCIENT_ACCESS",
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if ("domainVerified" in provider && provider.domainVerified) {
|
|
196
|
+
throw new APIError("CONFLICT", {
|
|
197
|
+
message: "Domain has already been verified",
|
|
198
|
+
code: "DOMAIN_VERIFIED",
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const activeVerification =
|
|
203
|
+
await ctx.context.adapter.findOne<Verification>({
|
|
204
|
+
model: "verification",
|
|
205
|
+
where: [
|
|
206
|
+
{
|
|
207
|
+
field: "identifier",
|
|
208
|
+
value: options.domainVerification?.tokenPrefix
|
|
209
|
+
? `${options.domainVerification?.tokenPrefix}-${provider.providerId}`
|
|
210
|
+
: `better-auth-token-${provider.providerId}`,
|
|
211
|
+
},
|
|
212
|
+
{ field: "expiresAt", value: new Date(), operator: "gt" },
|
|
213
|
+
],
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
if (!activeVerification) {
|
|
217
|
+
throw new APIError("NOT_FOUND", {
|
|
218
|
+
message: "No pending domain verification exists",
|
|
219
|
+
code: "NO_PENDING_VERIFICATION",
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
let records: string[] = [];
|
|
224
|
+
let dns: typeof import("node:dns/promises");
|
|
225
|
+
|
|
226
|
+
try {
|
|
227
|
+
dns = await import("node:dns/promises");
|
|
228
|
+
} catch (error) {
|
|
229
|
+
ctx.context.logger.error(
|
|
230
|
+
"The core node:dns module is required for the domain verification feature",
|
|
231
|
+
error,
|
|
232
|
+
);
|
|
233
|
+
throw new APIError("INTERNAL_SERVER_ERROR", {
|
|
234
|
+
message: "Unable to verify domain ownership due to server error",
|
|
235
|
+
code: "DOMAIN_VERIFICATION_FAILED",
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
try {
|
|
240
|
+
const dnsRecords = await dns.resolveTxt(
|
|
241
|
+
new URL(provider.domain).hostname,
|
|
242
|
+
);
|
|
243
|
+
records = dnsRecords.flat();
|
|
244
|
+
} catch (error) {
|
|
245
|
+
ctx.context.logger.warn(
|
|
246
|
+
"DNS resolution failure while validating domain ownership",
|
|
247
|
+
error,
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const record = records.find((record) =>
|
|
252
|
+
record.includes(
|
|
253
|
+
`${activeVerification.identifier}=${activeVerification.value}`,
|
|
254
|
+
),
|
|
255
|
+
);
|
|
256
|
+
if (!record) {
|
|
257
|
+
throw new APIError("BAD_GATEWAY", {
|
|
258
|
+
message: "Unable to verify domain ownership. Try again later",
|
|
259
|
+
code: "DOMAIN_VERIFICATION_FAILED",
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
await ctx.context.adapter.update<SSOProvider<SSOOptions>>({
|
|
264
|
+
model: "ssoProvider",
|
|
265
|
+
where: [{ field: "providerId", value: provider.providerId }],
|
|
266
|
+
update: {
|
|
267
|
+
domainVerified: true,
|
|
268
|
+
},
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
ctx.setStatus(204);
|
|
272
|
+
return;
|
|
273
|
+
},
|
|
274
|
+
);
|
|
275
|
+
};
|