mirror of
https://github.com/documenso/documenso.git
synced 2025-11-12 15:53:02 +10:00
feat: add auth fail logs
This commit is contained in:
@ -3,12 +3,12 @@ import type { NextApiRequest, NextApiResponse } from 'next';
|
|||||||
import NextAuth from 'next-auth';
|
import NextAuth from 'next-auth';
|
||||||
|
|
||||||
import { NEXT_AUTH_OPTIONS } from '@documenso/lib/next-auth/auth-options';
|
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 { prisma } from '@documenso/prisma';
|
||||||
import { UserSecurityAuditLogType } from '@documenso/prisma/client';
|
import { UserSecurityAuditLogType } from '@documenso/prisma/client';
|
||||||
|
|
||||||
export default async function auth(req: NextApiRequest, res: NextApiResponse) {
|
export default async function auth(req: NextApiRequest, res: NextApiResponse) {
|
||||||
const { ipAddress, userAgent } = extractRequestMetadata(req);
|
const { ipAddress, userAgent } = extractNextApiRequestMetadata(req);
|
||||||
|
|
||||||
return await NextAuth(req, res, {
|
return await NextAuth(req, res, {
|
||||||
...NEXT_AUTH_OPTIONS,
|
...NEXT_AUTH_OPTIONS,
|
||||||
|
|||||||
@ -18,6 +18,8 @@ export const USER_SECURITY_AUDIT_LOG_MAP: { [key in UserSecurityAuditLogType]: s
|
|||||||
[UserSecurityAuditLogType.AUTH_2FA_ENABLE]: '2FA Enabled',
|
[UserSecurityAuditLogType.AUTH_2FA_ENABLE]: '2FA Enabled',
|
||||||
[UserSecurityAuditLogType.PASSWORD_RESET]: 'Password reset',
|
[UserSecurityAuditLogType.PASSWORD_RESET]: 'Password reset',
|
||||||
[UserSecurityAuditLogType.PASSWORD_UPDATE]: 'Password updated',
|
[UserSecurityAuditLogType.PASSWORD_UPDATE]: 'Password updated',
|
||||||
[UserSecurityAuditLogType.SIGN_IN]: 'Signed In',
|
|
||||||
[UserSecurityAuditLogType.SIGN_OUT]: 'Signed Out',
|
[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',
|
||||||
};
|
};
|
||||||
|
|||||||
@ -9,11 +9,12 @@ import type { GoogleProfile } from 'next-auth/providers/google';
|
|||||||
import GoogleProvider from 'next-auth/providers/google';
|
import GoogleProvider from 'next-auth/providers/google';
|
||||||
|
|
||||||
import { prisma } from '@documenso/prisma';
|
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 { isTwoFactorAuthenticationEnabled } from '../server-only/2fa/is-2fa-availble';
|
||||||
import { validateTwoFactorAuthentication } from '../server-only/2fa/validate-2fa';
|
import { validateTwoFactorAuthentication } from '../server-only/2fa/validate-2fa';
|
||||||
import { getUserByEmail } from '../server-only/user/get-user-by-email';
|
import { getUserByEmail } from '../server-only/user/get-user-by-email';
|
||||||
|
import { extractNextAuthRequestMetadata } from '../universal/extract-request-metadata';
|
||||||
import { ErrorCode } from './error-codes';
|
import { ErrorCode } from './error-codes';
|
||||||
|
|
||||||
export const NEXT_AUTH_OPTIONS: AuthOptions = {
|
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' },
|
backupCode: { label: 'Backup Code', type: 'input', placeholder: 'Two-factor backup code' },
|
||||||
},
|
},
|
||||||
authorize: async (credentials, _req) => {
|
authorize: async (credentials, req) => {
|
||||||
if (!credentials) {
|
if (!credentials) {
|
||||||
throw new Error(ErrorCode.CREDENTIALS_NOT_FOUND);
|
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 isPasswordsSame = await compare(password, user.password);
|
||||||
|
const requestMetadata = extractNextAuthRequestMetadata(req);
|
||||||
|
|
||||||
if (!isPasswordsSame) {
|
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);
|
throw new Error(ErrorCode.INCORRECT_EMAIL_PASSWORD);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -62,6 +73,15 @@ export const NEXT_AUTH_OPTIONS: AuthOptions = {
|
|||||||
const isValid = await validateTwoFactorAuthentication({ backupCode, totpCode, user });
|
const isValid = await validateTwoFactorAuthentication({ backupCode, totpCode, user });
|
||||||
|
|
||||||
if (!isValid) {
|
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(
|
throw new Error(
|
||||||
totpCode
|
totpCode
|
||||||
? ErrorCode.INCORRECT_TWO_FACTOR_CODE
|
? ErrorCode.INCORRECT_TWO_FACTOR_CODE
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import type { NextApiRequest } from 'next';
|
import type { NextApiRequest } from 'next';
|
||||||
|
|
||||||
|
import type { RequestInternal } from 'next-auth';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
const ZIpSchema = z.string().ip();
|
const ZIpSchema = z.string().ip();
|
||||||
@ -9,7 +10,7 @@ export type RequestMetadata = {
|
|||||||
userAgent?: string;
|
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 parsedIp = ZIpSchema.safeParse(req.headers['x-forwarded-for'] || req.socket.remoteAddress);
|
||||||
|
|
||||||
const ipAddress = parsedIp.success ? parsedIp.data : undefined;
|
const ipAddress = parsedIp.success ? parsedIp.data : undefined;
|
||||||
@ -20,3 +21,17 @@ export const extractRequestMetadata = (req: NextApiRequest): RequestMetadata =>
|
|||||||
userAgent,
|
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,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
-- CreateEnum
|
-- 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
|
-- CreateTable
|
||||||
CREATE TABLE "UserSecurityAuditLog" (
|
CREATE TABLE "UserSecurityAuditLog" (
|
||||||
@ -57,6 +57,8 @@ enum UserSecurityAuditLogType {
|
|||||||
PASSWORD_UPDATE
|
PASSWORD_UPDATE
|
||||||
SIGN_OUT
|
SIGN_OUT
|
||||||
SIGN_IN
|
SIGN_IN
|
||||||
|
SIGN_IN_FAIL
|
||||||
|
SIGN_IN_2FA_FAIL
|
||||||
}
|
}
|
||||||
|
|
||||||
model UserSecurityAuditLog {
|
model UserSecurityAuditLog {
|
||||||
|
|||||||
@ -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 { sendConfirmationToken } from '@documenso/lib/server-only/user/send-confirmation-token';
|
||||||
import { updatePassword } from '@documenso/lib/server-only/user/update-password';
|
import { updatePassword } from '@documenso/lib/server-only/user/update-password';
|
||||||
import { updateProfile } from '@documenso/lib/server-only/user/update-profile';
|
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 { adminProcedure, authenticatedProcedure, procedure, router } from '../trpc';
|
||||||
import {
|
import {
|
||||||
@ -60,7 +60,7 @@ export const profileRouter = router({
|
|||||||
userId: ctx.user.id,
|
userId: ctx.user.id,
|
||||||
name,
|
name,
|
||||||
signature,
|
signature,
|
||||||
requestMetadata: extractRequestMetadata(ctx.req),
|
requestMetadata: extractNextApiRequestMetadata(ctx.req),
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
@ -83,7 +83,7 @@ export const profileRouter = router({
|
|||||||
userId: ctx.user.id,
|
userId: ctx.user.id,
|
||||||
password,
|
password,
|
||||||
currentPassword,
|
currentPassword,
|
||||||
requestMetadata: extractRequestMetadata(ctx.req),
|
requestMetadata: extractNextApiRequestMetadata(ctx.req),
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
let message =
|
let message =
|
||||||
@ -119,7 +119,7 @@ export const profileRouter = router({
|
|||||||
return await resetPassword({
|
return await resetPassword({
|
||||||
token,
|
token,
|
||||||
password,
|
password,
|
||||||
requestMetadata: extractRequestMetadata(ctx.req),
|
requestMetadata: extractNextApiRequestMetadata(ctx.req),
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
let message = 'We were unable to reset your password. Please try again.';
|
let message = 'We were unable to reset your password. Please try again.';
|
||||||
|
|||||||
@ -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 { getBackupCodes } from '@documenso/lib/server-only/2fa/get-backup-code';
|
||||||
import { setupTwoFactorAuthentication } from '@documenso/lib/server-only/2fa/setup-2fa';
|
import { setupTwoFactorAuthentication } from '@documenso/lib/server-only/2fa/setup-2fa';
|
||||||
import { compareSync } from '@documenso/lib/server-only/auth/hash';
|
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 { authenticatedProcedure, router } from '../trpc';
|
||||||
import {
|
import {
|
||||||
@ -41,7 +41,7 @@ export const twoFactorAuthenticationRouter = router({
|
|||||||
return await enableTwoFactorAuthentication({
|
return await enableTwoFactorAuthentication({
|
||||||
user,
|
user,
|
||||||
code,
|
code,
|
||||||
requestMetadata: extractRequestMetadata(ctx.req),
|
requestMetadata: extractNextApiRequestMetadata(ctx.req),
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
@ -65,7 +65,7 @@ export const twoFactorAuthenticationRouter = router({
|
|||||||
user,
|
user,
|
||||||
password,
|
password,
|
||||||
backupCode,
|
backupCode,
|
||||||
requestMetadata: extractRequestMetadata(ctx.req),
|
requestMetadata: extractNextApiRequestMetadata(ctx.req),
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
|
|||||||
Reference in New Issue
Block a user