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)
.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

View file

@ -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")
);

View file

@ -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,

View file

@ -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,

View file

@ -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<any> {
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,

View file

@ -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,

View file

@ -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,

View file

@ -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,

View file

@ -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,

View file

@ -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")
);

View file

@ -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);

View file

@ -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<null>(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")
);

View file

@ -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,

View file

@ -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")
);

View file

@ -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")
);

View file

@ -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")
);