@better-auth/sso 1.6.16 → 1.6.17
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/dist/client.d.mts
CHANGED
package/dist/client.mjs
CHANGED
|
@@ -1359,7 +1359,7 @@ declare const callbackSSOShared: (options?: SSOOptions) => better_call0.StrictEn
|
|
|
1359
1359
|
allowedMediaTypes: readonly ["application/x-www-form-urlencoded", "application/json"];
|
|
1360
1360
|
}, void>;
|
|
1361
1361
|
declare const callbackSSOSAML: (options?: SSOOptions) => better_call0.StrictEndpoint<"/sso/saml2/callback/:providerId", {
|
|
1362
|
-
method: ("
|
|
1362
|
+
method: ("GET" | "POST")[];
|
|
1363
1363
|
body: z.ZodOptional<z.ZodObject<{
|
|
1364
1364
|
SAMLResponse: z.ZodString;
|
|
1365
1365
|
RelayState: z.ZodOptional<z.ZodString>;
|
|
@@ -1410,7 +1410,7 @@ declare const acsEndpoint: (options?: SSOOptions) => better_call0.StrictEndpoint
|
|
|
1410
1410
|
};
|
|
1411
1411
|
}, never>;
|
|
1412
1412
|
declare const sloEndpoint: (options?: SSOOptions) => better_call0.StrictEndpoint<"/sso/saml2/sp/slo/:providerId", {
|
|
1413
|
-
method: ("
|
|
1413
|
+
method: ("GET" | "POST")[];
|
|
1414
1414
|
body: z.ZodOptional<z.ZodObject<{
|
|
1415
1415
|
SAMLRequest: z.ZodOptional<z.ZodString>;
|
|
1416
1416
|
SAMLResponse: z.ZodOptional<z.ZodString>;
|
package/dist/index.d.mts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { A as DataEncryptionAlgorithm, C as TimestampValidationOptions, D as SSOOptions, E as SAMLConfig, M as DigestAlgorithm, N as KeyEncryptionAlgorithm, O as SSOProvider, P as SignatureAlgorithm, S as SAMLConditions, T as OIDCConfig, _ as REQUIRED_DISCOVERY_FIELDS, a as fetchDiscoveryDocument, b as DEFAULT_MAX_SAML_METADATA_SIZE, c as normalizeUrl, d as validateDiscoveryUrl, f as DiscoverOIDCConfigParams, g as OIDCDiscoveryDocument, h as HydratedOIDCConfig, i as discoverOIDCConfig, j as DeprecatedAlgorithmBehavior, k as AlgorithmValidationOptions, l as selectTokenEndpointAuthMethod, m as DiscoveryErrorCode, n as sso, o as needsRuntimeDiscovery, p as DiscoveryError, r as computeDiscoveryUrl, s as normalizeDiscoveryUrls, t as SSOPlugin, u as validateDiscoveryDocument, v as RequiredDiscoveryField, w as validateSAMLTimestamp, x as DEFAULT_MAX_SAML_RESPONSE_SIZE, y as DEFAULT_CLOCK_SKEW_MS } from "./index-
|
|
1
|
+
import { A as DataEncryptionAlgorithm, C as TimestampValidationOptions, D as SSOOptions, E as SAMLConfig, M as DigestAlgorithm, N as KeyEncryptionAlgorithm, O as SSOProvider, P as SignatureAlgorithm, S as SAMLConditions, T as OIDCConfig, _ as REQUIRED_DISCOVERY_FIELDS, a as fetchDiscoveryDocument, b as DEFAULT_MAX_SAML_METADATA_SIZE, c as normalizeUrl, d as validateDiscoveryUrl, f as DiscoverOIDCConfigParams, g as OIDCDiscoveryDocument, h as HydratedOIDCConfig, i as discoverOIDCConfig, j as DeprecatedAlgorithmBehavior, k as AlgorithmValidationOptions, l as selectTokenEndpointAuthMethod, m as DiscoveryErrorCode, n as sso, o as needsRuntimeDiscovery, p as DiscoveryError, r as computeDiscoveryUrl, s as normalizeDiscoveryUrls, t as SSOPlugin, u as validateDiscoveryDocument, v as RequiredDiscoveryField, w as validateSAMLTimestamp, x as DEFAULT_MAX_SAML_RESPONSE_SIZE, y as DEFAULT_CLOCK_SKEW_MS } from "./index-D9brFUE1.mjs";
|
|
2
2
|
export { AlgorithmValidationOptions, DEFAULT_CLOCK_SKEW_MS, DEFAULT_MAX_SAML_METADATA_SIZE, DEFAULT_MAX_SAML_RESPONSE_SIZE, DataEncryptionAlgorithm, DeprecatedAlgorithmBehavior, DigestAlgorithm, DiscoverOIDCConfigParams, DiscoveryError, DiscoveryErrorCode, HydratedOIDCConfig, KeyEncryptionAlgorithm, OIDCConfig, OIDCDiscoveryDocument, REQUIRED_DISCOVERY_FIELDS, RequiredDiscoveryField, SAMLConditions, SAMLConfig, SSOOptions, SSOPlugin, SSOProvider, SignatureAlgorithm, TimestampValidationOptions, computeDiscoveryUrl, discoverOIDCConfig, fetchDiscoveryDocument, needsRuntimeDiscovery, normalizeDiscoveryUrls, normalizeUrl, selectTokenEndpointAuthMethod, sso, validateDiscoveryDocument, validateDiscoveryUrl, validateSAMLTimestamp };
|
package/dist/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { t as PACKAGE_VERSION } from "./version-
|
|
1
|
+
import { t as PACKAGE_VERSION } from "./version-BeZU0Td6.mjs";
|
|
2
2
|
import { APIError, createAuthEndpoint, createAuthMiddleware, getSessionFromCtx, sessionMiddleware } from "better-auth/api";
|
|
3
3
|
import { XMLParser, XMLValidator } from "fast-xml-parser";
|
|
4
4
|
import { X509Certificate } from "node:crypto";
|
|
@@ -24,8 +24,6 @@ import samlifyDefault from "samlify";
|
|
|
24
24
|
*/
|
|
25
25
|
/** Prefix for AuthnRequest IDs used in InResponseTo validation */
|
|
26
26
|
const AUTHN_REQUEST_KEY_PREFIX = "saml-authn-request:";
|
|
27
|
-
/** Prefix for used Assertion IDs used in replay protection */
|
|
28
|
-
const USED_ASSERTION_KEY_PREFIX = "saml-used-assertion:";
|
|
29
27
|
/** Prefix for SAML session data (NameID + SessionIndex) for SLO */
|
|
30
28
|
const SAML_SESSION_KEY_PREFIX = "saml-session:";
|
|
31
29
|
/** Prefix for reverse lookup of SAML session by Better Auth session ID */
|
|
@@ -86,6 +84,16 @@ const domainMatches = (searchDomain, domainList) => {
|
|
|
86
84
|
return domainList.split(",").map((d) => d.trim().toLowerCase()).filter(Boolean).some((d) => search === d || search.endsWith(`.${d}`));
|
|
87
85
|
};
|
|
88
86
|
/**
|
|
87
|
+
* Strictly parse a provider-supplied email-verification claim.
|
|
88
|
+
*
|
|
89
|
+
* OIDC userInfo, OIDC id-token, and SAML attribute values are frequently
|
|
90
|
+
* strings, so a loose `Boolean(value)` or truthy fallback treats the string
|
|
91
|
+
* `"false"` as verified. Only a boolean `true` or the exact string `"true"`
|
|
92
|
+
* count as verified; every other value, including `"false"`, `"0"`, `""`,
|
|
93
|
+
* numbers, arrays, and objects, is unverified.
|
|
94
|
+
*/
|
|
95
|
+
const parseProviderEmailVerified = (value) => value === true || value === "true";
|
|
96
|
+
/**
|
|
89
97
|
* Validates email domain against allowed domain(s).
|
|
90
98
|
* Supports comma-separated domains for multi-domain SSO.
|
|
91
99
|
*/
|
|
@@ -211,171 +219,6 @@ async function assignOrganizationByDomain(ctx, options) {
|
|
|
211
219
|
});
|
|
212
220
|
}
|
|
213
221
|
//#endregion
|
|
214
|
-
//#region src/routes/domain-verification.ts
|
|
215
|
-
const DNS_LABEL_MAX_LENGTH = 63;
|
|
216
|
-
const DEFAULT_TOKEN_PREFIX = "better-auth-token";
|
|
217
|
-
const domainVerificationBodySchema = z.object({ providerId: z.string() });
|
|
218
|
-
function getVerificationIdentifier(options, providerId) {
|
|
219
|
-
return `_${options.domainVerification?.tokenPrefix || DEFAULT_TOKEN_PREFIX}-${providerId}`;
|
|
220
|
-
}
|
|
221
|
-
const requestDomainVerification = (options) => {
|
|
222
|
-
return createAuthEndpoint("/sso/request-domain-verification", {
|
|
223
|
-
method: "POST",
|
|
224
|
-
body: domainVerificationBodySchema,
|
|
225
|
-
metadata: { openapi: {
|
|
226
|
-
summary: "Request a domain verification",
|
|
227
|
-
description: "Request a domain verification for the given SSO provider",
|
|
228
|
-
responses: {
|
|
229
|
-
"404": { description: "Provider not found" },
|
|
230
|
-
"409": { description: "Domain has already been verified" },
|
|
231
|
-
"201": { description: "Domain submitted for verification" }
|
|
232
|
-
}
|
|
233
|
-
} },
|
|
234
|
-
use: [sessionMiddleware]
|
|
235
|
-
}, async (ctx) => {
|
|
236
|
-
const body = ctx.body;
|
|
237
|
-
const provider = await ctx.context.adapter.findOne({
|
|
238
|
-
model: "ssoProvider",
|
|
239
|
-
where: [{
|
|
240
|
-
field: "providerId",
|
|
241
|
-
value: body.providerId
|
|
242
|
-
}]
|
|
243
|
-
});
|
|
244
|
-
if (!provider) throw new APIError("NOT_FOUND", {
|
|
245
|
-
message: "Provider not found",
|
|
246
|
-
code: "PROVIDER_NOT_FOUND"
|
|
247
|
-
});
|
|
248
|
-
const userId = ctx.context.session.user.id;
|
|
249
|
-
let isOrgMember = true;
|
|
250
|
-
if (provider.organizationId) isOrgMember = await ctx.context.adapter.count({
|
|
251
|
-
model: "member",
|
|
252
|
-
where: [{
|
|
253
|
-
field: "userId",
|
|
254
|
-
value: userId
|
|
255
|
-
}, {
|
|
256
|
-
field: "organizationId",
|
|
257
|
-
value: provider.organizationId
|
|
258
|
-
}]
|
|
259
|
-
}) > 0;
|
|
260
|
-
if (provider.userId !== userId || !isOrgMember) throw new APIError("FORBIDDEN", {
|
|
261
|
-
message: "User must be owner of or belong to the SSO provider organization",
|
|
262
|
-
code: "INSUFICCIENT_ACCESS"
|
|
263
|
-
});
|
|
264
|
-
if ("domainVerified" in provider && provider.domainVerified) throw new APIError("CONFLICT", {
|
|
265
|
-
message: "Domain has already been verified",
|
|
266
|
-
code: "DOMAIN_VERIFIED"
|
|
267
|
-
});
|
|
268
|
-
const identifier = getVerificationIdentifier(options, provider.providerId);
|
|
269
|
-
const activeVerification = await ctx.context.internalAdapter.findVerificationValue(identifier);
|
|
270
|
-
if (activeVerification && new Date(activeVerification.expiresAt) > /* @__PURE__ */ new Date()) {
|
|
271
|
-
ctx.setStatus(201);
|
|
272
|
-
return ctx.json({ domainVerificationToken: activeVerification.value });
|
|
273
|
-
}
|
|
274
|
-
const domainVerificationToken = generateRandomString(24);
|
|
275
|
-
await ctx.context.internalAdapter.createVerificationValue({
|
|
276
|
-
identifier,
|
|
277
|
-
value: domainVerificationToken,
|
|
278
|
-
expiresAt: new Date(Date.now() + 3600 * 24 * 7 * 1e3)
|
|
279
|
-
});
|
|
280
|
-
ctx.setStatus(201);
|
|
281
|
-
return ctx.json({ domainVerificationToken });
|
|
282
|
-
});
|
|
283
|
-
};
|
|
284
|
-
const verifyDomain = (options) => {
|
|
285
|
-
return createAuthEndpoint("/sso/verify-domain", {
|
|
286
|
-
method: "POST",
|
|
287
|
-
body: domainVerificationBodySchema,
|
|
288
|
-
metadata: { openapi: {
|
|
289
|
-
summary: "Verify the provider domain ownership",
|
|
290
|
-
description: "Verify the provider domain ownership via DNS records",
|
|
291
|
-
responses: {
|
|
292
|
-
"404": { description: "Provider not found" },
|
|
293
|
-
"409": { description: "Domain has already been verified or no pending verification exists" },
|
|
294
|
-
"502": { description: "Unable to verify domain ownership due to upstream validator error" },
|
|
295
|
-
"204": { description: "Domain ownership was verified" }
|
|
296
|
-
}
|
|
297
|
-
} },
|
|
298
|
-
use: [sessionMiddleware]
|
|
299
|
-
}, async (ctx) => {
|
|
300
|
-
const body = ctx.body;
|
|
301
|
-
const provider = await ctx.context.adapter.findOne({
|
|
302
|
-
model: "ssoProvider",
|
|
303
|
-
where: [{
|
|
304
|
-
field: "providerId",
|
|
305
|
-
value: body.providerId
|
|
306
|
-
}]
|
|
307
|
-
});
|
|
308
|
-
if (!provider) throw new APIError("NOT_FOUND", {
|
|
309
|
-
message: "Provider not found",
|
|
310
|
-
code: "PROVIDER_NOT_FOUND"
|
|
311
|
-
});
|
|
312
|
-
const userId = ctx.context.session.user.id;
|
|
313
|
-
let isOrgMember = true;
|
|
314
|
-
if (provider.organizationId) isOrgMember = await ctx.context.adapter.count({
|
|
315
|
-
model: "member",
|
|
316
|
-
where: [{
|
|
317
|
-
field: "userId",
|
|
318
|
-
value: userId
|
|
319
|
-
}, {
|
|
320
|
-
field: "organizationId",
|
|
321
|
-
value: provider.organizationId
|
|
322
|
-
}]
|
|
323
|
-
}) > 0;
|
|
324
|
-
if (provider.userId !== userId || !isOrgMember) throw new APIError("FORBIDDEN", {
|
|
325
|
-
message: "User must be owner of or belong to the SSO provider organization",
|
|
326
|
-
code: "INSUFICCIENT_ACCESS"
|
|
327
|
-
});
|
|
328
|
-
if ("domainVerified" in provider && provider.domainVerified) throw new APIError("CONFLICT", {
|
|
329
|
-
message: "Domain has already been verified",
|
|
330
|
-
code: "DOMAIN_VERIFIED"
|
|
331
|
-
});
|
|
332
|
-
const identifier = getVerificationIdentifier(options, provider.providerId);
|
|
333
|
-
if (identifier.length > DNS_LABEL_MAX_LENGTH) throw new APIError("BAD_REQUEST", {
|
|
334
|
-
message: `Verification identifier exceeds the DNS label limit of ${DNS_LABEL_MAX_LENGTH} characters`,
|
|
335
|
-
code: "IDENTIFIER_TOO_LONG"
|
|
336
|
-
});
|
|
337
|
-
const activeVerification = await ctx.context.internalAdapter.findVerificationValue(identifier);
|
|
338
|
-
if (!activeVerification || new Date(activeVerification.expiresAt) <= /* @__PURE__ */ new Date()) throw new APIError("NOT_FOUND", {
|
|
339
|
-
message: "No pending domain verification exists",
|
|
340
|
-
code: "NO_PENDING_VERIFICATION"
|
|
341
|
-
});
|
|
342
|
-
let records = [];
|
|
343
|
-
let dns;
|
|
344
|
-
try {
|
|
345
|
-
dns = await import("node:dns/promises");
|
|
346
|
-
} catch (error) {
|
|
347
|
-
ctx.context.logger.error("The core node:dns module is required for the domain verification feature", error);
|
|
348
|
-
throw new APIError("INTERNAL_SERVER_ERROR", {
|
|
349
|
-
message: "Unable to verify domain ownership due to server error",
|
|
350
|
-
code: "DOMAIN_VERIFICATION_FAILED"
|
|
351
|
-
});
|
|
352
|
-
}
|
|
353
|
-
const hostname = getHostnameFromDomain(provider.domain);
|
|
354
|
-
if (!hostname) throw new APIError("BAD_REQUEST", {
|
|
355
|
-
message: "Invalid domain",
|
|
356
|
-
code: "INVALID_DOMAIN"
|
|
357
|
-
});
|
|
358
|
-
try {
|
|
359
|
-
records = (await dns.resolveTxt(`${identifier}.${hostname}`)).flat();
|
|
360
|
-
} catch (error) {
|
|
361
|
-
ctx.context.logger.warn("DNS resolution failure while validating domain ownership", error);
|
|
362
|
-
}
|
|
363
|
-
if (!records.find((record) => record.includes(`${activeVerification.identifier}=${activeVerification.value}`))) throw new APIError("BAD_GATEWAY", {
|
|
364
|
-
message: "Unable to verify domain ownership. Try again later",
|
|
365
|
-
code: "DOMAIN_VERIFICATION_FAILED"
|
|
366
|
-
});
|
|
367
|
-
await ctx.context.adapter.update({
|
|
368
|
-
model: "ssoProvider",
|
|
369
|
-
where: [{
|
|
370
|
-
field: "providerId",
|
|
371
|
-
value: provider.providerId
|
|
372
|
-
}],
|
|
373
|
-
update: { domainVerified: true }
|
|
374
|
-
});
|
|
375
|
-
ctx.setStatus(204);
|
|
376
|
-
});
|
|
377
|
-
};
|
|
378
|
-
//#endregion
|
|
379
222
|
//#region src/oidc/types.ts
|
|
380
223
|
/**
|
|
381
224
|
* Custom error class for OIDC discovery failures.
|
|
@@ -1505,6 +1348,119 @@ const deleteSSOProvider = () => {
|
|
|
1505
1348
|
});
|
|
1506
1349
|
};
|
|
1507
1350
|
//#endregion
|
|
1351
|
+
//#region src/routes/domain-verification.ts
|
|
1352
|
+
const DNS_LABEL_MAX_LENGTH = 63;
|
|
1353
|
+
const DEFAULT_TOKEN_PREFIX = "better-auth-token";
|
|
1354
|
+
const domainVerificationBodySchema = z.object({ providerId: z.string() });
|
|
1355
|
+
function getVerificationIdentifier(options, providerId) {
|
|
1356
|
+
return `_${options.domainVerification?.tokenPrefix || DEFAULT_TOKEN_PREFIX}-${providerId}`;
|
|
1357
|
+
}
|
|
1358
|
+
const requestDomainVerification = (options) => {
|
|
1359
|
+
return createAuthEndpoint("/sso/request-domain-verification", {
|
|
1360
|
+
method: "POST",
|
|
1361
|
+
body: domainVerificationBodySchema,
|
|
1362
|
+
metadata: { openapi: {
|
|
1363
|
+
summary: "Request a domain verification",
|
|
1364
|
+
description: "Request a domain verification for the given SSO provider",
|
|
1365
|
+
responses: {
|
|
1366
|
+
"404": { description: "Provider not found" },
|
|
1367
|
+
"409": { description: "Domain has already been verified" },
|
|
1368
|
+
"201": { description: "Domain submitted for verification" }
|
|
1369
|
+
}
|
|
1370
|
+
} },
|
|
1371
|
+
use: [sessionMiddleware]
|
|
1372
|
+
}, async (ctx) => {
|
|
1373
|
+
const body = ctx.body;
|
|
1374
|
+
const provider = await checkProviderAccess(ctx, body.providerId);
|
|
1375
|
+
if (provider.domainVerified) throw new APIError("CONFLICT", {
|
|
1376
|
+
message: "Domain has already been verified",
|
|
1377
|
+
code: "DOMAIN_VERIFIED"
|
|
1378
|
+
});
|
|
1379
|
+
const identifier = getVerificationIdentifier(options, provider.providerId);
|
|
1380
|
+
const activeVerification = await ctx.context.internalAdapter.findVerificationValue(identifier);
|
|
1381
|
+
if (activeVerification && new Date(activeVerification.expiresAt) > /* @__PURE__ */ new Date()) {
|
|
1382
|
+
ctx.setStatus(201);
|
|
1383
|
+
return ctx.json({ domainVerificationToken: activeVerification.value });
|
|
1384
|
+
}
|
|
1385
|
+
const domainVerificationToken = generateRandomString(24);
|
|
1386
|
+
await ctx.context.internalAdapter.createVerificationValue({
|
|
1387
|
+
identifier,
|
|
1388
|
+
value: domainVerificationToken,
|
|
1389
|
+
expiresAt: new Date(Date.now() + 3600 * 24 * 7 * 1e3)
|
|
1390
|
+
});
|
|
1391
|
+
ctx.setStatus(201);
|
|
1392
|
+
return ctx.json({ domainVerificationToken });
|
|
1393
|
+
});
|
|
1394
|
+
};
|
|
1395
|
+
const verifyDomain = (options) => {
|
|
1396
|
+
return createAuthEndpoint("/sso/verify-domain", {
|
|
1397
|
+
method: "POST",
|
|
1398
|
+
body: domainVerificationBodySchema,
|
|
1399
|
+
metadata: { openapi: {
|
|
1400
|
+
summary: "Verify the provider domain ownership",
|
|
1401
|
+
description: "Verify the provider domain ownership via DNS records",
|
|
1402
|
+
responses: {
|
|
1403
|
+
"404": { description: "Provider not found" },
|
|
1404
|
+
"409": { description: "Domain has already been verified or no pending verification exists" },
|
|
1405
|
+
"502": { description: "Unable to verify domain ownership due to upstream validator error" },
|
|
1406
|
+
"204": { description: "Domain ownership was verified" }
|
|
1407
|
+
}
|
|
1408
|
+
} },
|
|
1409
|
+
use: [sessionMiddleware]
|
|
1410
|
+
}, async (ctx) => {
|
|
1411
|
+
const body = ctx.body;
|
|
1412
|
+
const provider = await checkProviderAccess(ctx, body.providerId);
|
|
1413
|
+
if (provider.domainVerified) throw new APIError("CONFLICT", {
|
|
1414
|
+
message: "Domain has already been verified",
|
|
1415
|
+
code: "DOMAIN_VERIFIED"
|
|
1416
|
+
});
|
|
1417
|
+
const identifier = getVerificationIdentifier(options, provider.providerId);
|
|
1418
|
+
if (identifier.length > DNS_LABEL_MAX_LENGTH) throw new APIError("BAD_REQUEST", {
|
|
1419
|
+
message: `Verification identifier exceeds the DNS label limit of ${DNS_LABEL_MAX_LENGTH} characters`,
|
|
1420
|
+
code: "IDENTIFIER_TOO_LONG"
|
|
1421
|
+
});
|
|
1422
|
+
const activeVerification = await ctx.context.internalAdapter.findVerificationValue(identifier);
|
|
1423
|
+
if (!activeVerification || new Date(activeVerification.expiresAt) <= /* @__PURE__ */ new Date()) throw new APIError("NOT_FOUND", {
|
|
1424
|
+
message: "No pending domain verification exists",
|
|
1425
|
+
code: "NO_PENDING_VERIFICATION"
|
|
1426
|
+
});
|
|
1427
|
+
let records = [];
|
|
1428
|
+
let dns;
|
|
1429
|
+
try {
|
|
1430
|
+
dns = await import("node:dns/promises");
|
|
1431
|
+
} catch (error) {
|
|
1432
|
+
ctx.context.logger.error("The core node:dns module is required for the domain verification feature", error);
|
|
1433
|
+
throw new APIError("INTERNAL_SERVER_ERROR", {
|
|
1434
|
+
message: "Unable to verify domain ownership due to server error",
|
|
1435
|
+
code: "DOMAIN_VERIFICATION_FAILED"
|
|
1436
|
+
});
|
|
1437
|
+
}
|
|
1438
|
+
const hostname = getHostnameFromDomain(provider.domain);
|
|
1439
|
+
if (!hostname) throw new APIError("BAD_REQUEST", {
|
|
1440
|
+
message: "Invalid domain",
|
|
1441
|
+
code: "INVALID_DOMAIN"
|
|
1442
|
+
});
|
|
1443
|
+
try {
|
|
1444
|
+
records = (await dns.resolveTxt(`${identifier}.${hostname}`)).flat();
|
|
1445
|
+
} catch (error) {
|
|
1446
|
+
ctx.context.logger.warn("DNS resolution failure while validating domain ownership", error);
|
|
1447
|
+
}
|
|
1448
|
+
if (!records.find((record) => record.includes(`${activeVerification.identifier}=${activeVerification.value}`))) throw new APIError("BAD_GATEWAY", {
|
|
1449
|
+
message: "Unable to verify domain ownership. Try again later",
|
|
1450
|
+
code: "DOMAIN_VERIFICATION_FAILED"
|
|
1451
|
+
});
|
|
1452
|
+
await ctx.context.adapter.update({
|
|
1453
|
+
model: "ssoProvider",
|
|
1454
|
+
where: [{
|
|
1455
|
+
field: "providerId",
|
|
1456
|
+
value: provider.providerId
|
|
1457
|
+
}],
|
|
1458
|
+
update: { domainVerified: true }
|
|
1459
|
+
});
|
|
1460
|
+
ctx.setStatus(204);
|
|
1461
|
+
});
|
|
1462
|
+
};
|
|
1463
|
+
//#endregion
|
|
1508
1464
|
//#region src/saml/error-codes.ts
|
|
1509
1465
|
const SAML_ERROR_CODES = defineErrorCodes({
|
|
1510
1466
|
SINGLE_LOGOUT_NOT_ENABLED: "Single Logout is not enabled",
|
|
@@ -1835,26 +1791,8 @@ async function processSAMLResponse(ctx, params, options) {
|
|
|
1835
1791
|
const conditions = extract.conditions;
|
|
1836
1792
|
const clockSkew = options?.saml?.clockSkew ?? 3e5;
|
|
1837
1793
|
const expiresAt = conditions?.notOnOrAfter ? new Date(conditions.notOnOrAfter).getTime() + clockSkew : Date.now() + DEFAULT_ASSERTION_TTL_MS;
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
if (existingAssertion) try {
|
|
1841
|
-
if (JSON.parse(existingAssertion.value).expiresAt >= Date.now()) isReplay = true;
|
|
1842
|
-
} catch (error) {
|
|
1843
|
-
ctx.context.logger.warn("Failed to parse stored assertion record", {
|
|
1844
|
-
assertionId,
|
|
1845
|
-
error
|
|
1846
|
-
});
|
|
1847
|
-
}
|
|
1848
|
-
if (isReplay) {
|
|
1849
|
-
ctx.context.logger.error("SAML assertion replay detected: assertion ID already used", {
|
|
1850
|
-
assertionId,
|
|
1851
|
-
issuer,
|
|
1852
|
-
providerId
|
|
1853
|
-
});
|
|
1854
|
-
throw ctx.redirect(`${samlRedirectUrl}?error=replay_detected&error_description=SAML+assertion+has+already+been+used`);
|
|
1855
|
-
}
|
|
1856
|
-
await ctx.context.internalAdapter.createVerificationValue({
|
|
1857
|
-
identifier: `${USED_ASSERTION_KEY_PREFIX}${assertionId}`,
|
|
1794
|
+
if (!await ctx.context.internalAdapter.reserveVerificationValue({
|
|
1795
|
+
identifier: `saml-used-assertion:${assertionId}`,
|
|
1858
1796
|
value: JSON.stringify({
|
|
1859
1797
|
assertionId,
|
|
1860
1798
|
issuer,
|
|
@@ -1863,7 +1801,14 @@ async function processSAMLResponse(ctx, params, options) {
|
|
|
1863
1801
|
expiresAt
|
|
1864
1802
|
}),
|
|
1865
1803
|
expiresAt: new Date(expiresAt)
|
|
1866
|
-
})
|
|
1804
|
+
})) {
|
|
1805
|
+
ctx.context.logger.error("SAML assertion replay detected: assertion ID already used", {
|
|
1806
|
+
assertionId,
|
|
1807
|
+
issuer,
|
|
1808
|
+
providerId
|
|
1809
|
+
});
|
|
1810
|
+
throw ctx.redirect(`${samlRedirectUrl}?error=replay_detected&error_description=SAML+assertion+has+already+been+used`);
|
|
1811
|
+
}
|
|
1867
1812
|
} else ctx.context.logger.warn("Could not extract assertion ID for replay protection", { providerId });
|
|
1868
1813
|
const attributes = extract.attributes || {};
|
|
1869
1814
|
const mapping = parsedSamlConfig.mapping ?? {};
|
|
@@ -1876,7 +1821,7 @@ async function processSAMLResponse(ctx, params, options) {
|
|
|
1876
1821
|
id: attr(mapping.id || "nameID") || extract.nameID,
|
|
1877
1822
|
email: (attr(mapping.email || "email") || extract.nameID || "").toLowerCase(),
|
|
1878
1823
|
name: [attr(mapping.firstName || "givenName"), attr(mapping.lastName || "surname")].filter(Boolean).join(" ") || attr(mapping.name || "displayName") || extract.nameID,
|
|
1879
|
-
emailVerified: options?.trustEmailVerified && mapping.emailVerified ? attr(mapping.emailVerified)
|
|
1824
|
+
emailVerified: options?.trustEmailVerified && mapping.emailVerified ? parseProviderEmailVerified(attr(mapping.emailVerified)) : false
|
|
1880
1825
|
};
|
|
1881
1826
|
if (!userInfo.id || !userInfo.email) {
|
|
1882
1827
|
ctx.context.logger.error("Missing essential user info from SAML response", {
|
|
@@ -1897,7 +1842,7 @@ async function processSAMLResponse(ctx, params, options) {
|
|
|
1897
1842
|
email: userInfo.email,
|
|
1898
1843
|
name: userInfo.name || userInfo.email,
|
|
1899
1844
|
id: userInfo.id,
|
|
1900
|
-
emailVerified:
|
|
1845
|
+
emailVerified: userInfo.emailVerified
|
|
1901
1846
|
},
|
|
1902
1847
|
account: {
|
|
1903
1848
|
providerId,
|
|
@@ -1933,7 +1878,7 @@ async function processSAMLResponse(ctx, params, options) {
|
|
|
1933
1878
|
providerId,
|
|
1934
1879
|
accountId: userInfo.id,
|
|
1935
1880
|
email: userInfo.email,
|
|
1936
|
-
emailVerified:
|
|
1881
|
+
emailVerified: userInfo.emailVerified,
|
|
1937
1882
|
rawAttributes: attributes
|
|
1938
1883
|
},
|
|
1939
1884
|
provider,
|
|
@@ -2777,7 +2722,7 @@ async function handleOIDCCallback(ctx, options, providerId, stateData) {
|
|
|
2777
2722
|
...Object.fromEntries(Object.entries(mapping.extraFields || {}).map(([key, value]) => [key, rawUserInfo[value]])),
|
|
2778
2723
|
id: rawUserInfo[mapping.id || "sub"],
|
|
2779
2724
|
email: rawUserInfo[mapping.email || "email"],
|
|
2780
|
-
emailVerified: options?.trustEmailVerified ? rawUserInfo[mapping.emailVerified || "email_verified"] : false,
|
|
2725
|
+
emailVerified: options?.trustEmailVerified ? parseProviderEmailVerified(rawUserInfo[mapping.emailVerified || "email_verified"]) : false,
|
|
2781
2726
|
name: rawUserInfo[mapping.name || "name"],
|
|
2782
2727
|
image: rawUserInfo[mapping.image || "picture"]
|
|
2783
2728
|
};
|
|
@@ -2796,7 +2741,7 @@ async function handleOIDCCallback(ctx, options, providerId, stateData) {
|
|
|
2796
2741
|
...Object.fromEntries(Object.entries(mapping.extraFields || {}).map(([key, value]) => [key, verified.payload[value]])),
|
|
2797
2742
|
id: idToken[mapping.id || "sub"],
|
|
2798
2743
|
email: idToken[mapping.email || "email"],
|
|
2799
|
-
emailVerified: options?.trustEmailVerified ? idToken[mapping.emailVerified || "email_verified"] : false,
|
|
2744
|
+
emailVerified: options?.trustEmailVerified ? parseProviderEmailVerified(idToken[mapping.emailVerified || "email_verified"]) : false,
|
|
2800
2745
|
name: idToken[mapping.name || "name"],
|
|
2801
2746
|
image: idToken[mapping.image || "picture"]
|
|
2802
2747
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@better-auth/sso",
|
|
3
|
-
"version": "1.6.
|
|
3
|
+
"version": "1.6.17",
|
|
4
4
|
"description": "SSO plugin for Better Auth",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -70,15 +70,15 @@
|
|
|
70
70
|
"express": "^5.2.1",
|
|
71
71
|
"oauth2-mock-server": "^8.2.2",
|
|
72
72
|
"tsdown": "0.21.1",
|
|
73
|
-
"
|
|
74
|
-
"better-auth": "1.6.
|
|
73
|
+
"better-auth": "1.6.17",
|
|
74
|
+
"@better-auth/core": "1.6.17"
|
|
75
75
|
},
|
|
76
76
|
"peerDependencies": {
|
|
77
77
|
"@better-auth/utils": "0.4.1",
|
|
78
|
-
"@better-fetch/fetch": "1.
|
|
78
|
+
"@better-fetch/fetch": "1.3.0",
|
|
79
79
|
"better-call": "1.3.6",
|
|
80
|
-
"@better-auth/core": "^1.6.
|
|
81
|
-
"better-auth": "^1.6.
|
|
80
|
+
"@better-auth/core": "^1.6.17",
|
|
81
|
+
"better-auth": "^1.6.17"
|
|
82
82
|
},
|
|
83
83
|
"scripts": {
|
|
84
84
|
"build": "tsdown",
|