Add logging for all auth

This commit is contained in:
Owen
2025-10-21 21:22:56 -07:00
parent 1142d6ac48
commit d392fb371e
4 changed files with 377 additions and 48 deletions

View File

@@ -672,27 +672,42 @@ export const setupTokens = pgTable("setupTokens", {
dateUsed: varchar("dateUsed") dateUsed: varchar("dateUsed")
}); });
export const requestAuditLog = pgTable("requestAuditLog", { export const requestAuditLog = pgTable(
id: serial("id").primaryKey(), "requestAuditLog",
timestamp: integer("timestamp").notNull(), // this is EPOCH time in seconds {
orgId: varchar("orgId") id: serial("id").primaryKey(),
.notNull() timestamp: integer("timestamp").notNull(), // this is EPOCH time in seconds
.references(() => orgs.orgId, { onDelete: "cascade" }), orgId: text("orgId")
actorType: varchar("actorType").notNull(), .notNull()
actor: varchar("actor").notNull(), .references(() => orgs.orgId, { onDelete: "cascade" }),
actorId: varchar("actorId").notNull(), action: boolean("action").notNull(),
resourceId: integer("resourceId"), reason: integer("reason").notNull(),
ip: varchar("ip").notNull(), actorType: text("actorType"),
type: varchar("type").notNull(), actor: text("actor"),
action: varchar("action").notNull(), actorId: text("actorId"),
event: varchar("event").notNull(), resourceId: integer("resourceId"),
location: varchar("location"), ip: text("ip"),
userAgent: varchar("userAgent"), type: text("type"),
metadata: text("details") location: text("location"),
}, (table) => ([ userAgent: text("userAgent"),
index("idx_requestAuditLog_timestamp").on(table.timestamp), metadata: text("details"),
index("idx_requestAuditLog_org_timestamp").on(table.orgId, table.timestamp) headers: text("headers"), // JSON blob
])); query: text("query"), // JSON blob
originalRequestURL: text("originalRequestURL"),
scheme: text("scheme"),
host: text("host"),
path: text("path"),
method: text("method"),
tls: boolean("tls")
},
(table) => [
index("idx_requestAuditLog_timestamp").on(table.timestamp),
index("idx_requestAuditLog_org_timestamp").on(
table.orgId,
table.timestamp
)
]
);
export type Org = InferSelectModel<typeof orgs>; export type Org = InferSelectModel<typeof orgs>;
export type User = InferSelectModel<typeof users>; export type User = InferSelectModel<typeof users>;
@@ -748,4 +763,4 @@ export type IdpOidcConfig = InferSelectModel<typeof idpOidcConfig>;
export type LicenseKey = InferSelectModel<typeof licenseKey>; export type LicenseKey = InferSelectModel<typeof licenseKey>;
export type SecurityKey = InferSelectModel<typeof securityKeys>; export type SecurityKey = InferSelectModel<typeof securityKeys>;
export type WebauthnChallenge = InferSelectModel<typeof webauthnChallenge>; export type WebauthnChallenge = InferSelectModel<typeof webauthnChallenge>;
export type RequestAuditLog = InferSelectModel<typeof requestAuditLog>; export type RequestAuditLog = InferSelectModel<typeof requestAuditLog>;

View File

