챌린지 Reply Attack 방어 구현

This commit is contained in:
static
2024-12-31 03:05:14 +09:00
parent b84d6fd5ad
commit a64e85848c
6 changed files with 24 additions and 3 deletions

View File

@@ -118,12 +118,21 @@ export const getUserClientChallenge = async (answer: string, ip: string) => {
eq(userClientChallenge.answer, answer),
eq(userClientChallenge.allowedIp, ip),
gt(userClientChallenge.expiresAt, new Date()),
eq(userClientChallenge.isUsed, false),
),
)
.execute();
return challenges[0] ?? null;
};
export const markUserClientChallengeAsUsed = async (id: number) => {
await db
.update(userClientChallenge)
.set({ isUsed: true })
.where(eq(userClientChallenge.id, id))
.execute();
};
export const cleanupExpiredUserClientChallenges = async () => {
await db
.delete(userClientChallenge)

View File

@@ -42,4 +42,5 @@ export const userClientChallenge = sqliteTable("user_client_challenge", {
answer: text("challenge").notNull().unique(), // Base64
allowedIp: text("allowed_ip").notNull(),
expiresAt: integer("expires_at", { mode: "timestamp_ms" }).notNull(),
isUsed: integer("is_used", { mode: "boolean" }).notNull().default(false),
});

View File

@@ -28,4 +28,5 @@ export const tokenUpgradeChallenge = sqliteTable("token_upgrade_challenge", {
answer: text("challenge").notNull().unique(), // Base64
allowedIp: text("allowed_ip").notNull(),
expiresAt: integer("expires_at", { mode: "timestamp_ms" }).notNull(),
isUsed: integer("is_used", { mode: "boolean" }).notNull().default(false),
});

View File

@@ -102,12 +102,21 @@ export const getTokenUpgradeChallenge = async (answer: string, ip: string) => {
eq(tokenUpgradeChallenge.answer, answer),
eq(tokenUpgradeChallenge.allowedIp, ip),
gt(tokenUpgradeChallenge.expiresAt, new Date()),
eq(tokenUpgradeChallenge.isUsed, false),
),
)
.execute();
return challenges[0] ?? null;
};
export const markTokenUpgradeChallengeAsUsed = async (id: number) => {
await db
.update(tokenUpgradeChallenge)
.set({ isUsed: true })
.where(eq(tokenUpgradeChallenge.id, id))
.execute();
};
export const cleanupExpiredTokenUpgradeChallenges = async () => {
await db
.delete(tokenUpgradeChallenge)

View File

@@ -13,6 +13,7 @@ import {
revokeRefreshToken,
registerTokenUpgradeChallenge,
getTokenUpgradeChallenge,
markTokenUpgradeChallengeAsUsed,
} from "$lib/server/db/token";
import { issueToken, verifyToken, TokenError } from "$lib/server/modules/auth";
import { verifySignature, generateChallenge } from "$lib/server/modules/crypto";
@@ -152,7 +153,7 @@ export const upgradeToken = async (
error(401, "Invalid challenge answer signature");
}
// TODO: Replay attack prevention
await markTokenUpgradeChallengeAsUsed(challenge.id);
const newJti = uuidv4();
if (!(await upgradeRefreshToken(oldJti, newJti, client.id))) {

View File

@@ -11,6 +11,7 @@ import {
setUserClientStateToPending,
registerUserClientChallenge,
getUserClientChallenge,
markUserClientChallengeAsUsed,
} from "$lib/server/db/client";
import { verifyPubKey, verifySignature, generateChallenge } from "$lib/server/modules/crypto";
import { isInitialMekNeeded } from "$lib/server/modules/mek";
@@ -107,7 +108,6 @@ export const verifyUserClient = async (
error(401, "Invalid challenge answer signature");
}
// TODO: Replay attack prevention
await markUserClientChallengeAsUsed(challenge.id);
await setUserClientStateToPending(userId, challenge.clientId);
};