Files
arkvault/src/lib/server/services/auth.ts

167 lines
4.8 KiB
TypeScript

import { error } from "@sveltejs/kit";
import argon2 from "argon2";
import ms from "ms";
import { v4 as uuidv4 } from "uuid";
import { getClient, getClientByPubKeys, getUserClient } from "$lib/server/db/client";
import { getUserByEmail } from "$lib/server/db/user";
import env from "$lib/server/loadenv";
import {
getRefreshToken,
registerRefreshToken,
rotateRefreshToken,
upgradeRefreshToken,
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";
const verifyPassword = async (hash: string, password: string) => {
return await argon2.verify(hash, password);
};
const issueAccessToken = (userId: number, clientId?: number) => {
return issueToken({ type: "access", userId, clientId });
};
const issueRefreshToken = async (userId: number, clientId?: number) => {
const jti = uuidv4();
const token = issueToken({ type: "refresh", jti });
if (!(await registerRefreshToken(userId, clientId ?? null, jti))) {
error(403, "Already logged in");
}
return token;
};
export const login = async (email: string, password: string) => {
const user = await getUserByEmail(email);
if (!user || !(await verifyPassword(user.password, password))) {
error(401, "Invalid email or password");
}
return {
accessToken: issueAccessToken(user.id),
refreshToken: await issueRefreshToken(user.id),
};
};
const verifyRefreshToken = async (refreshToken: string) => {
const tokenPayload = verifyToken(refreshToken);
if (tokenPayload === TokenError.EXPIRED) {
error(401, "Refresh token expired");
} else if (tokenPayload === TokenError.INVALID || tokenPayload.type !== "refresh") {
error(401, "Invalid refresh token");
}
const tokenData = await getRefreshToken(tokenPayload.jti);
if (!tokenData) {
error(500, "Refresh token not found");
}
return {
jti: tokenPayload.jti,
userId: tokenData.userId,
clientId: tokenData.clientId ?? undefined,
};
};
export const logout = async (refreshToken: string) => {
const { jti } = await verifyRefreshToken(refreshToken);
await revokeRefreshToken(jti);
};
export const refreshToken = async (refreshToken: string) => {
const { jti: oldJti, userId, clientId } = await verifyRefreshToken(refreshToken);
const newJti = uuidv4();
if (!(await rotateRefreshToken(oldJti, newJti))) {
error(500, "Refresh token not found");
}
return {
accessToken: issueAccessToken(userId, clientId),
refreshToken: issueToken({ type: "refresh", jti: newJti }),
};
};
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);
if (clientId) {
error(403, "Forbidden");
}
const challenge = await getTokenUpgradeChallenge(answer, ip);
if (!challenge) {
error(401, "Invalid challenge answer");
} else if (challenge.refreshTokenId !== oldJti) {
error(403, "Forbidden");
}
const client = await getClient(challenge.clientId);
if (!client) {
error(500, "Invalid challenge answer");
} else if (!verifySignature(Buffer.from(answer, "base64"), sigAnswer, client.sigPubKey)) {
error(401, "Invalid challenge answer signature");
}
await markTokenUpgradeChallengeAsUsed(challenge.id);
const newJti = uuidv4();
if (!(await upgradeRefreshToken(oldJti, newJti, client.id))) {
error(500, "Refresh token not found");
}
return {
accessToken: issueAccessToken(userId, client.id),
refreshToken: issueToken({ type: "refresh", jti: newJti }),
};
};