Skip to content

Commit

Permalink
feature: Log authentication failures to support fail2ban. Fixes #477 (#…
Browse files Browse the repository at this point in the history
…569)

* How do I set the variable "user" or "system" for AI inference #262
changed from system to user

* [Feature Request] Log failed login attempts for fail2ban implementation
#477
added logging of failed logins

* [Feature Request] Log failed login attempts for fail2ban implementation #477
added more logging for extension related logins

* Propagte IP to trpc

---------

Co-authored-by: Your Name <you@example.com>
  • Loading branch information
kamtschatka and Your Name authored Oct 19, 2024
1 parent e55362e commit 0debc6b
Show file tree
Hide file tree
Showing 9 changed files with 116 additions and 12 deletions.
2 changes: 2 additions & 0 deletions apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@
"react-syntax-highlighter": "^15.5.0",
"remark-breaks": "^4.0.0",
"remark-gfm": "^4.0.0",
"request-ip": "^3.3.0",
"sharp": "^0.33.3",
"superjson": "^2.2.1",
"tailwind-merge": "^2.2.1",
Expand All @@ -83,6 +84,7 @@
"@types/react": "^18.2.55",
"@types/react-dom": "^18.2.19",
"@types/react-syntax-highlighter": "^15.5.13",
"@types/request-ip": "^0.0.41",
"autoprefixer": "^10.4.17",
"postcss": "^8.4.35",
"tailwindcss": "^3.4.1",
Expand Down
29 changes: 26 additions & 3 deletions apps/web/server/api/client.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { headers } from "next/headers";
import { getServerAuthSession } from "@/server/auth";
import requestIp from "request-ip";

import { db } from "@hoarder/db";
import { Context, createCallerFactory } from "@hoarder/trpc";
Expand All @@ -8,25 +10,46 @@ import { appRouter } from "@hoarder/trpc/routers/_app";
export async function createContextFromRequest(req: Request) {
// TODO: This is a hack until we offer a proper REST API instead of the trpc based one.
// Check if the request has an Authorization token, if it does, assume that API key authentication is requested.
const ip = requestIp.getClientIp({
headers: Object.fromEntries(req.headers.entries()),
});
const authorizationHeader = req.headers.get("Authorization");
if (authorizationHeader && authorizationHeader.startsWith("Bearer ")) {
const token = authorizationHeader.split(" ")[1];
try {
const user = await authenticateApiKey(token);
return { user, db };
return {
user,
db,
req: {
ip,
},
};
} catch (e) {
// Fallthrough to cookie-based auth
}
}

return createContext();
return createContext(db, ip);
}

