Fix security issues in Better Auth migration

- Tighten login rate limit from 200/10s to 5/60s to prevent brute-force
- Encrypt OAuth tokens (access/refresh/id) in accounts table via databaseHooks
- Sync password changes to accounts.password so old passwords stop working
- Redact OAuth client secrets in server actions before returning to client
- Add trustHost config (default false) to prevent Host header poisoning
- Add audit logging for successful logins via session create hook
- Add audit logging to OAuth provider update/delete server actions
- Fix provider ID collision by appending name hash suffix to slug
- Fix nullable provider field causing incorrect hasOAuth detection
- Refuse to store plaintext secrets if encryption module fails to load

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
fuomag9
2026-04-12 21:50:48 +02:00
parent 3a16d6e9b1
commit 66f8e32df5
5 changed files with 100 additions and 14 deletions

View File

@@ -53,7 +53,7 @@ export default function ProfileClient({ user, enabledProviders, apiTokens }: Pro
const [copied, setCopied] = useState(false);
const hasPassword = !!user.passwordHash;
const hasOAuth = user.provider !== "credentials";
const hasOAuth = !!user.provider && user.provider !== "credentials";
const handlePasswordChange = async () => {
setError(null);

View File

@@ -676,10 +676,20 @@ export async function suppressWafRuleGloballyAction(ruleId: number): Promise<Act
}
}
function redactProviderSecrets<T extends { clientId: string; clientSecret: string }>(provider: T): T {
const clientId = provider.clientId;
return {
...provider,
clientId: clientId.length > 4 ? "••••" + clientId.slice(-4) : "••••",
clientSecret: "••••••••",
};
}
export async function getOAuthProvidersAction() {
await requireAdmin();
const { listOAuthProviders } = await import("@/src/lib/models/oauth-providers");
return listOAuthProviders();
const providers = await listOAuthProviders();
return providers.map(redactProviderSecrets);
}
export async function createOAuthProviderAction(data: {
@@ -709,7 +719,7 @@ export async function createOAuthProviderAction(data: {
data: JSON.stringify({ providerId: provider.id }),
});
revalidatePath("/settings");
return provider;
return redactProviderSecrets(provider);
}
export async function updateOAuthProviderAction(
@@ -728,21 +738,40 @@ export async function updateOAuthProviderAction(
enabled: boolean;
}>
) {
await requireAdmin();
const session = await requireAdmin();
const { updateOAuthProvider } = await import("@/src/lib/models/oauth-providers");
const { invalidateProviderCache } = await import("@/src/lib/auth-server");
const updated = await updateOAuthProvider(id, data);
invalidateProviderCache();
const { createAuditEvent } = await import("@/src/lib/models/audit");
await createAuditEvent({
userId: Number(session.user.id),
action: "oauth_provider_updated",
entityType: "oauth_provider",
entityId: null,
summary: `Updated OAuth provider "${id}"`,
data: JSON.stringify({ providerId: id, fields: Object.keys(data) }),
});
revalidatePath("/settings");
return updated;
return updated ? redactProviderSecrets(updated) : null;
}
export async function deleteOAuthProviderAction(id: string) {
await requireAdmin();
const { deleteOAuthProvider } = await import("@/src/lib/models/oauth-providers");
const session = await requireAdmin();
const { getOAuthProvider, deleteOAuthProvider } = await import("@/src/lib/models/oauth-providers");
const { invalidateProviderCache } = await import("@/src/lib/auth-server");
const existing = await getOAuthProvider(id);
await deleteOAuthProvider(id);
invalidateProviderCache();
const { createAuditEvent } = await import("@/src/lib/models/audit");
await createAuditEvent({
userId: Number(session.user.id),
action: "oauth_provider_deleted",
entityType: "oauth_provider",
entityId: null,
summary: `Deleted OAuth provider "${existing?.name ?? id}"`,
data: JSON.stringify({ providerId: id }),
});
revalidatePath("/settings");
}