security: await audit writes, add per-turn AssistantPrompt audit (#55)
- Auth.js authorize/signOut: await createAuditEntry on every branch so auth events land in the audit store before the JWT is minted / session closes. Previously these were fire-and-forget and would be dropped under DB load. - Assistant chat: make appendPromptInjectionGuard async and await its own SecurityAlert audit; add auditUserPromptTurn() that records every user message turn as an AssistantPrompt entry containing conversationId, length, SHA-256 fingerprint, pageContext and whether the injection guard fired. Raw prompt text is intentionally not stored — the hash lets a responder correlate a chat transcript with a forensic request without the audit store accumulating a plain-text corpus of everything users typed. - Replace bare crypto.* with explicit node:crypto imports. - Document the retention posture in docs/security-architecture.md §6. Fixes gitea #55.
This commit is contained in:
@@ -85,7 +85,7 @@ const config = {
|
||||
: await authRateLimiter(rateLimitKeys);
|
||||
if (!rateLimitResult.allowed) {
|
||||
// Audit failed login (rate limited)
|
||||
void createAuditEntry({
|
||||
await createAuditEntry({
|
||||
db: prisma,
|
||||
entityType: "Auth",
|
||||
entityId: email.toLowerCase(),
|
||||
@@ -109,7 +109,7 @@ const config = {
|
||||
if (!user?.passwordHash) {
|
||||
await verify(DUMMY_ARGON2_HASH, password).catch(() => false);
|
||||
logger.warn({ email, reason: "user_not_found" }, "Failed login attempt");
|
||||
void createAuditEntry({
|
||||
await createAuditEntry({
|
||||
db: prisma,
|
||||
entityType: "Auth",
|
||||
entityId: email.toLowerCase(),
|
||||
@@ -127,7 +127,7 @@ const config = {
|
||||
{ email, userId: user.id, reason: "account_deactivated" },
|
||||
"Login blocked — account deactivated",
|
||||
);
|
||||
void createAuditEntry({
|
||||
await createAuditEntry({
|
||||
db: prisma,
|
||||
entityType: "Auth",
|
||||
entityId: user.id,
|
||||
@@ -143,7 +143,7 @@ const config = {
|
||||
const isValid = await verify(user.passwordHash, password);
|
||||
if (!isValid) {
|
||||
logger.warn({ email, reason: "invalid_password" }, "Failed login attempt");
|
||||
void createAuditEntry({
|
||||
await createAuditEntry({
|
||||
db: prisma,
|
||||
entityType: "Auth",
|
||||
entityId: user.id,
|
||||
@@ -176,7 +176,7 @@ const config = {
|
||||
const delta = totpInstance.validate({ token: totp, window: 1 });
|
||||
if (delta === null) {
|
||||
logger.warn({ email, reason: "invalid_totp" }, "Failed MFA verification");
|
||||
void createAuditEntry({
|
||||
await createAuditEntry({
|
||||
db: prisma,
|
||||
entityType: "Auth",
|
||||
entityId: user.id,
|
||||
@@ -196,7 +196,7 @@ const config = {
|
||||
const accepted = await consumeTotpWindow(prisma, user.id);
|
||||
if (!accepted) {
|
||||
logger.warn({ email, reason: "totp_replay" }, "TOTP replay attack blocked");
|
||||
void createAuditEntry({
|
||||
await createAuditEntry({
|
||||
db: prisma,
|
||||
entityType: "Auth",
|
||||
entityId: user.id,
|
||||
@@ -230,8 +230,10 @@ const config = {
|
||||
});
|
||||
|
||||
logger.info({ email, userId: user.id }, "Successful login");
|
||||
// Audit successful login
|
||||
void createAuditEntry({
|
||||
// Audit successful login. Awaited (not fire-and-forget) so the entry
|
||||
// is durable before we return a session — forensic completeness
|
||||
// matters even if it adds a few ms to the login path.
|
||||
await createAuditEntry({
|
||||
db: prisma,
|
||||
entityType: "Auth",
|
||||
entityId: user.id,
|
||||
@@ -338,7 +340,7 @@ const config = {
|
||||
});
|
||||
}
|
||||
|
||||
void createAuditEntry({
|
||||
await createAuditEntry({
|
||||
db: prisma,
|
||||
entityType: "Auth",
|
||||
entityId: userId ?? email,
|
||||
|
||||
Reference in New Issue
Block a user