/api/auth/logout, /api/auth/refreshToken Endpoint 구현

This commit is contained in:
static
2024-12-26 17:44:44 +09:00
parent 45e214d49f
commit a42f26bab1
8 changed files with 160 additions and 51 deletions

View File

@@ -1,44 +0,0 @@
import argon2 from "argon2";
import jwt from "jsonwebtoken";
import { getClientByPubKey } from "$lib/server/db/client";
import { getUserByEmail } from "$lib/server/db/user";
import env from "$lib/server/loadenv";
interface TokenData {
type: "access" | "refresh";
userId: number;
clientId?: number;
}
const verifyPassword = async (hash: string, password: string) => {
return await argon2.verify(hash, password);
};
const issueToken = (type: "access" | "refresh", userId: number, clientId?: number) => {
return jwt.sign(
{
type,
userId,
clientId,
} satisfies TokenData,
env.jwt.secret,
{
expiresIn: type === "access" ? env.jwt.accessExp : env.jwt.refreshExp,
},
);
};
export const login = async (email: string, password: string, pubKey?: string) => {
const user = await getUserByEmail(email);
if (!user) return null;
const isValid = await verifyPassword(user.password, password);
if (!isValid) return null;
const client = pubKey ? await getClientByPubKey(pubKey) : null;
return {
accessToken: issueToken("access", user.id, client?.id),
refreshToken: issueToken("refresh", user.id, client?.id),
};
};

View File

@@ -5,3 +5,9 @@ export const user = sqliteTable("user", {
email: text("email").notNull().unique(),
password: text("password").notNull(),
});
export const revokedToken = sqliteTable("revoked_token", {
id: integer("id").primaryKey(),
token: text("token").notNull().unique(),
revokedAt: integer("revoked_at").notNull(),
});

View File

@@ -1,8 +1,27 @@
import { eq } from "drizzle-orm";
import db from "./drizzle";
import { user } from "./schema";
import { user, revokedToken } from "./schema";
export const getUserByEmail = async (email: string) => {
const users = await db.select().from(user).where(eq(user.email, email)).execute();
return users[0] ?? null;
};
export const revokeToken = async (token: string) => {
await db
.insert(revokedToken)
.values({
token,
revokedAt: Date.now(),
})
.execute();
};
export const isTokenRevoked = async (token: string) => {
const tokens = await db
.select()
.from(revokedToken)
.where(eq(revokedToken.token, token))
.execute();
return tokens.length > 0;
};

View File

@@ -0,0 +1,38 @@
import jwt from "jsonwebtoken";
import env from "$lib/server/loadenv";
interface TokenData {
type: "access" | "refresh";
userId: number;
clientId?: number;
}
export enum TokenError {
EXPIRED,
INVALID,
}
export const issueToken = (type: "access" | "refresh", userId: number, clientId?: number) => {
return jwt.sign(
{
type,
userId,
clientId,
} satisfies TokenData,
env.jwt.secret,
{
expiresIn: type === "access" ? env.jwt.accessExp : env.jwt.refreshExp,
},
);
};
export const verifyToken = (token: string) => {
try {
return jwt.verify(token, env.jwt.secret) as TokenData;
} catch (error) {
if (error instanceof jwt.TokenExpiredError) {
return TokenError.EXPIRED;
}
return TokenError.INVALID;
}
};

View File

@@ -0,0 +1,60 @@
import { error } from "@sveltejs/kit";
import argon2 from "argon2";
import { getClientByPubKey } from "$lib/server/db/client";
import { getUserByEmail, revokeToken, isTokenRevoked } from "$lib/server/db/user";
import { issueToken, verifyToken, TokenError } from "$lib/server/modules/auth";
const verifyPassword = async (hash: string, password: string) => {
return await argon2.verify(hash, password);
};
export const login = async (email: string, password: string, pubKey?: string) => {
const user = await getUserByEmail(email);
if (!user) {
error(401, "Invalid email or password");
}
const isValid = await verifyPassword(user.password, password);
if (!isValid) {
error(401, "Invalid email or password");
}
const client = pubKey ? await getClientByPubKey(pubKey) : undefined;
if (client === null) {
error(401, "Invalid public key");
}
return {
accessToken: issueToken("access", user.id, client?.id),
refreshToken: issueToken("refresh", user.id, client?.id),
};
};
const verifyRefreshToken = async (refreshToken: string) => {
const tokenData = verifyToken(refreshToken);
if (tokenData === TokenError.EXPIRED) {
error(401, "Token expired");
} else if (
tokenData === TokenError.INVALID ||
tokenData.type !== "refresh" ||
(await isTokenRevoked(refreshToken))
) {
error(401, "Invalid token");
}
return tokenData;
};
export const logout = async (refreshToken: string) => {
await verifyRefreshToken(refreshToken);
await revokeToken(refreshToken);
};
export const refreshToken = async (refreshToken: string) => {
const tokenData = await verifyRefreshToken(refreshToken);
await revokeToken(refreshToken);
return {
accessToken: issueToken("access", tokenData.userId, tokenData.clientId),
refreshToken: issueToken("refresh", tokenData.userId, tokenData.clientId),
};
};

View File

@@ -1,6 +1,6 @@
import { error, json } from "@sveltejs/kit";
import { z } from "zod";
import { login } from "$lib/server/auth";
import { login } from "$lib/server/services/auth";
import type { RequestHandler } from "./$types";
export const POST: RequestHandler = async ({ request }) => {
@@ -14,9 +14,5 @@ export const POST: RequestHandler = async ({ request }) => {
if (!zodRes.success) error(400, zodRes.error.message);
const { email, password, pubKey } = zodRes.data;
const loginRes = await login(email.trim(), password.trim(), pubKey?.trim());
if (!loginRes) error(401, "Invalid email, password, or public key");
const { accessToken, refreshToken } = loginRes;
return json({ accessToken, refreshToken });
return json(await login(email.trim(), password.trim(), pubKey?.trim()));
};

View File

@@ -0,0 +1,18 @@
import { error, text } from "@sveltejs/kit";
import { z } from "zod";
import { logout } from "$lib/server/services/auth";
import type { RequestHandler } from "./$types";
export const POST: RequestHandler = async ({ request }) => {
const zodRes = z
.object({
refreshToken: z.string().nonempty(),
})
.safeParse(await request.json());
if (!zodRes.success) error(400, zodRes.error.message);
const { refreshToken } = zodRes.data;
await logout(refreshToken.trim());
return text("Logged out");
};

View File

@@ -0,0 +1,16 @@
import { error, json } from "@sveltejs/kit";
import { z } from "zod";
import { refreshToken } from "$lib/server/services/auth";
import type { RequestHandler } from "./$types";
export const POST: RequestHandler = async ({ request }) => {
const zodRes = z
.object({
refreshToken: z.string().nonempty(),
})
.safeParse(await request.json());
if (!zodRes.success) error(400, zodRes.error.message);
const { refreshToken: token } = zodRes.data;
return json(await refreshToken(token.trim()));
};