mirror of
https://github.com/kmc7468/arkvault.git
synced 2025-12-16 23:18:48 +00:00
백엔드에서 JWT가 아닌 세션 ID 기반으로 인증하도록 변경
This commit is contained in:
@@ -1,90 +1,127 @@
|
||||
import { error, type Cookies } from "@sveltejs/kit";
|
||||
import jwt from "jsonwebtoken";
|
||||
import { error } from "@sveltejs/kit";
|
||||
import { getUserClient } from "$lib/server/db/client";
|
||||
import { IntegrityError } from "$lib/server/db/error";
|
||||
import { createSession, refreshSession } from "$lib/server/db/session";
|
||||
import env from "$lib/server/loadenv";
|
||||
import { issueSessionId, verifySessionId } from "$lib/server/modules/crypto";
|
||||
|
||||
type TokenPayload =
|
||||
| {
|
||||
type: "access";
|
||||
userId: number;
|
||||
clientId?: number;
|
||||
}
|
||||
| {
|
||||
type: "refresh";
|
||||
jti: string;
|
||||
};
|
||||
|
||||
export enum TokenError {
|
||||
EXPIRED,
|
||||
INVALID,
|
||||
interface Session {
|
||||
sessionId: string;
|
||||
userId: number;
|
||||
clientId?: number;
|
||||
}
|
||||
|
||||
type Permission = "pendingClient" | "activeClient";
|
||||
interface ClientSession extends Session {
|
||||
clientId: number;
|
||||
}
|
||||
|
||||
export const issueToken = (payload: TokenPayload) => {
|
||||
return jwt.sign(payload, env.jwt.secret, {
|
||||
expiresIn: (payload.type === "access" ? env.jwt.accessExp : env.jwt.refreshExp) / 1000,
|
||||
});
|
||||
export class AuthenticationError extends Error {
|
||||
constructor(
|
||||
public status: 400 | 401,
|
||||
message: string,
|
||||
) {
|
||||
super(message);
|
||||
this.name = "AuthenticationError";
|
||||
}
|
||||
}
|
||||
|
||||
export const startSession = async (userId: number, ip: string, userAgent: string) => {
|
||||
const { sessionId, sessionIdSigned } = await issueSessionId(32, env.session.secret);
|
||||
await createSession(userId, null, sessionId, ip, userAgent);
|
||||
return sessionIdSigned;
|
||||
};
|
||||
|
||||
export const verifyToken = (token: string) => {
|
||||
export const authenticate = async (sessionIdSigned: string, ip: string, userAgent: string) => {
|
||||
const sessionId = verifySessionId(sessionIdSigned, env.session.secret);
|
||||
if (!sessionId) {
|
||||
throw new AuthenticationError(400, "Invalid session id");
|
||||
}
|
||||
|
||||
try {
|
||||
return jwt.verify(token, env.jwt.secret) as TokenPayload;
|
||||
} catch (error) {
|
||||
if (error instanceof jwt.TokenExpiredError) {
|
||||
return TokenError.EXPIRED;
|
||||
const { userId, clientId } = await refreshSession(sessionId, ip, userAgent);
|
||||
return {
|
||||
id: sessionId,
|
||||
userId,
|
||||
clientId: clientId ?? undefined,
|
||||
};
|
||||
} catch (e) {
|
||||
if (e instanceof IntegrityError && e.message === "Session not found") {
|
||||
throw new AuthenticationError(401, "Invalid session id");
|
||||
}
|
||||
return TokenError.INVALID;
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
export const authenticate = (cookies: Cookies) => {
|
||||
const accessToken = cookies.get("accessToken");
|
||||
if (!accessToken) {
|
||||
error(401, "Access token not found");
|
||||
}
|
||||
|
||||
const tokenPayload = verifyToken(accessToken);
|
||||
if (tokenPayload === TokenError.EXPIRED) {
|
||||
error(401, "Access token expired");
|
||||
} else if (tokenPayload === TokenError.INVALID || tokenPayload.type !== "access") {
|
||||
error(401, "Invalid access token");
|
||||
}
|
||||
|
||||
return {
|
||||
userId: tokenPayload.userId,
|
||||
clientId: tokenPayload.clientId,
|
||||
};
|
||||
};
|
||||
export async function authorize(locals: App.Locals, requiredPermission: "any"): Promise<Session>;
|
||||
|
||||
export async function authorize(
|
||||
cookies: Cookies,
|
||||
locals: App.Locals,
|
||||
requiredPermission: "notClient",
|
||||
): Promise<Session>;
|
||||
|
||||
export async function authorize(
|
||||
locals: App.Locals,
|
||||
requiredPermission: "anyClient",
|
||||
): Promise<ClientSession>;
|
||||
|
||||
export async function authorize(
|
||||
locals: App.Locals,
|
||||
requiredPermission: "pendingClient",
|
||||
): Promise<{ userId: number; clientId: number }>;
|
||||
): Promise<ClientSession>;
|
||||
|
||||
export async function authorize(
|
||||
cookies: Cookies,
|
||||
locals: App.Locals,
|
||||
requiredPermission: "activeClient",
|
||||
): Promise<{ userId: number; clientId: number }>;
|
||||
): Promise<ClientSession>;
|
||||
|
||||
export async function authorize(
|
||||
cookies: Cookies,
|
||||
requiredPermission: Permission,
|
||||
): Promise<{ userId: number; clientId?: number }> {
|
||||
const tokenPayload = authenticate(cookies);
|
||||
const { userId, clientId } = tokenPayload;
|
||||
const userClient = clientId ? await getUserClient(userId, clientId) : undefined;
|
||||
locals: App.Locals,
|
||||
requiredPermission: "any" | "notClient" | "anyClient" | "pendingClient" | "activeClient",
|
||||
): Promise<Session> {
|
||||
if (!locals.session) {
|
||||
error(500, "Unauthenticated");
|
||||
}
|
||||
|
||||
const { id: sessionId, userId, clientId } = locals.session;
|
||||
|
||||
switch (requiredPermission) {
|
||||
case "pendingClient":
|
||||
if (!userClient || userClient.state !== "pending") {
|
||||
case "any":
|
||||
break;
|
||||
case "notClient":
|
||||
if (clientId) {
|
||||
error(403, "Forbidden");
|
||||
}
|
||||
return tokenPayload;
|
||||
case "activeClient":
|
||||
if (!userClient || userClient.state !== "active") {
|
||||
break;
|
||||
case "anyClient":
|
||||
if (!clientId) {
|
||||
error(403, "Forbidden");
|
||||
}
|
||||
return tokenPayload;
|
||||
break;
|
||||
case "pendingClient": {
|
||||
if (!clientId) {
|
||||
error(403, "Forbidden");
|
||||
}
|
||||
const userClient = await getUserClient(userId, clientId);
|
||||
if (!userClient) {
|
||||
error(500, "Invalid session id");
|
||||
} else if (userClient.state !== "pending") {
|
||||
error(403, "Forbidden");
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "activeClient": {
|
||||
if (!clientId) {
|
||||
error(403, "Forbidden");
|
||||
}
|
||||
const userClient = await getUserClient(userId, clientId);
|
||||
if (!userClient) {
|
||||
error(500, "Invalid session id");
|
||||
} else if (userClient.state !== "active") {
|
||||
error(403, "Forbidden");
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return { sessionId, userId, clientId };
|
||||
}
|
||||
|
||||
@@ -1,4 +1,12 @@
|
||||
import { constants, randomBytes, createPublicKey, publicEncrypt, verify } from "crypto";
|
||||
import {
|
||||
constants,
|
||||
randomBytes,
|
||||
createPublicKey,
|
||||
publicEncrypt,
|
||||
verify,
|
||||
createHmac,
|
||||
timingSafeEqual,
|
||||
} from "crypto";
|
||||
import { promisify } from "util";
|
||||
|
||||
const makePubKeyToPem = (pubKey: string) =>
|
||||
@@ -34,3 +42,26 @@ export const generateChallenge = async (length: number, encPubKey: string) => {
|
||||
const challenge = encryptAsymmetric(answer, encPubKey);
|
||||
return { answer, challenge };
|
||||
};
|
||||
|
||||
export const issueSessionId = async (length: number, secret: string) => {
|
||||
const sessionId = await promisify(randomBytes)(length);
|
||||
const sessionIdHex = sessionId.toString("hex");
|
||||
const sessionIdHmac = createHmac("sha256", secret).update(sessionId).digest("hex");
|
||||
return {
|
||||
sessionId: sessionIdHex,
|
||||
sessionIdSigned: `${sessionIdHex}.${sessionIdHmac}`,
|
||||
};
|
||||
};
|
||||
|
||||
export const verifySessionId = (sessionIdSigned: string, secret: string) => {
|
||||
const [sessionIdHex, sessionIdHmac] = sessionIdSigned.split(".");
|
||||
if (!sessionIdHex || !sessionIdHmac) return;
|
||||
if (
|
||||
timingSafeEqual(
|
||||
Buffer.from(sessionIdHmac, "hex"),
|
||||
createHmac("sha256", secret).update(Buffer.from(sessionIdHex, "hex")).digest(),
|
||||
)
|
||||
) {
|
||||
return sessionIdHex;
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user