* fix: api keys authentication does not work #1511 * chore: add ip and user-agent to logs of unauthenticated api-keys
This commit is contained in:
@@ -23,7 +23,7 @@ export const ApiKeysManagement = ({ apiKeys }: ApiKeysManagementProps) => {
|
|||||||
const { mutate, isPending } = clientApi.apiKeys.create.useMutation({
|
const { mutate, isPending } = clientApi.apiKeys.create.useMutation({
|
||||||
async onSuccess(data) {
|
async onSuccess(data) {
|
||||||
openModal({
|
openModal({
|
||||||
apiKey: data.randomToken,
|
apiKey: data.apiKey,
|
||||||
});
|
});
|
||||||
await revalidatePathActionAsync("/manage/tools/api");
|
await revalidatePathActionAsync("/manage/tools/api");
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,15 +1,21 @@
|
|||||||
|
import { headers } from "next/headers";
|
||||||
|
import { userAgent } from "next/server";
|
||||||
|
import type { NextRequest } from "next/server";
|
||||||
import { createOpenApiFetchHandler } from "trpc-swagger/build/index.mjs";
|
import { createOpenApiFetchHandler } from "trpc-swagger/build/index.mjs";
|
||||||
|
|
||||||
import { appRouter, createTRPCContext } from "@homarr/api";
|
import { appRouter, createTRPCContext } from "@homarr/api";
|
||||||
|
import { hashPasswordAsync } from "@homarr/auth";
|
||||||
import type { Session } from "@homarr/auth";
|
import type { Session } from "@homarr/auth";
|
||||||
import { createSessionAsync } from "@homarr/auth/server";
|
import { createSessionAsync } from "@homarr/auth/server";
|
||||||
import { db, eq } from "@homarr/db";
|
import { db, eq } from "@homarr/db";
|
||||||
import { apiKeys } from "@homarr/db/schema/sqlite";
|
import { apiKeys } from "@homarr/db/schema/sqlite";
|
||||||
import { logger } from "@homarr/log";
|
import { logger } from "@homarr/log";
|
||||||
|
|
||||||
const handlerAsync = async (req: Request) => {
|
const handlerAsync = async (req: NextRequest) => {
|
||||||
const apiKeyHeaderValue = req.headers.get("ApiKey");
|
const apiKeyHeaderValue = req.headers.get("ApiKey");
|
||||||
const session: Session | null = await getSessionOrDefaultFromHeadersAsync(apiKeyHeaderValue);
|
const ipAddress = req.ip ?? headers().get("x-forwarded-for");
|
||||||
|
const { ua } = userAgent(req);
|
||||||
|
const session: Session | null = await getSessionOrDefaultFromHeadersAsync(apiKeyHeaderValue, ipAddress, ua);
|
||||||
|
|
||||||
return createOpenApiFetchHandler({
|
return createOpenApiFetchHandler({
|
||||||
req,
|
req,
|
||||||
@@ -19,7 +25,11 @@ const handlerAsync = async (req: Request) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const getSessionOrDefaultFromHeadersAsync = async (apiKeyHeaderValue: string | null): Promise<Session | null> => {
|
const getSessionOrDefaultFromHeadersAsync = async (
|
||||||
|
apiKeyHeaderValue: string | null,
|
||||||
|
ipAdress: string | null,
|
||||||
|
userAgent: string,
|
||||||
|
): Promise<Session | null> => {
|
||||||
logger.info(
|
logger.info(
|
||||||
`Creating OpenAPI fetch handler for user ${apiKeyHeaderValue ? "with an api key" : "without an api key"}`,
|
`Creating OpenAPI fetch handler for user ${apiKeyHeaderValue ? "with an api key" : "without an api key"}`,
|
||||||
);
|
);
|
||||||
@@ -28,12 +38,21 @@ const getSessionOrDefaultFromHeadersAsync = async (apiKeyHeaderValue: string | n
|
|||||||
return 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 ip='${ipAdress}' userAgent='${userAgent}'`,
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const apiKeyFromDb = await db.query.apiKeys.findFirst({
|
const apiKeyFromDb = await db.query.apiKeys.findFirst({
|
||||||
where: eq(apiKeys.apiKey, apiKeyHeaderValue),
|
where: eq(apiKeys.id, apiKeyId),
|
||||||
columns: {
|
columns: {
|
||||||
id: true,
|
id: true,
|
||||||
apiKey: false,
|
apiKey: true,
|
||||||
salt: false,
|
salt: true,
|
||||||
},
|
},
|
||||||
with: {
|
with: {
|
||||||
user: {
|
user: {
|
||||||
@@ -47,8 +66,15 @@ const getSessionOrDefaultFromHeadersAsync = async (apiKeyHeaderValue: string | n
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (apiKeyFromDb === undefined) {
|
if (!apiKeyFromDb) {
|
||||||
logger.warn("An attempt to authenticate over API has failed");
|
logger.warn(`An attempt to authenticate over API has failed ip='${ipAdress}' userAgent='${userAgent}'`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hashedApiKey = await hashPasswordAsync(apiKey, apiKeyFromDb.salt);
|
||||||
|
|
||||||
|
if (apiKeyFromDb.apiKey !== hashedApiKey) {
|
||||||
|
logger.warn(`An attempt to authenticate over API has failed ip='${ipAdress}' userAgent='${userAgent}'`);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -28,14 +28,15 @@ export const apiKeysRouter = createTRPCRouter({
|
|||||||
const salt = await createSaltAsync();
|
const salt = await createSaltAsync();
|
||||||
const randomToken = generateSecureRandomToken(64);
|
const randomToken = generateSecureRandomToken(64);
|
||||||
const hashedRandomToken = await hashPasswordAsync(randomToken, salt);
|
const hashedRandomToken = await hashPasswordAsync(randomToken, salt);
|
||||||
|
const id = createId();
|
||||||
await db.insert(apiKeys).values({
|
await db.insert(apiKeys).values({
|
||||||
id: createId(),
|
id,
|
||||||
apiKey: hashedRandomToken,
|
apiKey: hashedRandomToken,
|
||||||
salt,
|
salt,
|
||||||
userId: ctx.session.user.id,
|
userId: ctx.session.user.id,
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
randomToken,
|
apiKey: `${id}.${randomToken}`,
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user