export const createContext = async (database?: typeof db): Promise<Context> => {
export const createContext = async (
database?: typeof db,
ip?: string | null,
): Promise<Context> => {
const session = await getServerAuthSession();
if (ip === undefined) {
const hdrs = headers();
ip = requestIp.getClientIp({
headers: Object.fromEntries(hdrs.entries()),
});
}
return {
user: session?.user ?? null,
db: database ?? db,
req: {
ip,
},
};
};

Expand Down
11 changes: 9 additions & 2 deletions apps/web/server/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import NextAuth, {
} from "next-auth";
import CredentialsProvider from "next-auth/providers/credentials";
import { Provider } from "next-auth/providers/index";
import requestIp from "request-ip";

import { db } from "@hoarder/db";
import {
Expand All @@ -17,7 +18,7 @@ import {
verificationTokens,
} from "@hoarder/db/schema";
import serverConfig from "@hoarder/shared/config";
import { validatePassword } from "@hoarder/trpc/auth";
import { logAuthenticationError, validatePassword } from "@hoarder/trpc/auth";

type UserRole = "admin" | "user";

Expand Down Expand Up @@ -77,7 +78,7 @@ const providers: Provider[] = [
email: { label: "Email", type: "email", placeholder: "Email" },
password: { label: "Password", type: "password" },
},
async authorize(credentials) {
async authorize(credentials, req) {
if (!credentials) {
return null;
}
Expand All @@ -88,6 +89,12 @@ const providers: Provider[] = [
credentials?.password,
);
} catch (e) {
const error = e as Error;
logAuthenticationError(
credentials?.email,
error.message,
requestIp.getClientIp({ headers: req.headers }),
);
return null;
}
},
Expand Down
19 changes: 19 additions & 0 deletions packages/shared/logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,22 @@ const logger = winston.createLogger({
});

export default logger;

export const authFailureLogger = winston.createLogger({
level: "debug",
format: winston.format.combine(
winston.format.timestamp(),
winston.format.printf(
(info) => `${info.timestamp} ${info.level}: ${info.message}`,
),
),
transports: [
new winston.transports.Console(),
new winston.transports.File({
filename: "auth_failures.log",
dirname: serverConfig.dataDir,
maxFiles: 2,
maxsize: 1024 * 1024,
}),
],
});
11 changes: 11 additions & 0 deletions packages/trpc/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import * as bcrypt from "bcryptjs";
import { db } from "@hoarder/db";
import { apiKeys } from "@hoarder/db/schema";
import serverConfig from "@hoarder/shared/config";
import { authFailureLogger } from "@hoarder/shared/logger";

// API Keys

Expand Down Expand Up @@ -102,3 +103,13 @@ export async function validatePassword(email: string, password: string) {

return user;
}

export function logAuthenticationError(
user: string,
message: string,
ip: string | null,
): void {
authFailureLogger.error(
`Authentication error. User: "${user}", Message: "${message}", IP-Address: "${ip}"`,
);
}
6 changes: 6 additions & 0 deletions packages/trpc/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,17 @@ interface User {
export interface Context {
user: User | null;
db: typeof db;
req: {
ip: string | null;
};
}

export interface AuthedContext {
user: User;
db: typeof db;
req: {
ip: string | null;
};
}

// Avoid exporting the entire t-object
Expand Down
27 changes: 20 additions & 7 deletions packages/trpc/routers/apiKeys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,12 @@ import { z } from "zod";
import { apiKeys } from "@hoarder/db/schema";
import serverConfig from "@hoarder/shared/config";

import { authenticateApiKey, generateApiKey, validatePassword } from "../auth";
import {
authenticateApiKey,
generateApiKey,
logAuthenticationError,
validatePassword,
} from "../auth";
import { authedProcedure, publicProcedure, router } from "../index";

const zApiKeySchema = z.object({
Expand Down Expand Up @@ -73,7 +78,7 @@ export const apiKeysAppRouter = router({
}),
)
.output(zApiKeySchema)
.mutation(async ({ input }) => {
.mutation(async ({ input, ctx }) => {
let user;
// Special handling as otherwise the extension would show "username or password is wrong"
if (serverConfig.auth.disablePasswordAuth) {
Expand All @@ -85,17 +90,25 @@ export const apiKeysAppRouter = router({
try {
user = await validatePassword(input.email, input.password);
} catch (e) {
const error = e as Error;
logAuthenticationError(input.email, error.message, ctx.req.ip);
throw new TRPCError({ code: "UNAUTHORIZED" });
}
return await generateApiKey(input.keyName, user.id);
}),
validate: publicProcedure
.input(z.object({ apiKey: z.string() }))
.output(z.object({ success: z.boolean() }))
.mutation(async ({ input }) => {
await authenticateApiKey(input.apiKey); // Throws if the key is invalid
return {
success: true,
};
.mutation(async ({ input, ctx }) => {
try {
await authenticateApiKey(input.apiKey); // Throws if the key is invalid
return {
success: true,
};
} catch (e) {
const error = e as Error;
logAuthenticationError("<unknown>", error.message, ctx.req.ip);
throw e;
}
}),
});
3 changes: 3 additions & 0 deletions packages/trpc/testUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ export function getApiCaller(db: TestDB, userId?: string, email?: string) {
}
: null,
db,
req: {
ip: null,
},
});
}

Expand Down
20 changes: 20 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 0debc6b

Please sign in to comment.