feat: add auth fail logs

This commit is contained in:
David Nguyen
2024-01-31 12:27:40 +11:00
parent 1bda74b3aa
commit ada46a5f47
8 changed files with 53 additions and 14 deletions

View File

@ -3,12 +3,12 @@ import type { NextApiRequest, NextApiResponse } from 'next';
import NextAuth from 'next-auth';
import { NEXT_AUTH_OPTIONS } from '@documenso/lib/next-auth/auth-options';
import { extractRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
import { extractNextApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
import { prisma } from '@documenso/prisma';
import { UserSecurityAuditLogType } from '@documenso/prisma/client';
export default async function auth(req: NextApiRequest, res: NextApiResponse) {
const { ipAddress, userAgent } = extractRequestMetadata(req);
const { ipAddress, userAgent } = extractNextApiRequestMetadata(req);
return await NextAuth(req, res, {
...NEXT_AUTH_OPTIONS,

View File

@ -18,6 +18,8 @@ export const USER_SECURITY_AUDIT_LOG_MAP: { [key in UserSecurityAuditLogType]: s
[UserSecurityAuditLogType.AUTH_2FA_ENABLE]: '2FA Enabled',
[UserSecurityAuditLogType.PASSWORD_RESET]: 'Password reset',
[UserSecurityAuditLogType.PASSWORD_UPDATE]: 'Password updated',
[UserSecurityAuditLogType.SIGN_IN]: 'Signed In',
[UserSecurityAuditLogType.SIGN_OUT]: 'Signed Out',
[UserSecurityAuditLogType.SIGN_IN]: 'Signed In',
[UserSecurityAuditLogType.SIGN_IN_FAIL]: 'Sign in attempt failed',
[UserSecurityAuditLogType.SIGN_IN_2FA_FAIL]: 'Sign in 2FA attempt failed',
};

View File

@ -9,11 +9,12 @@ import type { GoogleProfile } from 'next-auth/providers/google';
import GoogleProvider from 'next-auth/providers/google';
import { prisma } from '@documenso/prisma';
import { IdentityProvider } from '@documenso/prisma/client';
import { IdentityProvider, UserSecurityAuditLogType } from '@documenso/prisma/client';
import { isTwoFactorAuthenticationEnabled } from '../server-only/2fa/is-2fa-availble';
import { validateTwoFactorAuthentication } from '../server-only/2fa/validate-2fa';
import { getUserByEmail } from '../server-only/user/get-user-by-email';
import { extractNextAuthRequestMetadata } from '../universal/extract-request-metadata';
import { ErrorCode } from './error-codes';
export const NEXT_AUTH_OPTIONS: AuthOptions = {
@ -35,7 +36,7 @@ export const NEXT_AUTH_OPTIONS: AuthOptions = {
},
backupCode: { label: 'Backup Code', type: 'input', placeholder: 'Two-factor backup code' },
},
authorize: async (credentials, _req) => {
authorize: async (credentials, req) => {
if (!credentials) {
throw new Error(ErrorCode.CREDENTIALS_NOT_FOUND);
}
@ -51,8 +52,18 @@ export const NEXT_AUTH_OPTIONS: AuthOptions = {
}
const isPasswordsSame = await compare(password, user.password);
const requestMetadata = extractNextAuthRequestMetadata(req);
if (!isPasswordsSame) {
await prisma.userSecurityAuditLog.create({
data: {
userId: user.id,
ipAddress: requestMetadata.ipAddress,
userAgent: requestMetadata.userAgent,
type: UserSecurityAuditLogType.SIGN_IN_FAIL,
},
});
throw new Error(ErrorCode.INCORRECT_EMAIL_PASSWORD);
}
@ -62,6 +73,15 @@ export const NEXT_AUTH_OPTIONS: AuthOptions = {
const isValid = await validateTwoFactorAuthentication({ backupCode, totpCode, user });
if (!isValid) {
await prisma.userSecurityAuditLog.create({
data: {
userId: user.id,
ipAddress: requestMetadata.ipAddress,
userAgent: requestMetadata.userAgent,
type: UserSecurityAuditLogType.SIGN_IN_2FA_FAIL,
},
});
throw new Error(
totpCode
? ErrorCode.INCORRECT_TWO_FACTOR_CODE

View File

@ -1,5 +1,6 @@
import type { NextApiRequest } from 'next';
import type { RequestInternal } from 'next-auth';
import { z } from 'zod';
const ZIpSchema = z.string().ip();
@ -9,7 +10,7 @@ export type RequestMetadata = {
userAgent?: string;
};
export const extractRequestMetadata = (req: NextApiRequest): RequestMetadata => {
export const extractNextApiRequestMetadata = (req: NextApiRequest): RequestMetadata => {
const parsedIp = ZIpSchema.safeParse(req.headers['x-forwarded-for'] || req.socket.remoteAddress);
const ipAddress = parsedIp.success ? parsedIp.data : undefined;
@ -20,3 +21,17 @@ export const extractRequestMetadata = (req: NextApiRequest): RequestMetadata =>
userAgent,
};
};
export const extractNextAuthRequestMetadata = (
req: Pick<RequestInternal, 'body' | 'query' | 'headers' | 'method'>,
): RequestMetadata => {
const parsedIp = ZIpSchema.safeParse(req.headers?.['x-forwarded-for']);
const ipAddress = parsedIp.success ? parsedIp.data : undefined;
const userAgent = req.headers?.['user-agent'];
return {
ipAddress,
userAgent,
};
};

View File

@ -1,5 +1,5 @@
-- CreateEnum
CREATE TYPE "UserSecurityAuditLogType" AS ENUM ('ACCOUNT_PROFILE_UPDATE', 'ACCOUNT_SSO_LINK', 'AUTH_2FA_DISABLE', 'AUTH_2FA_ENABLE', 'PASSWORD_RESET', 'PASSWORD_UPDATE', 'SIGN_OUT', 'SIGN_IN');
CREATE TYPE "UserSecurityAuditLogType" AS ENUM ('ACCOUNT_PROFILE_UPDATE', 'ACCOUNT_SSO_LINK', 'AUTH_2FA_DISABLE', 'AUTH_2FA_ENABLE', 'PASSWORD_RESET', 'PASSWORD_UPDATE', 'SIGN_OUT', 'SIGN_IN', 'SIGN_IN_FAIL', 'SIGN_IN_2FA_FAIL');
-- CreateTable
CREATE TABLE "UserSecurityAuditLog" (

View File

@ -57,6 +57,8 @@ enum UserSecurityAuditLogType {
PASSWORD_UPDATE
SIGN_OUT
SIGN_IN
SIGN_IN_FAIL
SIGN_IN_2FA_FAIL
}
model UserSecurityAuditLog {

View File

@ -7,7 +7,7 @@ import { resetPassword } from '@documenso/lib/server-only/user/reset-password';
import { sendConfirmationToken } from '@documenso/lib/server-only/user/send-confirmation-token';
import { updatePassword } from '@documenso/lib/server-only/user/update-password';
import { updateProfile } from '@documenso/lib/server-only/user/update-profile';
import { extractRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
import { extractNextApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
import { adminProcedure, authenticatedProcedure, procedure, router } from '../trpc';
import {
@ -60,7 +60,7 @@ export const profileRouter = router({
userId: ctx.user.id,
name,
signature,
requestMetadata: extractRequestMetadata(ctx.req),
requestMetadata: extractNextApiRequestMetadata(ctx.req),
});
} catch (err) {
console.error(err);
@ -83,7 +83,7 @@ export const profileRouter = router({
userId: ctx.user.id,
password,
currentPassword,
requestMetadata: extractRequestMetadata(ctx.req),
requestMetadata: extractNextApiRequestMetadata(ctx.req),
});
} catch (err) {
let message =
@ -119,7 +119,7 @@ export const profileRouter = router({
return await resetPassword({
token,
password,
requestMetadata: extractRequestMetadata(ctx.req),
requestMetadata: extractNextApiRequestMetadata(ctx.req),
});
} catch (err) {
let message = 'We were unable to reset your password. Please try again.';

View File

@ -6,7 +6,7 @@ import { enableTwoFactorAuthentication } from '@documenso/lib/server-only/2fa/en
import { getBackupCodes } from '@documenso/lib/server-only/2fa/get-backup-code';
import { setupTwoFactorAuthentication } from '@documenso/lib/server-only/2fa/setup-2fa';
import { compareSync } from '@documenso/lib/server-only/auth/hash';
import { extractRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
import { extractNextApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
import { authenticatedProcedure, router } from '../trpc';
import {
@ -41,7 +41,7 @@ export const twoFactorAuthenticationRouter = router({
return await enableTwoFactorAuthentication({
user,
code,
requestMetadata: extractRequestMetadata(ctx.req),
requestMetadata: extractNextApiRequestMetadata(ctx.req),
});
} catch (err) {
console.error(err);
@ -65,7 +65,7 @@ export const twoFactorAuthenticationRouter = router({
user,
password,
backupCode,
requestMetadata: extractRequestMetadata(ctx.req),
requestMetadata: extractNextApiRequestMetadata(ctx.req),
});
} catch (err) {
console.error(err);