@better-auth/sso 1.7.20 → 1.7.21
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.mjs
CHANGED
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { t as PACKAGE_VERSION } from "./version-
|
|
2
|
-
import { APIError, addOAuthServerContext, createAuthEndpoint, createAuthMiddleware, getSessionFromCtx, sessionMiddleware } from "better-auth/api";
|
|
1
|
+
import { t as PACKAGE_VERSION } from "./version-BjN4UKdQ.mjs";
|
|
2
|
+
import { APIError, addOAuthServerContext, createAuthEndpoint, createAuthMiddleware, generateGenericState, getSessionFromCtx, parseGenericState, sessionMiddleware } from "better-auth/api";
|
|
3
3
|
import { XMLParser, XMLValidator } from "fast-xml-parser";
|
|
4
4
|
import { X509Certificate } from "node:crypto";
|
|
5
5
|
import { getHostname } from "tldts";
|
|
@@ -17,11 +17,12 @@ import { DOMParser } from "@xmldom/xmldom";
|
|
|
17
17
|
import samlifyDefault, * as samlifyNamespace from "samlify";
|
|
18
18
|
import { defineErrorCodes } from "@better-auth/core/utils/error-codes";
|
|
19
19
|
import { parseInputData, toZodSchema } from "better-auth/db";
|
|
20
|
+
import { HIDE_METADATA } from "@better-auth/core/api";
|
|
21
|
+
import { PRIVATE_KEY_JWT_SIGNING_ALGORITHMS, authorizationCodeRequest, createAuthorizationURL, createPrivateKeyJwtClientAssertionGetter, getOAuth2Tokens } from "@better-auth/core/oauth2";
|
|
20
22
|
import { isAPIError } from "@better-auth/core/utils/is-api-error";
|
|
21
23
|
import { appendQueryParams } from "@better-auth/core/utils/url";
|
|
22
|
-
import { HIDE_METADATA, PRIVATE_KEY_JWT_SIGNING_ALGORITHMS, authorizationCodeRequest, createAuthorizationURL, createPrivateKeyJwtClientAssertionGetter, generateGenericState, generateState, getOAuth2Tokens, parseGenericState, parseState } from "better-auth";
|
|
23
24
|
import { deleteSessionCookie, setAccountCookie, setSessionCookie } from "better-auth/cookies";
|
|
24
|
-
import { additionalAuthorizationParamsSchema, handleOAuthUserInfo } from "better-auth/oauth2";
|
|
25
|
+
import { additionalAuthorizationParamsSchema, generateState, handleOAuthUserInfo, parseState } from "better-auth/oauth2";
|
|
25
26
|
//#region src/constants.ts
|
|
26
27
|
/**
|
|
27
28
|
* SAML Constants
|
|
@@ -2823,20 +2824,16 @@ async function getSAMLResponseBindingContent(sp, samlContent) {
|
|
|
2823
2824
|
/**
|
|
2824
2825
|
* Extracts the Assertion ID from a SAML response XML.
|
|
2825
2826
|
* Used for replay protection per SAML 2.0 Core section 2.3.3.
|
|
2827
|
+
*
|
|
2828
|
+
* Reads the ID through the DOM so character references are decoded the same
|
|
2829
|
+
* way the signature check decodes them: re-spelling the ID keeps the
|
|
2830
|
+
* signature valid, so it must not change the replay key.
|
|
2826
2831
|
*/
|
|
2827
2832
|
function extractAssertionId(samlContent) {
|
|
2828
2833
|
try {
|
|
2829
|
-
const
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
removeNSPrefix: true
|
|
2833
|
-
}).parse(samlContent);
|
|
2834
|
-
const response = parsed.Response || parsed["samlp:Response"];
|
|
2835
|
-
if (!response) return null;
|
|
2836
|
-
const rawAssertion = response.Assertion || response["saml:Assertion"];
|
|
2837
|
-
const assertion = Array.isArray(rawAssertion) ? rawAssertion[0] : rawAssertion;
|
|
2838
|
-
if (!assertion) return null;
|
|
2839
|
-
return assertion["@_ID"] || null;
|
|
2834
|
+
const response = new DOMParser().parseFromString(samlContent, "text/xml").documentElement;
|
|
2835
|
+
if (response?.localName !== "Response") return null;
|
|
2836
|
+
return Array.from(response.childNodes).find((node) => node.nodeType === 1 && node.localName === "Assertion")?.getAttribute("ID") || null;
|
|
2840
2837
|
} catch {
|
|
2841
2838
|
return null;
|
|
2842
2839
|
}
|
|
@@ -4423,7 +4420,7 @@ const initiateSLO = (options) => {
|
|
|
4423
4420
|
const session = ctx.context.session;
|
|
4424
4421
|
const sessionLookupKey = `${SAML_SESSION_BY_ID_PREFIX}${session.session.id}`;
|
|
4425
4422
|
const sessionLookup = await ctx.context.internalAdapter.findVerificationValue(sessionLookupKey);
|
|
4426
|
-
let nameID
|
|
4423
|
+
let nameID;
|
|
4427
4424
|
let sessionIndex;
|
|
4428
4425
|
let samlSessionKey;
|
|
4429
4426
|
if (sessionLookup) {
|
|
@@ -4432,11 +4429,15 @@ const initiateSLO = (options) => {
|
|
|
4432
4429
|
if (stored) {
|
|
4433
4430
|
const data = safeJsonParse(stored.value);
|
|
4434
4431
|
if (data) {
|
|
4435
|
-
nameID = data.nameID
|
|
4432
|
+
nameID = data.nameID;
|
|
4436
4433
|
sessionIndex = data.sessionIndex;
|
|
4437
4434
|
}
|
|
4438
4435
|
}
|
|
4439
4436
|
}
|
|
4437
|
+
if (!nameID) {
|
|
4438
|
+
const [account] = (await ctx.context.internalAdapter.findAccounts(session.user.id)).filter((account) => account.providerId === providerId).sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime());
|
|
4439
|
+
nameID = account?.accountId ?? session.user.email;
|
|
4440
|
+
}
|
|
4440
4441
|
const logoutRequest = sp.createLogoutRequest(idp, "redirect", {
|
|
4441
4442
|
logoutNameID: nameID,
|
|
4442
4443
|
sessionIndex,
|
|
@@ -4501,6 +4502,53 @@ function assertNoAdditionalFieldCollisions(options) {
|
|
|
4501
4502
|
if (builtInFieldNames.has(fieldName)) throw new Error(`ssoProvider additional field "${key}" maps to built-in field "${fieldName}"`);
|
|
4502
4503
|
}
|
|
4503
4504
|
}
|
|
4505
|
+
const SAML_SESSION_RECORD_PAGE_SIZE = 100;
|
|
4506
|
+
async function findSAMLSessionRecord(ctx, identifier, isOwnRecord) {
|
|
4507
|
+
for (let offset = 0;; offset += SAML_SESSION_RECORD_PAGE_SIZE) {
|
|
4508
|
+
const page = await ctx.adapter.findMany({
|
|
4509
|
+
model: "verification",
|
|
4510
|
+
where: [{
|
|
4511
|
+
field: "identifier",
|
|
4512
|
+
value: identifier
|
|
4513
|
+
}],
|
|
4514
|
+
sortBy: {
|
|
4515
|
+
field: "createdAt",
|
|
4516
|
+
direction: "desc"
|
|
4517
|
+
},
|
|
4518
|
+
limit: SAML_SESSION_RECORD_PAGE_SIZE,
|
|
4519
|
+
offset
|
|
4520
|
+
});
|
|
4521
|
+
const record = page.find(isOwnRecord);
|
|
4522
|
+
if (record || page.length < SAML_SESSION_RECORD_PAGE_SIZE) return record;
|
|
4523
|
+
}
|
|
4524
|
+
}
|
|
4525
|
+
/**
|
|
4526
|
+
* SLO records are written with the session's expiry at sign-in. Move it when a
|
|
4527
|
+
* refresh extends the session, so expired-row cleanup doesn't drop them early.
|
|
4528
|
+
*/
|
|
4529
|
+
async function extendSAMLSessionRecords(ctx, session) {
|
|
4530
|
+
const lookupKey = `${SAML_SESSION_BY_ID_PREFIX}${session.id}`;
|
|
4531
|
+
const lookup = await ctx.internalAdapter.findVerificationValue(lookupKey);
|
|
4532
|
+
const expiresAt = new Date(session.expiresAt);
|
|
4533
|
+
if (!lookup || new Date(lookup.expiresAt) >= expiresAt) return;
|
|
4534
|
+
const isOwnRecord = (record) => safeJsonParse(record.value)?.sessionId === session.id;
|
|
4535
|
+
const latest = await ctx.internalAdapter.findVerificationValue(lookup.value);
|
|
4536
|
+
const secondaryStorageOnly = ctx.options.secondaryStorage && !ctx.options.verification?.storeInDatabase;
|
|
4537
|
+
if (latest && secondaryStorageOnly) {
|
|
4538
|
+
if (isOwnRecord(latest)) await ctx.internalAdapter.updateVerificationByIdentifier(lookup.value, { expiresAt });
|
|
4539
|
+
} else if (latest) {
|
|
4540
|
+
const record = await findSAMLSessionRecord(ctx, latest.identifier, isOwnRecord);
|
|
4541
|
+
if (record) await ctx.adapter.update({
|
|
4542
|
+
model: "verification",
|
|
4543
|
+
where: [{
|
|
4544
|
+
field: "id",
|
|
4545
|
+
value: record.id
|
|
4546
|
+
}],
|
|
4547
|
+
update: { expiresAt }
|
|
4548
|
+
});
|
|
4549
|
+
}
|
|
4550
|
+
await ctx.internalAdapter.updateVerificationByIdentifier(lookupKey, { expiresAt });
|
|
4551
|
+
}
|
|
4504
4552
|
function sso(options) {
|
|
4505
4553
|
assertNoAdditionalFieldCollisions(options);
|
|
4506
4554
|
const optionsWithStore = options;
|
|
@@ -4533,8 +4581,13 @@ function sso(options) {
|
|
|
4533
4581
|
version: PACKAGE_VERSION,
|
|
4534
4582
|
init(ctx) {
|
|
4535
4583
|
const existing = ctx.skipOriginCheck;
|
|
4536
|
-
|
|
4537
|
-
|
|
4584
|
+
return {
|
|
4585
|
+
...existing !== true && { context: { skipOriginCheck: [...Array.isArray(existing) ? existing : [], ...SAML_SKIP_ORIGIN_CHECK_PATHS] } },
|
|
4586
|
+
options: { databaseHooks: { session: { update: { async after(session) {
|
|
4587
|
+
if (!options?.saml?.enableSingleLogout) return;
|
|
4588
|
+
await extendSAMLSessionRecords(ctx, session).catch((error) => ctx.logger.warn("Failed to extend SAML session records", { error }));
|
|
4589
|
+
} } } } }
|
|
4590
|
+
};
|
|
4538
4591
|
},
|
|
4539
4592
|
endpoints,
|
|
4540
4593
|
hooks: {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@better-auth/sso",
|
|
3
|
-
"version": "1.7.
|
|
3
|
+
"version": "1.7.21",
|
|
4
4
|
"description": "SSO plugin for Better Auth",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -72,16 +72,16 @@
|
|
|
72
72
|
"kysely": "^0.28.17 || ^0.29.0",
|
|
73
73
|
"oauth2-mock-server": "^9.0.0",
|
|
74
74
|
"tsdown": "0.23.1",
|
|
75
|
-
"@better-auth/core": "1.7.
|
|
76
|
-
"@better-auth/kysely-adapter": "1.7.
|
|
77
|
-
"better-auth": "1.7.
|
|
75
|
+
"@better-auth/core": "1.7.21",
|
|
76
|
+
"@better-auth/kysely-adapter": "1.7.21",
|
|
77
|
+
"better-auth": "1.7.21"
|
|
78
78
|
},
|
|
79
79
|
"peerDependencies": {
|
|
80
80
|
"@better-auth/utils": "^0.4.2 || ^0.5.0",
|
|
81
81
|
"@better-fetch/fetch": "1.3.3",
|
|
82
82
|
"better-call": "1.4.5",
|
|
83
|
-
"@better-auth/core": "^1.7.
|
|
84
|
-
"better-auth": "^1.7.
|
|
83
|
+
"@better-auth/core": "^1.7.21",
|
|
84
|
+
"better-auth": "^1.7.21"
|
|
85
85
|
},
|
|
86
86
|
"scripts": {
|
|
87
87
|
"build": "tsdown",
|