fix(sync): address behavioral review findings

This commit is contained in:
Gustavo Valverde
2026-07-22 14:22:40 -04:00
parent 337e0f7d75
commit cd4eb5627f
10 changed files with 147 additions and 5 deletions
+2
View File
@@ -1530,6 +1530,8 @@ await authClient.signIn.sso({
For unsolicited **IdP-initiated** flows where `signIn.sso()` is not used (and therefore no client-side `callbackURL` is set), configure `idpInitiatedCallbackUrl` in the provider's `samlConfig` or in the global `saml` plugin options. The provider setting takes precedence over the global setting, and the same fallback applies to validation error redirects. Better Auth-generated `RelayState` success and error callbacks retain higher priority. If neither fallback is configured, the existing redirect behavior is preserved.
When updating a provider, pass `null` for `samlConfig.idpInitiatedCallbackUrl` to remove its override and fall back to the global setting.
The callback route supports both GET and POST methods automatically, so you don't need to create any additional route handlers in your framework.
### Additional Provider Fields
+1 -1
View File
@@ -8,7 +8,7 @@
"url": "git+https://github.com/better-auth/better-auth.git"
},
"scripts": {
"prepare": "git rev-parse --git-dir >/dev/null 2>&1 && lefthook install --reset-hooks-path || echo \"skipping lefthook (not a git repo)\"",
"prepare": "if git rev-parse --git-dir >/dev/null 2>&1; then lefthook install --reset-hooks-path; else echo \"skipping lefthook (not a git repo)\"; fi",
"build": "turbo build --filter=./packages/*",
"dev": "turbo dev --filter=./packages/* --filter=!./packages/cli",
"clean": "turbo clean --filter=./packages/* && rm -rf node_modules",
@@ -1096,7 +1096,7 @@ describe("lastLoginMethod", async () => {
consentGiven = true;
// Second login with consent - cookie should be set
await client.signOut();
await client.signOut({ fetchOptions: { headers: headers1 } });
const headers2 = new Headers();
await client.signIn.email(
{
@@ -440,7 +440,7 @@ function dbFieldToRequestBodyProperty(field: DBFieldAttribute): OpenAPISchema {
return { type: "string", format: "date-time" };
}
if (field.type === "json") {
return { type: "object", additionalProperties: true };
return {};
}
if (field.type === "string[]") {
return { type: "array", items: { type: "string" } };
@@ -402,6 +402,37 @@ describe("open-api", async () => {
expect(schemas["User"]!.required).toContain("scores");
});
/**
* @see https://github.com/better-auth/better-auth/issues/10430
*/
it("should allow every JSON value in additional field request bodies", async () => {
const { auth } = await getTestInstance(
{
plugins: [openAPI()],
user: {
additionalFields: {
metadata: {
type: "json",
},
},
},
},
{ disableTestUser: true },
);
const schema = await auth.api.generateOpenAPISchema();
const paths = schema.paths as Record<string, Path>;
const signUpSchema = getPostRequestBody(paths, "/sign-up/email").content[
"application/json"
].schema;
expect(getSchemaProperty(signUpSchema, "metadata")).toEqual({});
const updateUserSchema = getPostRequestBody(paths, "/update-user").content[
"application/json"
].schema;
expect(getSchemaProperty(updateUserSchema, "metadata")).toEqual({});
});
it("should include additionalFields on sign-up and update-user request bodies", async () => {
const schema = await auth.api.generateOpenAPISchema();
const paths = schema.paths as Record<string, Path>;
+1
View File
@@ -167,6 +167,7 @@ async function generateAction(opts: any) {
);
return;
}
await removeGeneratedStub();
let adapter: DBAdapter;
if (options.adapter) {
+63
View File
@@ -128,6 +128,69 @@ export const auth = betterAuth({
}
}
});
/**
* @see https://github.com/better-auth/better-auth/issues/10136
*/
it("should remove a config import stub before generating a new Prisma schema", async () => {
const cacheDir = path.join(
process.cwd(),
"node_modules",
".cache",
"generate-output-",
);
fs.mkdirSync(path.dirname(cacheDir), { recursive: true });
const tmpDir = fs.mkdtempSync(cacheDir);
const outputPath = path.join(tmpDir, "schema.ts");
fs.writeFileSync(
path.join(tmpDir, "auth.ts"),
`import schema from "./schema";
import { betterAuth } from "better-auth";
export const auth = betterAuth({
secret: "test-secret",
baseURL: "http://localhost:3000",
});
export const __schema = schema;
`,
);
try {
await execFileAsync(
process.execPath,
[
cliPath,
"generate",
"--cwd",
tmpDir,
"--config",
"auth.ts",
"--adapter",
"prisma",
"--dialect",
"sqlite",
"--output",
"schema.ts",
"--yes",
],
{
cwd: tmpDir,
env: {
...process.env,
BETTER_AUTH_TELEMETRY_DISABLED: "true",
},
},
);
const schema = fs.readFileSync(outputPath, "utf-8");
expect(schema).toContain("generator client");
expect(schema).toContain("datasource db");
expect(schema).toContain("model User");
} finally {
fs.rmSync(tmpDir, { recursive: true, force: true });
}
});
});
it("should generate prisma schema", async () => {
+41
View File
@@ -1068,6 +1068,47 @@ kBGIJYs=
);
});
/**
* @see https://github.com/better-auth/better-auth/issues/10329
*/
it("should clear a provider-level IdP-initiated callback URL", async () => {
const { auth, getAuthHeaders, data } = createTestAuth(false);
const headers = await getAuthHeaders({
email: "owner@example.com",
password: "password123",
name: "Owner",
});
await auth.api.registerSSOProvider({
body: {
providerId: "my-saml-provider",
issuer: "https://idp.example.com",
domain: "example.com",
samlConfig: {
entryPoint: "https://idp.example.com/sso",
cert: TEST_CERT,
idpInitiatedCallbackUrl: "/dashboard",
idpMetadata: { entityID: "https://idp.example.com" },
},
},
headers,
});
const updated = await auth.api.updateSSOProvider({
body: {
providerId: "my-saml-provider",
samlConfig: { idpInitiatedCallbackUrl: null },
},
headers,
});
expect(updated.samlConfig?.idpInitiatedCallbackUrl).toBeUndefined();
const storedConfig = safeJsonParse<SAMLConfig>(
data.ssoProvider[0]!.samlConfig!,
);
expect(storedConfig?.idpInitiatedCallbackUrl).toBeUndefined();
});
it("should perform partial update on OIDC provider", async () => {
const { auth, getAuthHeaders, createOIDCProviderData, data } =
createTestAuth(false);
+5 -2
View File
@@ -590,8 +590,9 @@ function parseAndValidateConfig<T>(
type SAMLConfigUpdate = Omit<
Partial<SAMLConfig>,
"idpMetadata" | "spMetadata"
"idpInitiatedCallbackUrl" | "idpMetadata" | "spMetadata"
> & {
idpInitiatedCallbackUrl?: string | null | undefined;
idpMetadata?: Partial<SAMLIdentityProviderMetadata> | undefined;
spMetadata?: Partial<NonNullable<SAMLConfig["spMetadata"]>> | undefined;
};
@@ -631,7 +632,9 @@ function mergeSAMLConfig(
audience: updates.audience ?? current.audience,
callbackUrl: updates.callbackUrl ?? current.callbackUrl,
idpInitiatedCallbackUrl:
updates.idpInitiatedCallbackUrl ?? current.idpInitiatedCallbackUrl,
updates.idpInitiatedCallbackUrl === null
? undefined
: (updates.idpInitiatedCallbackUrl ?? current.idpInitiatedCallbackUrl),
wantAssertionsSigned:
updates.wantAssertionsSigned ?? current.wantAssertionsSigned,
authnRequestsSigned:
+1
View File
@@ -326,6 +326,7 @@ const updateSSOProviderBodySchema = z.object({
.omit({ idpMetadata: true })
.partial()
.extend({
idpInitiatedCallbackUrl: samlRedirectUrlSchema.nullable().optional(),
idpMetadata: samlIdentityProviderMetadataUpdateSchema.optional(),
})
.optional(),