@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
@@ -1,4 +1,4 @@
1
- import { t as PACKAGE_VERSION } from "./version-COmqWYke.mjs";
1
+ import { t as PACKAGE_VERSION } from "./version-BjN4UKdQ.mjs";
2
2
  //#region src/client.ts
3
3
  const ssoClient = (options) => {
4
4
  return {
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
- import { t as PACKAGE_VERSION } from "./version-COmqWYke.mjs";
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 parsed = new XMLParser({
2830
- ignoreAttributes: false,
2831
- attributeNamePrefix: "@_",
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 = session.user.email;
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 || 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
- if (existing === true) return {};
4537
- return { context: { skipOriginCheck: [...Array.isArray(existing) ? existing : [], ...SAML_SKIP_ORIGIN_CHECK_PATHS] } };
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: {
@@ -1,5 +1,5 @@
1
1
  //#endregion
2
2
  //#region src/version.ts
3
- const PACKAGE_VERSION = "1.7.20";
3
+ const PACKAGE_VERSION = "1.7.21";
4
4
  //#endregion
5
5
  export { PACKAGE_VERSION as t };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@better-auth/sso",
3
- "version": "1.7.20",
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.20",
76
- "@better-auth/kysely-adapter": "1.7.20",
77
- "better-auth": "1.7.20"
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.20",
84
- "better-auth": "^1.7.20"
83
+ "@better-auth/core": "^1.7.21",
84
+ "better-auth": "^1.7.21"
85
85
  },
86
86
  "scripts": {
87
87
  "build": "tsdown",