@@ -1,6 +1,7 @@
import { randomUUID } from "crypto"; import { randomUUID } from "crypto";
import { InferSelectModel } from "drizzle-orm"; import { InferSelectModel } from "drizzle-orm";
import { sqliteTable, text, integer, index } from "drizzle-orm/sqlite-core"; import { sqliteTable, text, integer, index } from "drizzle-orm/sqlite-core";
import { boolean } from "yargs";
export const domains = sqliteTable("domains", { export const domains = sqliteTable("domains", {
domainId: text("domainId").primaryKey(), domainId: text("domainId").primaryKey(),
@@ -142,11 +143,15 @@ export const targets = sqliteTable("targets", {
}); });
export const targetHealthCheck = sqliteTable("targetHealthCheck", { export const targetHealthCheck = sqliteTable("targetHealthCheck", {
targetHealthCheckId: integer("targetHealthCheckId").primaryKey({ autoIncrement: true }), targetHealthCheckId: integer("targetHealthCheckId").primaryKey({
autoIncrement: true
}),
targetId: integer("targetId") targetId: integer("targetId")
.notNull() .notNull()
.references(() => targets.targetId, { onDelete: "cascade" }), .references(() => targets.targetId, { onDelete: "cascade" }),
hcEnabled: integer("hcEnabled", { mode: "boolean" }).notNull().default(false), hcEnabled: integer("hcEnabled", { mode: "boolean" })
.notNull()
.default(false),
hcPath: text("hcPath"), hcPath: text("hcPath"),
hcScheme: text("hcScheme"), hcScheme: text("hcScheme"),
hcMode: text("hcMode").default("http"), hcMode: text("hcMode").default("http"),
@@ -156,7 +161,9 @@ export const targetHealthCheck = sqliteTable("targetHealthCheck", {
hcUnhealthyInterval: integer("hcUnhealthyInterval").default(30), // in seconds hcUnhealthyInterval: integer("hcUnhealthyInterval").default(30), // in seconds
hcTimeout: integer("hcTimeout").default(5), // in seconds hcTimeout: integer("hcTimeout").default(5), // in seconds
hcHeaders: text("hcHeaders"), hcHeaders: text("hcHeaders"),
hcFollowRedirects: integer("hcFollowRedirects", { mode: "boolean" }).default(true), hcFollowRedirects: integer("hcFollowRedirects", {
mode: "boolean"
}).default(true),
hcMethod: text("hcMethod").default("GET"), hcMethod: text("hcMethod").default("GET"),
hcStatus: integer("hcStatus"), // http code hcStatus: integer("hcStatus"), // http code
hcHealth: text("hcHealth").default("unknown") // "unknown", "healthy", "unhealthy" hcHealth: text("hcHealth").default("unknown") // "unknown", "healthy", "unhealthy"
@@ -710,27 +717,42 @@ export const idpOrg = sqliteTable("idpOrg", {
orgMapping: text("orgMapping") orgMapping: text("orgMapping")
}); });
export const requestAuditLog = sqliteTable("requestAuditLog", { export const requestAuditLog = sqliteTable(
id: integer("id").primaryKey({ autoIncrement: true }), "requestAuditLog",
timestamp: integer("timestamp").notNull(), // this is EPOCH time in seconds {
orgId: text("orgId") id: integer("id").primaryKey({ autoIncrement: true }),
.notNull() timestamp: integer("timestamp").notNull(), // this is EPOCH time in seconds
.references(() => orgs.orgId, { onDelete: "cascade" }), orgId: text("orgId")
actorType: text("actorType").notNull(), .notNull()
actor: text("actor").notNull(), .references(() => orgs.orgId, { onDelete: "cascade" }),
actorId: text("actorId").notNull(), action: integer("action", { mode: "boolean" }).notNull(),
resourceId: integer("resourceId"), reason: integer("reason").notNull(),
ip: text("ip").notNull(), actorType: text("actorType"),
type: text("type").notNull(), actor: text("actor"),
action: text("action").notNull(), actorId: text("actorId"),
event: text("event").notNull(), resourceId: integer("resourceId"),
location: text("location"), ip: text("ip"),
userAgent: text("userAgent"), type: text("type"),
metadata: text("details") location: text("location"),
}, (table) => ([ userAgent: text("userAgent"),
index("idx_requestAuditLog_timestamp").on(table.timestamp), metadata: text("details"),
index("idx_requestAuditLog_org_timestamp").on(table.orgId, table.timestamp) headers: text("headers"), // JSON blob
])); query: text("query"), // JSON blob
originalRequestURL: text("originalRequestURL"),
scheme: text("scheme"),
host: text("host"),
path: text("path"),
method: text("method"),
tls: integer("tls", { mode: "boolean" })
},
(table) => [
index("idx_requestAuditLog_timestamp").on(table.timestamp),
index("idx_requestAuditLog_org_timestamp").on(
table.orgId,
table.timestamp
)
]
);
export type Org = InferSelectModel<typeof orgs>; export type Org = InferSelectModel<typeof orgs>;
export type User = InferSelectModel<typeof users>; export type User = InferSelectModel<typeof users>;
@@ -786,4 +808,4 @@ export type IdpOidcConfig = InferSelectModel<typeof idpOidcConfig>;
export type LicenseKey = InferSelectModel<typeof licenseKey>; export type LicenseKey = InferSelectModel<typeof licenseKey>;
export type SecurityKey = InferSelectModel<typeof securityKeys>; export type SecurityKey = InferSelectModel<typeof securityKeys>;
export type WebauthnChallenge = InferSelectModel<typeof webauthnChallenge>; export type WebauthnChallenge = InferSelectModel<typeof webauthnChallenge>;
export type RequestAuditLog = InferSelectModel<typeof requestAuditLog>; export type RequestAuditLog = InferSelectModel<typeof requestAuditLog>;

View File

@@ -0,0 +1,114 @@
import { db, requestAuditLog } from "@server/db";
import logger from "@server/logger";
/**
Reasons:
100 - Allowed by Rule
101 - Allowed No Auth
102 - Valid Access Token
103 - Valid header auth
104 - Valid Pincode
105 - Valid Password
106 - Valid email
107 - Valid SSO
201 - Resource Not Found
202 - Resource Blocked
203 - Dropped by Rule
204 - No Sessions
205 - Temporary Request Token
299 - No More Auth Methods
*/
export async function logRequestAudit(
data: {
action: boolean;
reason: number;
user?: { username: string; userId: string; orgId: string };
apiKey?: { name: string; apiKeyId: string; orgId: string };
metadata?: any;
resouceId?: number;
type?: string;
location?: string;
// userAgent?: string;
},
body: {
path: string;
originalRequestURL: string;
scheme: string;
host: string;
method: string;
tls: boolean;
sessions?: Record<string, string> | undefined;
headers?: Record<string, string> | undefined;
query?: Record<string, string> | undefined;
requestIp?: string | undefined;
}
) {
try {
let orgId: string | undefined;
let actorType: string | undefined;
let actor: string | undefined;
let actorId: string | undefined;
const user = data.user;
if (user) {
orgId = user.orgId;
actorType = "user";
actor = user.username;
actorId = user.userId;
}
const apiKey = data.apiKey;
if (apiKey) {
orgId = apiKey.orgId;
actorType = "apiKey";
actor = apiKey.name;
actorId = apiKey.apiKeyId;
}
if (!orgId) {
logger.warn("logRequestAudit: No organization context found");
return;
}
// if (!actorType || !actor || !actorId) {
// logger.warn("logRequestAudit: Incomplete actor information");
// return;
// }
const timestamp = Math.floor(Date.now() / 1000);
let metadata = null;
if (metadata) {
metadata = JSON.stringify(metadata);
}
await db.insert(requestAuditLog).values({
timestamp,
orgId,
actorType,
actor,
actorId,
metadata,
action: data.action,
resourceId: data.resouceId,
type: data.type,
reason: data.reason,
location: data.location,
// userAgent: data.userAgent, // TODO: add this
// headers: data.body.headers,
// query: data.body.query,
originalRequestURL: body.originalRequestURL,
scheme: body.scheme,
host: body.host,
path: body.path,
method: body.method,
ip: body.requestIp,
tls: body.tls
});
} catch (error) {
logger.error(error);
}
}

View File

@@ -37,6 +37,7 @@ import { getCountryCodeForIp } from "@server/lib/geoip";
import { getOrgTierData } from "#dynamic/lib/billing"; import { getOrgTierData } from "#dynamic/lib/billing";
import { TierId } from "@server/lib/billing/tiers"; import { TierId } from "@server/lib/billing/tiers";
import { verifyPassword } from "@server/auth/password"; import { verifyPassword } from "@server/auth/password";
import { logRequestAudit } from "./logRequestAudit";
// We'll see if this speeds anything up // We'll see if this speeds anything up
const cache = new NodeCache({ const cache = new NodeCache({
@@ -149,6 +150,15 @@ export async function verifyResourceSession(
if (!result) { if (!result) {
logger.debug(`Resource not found ${cleanHost}`); logger.debug(`Resource not found ${cleanHost}`);
logRequestAudit(
{
action: false,
reason: 201 //resource not found
},
parsedBody.data
);
return notAllowed(res); return notAllowed(res);
} }
@@ -160,6 +170,15 @@ export async function verifyResourceSession(
if (!resource) { if (!resource) {
logger.debug(`Resource not found ${cleanHost}`); logger.debug(`Resource not found ${cleanHost}`);
logRequestAudit(
{
action: false,
reason: 201 //resource not found
},
parsedBody.data
);
return notAllowed(res); return notAllowed(res);
} }
@@ -167,6 +186,15 @@ export async function verifyResourceSession(
if (blockAccess) { if (blockAccess) {
logger.debug("Resource blocked", host); logger.debug("Resource blocked", host);
logRequestAudit(
{
action: false,
reason: 202 //resource blocked
},
parsedBody.data
);
return notAllowed(res); return notAllowed(res);
} }
@@ -180,9 +208,28 @@ export async function verifyResourceSession(
if (action == "ACCEPT") { if (action == "ACCEPT") {
logger.debug("Resource allowed by rule"); logger.debug("Resource allowed by rule");
logRequestAudit(
{
action: true,
reason: 100 // allowed by rule
},
parsedBody.data
);
return allowed(res); return allowed(res);
} else if (action == "DROP") { } else if (action == "DROP") {
logger.debug("Resource denied by rule"); logger.debug("Resource denied by rule");
// TODO: add rules type
logRequestAudit(
{
action: false,
reason: 203 // dropped by rules
},
parsedBody.data
);
return notAllowed(res); return notAllowed(res);
} else if (action == "PASS") { } else if (action == "PASS") {
logger.debug( logger.debug(
@@ -203,6 +250,15 @@ export async function verifyResourceSession(
!headerAuth !headerAuth
) { ) {
logger.debug("Resource allowed because no auth"); logger.debug("Resource allowed because no auth");
logRequestAudit(
{
action: true,
reason: 101 // allowed no auth
},
parsedBody.data
);
return allowed(res); return allowed(res);
} }
@@ -254,6 +310,14 @@ export async function verifyResourceSession(
} }
if (valid && tokenItem) { if (valid && tokenItem) {
logRequestAudit(
{
action: true,
reason: 102 // valid access token
},
parsedBody.data
);
return allowed(res); return allowed(res);
} }
} }
@@ -290,6 +354,14 @@ export async function verifyResourceSession(
} }
if (valid && tokenItem) { if (valid && tokenItem) {
logRequestAudit(
{
action: true,
reason: 102 // valid access token
},
parsedBody.data
);
return allowed(res); return allowed(res);
} }
} }
@@ -301,6 +373,15 @@ export async function verifyResourceSession(
logger.debug( logger.debug(
"Resource allowed because header auth is valid (cached)" "Resource allowed because header auth is valid (cached)"
); );
logRequestAudit(
{
action: true,
reason: 103 // valid header auth
},
parsedBody.data
);
return allowed(res); return allowed(res);
} else if ( } else if (
await verifyPassword( await verifyPassword(
@@ -310,15 +391,33 @@ export async function verifyResourceSession(
) { ) {
cache.set(clientHeaderAuthKey, clientHeaderAuth); cache.set(clientHeaderAuthKey, clientHeaderAuth);
logger.debug("Resource allowed because header auth is valid"); logger.debug("Resource allowed because header auth is valid");
logRequestAudit(
{
action: true,
reason: 103 // valid header auth
},
parsedBody.data
);
return allowed(res); return allowed(res);
} }
if ( // we dont want to redirect if this is the only auth method and we did not pass here if (
// we dont want to redirect if this is the only auth method and we did not pass here
!sso && !sso &&
!pincode && !pincode &&
!password && !password &&
!resource.emailWhitelistEnabled !resource.emailWhitelistEnabled
) { ) {
logRequestAudit(
{
action: false,
reason: 299 // no more auth methods
},
parsedBody.data
);
return notAllowed(res); return notAllowed(res);
} }
} else if (headerAuth) { } else if (headerAuth) {
@@ -329,6 +428,14 @@ export async function verifyResourceSession(
!password && !password &&
!resource.emailWhitelistEnabled !resource.emailWhitelistEnabled
) { ) {
logRequestAudit(
{
action: false,
reason: 299 // no more auth methods
},
parsedBody.data
);
return notAllowed(res); return notAllowed(res);
} }
} }
@@ -341,6 +448,15 @@ export async function verifyResourceSession(
}. IP: ${clientIp}.` }. IP: ${clientIp}.`
); );
} }
logRequestAudit(
{
action: false,
reason: 204 // no sessions
},
parsedBody.data
);
return notAllowed(res); return notAllowed(res);
} }
@@ -374,6 +490,15 @@ export async function verifyResourceSession(
}. IP: ${clientIp}.` }. IP: ${clientIp}.`
); );
} }
logRequestAudit(
{
action: false,
reason: 205 // temporary request token
},
parsedBody.data
);
return notAllowed(res); return notAllowed(res);
} }
@@ -382,6 +507,15 @@ export async function verifyResourceSession(
logger.debug( logger.debug(
"Resource allowed because pincode session is valid" "Resource allowed because pincode session is valid"
); );
logRequestAudit(
{
action: true,
reason: 104 // valid pincode
},
parsedBody.data
);
return allowed(res); return allowed(res);
} }
@@ -389,6 +523,15 @@ export async function verifyResourceSession(
logger.debug( logger.debug(
"Resource allowed because password session is valid" "Resource allowed because password session is valid"
); );
logRequestAudit(
{
action: true,
reason: 105 // valid password
},
parsedBody.data
);
return allowed(res); return allowed(res);
} }
@@ -399,6 +542,15 @@ export async function verifyResourceSession(
logger.debug( logger.debug(
"Resource allowed because whitelist session is valid" "Resource allowed because whitelist session is valid"
); );
logRequestAudit(
{
action: true,
reason: 106 // valid email
},
parsedBody.data
);
return allowed(res); return allowed(res);
} }
@@ -406,6 +558,15 @@ export async function verifyResourceSession(
logger.debug( logger.debug(
"Resource allowed because access token session is valid" "Resource allowed because access token session is valid"
); );
logRequestAudit(
{
action: true,
reason: 102 // valid access token
},
parsedBody.data
);
return allowed(res); return allowed(res);
} }
@@ -433,6 +594,15 @@ export async function verifyResourceSession(
logger.debug( logger.debug(
"Resource allowed because user session is valid" "Resource allowed because user session is valid"
); );
logRequestAudit(
{
action: true,
reason: 107 // valid sso
},
parsedBody.data
);
return allowed(res, allowedUserData); return allowed(res, allowedUserData);
} }
} }
@@ -451,6 +621,14 @@ export async function verifyResourceSession(
logger.debug(`Redirecting to login at ${redirectPath}`); logger.debug(`Redirecting to login at ${redirectPath}`);
logRequestAudit(
{
action: false,
reason: 299 // no more auth methods
},
parsedBody.data
);
return notAllowed(res, redirectPath, resource.orgId); return notAllowed(res, redirectPath, resource.orgId);
} catch (e) { } catch (e) {
console.error(e); console.error(e);