128 lines
4.6 KiB
TypeScript
128 lines
4.6 KiB
TypeScript
import { FastifyInstance } from "fastify";
|
|
import fp from "fastify-plugin";
|
|
import { secrets } from "../utils/secrets";
|
|
import { and, eq } from "drizzle-orm";
|
|
import { authUsers, m2mApiKeys } from "../../db/schema";
|
|
import { createHash } from "node:crypto";
|
|
|
|
/**
|
|
* Fastify Plugin für Machine-to-Machine Authentifizierung.
|
|
*
|
|
* Dieses Plugin prüft, ob der Header `x-api-key` vorhanden ist
|
|
* und mit dem in der .env hinterlegten M2M_API_KEY übereinstimmt.
|
|
*
|
|
* Verwendung:
|
|
* server.register(m2mAuthPlugin, { allowedPrefix: '/internal' })
|
|
*/
|
|
export default fp(async (server: FastifyInstance, opts: { allowedPrefix?: string } = {}) => {
|
|
const hashApiKey = (apiKey: string) =>
|
|
createHash("sha256").update(apiKey, "utf8").digest("hex")
|
|
|
|
server.addHook("preHandler", async (req, reply) => {
|
|
try {
|
|
const apiKeyHeader = req.headers["x-api-key"];
|
|
const apiKey = Array.isArray(apiKeyHeader) ? apiKeyHeader[0] : apiKeyHeader;
|
|
|
|
if (!apiKey) {
|
|
server.log.warn(`[M2M Auth] Ungültiger oder fehlender API-Key bei ${req.url}`);
|
|
return reply.status(401).send({ error: "Unauthorized" });
|
|
}
|
|
|
|
const keyHash = hashApiKey(apiKey);
|
|
|
|
const keyRows = await server.db
|
|
.select({
|
|
id: m2mApiKeys.id,
|
|
tenantId: m2mApiKeys.tenantId,
|
|
userId: m2mApiKeys.userId,
|
|
active: m2mApiKeys.active,
|
|
expiresAt: m2mApiKeys.expiresAt,
|
|
name: m2mApiKeys.name,
|
|
userEmail: authUsers.email,
|
|
})
|
|
.from(m2mApiKeys)
|
|
.innerJoin(authUsers, eq(authUsers.id, m2mApiKeys.userId))
|
|
.where(and(
|
|
eq(m2mApiKeys.keyHash, keyHash),
|
|
eq(m2mApiKeys.active, true)
|
|
))
|
|
.limit(1)
|
|
|
|
let key = keyRows[0]
|
|
if (!key) {
|
|
const fallbackValid = apiKey === secrets.M2M_API_KEY
|
|
if (!fallbackValid) {
|
|
server.log.warn(`[M2M Auth] Ungültiger API-Key bei ${req.url}`)
|
|
return reply.status(401).send({ error: "Unauthorized" })
|
|
}
|
|
|
|
// Backward compatibility mode for one global key.
|
|
// The caller must provide user/tenant identifiers in headers.
|
|
const tenantIdHeader = req.headers["x-tenant-id"]
|
|
const userIdHeader = req.headers["x-user-id"]
|
|
const tenantId = Number(Array.isArray(tenantIdHeader) ? tenantIdHeader[0] : tenantIdHeader)
|
|
const userId = Array.isArray(userIdHeader) ? userIdHeader[0] : userIdHeader
|
|
|
|
if (!tenantId || !userId) {
|
|
return reply.status(401).send({ error: "Missing x-tenant-id or x-user-id for legacy M2M key" })
|
|
}
|
|
|
|
const users = await server.db
|
|
.select({ email: authUsers.email })
|
|
.from(authUsers)
|
|
.where(eq(authUsers.id, userId))
|
|
.limit(1)
|
|
|
|
if (!users[0]) {
|
|
return reply.status(401).send({ error: "Unknown user for legacy M2M key" })
|
|
}
|
|
|
|
req.user = {
|
|
user_id: userId,
|
|
email: users[0].email,
|
|
tenant_id: tenantId
|
|
}
|
|
} else {
|
|
if (key.expiresAt && new Date(key.expiresAt).getTime() < Date.now()) {
|
|
return reply.status(401).send({ error: "Expired API key" })
|
|
}
|
|
|
|
req.user = {
|
|
user_id: key.userId,
|
|
email: key.userEmail,
|
|
tenant_id: key.tenantId
|
|
}
|
|
|
|
await server.db
|
|
.update(m2mApiKeys)
|
|
.set({ lastUsedAt: new Date(), updatedAt: new Date() })
|
|
.where(eq(m2mApiKeys.id, key.id))
|
|
}
|
|
|
|
(req as any).m2m = {
|
|
verified: true,
|
|
type: "internal",
|
|
key: apiKey,
|
|
};
|
|
|
|
req.role = "m2m"
|
|
req.permissions = []
|
|
req.hasPermission = () => false
|
|
} catch (err) {
|
|
// @ts-ignore
|
|
server.log.error("[M2M Auth] Fehler beim Prüfen des API-Keys:", err);
|
|
return reply.status(500).send({ error: "Internal Server Error" });
|
|
}
|
|
});
|
|
});
|
|
|
|
declare module "fastify" {
|
|
interface FastifyRequest {
|
|
m2m?: {
|
|
verified: boolean;
|
|
type: "internal";
|
|
key: string;
|
|
};
|
|
}
|
|
}
|