mirror of
https://github.com/kmc7468/arkvault.git
synced 2025-12-12 21:08:46 +00:00
Token Upgrade시 챌린지를 거치도록 변경
This commit is contained in:
@@ -5,4 +5,5 @@ JWT_SECRET=
|
|||||||
DATABASE_URL=
|
DATABASE_URL=
|
||||||
JWT_ACCESS_TOKEN_EXPIRES=
|
JWT_ACCESS_TOKEN_EXPIRES=
|
||||||
JWT_REFRESH_TOKEN_EXPIRES=
|
JWT_REFRESH_TOKEN_EXPIRES=
|
||||||
PUBKEY_CHALLENGE_EXPIRES=
|
USER_CLIENT_CHALLENGE_EXPIRES=
|
||||||
|
TOKEN_UPGRADE_CHALLENGE_EXPIRES=
|
||||||
|
|||||||
@@ -2,7 +2,10 @@ import { redirect, type ServerInit, type Handle } from "@sveltejs/kit";
|
|||||||
import schedule from "node-schedule";
|
import schedule from "node-schedule";
|
||||||
import { cleanupExpiredUserClientChallenges } from "$lib/server/db/client";
|
import { cleanupExpiredUserClientChallenges } from "$lib/server/db/client";
|
||||||
import { migrateDB } from "$lib/server/db/drizzle";
|
import { migrateDB } from "$lib/server/db/drizzle";
|
||||||
import { cleanupExpiredRefreshTokens } from "$lib/server/db/token";
|
import {
|
||||||
|
cleanupExpiredRefreshTokens,
|
||||||
|
cleanupExpiredTokenUpgradeChallenges,
|
||||||
|
} from "$lib/server/db/token";
|
||||||
|
|
||||||
export const init: ServerInit = () => {
|
export const init: ServerInit = () => {
|
||||||
migrateDB();
|
migrateDB();
|
||||||
@@ -10,6 +13,7 @@ export const init: ServerInit = () => {
|
|||||||
schedule.scheduleJob("0 * * * *", () => {
|
schedule.scheduleJob("0 * * * *", () => {
|
||||||
cleanupExpiredUserClientChallenges();
|
cleanupExpiredUserClientChallenges();
|
||||||
cleanupExpiredRefreshTokens();
|
cleanupExpiredRefreshTokens();
|
||||||
|
cleanupExpiredTokenUpgradeChallenges();
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ export const setUserClientStateToActive = async (userId: number, clientId: numbe
|
|||||||
.execute();
|
.execute();
|
||||||
};
|
};
|
||||||
|
|
||||||
export const createUserClientChallenge = async (
|
export const registerUserClientChallenge = async (
|
||||||
userId: number,
|
userId: number,
|
||||||
clientId: number,
|
clientId: number,
|
||||||
answer: string,
|
answer: string,
|
||||||
|
|||||||
@@ -16,3 +16,16 @@ export const refreshToken = sqliteTable(
|
|||||||
unq: unique().on(t.userId, t.clientId),
|
unq: unique().on(t.userId, t.clientId),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const tokenUpgradeChallenge = sqliteTable("token_upgrade_challenge", {
|
||||||
|
id: integer("id").primaryKey(),
|
||||||
|
refreshTokenId: text("refresh_token_id")
|
||||||
|
.notNull()
|
||||||
|
.references(() => refreshToken.id),
|
||||||
|
clientId: integer("client_id")
|
||||||
|
.notNull()
|
||||||
|
.references(() => client.id),
|
||||||
|
answer: text("challenge").notNull().unique(), // Base64
|
||||||
|
allowedIp: text("allowed_ip").notNull(),
|
||||||
|
expiresAt: integer("expires_at", { mode: "timestamp_ms" }).notNull(),
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { SqliteError } from "better-sqlite3";
|
import { SqliteError } from "better-sqlite3";
|
||||||
import { eq, lte } from "drizzle-orm";
|
import { and, eq, gt, lte } from "drizzle-orm";
|
||||||
import ms from "ms";
|
import ms from "ms";
|
||||||
import env from "$lib/server/loadenv";
|
import env from "$lib/server/loadenv";
|
||||||
import db from "./drizzle";
|
import db from "./drizzle";
|
||||||
import { refreshToken } from "./schema";
|
import { refreshToken, tokenUpgradeChallenge } from "./schema";
|
||||||
|
|
||||||
const expiresIn = ms(env.jwt.refreshExp);
|
const expiresIn = ms(env.jwt.refreshExp);
|
||||||
const expiresAt = () => new Date(Date.now() + expiresIn);
|
const expiresAt = () => new Date(Date.now() + expiresIn);
|
||||||
@@ -73,3 +73,44 @@ export const revokeRefreshToken = async (tokenId: string) => {
|
|||||||
export const cleanupExpiredRefreshTokens = async () => {
|
export const cleanupExpiredRefreshTokens = async () => {
|
||||||
await db.delete(refreshToken).where(lte(refreshToken.expiresAt, new Date())).execute();
|
await db.delete(refreshToken).where(lte(refreshToken.expiresAt, new Date())).execute();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const registerTokenUpgradeChallenge = async (
|
||||||
|
tokenId: string,
|
||||||
|
clientId: number,
|
||||||
|
answer: string,
|
||||||
|
allowedIp: string,
|
||||||
|
expiresAt: Date,
|
||||||
|
) => {
|
||||||
|
await db
|
||||||
|
.insert(tokenUpgradeChallenge)
|
||||||
|
.values({
|
||||||
|
refreshTokenId: tokenId,
|
||||||
|
clientId,
|
||||||
|
answer,
|
||||||
|
allowedIp,
|
||||||
|
expiresAt,
|
||||||
|
})
|
||||||
|
.execute();
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getTokenUpgradeChallenge = async (answer: string, ip: string) => {
|
||||||
|
const challenges = await db
|
||||||
|
.select()
|
||||||
|
.from(tokenUpgradeChallenge)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(tokenUpgradeChallenge.answer, answer),
|
||||||
|
eq(tokenUpgradeChallenge.allowedIp, ip),
|
||||||
|
gt(tokenUpgradeChallenge.expiresAt, new Date()),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.execute();
|
||||||
|
return challenges[0] ?? null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const cleanupExpiredTokenUpgradeChallenges = async () => {
|
||||||
|
await db
|
||||||
|
.delete(tokenUpgradeChallenge)
|
||||||
|
.where(lte(tokenUpgradeChallenge.expiresAt, new Date()))
|
||||||
|
.execute();
|
||||||
|
};
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ export default {
|
|||||||
refreshExp: env.JWT_REFRESH_TOKEN_EXPIRES || "14d",
|
refreshExp: env.JWT_REFRESH_TOKEN_EXPIRES || "14d",
|
||||||
},
|
},
|
||||||
challenge: {
|
challenge: {
|
||||||
pubKeyExp: env.PUBKEY_CHALLENGE_EXPIRES || "5m",
|
userClientExp: env.USER_CLIENT_CHALLENGE_EXPIRES || "5m",
|
||||||
|
tokenUpgradeExp: env.TOKEN_UPGRADE_CHALLENGE_EXPIRES || "5m",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,10 +1,6 @@
|
|||||||
import { constants, randomBytes, createPublicKey, publicEncrypt, verify } from "crypto";
|
import { constants, randomBytes, createPublicKey, publicEncrypt, verify } from "crypto";
|
||||||
import { promisify } from "util";
|
import { promisify } from "util";
|
||||||
|
|
||||||
export const generateRandomBytes = async (length: number) => {
|
|
||||||
return await promisify(randomBytes)(length);
|
|
||||||
};
|
|
||||||
|
|
||||||
const makePubKeyPem = (pubKey: string) =>
|
const makePubKeyPem = (pubKey: string) =>
|
||||||
`-----BEGIN PUBLIC KEY-----\n${pubKey}\n-----END PUBLIC KEY-----`;
|
`-----BEGIN PUBLIC KEY-----\n${pubKey}\n-----END PUBLIC KEY-----`;
|
||||||
|
|
||||||
@@ -32,3 +28,9 @@ export const verifySignature = (data: string, signature: string, sigPubKey: stri
|
|||||||
Buffer.from(signature, "base64"),
|
Buffer.from(signature, "base64"),
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const generateChallenge = async (length: number, encPubKey: string) => {
|
||||||
|
const answer = await promisify(randomBytes)(length);
|
||||||
|
const challenge = encryptAsymmetric(answer, encPubKey);
|
||||||
|
return { answer, challenge };
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,16 +1,21 @@
|
|||||||
import { error } from "@sveltejs/kit";
|
import { error } from "@sveltejs/kit";
|
||||||
import argon2 from "argon2";
|
import argon2 from "argon2";
|
||||||
|
import ms from "ms";
|
||||||
import { v4 as uuidv4 } from "uuid";
|
import { v4 as uuidv4 } from "uuid";
|
||||||
import { getClientByPubKey, getUserClient } from "$lib/server/db/client";
|
import { getClient, getClientByPubKeys, getUserClient } from "$lib/server/db/client";
|
||||||
import { getUserByEmail } from "$lib/server/db/user";
|
import { getUserByEmail } from "$lib/server/db/user";
|
||||||
|
import env from "$lib/server/loadenv";
|
||||||
import {
|
import {
|
||||||
getRefreshToken,
|
getRefreshToken,
|
||||||
registerRefreshToken,
|
registerRefreshToken,
|
||||||
rotateRefreshToken,
|
rotateRefreshToken,
|
||||||
upgradeRefreshToken,
|
upgradeRefreshToken,
|
||||||
revokeRefreshToken,
|
revokeRefreshToken,
|
||||||
|
registerTokenUpgradeChallenge,
|
||||||
|
getTokenUpgradeChallenge,
|
||||||
} from "$lib/server/db/token";
|
} from "$lib/server/db/token";
|
||||||
import { issueToken, verifyToken, TokenError } from "$lib/server/modules/auth";
|
import { issueToken, verifyToken, TokenError } from "$lib/server/modules/auth";
|
||||||
|
import { verifySignature, generateChallenge } from "$lib/server/modules/crypto";
|
||||||
|
|
||||||
const verifyPassword = async (hash: string, password: string) => {
|
const verifyPassword = async (hash: string, password: string) => {
|
||||||
return await argon2.verify(hash, password);
|
return await argon2.verify(hash, password);
|
||||||
@@ -30,23 +35,15 @@ const issueRefreshToken = async (userId: number, clientId?: number) => {
|
|||||||
return token;
|
return token;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const login = async (email: string, password: string, pubKey?: string) => {
|
export const login = async (email: string, password: string) => {
|
||||||
const user = await getUserByEmail(email);
|
const user = await getUserByEmail(email);
|
||||||
if (!user || !(await verifyPassword(user.password, password))) {
|
if (!user || !(await verifyPassword(user.password, password))) {
|
||||||
error(401, "Invalid email or password");
|
error(401, "Invalid email or password");
|
||||||
}
|
}
|
||||||
|
|
||||||
const client = pubKey ? await getClientByPubKey(pubKey) : undefined;
|
|
||||||
const userClient = client ? await getUserClient(user.id, client.id) : undefined;
|
|
||||||
if (client === null) {
|
|
||||||
error(401, "Invalid public key");
|
|
||||||
} else if (client && (!userClient || userClient.state === "challenging")) {
|
|
||||||
error(401, "Unregistered public key");
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
accessToken: issueAccessToken(user.id, client?.id),
|
accessToken: issueAccessToken(user.id),
|
||||||
refreshToken: await issueRefreshToken(user.id, client?.id),
|
refreshToken: await issueRefreshToken(user.id),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -75,7 +72,7 @@ export const logout = async (refreshToken: string) => {
|
|||||||
await revokeRefreshToken(jti);
|
await revokeRefreshToken(jti);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const refreshTokens = async (refreshToken: string) => {
|
export const refreshToken = async (refreshToken: string) => {
|
||||||
const { jti: oldJti, userId, clientId } = await verifyRefreshToken(refreshToken);
|
const { jti: oldJti, userId, clientId } = await verifyRefreshToken(refreshToken);
|
||||||
const newJti = uuidv4();
|
const newJti = uuidv4();
|
||||||
|
|
||||||
@@ -88,20 +85,75 @@ export const refreshTokens = async (refreshToken: string) => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const upgradeTokens = async (refreshToken: string, pubKey: string) => {
|
const expiresIn = ms(env.challenge.tokenUpgradeExp);
|
||||||
|
const expiresAt = () => new Date(Date.now() + expiresIn);
|
||||||
|
|
||||||
|
const createChallenge = async (
|
||||||
|
ip: string,
|
||||||
|
tokenId: string,
|
||||||
|
clientId: number,
|
||||||
|
encPubKey: string,
|
||||||
|
) => {
|
||||||
|
const { answer, challenge } = await generateChallenge(32, encPubKey);
|
||||||
|
await registerTokenUpgradeChallenge(
|
||||||
|
tokenId,
|
||||||
|
clientId,
|
||||||
|
answer.toString("base64"),
|
||||||
|
ip,
|
||||||
|
expiresAt(),
|
||||||
|
);
|
||||||
|
return challenge.toString("base64");
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createTokenUpgradeChallenge = async (
|
||||||
|
refreshToken: string,
|
||||||
|
ip: string,
|
||||||
|
encPubKey: string,
|
||||||
|
sigPubKey: string,
|
||||||
|
) => {
|
||||||
|
const { jti, userId, clientId } = await verifyRefreshToken(refreshToken);
|
||||||
|
if (clientId) {
|
||||||
|
error(403, "Forbidden");
|
||||||
|
}
|
||||||
|
|
||||||
|
const client = await getClientByPubKeys(encPubKey, sigPubKey);
|
||||||
|
const userClient = client ? await getUserClient(userId, client.id) : undefined;
|
||||||
|
if (!client) {
|
||||||
|
error(401, "Invalid public key(s)");
|
||||||
|
} else if (!userClient || userClient.state === "challenging") {
|
||||||
|
error(401, "Unregistered client");
|
||||||
|
}
|
||||||
|
|
||||||
|
return { challenge: await createChallenge(ip, jti, client.id, encPubKey) };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const upgradeToken = async (
|
||||||
|
refreshToken: string,
|
||||||
|
ip: string,
|
||||||
|
answer: string,
|
||||||
|
sigAnswer: string,
|
||||||
|
) => {
|
||||||
const { jti: oldJti, userId, clientId } = await verifyRefreshToken(refreshToken);
|
const { jti: oldJti, userId, clientId } = await verifyRefreshToken(refreshToken);
|
||||||
if (clientId) {
|
if (clientId) {
|
||||||
error(403, "Forbidden");
|
error(403, "Forbidden");
|
||||||
}
|
}
|
||||||
|
|
||||||
const client = await getClientByPubKey(pubKey);
|
const challenge = await getTokenUpgradeChallenge(answer, ip);
|
||||||
const userClient = client ? await getUserClient(userId, client.id) : undefined;
|
if (!challenge) {
|
||||||
if (!client) {
|
error(401, "Invalid challenge answer");
|
||||||
error(401, "Invalid public key");
|
} else if (challenge.refreshTokenId !== oldJti) {
|
||||||
} else if (client && (!userClient || userClient.state === "challenging")) {
|
error(403, "Forbidden");
|
||||||
error(401, "Unregistered public key");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const client = await getClient(challenge.clientId);
|
||||||
|
if (!client) {
|
||||||
|
error(500, "Invalid challenge answer");
|
||||||
|
} else if (!verifySignature(answer, sigAnswer, client.sigPubKey)) {
|
||||||
|
error(401, "Invalid challenge answer signature");
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Replay attack prevention
|
||||||
|
|
||||||
const newJti = uuidv4();
|
const newJti = uuidv4();
|
||||||
if (!(await upgradeRefreshToken(oldJti, newJti, client.id))) {
|
if (!(await upgradeRefreshToken(oldJti, newJti, client.id))) {
|
||||||
error(500, "Refresh token not found");
|
error(500, "Refresh token not found");
|
||||||
|
|||||||
@@ -9,15 +9,10 @@ import {
|
|||||||
getAllUserClients,
|
getAllUserClients,
|
||||||
getUserClient,
|
getUserClient,
|
||||||
setUserClientStateToPending,
|
setUserClientStateToPending,
|
||||||
createUserClientChallenge,
|
registerUserClientChallenge,
|
||||||
getUserClientChallenge,
|
getUserClientChallenge,
|
||||||
} from "$lib/server/db/client";
|
} from "$lib/server/db/client";
|
||||||
import {
|
import { verifyPubKey, verifySignature, generateChallenge } from "$lib/server/modules/crypto";
|
||||||
generateRandomBytes,
|
|
||||||
verifyPubKey,
|
|
||||||
encryptAsymmetric,
|
|
||||||
verifySignature,
|
|
||||||
} from "$lib/server/modules/crypto";
|
|
||||||
import { isInitialMekNeeded } from "$lib/server/modules/mek";
|
import { isInitialMekNeeded } from "$lib/server/modules/mek";
|
||||||
import env from "$lib/server/loadenv";
|
import env from "$lib/server/loadenv";
|
||||||
|
|
||||||
@@ -31,20 +26,17 @@ export const getUserClientList = async (userId: number) => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const expiresIn = ms(env.challenge.pubKeyExp);
|
const expiresIn = ms(env.challenge.userClientExp);
|
||||||
const expiresAt = () => new Date(Date.now() + expiresIn);
|
const expiresAt = () => new Date(Date.now() + expiresIn);
|
||||||
|
|
||||||
const generateChallenge = async (
|
const createUserClientChallenge = async (
|
||||||
userId: number,
|
userId: number,
|
||||||
ip: string,
|
ip: string,
|
||||||
clientId: number,
|
clientId: number,
|
||||||
encPubKey: string,
|
encPubKey: string,
|
||||||
) => {
|
) => {
|
||||||
const answer = await generateRandomBytes(32);
|
const { answer, challenge } = await generateChallenge(32, encPubKey);
|
||||||
const answerBase64 = answer.toString("base64");
|
await registerUserClientChallenge(userId, clientId, answer.toString("base64"), ip, expiresAt());
|
||||||
await createUserClientChallenge(userId, clientId, answerBase64, ip, expiresAt());
|
|
||||||
|
|
||||||
const challenge = encryptAsymmetric(answer, encPubKey);
|
|
||||||
return challenge.toString("base64");
|
return challenge.toString("base64");
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -80,7 +72,7 @@ export const registerUserClient = async (
|
|||||||
clientId = await createClient(encPubKey, sigPubKey, userId);
|
clientId = await createClient(encPubKey, sigPubKey, userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
return { challenge: await generateChallenge(userId, ip, clientId, encPubKey) };
|
return { challenge: await createUserClientChallenge(userId, ip, clientId, encPubKey) };
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getUserClientStatus = async (userId: number, clientId: number) => {
|
export const getUserClientStatus = async (userId: number, clientId: number) => {
|
||||||
@@ -115,5 +107,7 @@ export const verifyUserClient = async (
|
|||||||
error(401, "Invalid challenge answer signature");
|
error(401, "Invalid challenge answer signature");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Replay attack prevention
|
||||||
|
|
||||||
await setUserClientStateToPending(userId, challenge.clientId);
|
await setUserClientStateToPending(userId, challenge.clientId);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -10,14 +10,12 @@ export const POST: RequestHandler = async ({ request, cookies }) => {
|
|||||||
.object({
|
.object({
|
||||||
email: z.string().email().nonempty(),
|
email: z.string().email().nonempty(),
|
||||||
password: z.string().nonempty(),
|
password: z.string().nonempty(),
|
||||||
pubKey: z.string().base64().nonempty().optional(),
|
|
||||||
})
|
})
|
||||||
.safeParse(await request.json());
|
.safeParse(await request.json());
|
||||||
if (!zodRes.success) error(400, "Invalid request body");
|
if (!zodRes.success) error(400, "Invalid request body");
|
||||||
|
const { email, password } = zodRes.data;
|
||||||
|
|
||||||
const { email, password, pubKey } = zodRes.data;
|
const { accessToken, refreshToken } = await login(email.trim(), password.trim());
|
||||||
const { accessToken, refreshToken } = await login(email.trim(), password.trim(), pubKey?.trim());
|
|
||||||
|
|
||||||
cookies.set("accessToken", accessToken, {
|
cookies.set("accessToken", accessToken, {
|
||||||
path: "/",
|
path: "/",
|
||||||
maxAge: Math.floor(ms(env.jwt.accessExp) / 1000),
|
maxAge: Math.floor(ms(env.jwt.accessExp) / 1000),
|
||||||
@@ -28,5 +26,6 @@ export const POST: RequestHandler = async ({ request, cookies }) => {
|
|||||||
maxAge: Math.floor(ms(env.jwt.refreshExp) / 1000),
|
maxAge: Math.floor(ms(env.jwt.refreshExp) / 1000),
|
||||||
sameSite: "strict",
|
sameSite: "strict",
|
||||||
});
|
});
|
||||||
|
|
||||||
return text("Logged in", { headers: { "Content-Type": "text/plain" } });
|
return text("Logged in", { headers: { "Content-Type": "text/plain" } });
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ export const POST: RequestHandler = async ({ cookies }) => {
|
|||||||
if (!token) error(401, "Refresh token not found");
|
if (!token) error(401, "Refresh token not found");
|
||||||
|
|
||||||
await logout(token.trim());
|
await logout(token.trim());
|
||||||
|
|
||||||
cookies.delete("accessToken", { path: "/" });
|
cookies.delete("accessToken", { path: "/" });
|
||||||
cookies.delete("refreshToken", { path: "/api/auth" });
|
cookies.delete("refreshToken", { path: "/api/auth" });
|
||||||
|
|
||||||
return text("Logged out", { headers: { "Content-Type": "text/plain" } });
|
return text("Logged out", { headers: { "Content-Type": "text/plain" } });
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
import { error, text } from "@sveltejs/kit";
|
import { error, text } from "@sveltejs/kit";
|
||||||
import { refreshTokens } from "$lib/server/services/auth";
|
import { refreshToken as doRefreshToken } from "$lib/server/services/auth";
|
||||||
import type { RequestHandler } from "./$types";
|
import type { RequestHandler } from "./$types";
|
||||||
|
|
||||||
export const POST: RequestHandler = async ({ cookies }) => {
|
export const POST: RequestHandler = async ({ cookies }) => {
|
||||||
const token = cookies.get("refreshToken");
|
const token = cookies.get("refreshToken");
|
||||||
if (!token) error(401, "Refresh token not found");
|
if (!token) error(401, "Refresh token not found");
|
||||||
|
|
||||||
const { accessToken, refreshToken } = await refreshTokens(token.trim());
|
const { accessToken, refreshToken } = await doRefreshToken(token.trim());
|
||||||
|
|
||||||
cookies.set("accessToken", accessToken, {
|
cookies.set("accessToken", accessToken, {
|
||||||
path: "/",
|
path: "/",
|
||||||
sameSite: "strict",
|
sameSite: "strict",
|
||||||
@@ -16,5 +15,6 @@ export const POST: RequestHandler = async ({ cookies }) => {
|
|||||||
path: "/api/auth",
|
path: "/api/auth",
|
||||||
sameSite: "strict",
|
sameSite: "strict",
|
||||||
});
|
});
|
||||||
|
|
||||||
return text("Token refreshed", { headers: { "Content-Type": "text/plain" } });
|
return text("Token refreshed", { headers: { "Content-Type": "text/plain" } });
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,29 +1,26 @@
|
|||||||
import { error, text } from "@sveltejs/kit";
|
import { error, json } from "@sveltejs/kit";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { upgradeTokens } from "$lib/server/services/auth";
|
import { createTokenUpgradeChallenge } from "$lib/server/services/auth";
|
||||||
import type { RequestHandler } from "./$types";
|
import type { RequestHandler } from "./$types";
|
||||||
|
|
||||||
export const POST: RequestHandler = async ({ request, cookies }) => {
|
export const POST: RequestHandler = async ({ request, cookies, getClientAddress }) => {
|
||||||
const token = cookies.get("refreshToken");
|
const token = cookies.get("refreshToken");
|
||||||
if (!token) error(401, "Refresh token not found");
|
if (!token) error(401, "Refresh token not found");
|
||||||
|
|
||||||
const zodRes = z
|
const zodRes = z
|
||||||
.object({
|
.object({
|
||||||
pubKey: z.string().base64().nonempty(),
|
encPubKey: z.string().base64().nonempty(),
|
||||||
|
sigPubKey: z.string().base64().nonempty(),
|
||||||
})
|
})
|
||||||
.safeParse(await request.json());
|
.safeParse(await request.json());
|
||||||
if (!zodRes.success) error(400, "Invalid request body");
|
if (!zodRes.success) error(400, "Invalid request body");
|
||||||
|
const { encPubKey, sigPubKey } = zodRes.data;
|
||||||
|
|
||||||
const { pubKey } = zodRes.data;
|
const { challenge } = await createTokenUpgradeChallenge(
|
||||||
const { accessToken, refreshToken } = await upgradeTokens(token.trim(), pubKey.trim());
|
token.trim(),
|
||||||
|
getClientAddress(),
|
||||||
cookies.set("accessToken", accessToken, {
|
encPubKey.trim(),
|
||||||
path: "/",
|
sigPubKey.trim(),
|
||||||
sameSite: "strict",
|
);
|
||||||
});
|
return json({ challenge });
|
||||||
cookies.set("refreshToken", refreshToken, {
|
|
||||||
path: "/api/auth",
|
|
||||||
sameSite: "strict",
|
|
||||||
});
|
|
||||||
return text("Token upgraded", { headers: { "Content-Type": "text/plain" } });
|
|
||||||
};
|
};
|
||||||
|
|||||||
35
src/routes/api/auth/upgradeToken/verify/+server.ts
Normal file
35
src/routes/api/auth/upgradeToken/verify/+server.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import { error, text } from "@sveltejs/kit";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { upgradeToken } from "$lib/server/services/auth";
|
||||||
|
import type { RequestHandler } from "./$types";
|
||||||
|
|
||||||
|
export const POST: RequestHandler = async ({ request, cookies, getClientAddress }) => {
|
||||||
|
const token = cookies.get("refreshToken");
|
||||||
|
if (!token) error(401, "Refresh token not found");
|
||||||
|
|
||||||
|
const zodRes = z
|
||||||
|
.object({
|
||||||
|
answer: z.string().base64().nonempty(),
|
||||||
|
sigAnswer: z.string().base64().nonempty(),
|
||||||
|
})
|
||||||
|
.safeParse(await request.json());
|
||||||
|
if (!zodRes.success) error(400, "Invalid request body");
|
||||||
|
const { answer, sigAnswer } = zodRes.data;
|
||||||
|
|
||||||
|
const { accessToken, refreshToken } = await upgradeToken(
|
||||||
|
token.trim(),
|
||||||
|
getClientAddress(),
|
||||||
|
answer.trim(),
|
||||||
|
sigAnswer.trim(),
|
||||||
|
);
|
||||||
|
cookies.set("accessToken", accessToken, {
|
||||||
|
path: "/",
|
||||||
|
sameSite: "strict",
|
||||||
|
});
|
||||||
|
cookies.set("refreshToken", refreshToken, {
|
||||||
|
path: "/api/auth",
|
||||||
|
sameSite: "strict",
|
||||||
|
});
|
||||||
|
|
||||||
|
return text("Token upgraded", { headers: { "Content-Type": "text/plain" } });
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user