feat(auth): extend API key authentication to tRPC endpoints (#4732)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -3,21 +3,24 @@ import { userAgent } from "next/server";
|
||||
import { createOpenApiFetchHandler } from "trpc-to-openapi";
|
||||
|
||||
import { appRouter, createTRPCContext } from "@homarr/api";
|
||||
import type { Session } from "@homarr/auth";
|
||||
import { hashPasswordAsync } from "@homarr/auth";
|
||||
import { createSessionAsync } from "@homarr/auth/server";
|
||||
import { API_KEY_HEADER_NAME, getSessionFromApiKeyAsync } from "@homarr/auth/api-key";
|
||||
import { ipAddressFromHeaders } from "@homarr/common/server";
|
||||
import { createLogger } from "@homarr/core/infrastructure/logs";
|
||||
import { ErrorWithMetadata } from "@homarr/core/infrastructure/logs/error";
|
||||
import { db, eq } from "@homarr/db";
|
||||
import { apiKeys } from "@homarr/db/schema";
|
||||
import { db } from "@homarr/db";
|
||||
|
||||
const logger = createLogger({ module: "trpcOpenApiRoute" });
|
||||
|
||||
const handlerAsync = async (req: NextRequest) => {
|
||||
const apiKeyHeaderValue = req.headers.get("ApiKey");
|
||||
const ipAddress = req.headers.get("x-forwarded-for");
|
||||
const apiKeyHeaderValue = req.headers.get(API_KEY_HEADER_NAME);
|
||||
const ipAddress = ipAddressFromHeaders(req.headers);
|
||||
const { ua } = userAgent(req);
|
||||
const session: Session | null = await getSessionOrDefaultFromHeadersAsync(apiKeyHeaderValue, ipAddress, ua);
|
||||
|
||||
logger.info(
|
||||
`Creating OpenAPI fetch handler for user ${apiKeyHeaderValue ? "with an api key" : "without an api key"}`,
|
||||
);
|
||||
|
||||
const session = await getSessionFromApiKeyAsync(db, apiKeyHeaderValue, ipAddress, ua);
|
||||
|
||||
// Fallback to JSON if no content type is set
|
||||
if (!req.headers.has("Content-Type")) {
|
||||
@@ -35,67 +38,6 @@ const handlerAsync = async (req: NextRequest) => {
|
||||
});
|
||||
};
|
||||
|
||||
const getSessionOrDefaultFromHeadersAsync = async (
|
||||
apiKeyHeaderValue: string | null,
|
||||
ipAdress: string | null,
|
||||
userAgent: string,
|
||||
): Promise<Session | null> => {
|
||||
logger.info(
|
||||
`Creating OpenAPI fetch handler for user ${apiKeyHeaderValue ? "with an api key" : "without an api key"}`,
|
||||
);
|
||||
|
||||
if (apiKeyHeaderValue === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const [apiKeyId, apiKey] = apiKeyHeaderValue.split(".");
|
||||
|
||||
if (!apiKeyId || !apiKey) {
|
||||
logger.warn("An attempt to authenticate over API has failed due to invalid API key format", {
|
||||
ipAdress,
|
||||
userAgent,
|
||||
});
|
||||
return null;
|
||||
}
|
||||
|
||||
const apiKeyFromDb = await db.query.apiKeys.findFirst({
|
||||
where: eq(apiKeys.id, apiKeyId),
|
||||
columns: {
|
||||
id: true,
|
||||
apiKey: true,
|
||||
salt: true,
|
||||
},
|
||||
with: {
|
||||
user: {
|
||||
columns: {
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
emailVerified: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!apiKeyFromDb) {
|
||||
logger.warn("An attempt to authenticate over API has failed", { ipAdress, userAgent });
|
||||
return null;
|
||||
}
|
||||
|
||||
const hashedApiKey = await hashPasswordAsync(apiKey, apiKeyFromDb.salt);
|
||||
|
||||
if (apiKeyFromDb.apiKey !== hashedApiKey) {
|
||||
logger.warn("An attempt to authenticate over API has failed", { ipAdress, userAgent });
|
||||
return null;
|
||||
}
|
||||
|
||||
logger.info("Read session from API request and found user", {
|
||||
name: apiKeyFromDb.user.name,
|
||||
id: apiKeyFromDb.user.id,
|
||||
});
|
||||
return await createSessionAsync(db, apiKeyFromDb.user);
|
||||
};
|
||||
|
||||
export {
|
||||
handlerAsync as DELETE,
|
||||
handlerAsync as GET,
|
||||
|
||||
Reference in New Issue
Block a user