From ada46a5f47e764ba523e8cb55f04d9dbae36bfcd Mon Sep 17 00:00:00 2001 From: David Nguyen Date: Wed, 31 Jan 2024 12:27:40 +1100 Subject: [PATCH] feat: add auth fail logs --- apps/web/src/pages/api/auth/[...nextauth].ts | 4 ++-- packages/lib/constants/auth.ts | 4 +++- packages/lib/next-auth/auth-options.ts | 24 +++++++++++++++++-- .../lib/universal/extract-request-metadata.ts | 17 ++++++++++++- .../migration.sql | 2 +- packages/prisma/schema.prisma | 2 ++ packages/trpc/server/profile-router/router.ts | 8 +++---- .../router.ts | 6 ++--- 8 files changed, 53 insertions(+), 14 deletions(-) rename packages/prisma/migrations/{20240130073345_add_user_security_audit_logs => 20240131004516_add_user_security_audit_logs}/migration.sql (86%) diff --git a/apps/web/src/pages/api/auth/[...nextauth].ts b/apps/web/src/pages/api/auth/[...nextauth].ts index 7666dd104..365b6ec40 100644 --- a/apps/web/src/pages/api/auth/[...nextauth].ts +++ b/apps/web/src/pages/api/auth/[...nextauth].ts @@ -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, diff --git a/packages/lib/constants/auth.ts b/packages/lib/constants/auth.ts index 54fc9f6a8..1918e2db0 100644 --- a/packages/lib/constants/auth.ts +++ b/packages/lib/constants/auth.ts @@ -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', }; diff --git a/packages/lib/next-auth/auth-options.ts b/packages/lib/next-auth/auth-options.ts index 9babae987..f23295a81 100644 --- a/packages/lib/next-auth/auth-options.ts +++ b/packages/lib/next-auth/auth-options.ts @@ -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 diff --git a/packages/lib/universal/extract-request-metadata.ts b/packages/lib/universal/extract-request-metadata.ts index ceb4ad35f..5549e5de7 100644 --- a/packages/lib/universal/extract-request-metadata.ts +++ b/packages/lib/universal/extract-request-metadata.ts @@ -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, +): 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, + }; +}; diff --git a/packages/prisma/migrations/20240130073345_add_user_security_audit_logs/migration.sql b/packages/prisma/migrations/20240131004516_add_user_security_audit_logs/migration.sql similarity index 86% rename from packages/prisma/migrations/20240130073345_add_user_security_audit_logs/migration.sql rename to packages/prisma/migrations/20240131004516_add_user_security_audit_logs/migration.sql index da643eee3..491012380 100644 --- a/packages/prisma/migrations/20240130073345_add_user_security_audit_logs/migration.sql +++ b/packages/prisma/migrations/20240131004516_add_user_security_audit_logs/migration.sql @@ -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" ( diff --git a/packages/prisma/schema.prisma b/packages/prisma/schema.prisma index 596013a85..353a855ae 100644 --- a/packages/prisma/schema.prisma +++ b/packages/prisma/schema.prisma @@ -57,6 +57,8 @@ enum UserSecurityAuditLogType { PASSWORD_UPDATE SIGN_OUT SIGN_IN + SIGN_IN_FAIL + SIGN_IN_2FA_FAIL } model UserSecurityAuditLog { diff --git a/packages/trpc/server/profile-router/router.ts b/packages/trpc/server/profile-router/router.ts index c595c628c..4a0d47345 100644 --- a/packages/trpc/server/profile-router/router.ts +++ b/packages/trpc/server/profile-router/router.ts @@ -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.'; diff --git a/packages/trpc/server/two-factor-authentication-router/router.ts b/packages/trpc/server/two-factor-authentication-router/router.ts index b499de703..36fe93a60 100644 --- a/packages/trpc/server/two-factor-authentication-router/router.ts +++ b/packages/trpc/server/two-factor-authentication-router/router.ts @@ -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);