diff --git a/src/hooks.server.ts b/src/hooks.server.ts index ed01a2b..1cf4b38 100644 --- a/src/hooks.server.ts +++ b/src/hooks.server.ts @@ -2,6 +2,8 @@ import type { Handle } from '@sveltejs/kit'; import * as auth from '$lib/server/auth'; import { db } from '$lib/server/db'; import * as table from '$lib/server/db/schema'; +import {compare, hash} from "bcryptjs"; +import {eq} from "drizzle-orm"; const handleAuth: Handle = async ({ event, resolve }) => { // 1. 먼저 액세스 토큰 확인 @@ -18,45 +20,61 @@ const handleAuth: Handle = async ({ event, resolve }) => { } // 2. 액세스 토큰이 없거나 유효하지 않을 때 리프레시 토큰 확인 - const refreshToken = event.cookies.get(auth.refreshCookieName); - - if (!refreshToken) { + const refreshTokenCookie = event.cookies.get(auth.refreshCookieName); + if (!refreshTokenCookie) { // 두 토큰 모두 없거나 유효하지 않으면 인증되지 않은 사용자로 처리 event.locals.user = null; return resolve(event); } // 리프레시 토큰 검증 - const { userId, isValid } = auth.validateRefreshToken(refreshToken); - + const { userId, isValid } = auth.validateRefreshToken(refreshTokenCookie); if (!isValid || !userId) { // 리프레시 토큰이 유효하지 않으면 모든 쿠키 삭제 auth.deleteAuthCookies(event); event.locals.user = null; return resolve(event); } + // 리프레시 토큰이 유효하면 사용자 DB의 리프레시토큰해시와 비교 + const results = await db + .select({refreshTokenHash: table.user.refreshTokenHash}) + .from(table.user) + .where(eq(table.user.id, userId)); - // 유효한 리프레시 토큰이 있으면 사용자 정보 조회 - const user = await db.query.refreshTokens.findFirst({ - where: (refreshTokens, { eq }) => eq(refreshTokens.id, userId) - }); + const refreshToken_data = results.at(0); - if (!user) { + + if (!refreshToken_data) { auth.deleteAuthCookies(event); event.locals.user = null; return resolve(event); } + console.log("refreshToken_cookie: ",refreshTokenCookie); + console.log("refreshToken_hash: ",refreshToken_data.refreshTokenHash); - // 새 액세스 토큰 발급 - const newAccessToken = auth.generateAccessToken(user.id); - auth.setAccessTokenCookie(event, newAccessToken); + const validRefreshToken = await compare(refreshTokenCookie, refreshToken_data.refreshTokenHash); + console.log("validRefreshToken: ",validRefreshToken); + if(!validRefreshToken){ + auth.deleteAuthCookies(event); + event.locals.user = null; + return resolve(event); - // 사용자 정보 설정 - event.locals.user = { - id: user.id, - }; + } else { + // 새 액세스 토큰 발급 + const newAccessToken = auth.generateAccessToken(userId); + const newRefreshToken = auth.generateRefreshToken(userId); + auth.setAccessTokenCookie(event, newAccessToken); + auth.setRefreshTokenCookie(event, newRefreshToken); + const refreshTokenHash = await hash(refreshTokenCookie, 10) ; + await db.update(table.user).set({ refreshTokenHash: refreshTokenHash }).where(eq(table.user.id,userId)); + // 사용자 정보 설정 + event.locals.user = { + id: userId, + }; + + return resolve(event); + } - return resolve(event); }; export const handle: Handle = handleAuth; \ No newline at end of file diff --git a/src/lib/server/auth.ts b/src/lib/server/auth.ts index 1634cd5..68a6204 100644 --- a/src/lib/server/auth.ts +++ b/src/lib/server/auth.ts @@ -74,4 +74,8 @@ export function setRefreshTokenCookie(event: RequestEvent, token: string) { export function deleteAuthCookies(event: RequestEvent) { event.cookies.delete(jwtCookieName, { path: '/' }); event.cookies.delete(refreshCookieName, { path: '/' }); +} + +export function deleteJwtCookie(event: RequestEvent) { + event.cookies.delete(jwtCookieName, { path: '/' }); } \ No newline at end of file diff --git a/src/lib/server/db/schema.ts b/src/lib/server/db/schema.ts index e26af66..717a906 100644 --- a/src/lib/server/db/schema.ts +++ b/src/lib/server/db/schema.ts @@ -3,15 +3,9 @@ import { pgTable, serial, integer, text, timestamp } from 'drizzle-orm/pg-core'; export const user = pgTable('user', { id: text('id').primaryKey(), email: text('email').notNull().unique(), - passwordHash: text('password_hash').notNull() + passwordHash: text('password_hash').notNull(), + refreshTokenHash: text('refresh_token_hash').notNull(), + createdAt: timestamp('created_at').defaultNow().notNull(), }); -export const refreshTokens = pgTable('refresh_tokens', { - id: text('id').primaryKey(), - userId: text('user_id').notNull(), - expiresAt: timestamp('expires_at').notNull(), - createdAt: timestamp('created_at').defaultNow().notNull() -}); - -export type RefreshTokens = typeof refreshTokens.$inferSelect; export type User = typeof user.$inferSelect; diff --git a/src/routes/demo/lucia/+page.server.ts b/src/routes/demo/lucia/+page.server.ts index 25f8f63..ed68956 100644 --- a/src/routes/demo/lucia/+page.server.ts +++ b/src/routes/demo/lucia/+page.server.ts @@ -1,23 +1,17 @@ -// D:/gitea/Jwt/src/routes/demo/lucia/+page.server.ts - import * as auth from '$lib/server/auth'; -import { redirect } from '@sveltejs/kit'; // `fail`은 사용되지 않으므로 제거합니다. +import { redirect } from '@sveltejs/kit'; import type { Actions, PageServerLoad } from './$types'; +import {db} from "$lib/server/db"; +import * as table from "$lib/server/db/schema"; +import {eq} from "drizzle-orm"; -// `load` 함수는 event 객체에서 locals와 cookies를 직접 받아옵니다. export const load: PageServerLoad = async ({ locals, cookies }) => { - // 1. `locals`에서 직접 사용자 정보를 확인합니다. 이것이 표준 방식입니다. if (!locals.user) { - // 2. 사용자가 없으면 여기서 바로 리다이렉트합니다. throw redirect(302, '/demo/lucia/login'); } - - // 이 코드는 사용자가 로그인한 경우에만 실행됩니다. - // 이전에 정의한 쿠키 이름으로 수정했습니다. const accessToken = cookies.get(auth.jwtCookieName); const refreshToken = cookies.get(auth.refreshCookieName); - // 3. `locals`의 사용자와 쿠키에서 읽은 토큰을 반환합니다. return { user: locals.user, accessToken: accessToken ?? 'N/A', // 토큰이 없을 경우를 대비해 기본값 설정 @@ -27,17 +21,13 @@ export const load: PageServerLoad = async ({ locals, cookies }) => { export const actions: Actions = { logout: async (event) => { - // 더 안전한 로그아웃을 위해 DB의 리프레시 토큰을 무효화합니다. - const refreshTokenFromCookie = event.cookies.get(auth.refreshCookieName); - // if (refreshTokenFromCookie) { - // // auth.ts에 구현된 invalidateRefreshToken 함수를 호출합니다. - // await auth.invalidateRefreshToken(refreshTokenFromCookie); - // } - - // 그 다음 쿠키를 삭제합니다. + if (event.locals.user) { + await db.update(table.user).set({refreshTokenHash: ''}).where(eq(table.user.id, event.locals.user.id)); + } auth.deleteAuthCookies(event); - - // `actions`에서는 throw redirect(...)를 사용하는 것이 표준입니다. throw redirect(302, '/demo/lucia/login'); + }, + removeJwtCookie: async (event) => { + auth.deleteJwtCookie(event); } }; \ No newline at end of file diff --git a/src/routes/demo/lucia/+page.svelte b/src/routes/demo/lucia/+page.svelte index 5d383c4..2f9a511 100644 --- a/src/routes/demo/lucia/+page.svelte +++ b/src/routes/demo/lucia/+page.svelte @@ -13,3 +13,7 @@
+ + diff --git a/src/routes/demo/lucia/login/+page.server.ts b/src/routes/demo/lucia/login/+page.server.ts index 095d49f..b7536ad 100644 --- a/src/routes/demo/lucia/login/+page.server.ts +++ b/src/routes/demo/lucia/login/+page.server.ts @@ -45,15 +45,25 @@ export const actions: Actions = { if (!validPassword) { return fail(400, { message: 'Incorrect username or password' }); } - + const saltRounds = 10; // 두 토큰 모두 생성 const accessToken = auth.generateAccessToken(existingUser.id); const refreshToken = auth.generateRefreshToken(existingUser.id); - + const refreshTokenHash = await hash(refreshToken, saltRounds) ; // 두 쿠키 모두 설정 auth.setAccessTokenCookie(event, accessToken); auth.setRefreshTokenCookie(event, refreshToken); + try { + await db.update(table.user).set({ refreshTokenHash: refreshTokenHash }).where(eq(table.user.id, existingUser.id)); + // 두 토큰 모두 생성 + // 두 쿠키 모두 설정 + auth.setAccessTokenCookie(event, accessToken); + auth.setRefreshTokenCookie(event, refreshToken); + + } catch { + return fail(500, { message: 'An error has occurred' }); + } return redirect(302, '/demo/lucia'); }, register: async (event) => { @@ -62,6 +72,7 @@ export const actions: Actions = { const password = formData.get('password'); const userId = generateUserId(); + const refreshId = generateUserId(); // 타입 체크 및 검증 if (!email || typeof email !== 'string') { return fail(400, { message: '이메일이 필요합니다' }); @@ -76,13 +87,12 @@ export const actions: Actions = { const passwordHash = await hash(password, saltRounds); try { - await db.insert(table.user).values({ id: userId, email, passwordHash }); - - // 두 토큰 모두 생성 const accessToken = auth.generateAccessToken(userId); const refreshToken = auth.generateRefreshToken(userId); + const refreshTokenHash = await hash(auth.generateRefreshToken(userId), saltRounds) ; + + await db.insert(table.user).values({ id: userId, email, passwordHash, refreshTokenHash: refreshTokenHash }); - // 두 쿠키 모두 설정 auth.setAccessTokenCookie(event, accessToken); auth.setRefreshTokenCookie(event, refreshToken);