diff --git a/server/lib/config.ts b/server/lib/config.ts index e44cff5c..9476b505 100644 --- a/server/lib/config.ts +++ b/server/lib/config.ts @@ -40,7 +40,8 @@ const configSchema = z.object({ .pipe(hostnameSchema) .transform((url) => url.toLowerCase()), log_level: z.enum(["debug", "info", "warn", "error"]), - save_logs: z.boolean() + save_logs: z.boolean(), + log_failed_attempts: z.boolean().optional(), }), server: z.object({ external_port: portSchema diff --git a/server/middlewares/verifyUser.ts b/server/middlewares/verifyUser.ts index 0f52f2e3..72680352 100644 --- a/server/middlewares/verifyUser.ts +++ b/server/middlewares/verifyUser.ts @@ -8,6 +8,7 @@ import HttpCode from "@server/types/HttpCode"; import config from "@server/lib/config"; import { verifySession } from "@server/auth/sessions/verifySession"; import { unauthorized } from "@server/auth/unauthorizedResponse"; +import logger from "@server/logger"; export const verifySessionUserMiddleware = async ( req: any, @@ -16,6 +17,9 @@ export const verifySessionUserMiddleware = async ( ) => { const { session, user } = await verifySession(req); if (!session || !user) { + if (config.getRawConfig().app.log_failed_attempts) { + logger.info(`User session not found. IP: ${req.ip}.`); + } return next(unauthorized()); } @@ -25,6 +29,9 @@ export const verifySessionUserMiddleware = async ( .where(eq(users.userId, user.userId)); if (!existingUser || !existingUser[0]) { + if (config.getRawConfig().app.log_failed_attempts) { + logger.info(`User session not found. IP: ${req.ip}.`); + } return next( createHttpError(HttpCode.BAD_REQUEST, "User does not exist") ); diff --git a/server/routers/auth/disable2fa.ts b/server/routers/auth/disable2fa.ts index e27d75df..b93e9cc3 100644 --- a/server/routers/auth/disable2fa.ts +++ b/server/routers/auth/disable2fa.ts @@ -79,6 +79,11 @@ export async function disable2fa( ); if (!validOTP) { + if (config.getRawConfig().app.log_failed_attempts) { + logger.info( + `Two-factor authentication code is incorrect. Email: ${user.email}. IP: ${req.ip}.` + ); + } return next( createHttpError( HttpCode.BAD_REQUEST, diff --git a/server/routers/auth/login.ts b/server/routers/auth/login.ts index 7ee0d927..09bd9661 100644 --- a/server/routers/auth/login.ts +++ b/server/routers/auth/login.ts @@ -71,6 +71,11 @@ export async function login( .from(users) .where(eq(users.email, email)); if (!existingUserRes || !existingUserRes.length) { + if (config.getRawConfig().app.log_failed_attempts) { + logger.info( + `Username or password incorrect. Email: ${email}. IP: ${req.ip}.` + ); + } return next( createHttpError( HttpCode.BAD_REQUEST, @@ -86,6 +91,11 @@ export async function login( existingUser.passwordHash ); if (!validPassword) { + if (config.getRawConfig().app.log_failed_attempts) { + logger.info( + `Username or password incorrect. Email: ${email}. IP: ${req.ip}.` + ); + } return next( createHttpError( HttpCode.BAD_REQUEST, @@ -112,6 +122,11 @@ export async function login( ); if (!validOTP) { + if (config.getRawConfig().app.log_failed_attempts) { + logger.info( + `Two-factor code incorrect. Email: ${email}. IP: ${req.ip}.` + ); + } return next( createHttpError( HttpCode.BAD_REQUEST, diff --git a/server/routers/auth/logout.ts b/server/routers/auth/logout.ts index e9e51a31..a2e0a7f1 100644 --- a/server/routers/auth/logout.ts +++ b/server/routers/auth/logout.ts @@ -8,6 +8,7 @@ import { invalidateSession } from "@server/auth/sessions/app"; import { verifySession } from "@server/auth/sessions/verifySession"; +import config from "@server/lib/config"; export async function logout( req: Request, @@ -16,6 +17,11 @@ export async function logout( ): Promise { const { user, session } = await verifySession(req); if (!user || !session) { + if (config.getRawConfig().app.log_failed_attempts) { + logger.info( + `Log out failed because missing or invalid session. IP: ${req.ip}.` + ); + } return next( createHttpError( HttpCode.BAD_REQUEST, diff --git a/server/routers/auth/resetPassword.ts b/server/routers/auth/resetPassword.ts index 45c8652b..d112e98b 100644 --- a/server/routers/auth/resetPassword.ts +++ b/server/routers/auth/resetPassword.ts @@ -60,6 +60,11 @@ export async function resetPassword( .where(eq(passwordResetTokens.email, email)); if (!resetRequest || !resetRequest.length) { + if (config.getRawConfig().app.log_failed_attempts) { + logger.info( + `Password reset code is incorrect. Email: ${email}. IP: ${req.ip}.` + ); + } return next( createHttpError( HttpCode.BAD_REQUEST, @@ -109,6 +114,11 @@ export async function resetPassword( ); if (!validOTP) { + if (config.getRawConfig().app.log_failed_attempts) { + logger.info( + `Two-factor authentication code is incorrect. Email: ${email}. IP: ${req.ip}.` + ); + } return next( createHttpError( HttpCode.BAD_REQUEST, @@ -124,6 +134,11 @@ export async function resetPassword( ); if (!isTokenValid) { + if (config.getRawConfig().app.log_failed_attempts) { + logger.info( + `Password reset code is incorrect. Email: ${email}. IP: ${req.ip}.` + ); + } return next( createHttpError( HttpCode.BAD_REQUEST, diff --git a/server/routers/auth/signup.ts b/server/routers/auth/signup.ts index 2a4bb127..4bb5394e 100644 --- a/server/routers/auth/signup.ts +++ b/server/routers/auth/signup.ts @@ -23,7 +23,10 @@ import { checkValidInvite } from "@server/auth/checkValidInvite"; import { passwordSchema } from "@server/auth/passwordSchema"; export const signupBodySchema = z.object({ - email: z.string().email().transform((v) => v.toLowerCase()), + email: z + .string() + .email() + .transform((v) => v.toLowerCase()), password: passwordSchema, inviteToken: z.string().optional(), inviteId: z.string().optional() @@ -60,6 +63,11 @@ export async function signup( if (config.getRawConfig().flags?.disable_signup_without_invite) { if (!inviteToken || !inviteId) { + if (config.getRawConfig().app.log_failed_attempts) { + logger.info( + `Signup blocked without invite. Email: ${email}. IP: ${req.ip}.` + ); + } return next( createHttpError( HttpCode.BAD_REQUEST, @@ -84,6 +92,11 @@ export async function signup( } if (existingInvite.email !== email) { + if (config.getRawConfig().app.log_failed_attempts) { + logger.info( + `User attempted to use an invite for another user. Email: ${email}. IP: ${req.ip}.` + ); + } return next( createHttpError( HttpCode.BAD_REQUEST, @@ -185,6 +198,11 @@ export async function signup( }); } catch (e) { if (e instanceof SqliteError && e.code === "SQLITE_CONSTRAINT_UNIQUE") { + if (config.getRawConfig().app.log_failed_attempts) { + logger.info( + `Account already exists with that email. Email: ${email}. IP: ${req.ip}.` + ); + } return next( createHttpError( HttpCode.BAD_REQUEST, diff --git a/server/routers/auth/verifyEmail.ts b/server/routers/auth/verifyEmail.ts index 45ef4230..e189e9a6 100644 --- a/server/routers/auth/verifyEmail.ts +++ b/server/routers/auth/verifyEmail.ts @@ -75,6 +75,11 @@ export async function verifyEmail( .where(eq(users.userId, user.userId)); }); } else { + if (config.getRawConfig().app.log_failed_attempts) { + logger.info( + `Email verification code incorrect. Email: ${user.email}. IP: ${req.ip}.` + ); + } return next( createHttpError( HttpCode.BAD_REQUEST, diff --git a/server/routers/auth/verifyTotp.ts b/server/routers/auth/verifyTotp.ts index f7b8eb38..36bbf348 100644 --- a/server/routers/auth/verifyTotp.ts +++ b/server/routers/auth/verifyTotp.ts @@ -96,6 +96,11 @@ export async function verifyTotp( } if (!valid) { + if (config.getRawConfig().app.log_failed_attempts) { + logger.info( + `Two-factor authentication code is incorrect. Email: ${user.email}. IP: ${req.ip}.` + ); + } return next( createHttpError( HttpCode.BAD_REQUEST, diff --git a/server/routers/badger/exchangeSession.ts b/server/routers/badger/exchangeSession.ts index eaf47e6a..1af960aa 100644 --- a/server/routers/badger/exchangeSession.ts +++ b/server/routers/badger/exchangeSession.ts @@ -20,7 +20,8 @@ import { response } from "@server/lib"; const exchangeSessionBodySchema = z.object({ requestToken: z.string(), - host: z.string() + host: z.string(), + requestIp: z.string().optional() }); export type ExchangeSessionBodySchema = z.infer< @@ -51,7 +52,9 @@ export async function exchangeSession( } try { - const { requestToken, host } = parsedBody.data; + const { requestToken, host, requestIp } = parsedBody.data; + + const clientIp = requestIp?.split(":")[0]; const [resource] = await db .select() @@ -75,12 +78,22 @@ export async function exchangeSession( ); if (!requestSession) { + if (config.getRawConfig().app.log_failed_attempts) { + logger.info( + `Exchange token is invalid. Resource ID: ${resource.resourceId}. IP: ${clientIp}.` + ); + } return next( createHttpError(HttpCode.UNAUTHORIZED, "Invalid request token") ); } if (!requestSession.isRequestToken) { + if (config.getRawConfig().app.log_failed_attempts) { + logger.info( + `Exchange token is invalid. Resource ID: ${resource.resourceId}. IP: ${clientIp}.` + ); + } return next( createHttpError(HttpCode.UNAUTHORIZED, "Invalid request token") ); diff --git a/server/routers/badger/verifySession.ts b/server/routers/badger/verifySession.ts index ae6d2edc..a71e6b3c 100644 --- a/server/routers/badger/verifySession.ts +++ b/server/routers/badger/verifySession.ts @@ -42,7 +42,8 @@ const verifyResourceSessionSchema = z.object({ path: z.string(), method: z.string(), accessToken: z.string().optional(), - tls: z.boolean() + tls: z.boolean(), + requestIp: z.string().optional() }); export type VerifyResourceSessionSchema = z.infer< @@ -77,9 +78,12 @@ export async function verifyResourceSession( sessions, host, originalRequestURL, + requestIp, accessToken: token } = parsedBody.data; + const clientIp = requestIp?.split(":")[0]; + const resourceCacheKey = `resource:${host}`; let resourceData: | { @@ -160,6 +164,14 @@ export async function verifyResourceSession( logger.debug("Access token invalid: " + error); } + if (!valid) { + if (config.getRawConfig().app.log_failed_attempts) { + logger.info( + `Resource access token is invalid. Resource ID: ${resource.resourceId}. IP: ${clientIp}.` + ); + } + } + if (valid && tokenItem) { validAccessToken = tokenItem; @@ -174,6 +186,11 @@ export async function verifyResourceSession( } if (!sessions) { + if (config.getRawConfig().app.log_failed_attempts) { + logger.info( + `Missing resource sessions. Resource ID: ${resource.resourceId}. IP: ${clientIp}.` + ); + } return notAllowed(res); } @@ -200,6 +217,11 @@ export async function verifyResourceSession( logger.debug( "Resource not allowed because session is a temporary request token" ); + if (config.getRawConfig().app.log_failed_attempts) { + logger.info( + `Resource session is an exchange token. Resource ID: ${resource.resourceId}. IP: ${clientIp}.` + ); + } return notAllowed(res); } @@ -271,6 +293,12 @@ export async function verifyResourceSession( } logger.debug("No more auth to check, resource not allowed"); + + if (config.getRawConfig().app.log_failed_attempts) { + logger.info( + `Resource access not allowed. Resource ID: ${resource.resourceId}. IP: ${clientIp}.` + ); + } return notAllowed(res, redirectUrl); } catch (e) { console.error(e); diff --git a/server/routers/newt/getToken.ts b/server/routers/newt/getToken.ts index f13cbac0..e6ae0cd6 100644 --- a/server/routers/newt/getToken.ts +++ b/server/routers/newt/getToken.ts @@ -1,6 +1,4 @@ -import { - generateSessionToken, -} from "@server/auth/sessions/app"; +import { generateSessionToken } from "@server/auth/sessions/app"; import db from "@server/db"; import { newts } from "@server/db/schema"; import HttpCode from "@server/types/HttpCode"; @@ -10,8 +8,13 @@ import { NextFunction, Request, Response } from "express"; import createHttpError from "http-errors"; import { z } from "zod"; import { fromError } from "zod-validation-error"; -import { createNewtSession, validateNewtSessionToken } from "@server/auth/sessions/newt"; +import { + createNewtSession, + validateNewtSessionToken +} from "@server/auth/sessions/newt"; import { verifyPassword } from "@server/auth/password"; +import logger from "@server/logger"; +import config from "@server/lib/config"; export const newtGetTokenBodySchema = z.object({ newtId: z.string(), @@ -43,6 +46,11 @@ export async function getToken( if (token) { const { session, newt } = await validateNewtSessionToken(token); if (session) { + if (config.getRawConfig().app.log_failed_attempts) { + logger.info( + `Newt session already valid. Newt ID: ${newtId}. IP: ${req.ip}.` + ); + } return response(res, { data: null, success: true, @@ -73,6 +81,11 @@ export async function getToken( existingNewt.secretHash ); if (!validSecret) { + if (config.getRawConfig().app.log_failed_attempts) { + logger.info( + `Newt id or secret is incorrect. Newt: ID ${newtId}. IP: ${req.ip}.` + ); + } return next( createHttpError(HttpCode.BAD_REQUEST, "Secret is incorrect") ); diff --git a/server/routers/resource/authWithAccessToken.ts b/server/routers/resource/authWithAccessToken.ts index add5f275..03dac735 100644 --- a/server/routers/resource/authWithAccessToken.ts +++ b/server/routers/resource/authWithAccessToken.ts @@ -8,11 +8,10 @@ import { NextFunction, Request, Response } from "express"; import createHttpError from "http-errors"; import { z } from "zod"; import { fromError } from "zod-validation-error"; -import { - createResourceSession, -} from "@server/auth/sessions/resource"; +import { createResourceSession } from "@server/auth/sessions/resource"; import logger from "@server/logger"; import { verifyResourceAccessToken } from "@server/auth/verifyResourceAccessToken"; +import config from "@server/lib/config"; const authWithAccessTokenBodySchema = z .object({ @@ -84,6 +83,11 @@ export async function authWithAccessToken( }); if (!valid) { + if (config.getRawConfig().app.log_failed_attempts) { + logger.info( + `Resource access token invalid. Resource ID: ${resource.resourceId}. IP: ${req.ip}.` + ); + } return next( createHttpError( HttpCode.UNAUTHORIZED, diff --git a/server/routers/resource/authWithPassword.ts b/server/routers/resource/authWithPassword.ts index 1aa5d632..a69d42b6 100644 --- a/server/routers/resource/authWithPassword.ts +++ b/server/routers/resource/authWithPassword.ts @@ -9,11 +9,10 @@ import { NextFunction, Request, Response } from "express"; import createHttpError from "http-errors"; import { z } from "zod"; import { fromError } from "zod-validation-error"; -import { - createResourceSession, -} from "@server/auth/sessions/resource"; +import { createResourceSession } from "@server/auth/sessions/resource"; import logger from "@server/logger"; import { verifyPassword } from "@server/auth/password"; +import config from "@server/lib/config"; export const authWithPasswordBodySchema = z .object({ @@ -82,7 +81,7 @@ export async function authWithPassword( if (!org) { return next( - createHttpError(HttpCode.BAD_REQUEST, "Resource does not exist") + createHttpError(HttpCode.BAD_REQUEST, "Org does not exist") ); } @@ -109,6 +108,11 @@ export async function authWithPassword( definedPassword.passwordHash ); if (!validPassword) { + if (config.getRawConfig().app.log_failed_attempts) { + logger.info( + `Resource password incorrect. Resource ID: ${resource.resourceId}. IP: ${req.ip}.` + ); + } return next( createHttpError(HttpCode.UNAUTHORIZED, "Incorrect password") ); diff --git a/server/routers/resource/authWithPincode.ts b/server/routers/resource/authWithPincode.ts index 6d83ba22..c648e36c 100644 --- a/server/routers/resource/authWithPincode.ts +++ b/server/routers/resource/authWithPincode.ts @@ -1,10 +1,6 @@ import { generateSessionToken } from "@server/auth/sessions/app"; import db from "@server/db"; -import { - orgs, - resourcePincode, - resources, -} from "@server/db/schema"; +import { orgs, resourcePincode, resources } from "@server/db/schema"; import HttpCode from "@server/types/HttpCode"; import response from "@server/lib/response"; import { eq } from "drizzle-orm"; @@ -12,11 +8,10 @@ import { NextFunction, Request, Response } from "express"; import createHttpError from "http-errors"; import { z } from "zod"; import { fromError } from "zod-validation-error"; -import { - createResourceSession, -} from "@server/auth/sessions/resource"; +import { createResourceSession } from "@server/auth/sessions/resource"; import logger from "@server/logger"; import { verifyPassword } from "@server/auth/password"; +import config from "@server/lib/config"; export const authWithPincodeBodySchema = z .object({ @@ -112,6 +107,11 @@ export async function authWithPincode( definedPincode.pincodeHash ); if (!validPincode) { + if (config.getRawConfig().app.log_failed_attempts) { + logger.info( + `Resource pin code incorrect. Resource ID: ${resource.resourceId}. IP: ${req.ip}.` + ); + } return next( createHttpError(HttpCode.UNAUTHORIZED, "Incorrect PIN") ); diff --git a/server/routers/resource/authWithWhitelist.ts b/server/routers/resource/authWithWhitelist.ts index f97b3035..dae36b24 100644 --- a/server/routers/resource/authWithWhitelist.ts +++ b/server/routers/resource/authWithWhitelist.ts @@ -16,6 +16,7 @@ import { fromError } from "zod-validation-error"; import { createResourceSession } from "@server/auth/sessions/resource"; import { isValidOtp, sendResourceOtpEmail } from "@server/auth/resourceOtp"; import logger from "@server/logger"; +import config from "@server/lib/config"; const authWithWhitelistBodySchema = z .object({ @@ -96,7 +97,7 @@ export async function authWithWhitelist( // if email is not found, check for wildcard email const wildcard = "*@" + email.split("@")[1]; - logger.debug("Checking for wildcard email: " + wildcard) + logger.debug("Checking for wildcard email: " + wildcard); const [result] = await db .select() @@ -120,6 +121,11 @@ export async function authWithWhitelist( // if wildcard is still not found, return unauthorized if (!whitelistedEmail) { + if (config.getRawConfig().app.log_failed_attempts) { + logger.info( + `Email is not whitelisted. Resource ID: ${resource?.resourceId}. Email: ${email}. IP: ${req.ip}.` + ); + } return next( createHttpError( HttpCode.UNAUTHORIZED, @@ -151,6 +157,11 @@ export async function authWithWhitelist( otp ); if (!isValidCode) { + if (config.getRawConfig().app.log_failed_attempts) { + logger.info( + `Resource email otp incorrect. Resource ID: ${resource.resourceId}. Email: ${email}. IP: ${req.ip}.` + ); + } return next( createHttpError(HttpCode.UNAUTHORIZED, "Incorrect OTP") );