add failed auth logging

This commit is contained in:
Milo Schwartz 2025-01-27 22:43:32 -05:00
parent fdb1ab4bd9
commit 0bd8217d9e
No known key found for this signature in database
16 changed files with 175 additions and 25 deletions

View file

@ -40,7 +40,8 @@ const configSchema = z.object({
.pipe(hostnameSchema) .pipe(hostnameSchema)
.transform((url) => url.toLowerCase()), .transform((url) => url.toLowerCase()),
log_level: z.enum(["debug", "info", "warn", "error"]), 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({ server: z.object({
external_port: portSchema external_port: portSchema

View file

@ -8,6 +8,7 @@ import HttpCode from "@server/types/HttpCode";
import config from "@server/lib/config"; import config from "@server/lib/config";
import { verifySession } from "@server/auth/sessions/verifySession"; import { verifySession } from "@server/auth/sessions/verifySession";
import { unauthorized } from "@server/auth/unauthorizedResponse"; import { unauthorized } from "@server/auth/unauthorizedResponse";
import logger from "@server/logger";
export const verifySessionUserMiddleware = async ( export const verifySessionUserMiddleware = async (
req: any, req: any,
@ -16,6 +17,9 @@ export const verifySessionUserMiddleware = async (
) => { ) => {
const { session, user } = await verifySession(req); const { session, user } = await verifySession(req);
if (!session || !user) { if (!session || !user) {
if (config.getRawConfig().app.log_failed_attempts) {
logger.info(`User session not found. IP: ${req.ip}.`);
}
return next(unauthorized()); return next(unauthorized());
} }
@ -25,6 +29,9 @@ export const verifySessionUserMiddleware = async (
.where(eq(users.userId, user.userId)); .where(eq(users.userId, user.userId));
if (!existingUser || !existingUser[0]) { if (!existingUser || !existingUser[0]) {
if (config.getRawConfig().app.log_failed_attempts) {
logger.info(`User session not found. IP: ${req.ip}.`);
}
return next( return next(
createHttpError(HttpCode.BAD_REQUEST, "User does not exist") createHttpError(HttpCode.BAD_REQUEST, "User does not exist")
); );

View file

@ -79,6 +79,11 @@ export async function disable2fa(
); );
if (!validOTP) { 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( return next(
createHttpError( createHttpError(
HttpCode.BAD_REQUEST, HttpCode.BAD_REQUEST,

View file

@ -71,6 +71,11 @@ export async function login(
.from(users) .from(users)
.where(eq(users.email, email)); .where(eq(users.email, email));
if (!existingUserRes || !existingUserRes.length) { if (!existingUserRes || !existingUserRes.length) {
if (config.getRawConfig().app.log_failed_attempts) {
logger.info(
`Username or password incorrect. Email: ${email}. IP: ${req.ip}.`
);
}
return next( return next(
createHttpError( createHttpError(
HttpCode.BAD_REQUEST, HttpCode.BAD_REQUEST,
@ -86,6 +91,11 @@ export async function login(
existingUser.passwordHash existingUser.passwordHash
); );
if (!validPassword) { if (!validPassword) {
if (config.getRawConfig().app.log_failed_attempts) {
logger.info(
`Username or password incorrect. Email: ${email}. IP: ${req.ip}.`
);
}
return next( return next(
createHttpError( createHttpError(
HttpCode.BAD_REQUEST, HttpCode.BAD_REQUEST,
@ -112,6 +122,11 @@ export async function login(
); );
if (!validOTP) { if (!validOTP) {
if (config.getRawConfig().app.log_failed_attempts) {
logger.info(
`Two-factor code incorrect. Email: ${email}. IP: ${req.ip}.`
);
}
return next( return next(
createHttpError( createHttpError(
HttpCode.BAD_REQUEST, HttpCode.BAD_REQUEST,

View file

@ -8,6 +8,7 @@ import {
invalidateSession invalidateSession
} from "@server/auth/sessions/app"; } from "@server/auth/sessions/app";
import { verifySession } from "@server/auth/sessions/verifySession"; import { verifySession } from "@server/auth/sessions/verifySession";
import config from "@server/lib/config";
export async function logout( export async function logout(
req: Request, req: Request,
@ -16,6 +17,11 @@ export async function logout(
): Promise<any> { ): Promise<any> {
const { user, session } = await verifySession(req); const { user, session } = await verifySession(req);
if (!user || !session) { 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( return next(
createHttpError( createHttpError(
HttpCode.BAD_REQUEST, HttpCode.BAD_REQUEST,

View file

@ -60,6 +60,11 @@ export async function resetPassword(
.where(eq(passwordResetTokens.email, email)); .where(eq(passwordResetTokens.email, email));
if (!resetRequest || !resetRequest.length) { 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( return next(
createHttpError( createHttpError(
HttpCode.BAD_REQUEST, HttpCode.BAD_REQUEST,
@ -109,6 +114,11 @@ export async function resetPassword(
); );
if (!validOTP) { if (!validOTP) {
if (config.getRawConfig().app.log_failed_attempts) {
logger.info(
`Two-factor authentication code is incorrect. Email: ${email}. IP: ${req.ip}.`
);
}
return next( return next(
createHttpError( createHttpError(
HttpCode.BAD_REQUEST, HttpCode.BAD_REQUEST,
@ -124,6 +134,11 @@ export async function resetPassword(
); );
if (!isTokenValid) { if (!isTokenValid) {
if (config.getRawConfig().app.log_failed_attempts) {
logger.info(
`Password reset code is incorrect. Email: ${email}. IP: ${req.ip}.`
);
}
return next( return next(
createHttpError( createHttpError(
HttpCode.BAD_REQUEST, HttpCode.BAD_REQUEST,

View file

@ -23,7 +23,10 @@ import { checkValidInvite } from "@server/auth/checkValidInvite";
import { passwordSchema } from "@server/auth/passwordSchema"; import { passwordSchema } from "@server/auth/passwordSchema";
export const signupBodySchema = z.object({ export const signupBodySchema = z.object({
email: z.string().email().transform((v) => v.toLowerCase()), email: z
.string()
.email()
.transform((v) => v.toLowerCase()),
password: passwordSchema, password: passwordSchema,
inviteToken: z.string().optional(), inviteToken: z.string().optional(),
inviteId: z.string().optional() inviteId: z.string().optional()
@ -60,6 +63,11 @@ export async function signup(
if (config.getRawConfig().flags?.disable_signup_without_invite) { if (config.getRawConfig().flags?.disable_signup_without_invite) {
if (!inviteToken || !inviteId) { if (!inviteToken || !inviteId) {
if (config.getRawConfig().app.log_failed_attempts) {
logger.info(
`Signup blocked without invite. Email: ${email}. IP: ${req.ip}.`
);
}
return next( return next(
createHttpError( createHttpError(
HttpCode.BAD_REQUEST, HttpCode.BAD_REQUEST,
@ -84,6 +92,11 @@ export async function signup(
} }
if (existingInvite.email !== email) { 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( return next(
createHttpError( createHttpError(
HttpCode.BAD_REQUEST, HttpCode.BAD_REQUEST,
@ -185,6 +198,11 @@ export async function signup(
}); });
} catch (e) { } catch (e) {
if (e instanceof SqliteError && e.code === "SQLITE_CONSTRAINT_UNIQUE") { 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( return next(
createHttpError( createHttpError(
HttpCode.BAD_REQUEST, HttpCode.BAD_REQUEST,

View file

@ -75,6 +75,11 @@ export async function verifyEmail(
.where(eq(users.userId, user.userId)); .where(eq(users.userId, user.userId));
}); });
} else { } else {
if (config.getRawConfig().app.log_failed_attempts) {
logger.info(
`Email verification code incorrect. Email: ${user.email}. IP: ${req.ip}.`
);
}
return next( return next(
createHttpError( createHttpError(
HttpCode.BAD_REQUEST, HttpCode.BAD_REQUEST,

View file

@ -96,6 +96,11 @@ export async function verifyTotp(
} }
if (!valid) { 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( return next(
createHttpError( createHttpError(
HttpCode.BAD_REQUEST, HttpCode.BAD_REQUEST,

View file

@ -20,7 +20,8 @@ import { response } from "@server/lib";
const exchangeSessionBodySchema = z.object({ const exchangeSessionBodySchema = z.object({
requestToken: z.string(), requestToken: z.string(),
host: z.string() host: z.string(),
requestIp: z.string().optional()
}); });
export type ExchangeSessionBodySchema = z.infer< export type ExchangeSessionBodySchema = z.infer<
@ -51,7 +52,9 @@ export async function exchangeSession(
} }
try { try {
const { requestToken, host } = parsedBody.data; const { requestToken, host, requestIp } = parsedBody.data;
const clientIp = requestIp?.split(":")[0];
const [resource] = await db const [resource] = await db
.select() .select()
@ -75,12 +78,22 @@ export async function exchangeSession(
); );
if (!requestSession) { if (!requestSession) {
if (config.getRawConfig().app.log_failed_attempts) {
logger.info(
`Exchange token is invalid. Resource ID: ${resource.resourceId}. IP: ${clientIp}.`
);
}
return next( return next(
createHttpError(HttpCode.UNAUTHORIZED, "Invalid request token") createHttpError(HttpCode.UNAUTHORIZED, "Invalid request token")
); );
} }
if (!requestSession.isRequestToken) { if (!requestSession.isRequestToken) {
if (config.getRawConfig().app.log_failed_attempts) {
logger.info(
`Exchange token is invalid. Resource ID: ${resource.resourceId}. IP: ${clientIp}.`
);
}
return next( return next(
createHttpError(HttpCode.UNAUTHORIZED, "Invalid request token") createHttpError(HttpCode.UNAUTHORIZED, "Invalid request token")
); );

View file

@ -42,7 +42,8 @@ const verifyResourceSessionSchema = z.object({
path: z.string(), path: z.string(),
method: z.string(), method: z.string(),
accessToken: z.string().optional(), accessToken: z.string().optional(),
tls: z.boolean() tls: z.boolean(),
requestIp: z.string().optional()
}); });
export type VerifyResourceSessionSchema = z.infer< export type VerifyResourceSessionSchema = z.infer<
@ -77,9 +78,12 @@ export async function verifyResourceSession(
sessions, sessions,
host, host,
originalRequestURL, originalRequestURL,
requestIp,
accessToken: token accessToken: token
} = parsedBody.data; } = parsedBody.data;
const clientIp = requestIp?.split(":")[0];
const resourceCacheKey = `resource:${host}`; const resourceCacheKey = `resource:${host}`;
let resourceData: let resourceData:
| { | {
@ -160,6 +164,14 @@ export async function verifyResourceSession(
logger.debug("Access token invalid: " + error); 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) { if (valid && tokenItem) {
validAccessToken = tokenItem; validAccessToken = tokenItem;
@ -174,6 +186,11 @@ export async function verifyResourceSession(
} }
if (!sessions) { if (!sessions) {
if (config.getRawConfig().app.log_failed_attempts) {
logger.info(
`Missing resource sessions. Resource ID: ${resource.resourceId}. IP: ${clientIp}.`
);
}
return notAllowed(res); return notAllowed(res);
} }
@ -200,6 +217,11 @@ export async function verifyResourceSession(
logger.debug( logger.debug(
"Resource not allowed because session is a temporary request token" "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); return notAllowed(res);
} }
@ -271,6 +293,12 @@ export async function verifyResourceSession(
} }
logger.debug("No more auth to check, resource not allowed"); 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); return notAllowed(res, redirectUrl);
} catch (e) { } catch (e) {
console.error(e); console.error(e);

View file

@ -1,6 +1,4 @@
import { import { generateSessionToken } from "@server/auth/sessions/app";
generateSessionToken,
} from "@server/auth/sessions/app";
import db from "@server/db"; import db from "@server/db";
import { newts } from "@server/db/schema"; import { newts } from "@server/db/schema";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";
@ -10,8 +8,13 @@ import { NextFunction, Request, Response } from "express";
import createHttpError from "http-errors"; import createHttpError from "http-errors";
import { z } from "zod"; import { z } from "zod";
import { fromError } from "zod-validation-error"; 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 { verifyPassword } from "@server/auth/password";
import logger from "@server/logger";
import config from "@server/lib/config";
export const newtGetTokenBodySchema = z.object({ export const newtGetTokenBodySchema = z.object({
newtId: z.string(), newtId: z.string(),
@ -43,6 +46,11 @@ export async function getToken(
if (token) { if (token) {
const { session, newt } = await validateNewtSessionToken(token); const { session, newt } = await validateNewtSessionToken(token);
if (session) { if (session) {
if (config.getRawConfig().app.log_failed_attempts) {
logger.info(
`Newt session already valid. Newt ID: ${newtId}. IP: ${req.ip}.`
);
}
return response<null>(res, { return response<null>(res, {
data: null, data: null,
success: true, success: true,
@ -73,6 +81,11 @@ export async function getToken(
existingNewt.secretHash existingNewt.secretHash
); );
if (!validSecret) { 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( return next(
createHttpError(HttpCode.BAD_REQUEST, "Secret is incorrect") createHttpError(HttpCode.BAD_REQUEST, "Secret is incorrect")
); );

View file

@ -8,11 +8,10 @@ import { NextFunction, Request, Response } from "express";
import createHttpError from "http-errors"; import createHttpError from "http-errors";
import { z } from "zod"; import { z } from "zod";
import { fromError } from "zod-validation-error"; import { fromError } from "zod-validation-error";
import { import { createResourceSession } from "@server/auth/sessions/resource";
createResourceSession,
} from "@server/auth/sessions/resource";
import logger from "@server/logger"; import logger from "@server/logger";
import { verifyResourceAccessToken } from "@server/auth/verifyResourceAccessToken"; import { verifyResourceAccessToken } from "@server/auth/verifyResourceAccessToken";
import config from "@server/lib/config";
const authWithAccessTokenBodySchema = z const authWithAccessTokenBodySchema = z
.object({ .object({
@ -84,6 +83,11 @@ export async function authWithAccessToken(
}); });
if (!valid) { if (!valid) {
if (config.getRawConfig().app.log_failed_attempts) {
logger.info(
`Resource access token invalid. Resource ID: ${resource.resourceId}. IP: ${req.ip}.`
);
}
return next( return next(
createHttpError( createHttpError(
HttpCode.UNAUTHORIZED, HttpCode.UNAUTHORIZED,

View file

@ -9,11 +9,10 @@ import { NextFunction, Request, Response } from "express";
import createHttpError from "http-errors"; import createHttpError from "http-errors";
import { z } from "zod"; import { z } from "zod";
import { fromError } from "zod-validation-error"; import { fromError } from "zod-validation-error";
import { import { createResourceSession } from "@server/auth/sessions/resource";
createResourceSession,
} from "@server/auth/sessions/resource";
import logger from "@server/logger"; import logger from "@server/logger";
import { verifyPassword } from "@server/auth/password"; import { verifyPassword } from "@server/auth/password";
import config from "@server/lib/config";
export const authWithPasswordBodySchema = z export const authWithPasswordBodySchema = z
.object({ .object({
@ -82,7 +81,7 @@ export async function authWithPassword(
if (!org) { if (!org) {
return next( 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 definedPassword.passwordHash
); );
if (!validPassword) { if (!validPassword) {
if (config.getRawConfig().app.log_failed_attempts) {
logger.info(
`Resource password incorrect. Resource ID: ${resource.resourceId}. IP: ${req.ip}.`
);
}
return next( return next(
createHttpError(HttpCode.UNAUTHORIZED, "Incorrect password") createHttpError(HttpCode.UNAUTHORIZED, "Incorrect password")
); );

View file

@ -1,10 +1,6 @@
import { generateSessionToken } from "@server/auth/sessions/app"; import { generateSessionToken } from "@server/auth/sessions/app";
import db from "@server/db"; import db from "@server/db";
import { import { orgs, resourcePincode, resources } from "@server/db/schema";
orgs,
resourcePincode,
resources,
} from "@server/db/schema";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";
import response from "@server/lib/response"; import response from "@server/lib/response";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
@ -12,11 +8,10 @@ import { NextFunction, Request, Response } from "express";
import createHttpError from "http-errors"; import createHttpError from "http-errors";
import { z } from "zod"; import { z } from "zod";
import { fromError } from "zod-validation-error"; import { fromError } from "zod-validation-error";
import { import { createResourceSession } from "@server/auth/sessions/resource";
createResourceSession,
} from "@server/auth/sessions/resource";
import logger from "@server/logger"; import logger from "@server/logger";
import { verifyPassword } from "@server/auth/password"; import { verifyPassword } from "@server/auth/password";
import config from "@server/lib/config";
export const authWithPincodeBodySchema = z export const authWithPincodeBodySchema = z
.object({ .object({
@ -112,6 +107,11 @@ export async function authWithPincode(
definedPincode.pincodeHash definedPincode.pincodeHash
); );
if (!validPincode) { if (!validPincode) {
if (config.getRawConfig().app.log_failed_attempts) {
logger.info(
`Resource pin code incorrect. Resource ID: ${resource.resourceId}. IP: ${req.ip}.`
);
}
return next( return next(
createHttpError(HttpCode.UNAUTHORIZED, "Incorrect PIN") createHttpError(HttpCode.UNAUTHORIZED, "Incorrect PIN")
); );

View file

@ -16,6 +16,7 @@ import { fromError } from "zod-validation-error";
import { createResourceSession } from "@server/auth/sessions/resource"; import { createResourceSession } from "@server/auth/sessions/resource";
import { isValidOtp, sendResourceOtpEmail } from "@server/auth/resourceOtp"; import { isValidOtp, sendResourceOtpEmail } from "@server/auth/resourceOtp";
import logger from "@server/logger"; import logger from "@server/logger";
import config from "@server/lib/config";
const authWithWhitelistBodySchema = z const authWithWhitelistBodySchema = z
.object({ .object({
@ -96,7 +97,7 @@ export async function authWithWhitelist(
// if email is not found, check for wildcard email // if email is not found, check for wildcard email
const wildcard = "*@" + email.split("@")[1]; const wildcard = "*@" + email.split("@")[1];
logger.debug("Checking for wildcard email: " + wildcard) logger.debug("Checking for wildcard email: " + wildcard);
const [result] = await db const [result] = await db
.select() .select()
@ -120,6 +121,11 @@ export async function authWithWhitelist(
// if wildcard is still not found, return unauthorized // if wildcard is still not found, return unauthorized
if (!whitelistedEmail) { 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( return next(
createHttpError( createHttpError(
HttpCode.UNAUTHORIZED, HttpCode.UNAUTHORIZED,
@ -151,6 +157,11 @@ export async function authWithWhitelist(
otp otp
); );
if (!isValidCode) { 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( return next(
createHttpError(HttpCode.UNAUTHORIZED, "Incorrect OTP") createHttpError(HttpCode.UNAUTHORIZED, "Incorrect OTP")
); );