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:
@@ -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);
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user