diff --git a/README.md b/README.md index f2d2028..45f5c5a 100644 --- a/README.md +++ b/README.md @@ -30,12 +30,11 @@ docker compose up --build -d 필수 환경 변수가 아닌 경우, 설정해야 하는 특별한 이유가 없다면 기본값을 사용하는 것이 좋아요. |이름|필수|기본값|설명| -|-:|:-:|:-:|:-| -|`JWT_SECRET`|Y||JWT의 서명을 위해 사용돼요. 안전한 값으로 설정해 주세요.| -|`JWT_ACCESS_TOKEN_EXPIRES`||`5m`|Access Token의 유효 시간이에요.| -|`JWT_REFRESH_TOKEN_EXPIRES`||`14d`|Refresh Token의 유효 시간이에요.| +|:-|:-:|:-:|:-| +|`SESSION_SECRET`|Y||Session ID의 서명을 위해 사용돼요. 안전한 값으로 설정해 주세요.| +|`SESSION_EXPIRES`||`14d`|Session의 유효 시간이에요. Session은 마지막으로 사용된 후 설정된 유효 시간이 지나면 자동으로 삭제돼요.| |`USER_CLIENT_CHALLENGE_EXPIRES`||`5m`|암호 키를 서버에 처음 등록할 때 사용되는 챌린지의 유효 시간이에요.| -|`TOKEN_UPGRADE_CHALLENGE_EXPIRES`||`5m`|암호 키와 함께 로그인할 때 사용되는 챌린지의 유효 시간이에요.| +|`SESSION_UPGRADE_CHALLENGE_EXPIRES`||`5m`|암호 키와 함께 로그인할 때 사용되는 챌린지의 유효 시간이에요.| |`TRUST_PROXY`|||신뢰할 수 있는 리버스 프록시의 수예요. 설정할 경우 1 이상의 정수로 설정해 주세요. 프록시에서 `X-Forwarded-For` HTTP 헤더를 올바르게 설정하도록 구성해 주세요.| |`NODE_ENV`||`production`|ArkVault의 사용 용도예요. `production`인 경우, 컨테이너가 실행될 때마다 DB 마이그레이션이 자동으로 실행돼요.| |`PORT`||`80`|ArkVault 서버의 포트예요.| diff --git a/drizzle/0000_handy_captain_marvel.sql b/drizzle/0000_spooky_lady_bullseye.sql similarity index 83% rename from drizzle/0000_handy_captain_marvel.sql rename to drizzle/0000_spooky_lady_bullseye.sql index 05d5e02..d9b520c 100644 --- a/drizzle/0000_handy_captain_marvel.sql +++ b/drizzle/0000_spooky_lady_bullseye.sql @@ -17,12 +17,12 @@ CREATE TABLE `user_client_challenge` ( `id` integer PRIMARY KEY NOT NULL, `user_id` integer NOT NULL, `client_id` integer NOT NULL, - `challenge` text NOT NULL, + `answer` text NOT NULL, `allowed_ip` text NOT NULL, `expires_at` integer NOT NULL, - `is_used` integer DEFAULT false NOT NULL, FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE no action, - FOREIGN KEY (`client_id`) REFERENCES `client`(`id`) ON UPDATE no action ON DELETE no action + FOREIGN KEY (`client_id`) REFERENCES `client`(`id`) ON UPDATE no action ON DELETE no action, + FOREIGN KEY (`user_id`,`client_id`) REFERENCES `user_client`(`user_id`,`client_id`) ON UPDATE no action ON DELETE no action ); --> statement-breakpoint CREATE TABLE `directory` ( @@ -80,24 +80,26 @@ CREATE TABLE `master_encryption_key` ( FOREIGN KEY (`created_by`) REFERENCES `client`(`id`) ON UPDATE no action ON DELETE no action ); --> statement-breakpoint -CREATE TABLE `refresh_token` ( +CREATE TABLE `session` ( `id` text PRIMARY KEY NOT NULL, `user_id` integer NOT NULL, `client_id` integer, - `expires_at` integer NOT NULL, + `created_at` integer NOT NULL, + `last_used_at` integer NOT NULL, + `last_used_by_ip` text, + `last_used_by_user_agent` text, FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE no action, FOREIGN KEY (`client_id`) REFERENCES `client`(`id`) ON UPDATE no action ON DELETE no action ); --> statement-breakpoint -CREATE TABLE `token_upgrade_challenge` ( +CREATE TABLE `session_upgrade_challenge` ( `id` integer PRIMARY KEY NOT NULL, - `refresh_token_id` text NOT NULL, + `session_id` text NOT NULL, `client_id` integer NOT NULL, - `challenge` text NOT NULL, + `answer` text NOT NULL, `allowed_ip` text NOT NULL, `expires_at` integer NOT NULL, - `is_used` integer DEFAULT false NOT NULL, - FOREIGN KEY (`refresh_token_id`) REFERENCES `refresh_token`(`id`) ON UPDATE no action ON DELETE no action, + FOREIGN KEY (`session_id`) REFERENCES `session`(`id`) ON UPDATE no action ON DELETE no action, FOREIGN KEY (`client_id`) REFERENCES `client`(`id`) ON UPDATE no action ON DELETE no action ); --> statement-breakpoint @@ -110,10 +112,11 @@ CREATE TABLE `user` ( CREATE UNIQUE INDEX `client_encryption_public_key_unique` ON `client` (`encryption_public_key`);--> statement-breakpoint CREATE UNIQUE INDEX `client_signature_public_key_unique` ON `client` (`signature_public_key`);--> statement-breakpoint CREATE UNIQUE INDEX `client_encryption_public_key_signature_public_key_unique` ON `client` (`encryption_public_key`,`signature_public_key`);--> statement-breakpoint -CREATE UNIQUE INDEX `user_client_challenge_challenge_unique` ON `user_client_challenge` (`challenge`);--> statement-breakpoint +CREATE UNIQUE INDEX `user_client_challenge_answer_unique` ON `user_client_challenge` (`answer`);--> statement-breakpoint CREATE UNIQUE INDEX `directory_encrypted_data_encryption_key_unique` ON `directory` (`encrypted_data_encryption_key`);--> statement-breakpoint CREATE UNIQUE INDEX `file_path_unique` ON `file` (`path`);--> statement-breakpoint CREATE UNIQUE INDEX `file_encrypted_data_encryption_key_unique` ON `file` (`encrypted_data_encryption_key`);--> statement-breakpoint -CREATE UNIQUE INDEX `refresh_token_user_id_client_id_unique` ON `refresh_token` (`user_id`,`client_id`);--> statement-breakpoint -CREATE UNIQUE INDEX `token_upgrade_challenge_challenge_unique` ON `token_upgrade_challenge` (`challenge`);--> statement-breakpoint +CREATE UNIQUE INDEX `session_user_id_client_id_unique` ON `session` (`user_id`,`client_id`);--> statement-breakpoint +CREATE UNIQUE INDEX `session_upgrade_challenge_session_id_unique` ON `session_upgrade_challenge` (`session_id`);--> statement-breakpoint +CREATE UNIQUE INDEX `session_upgrade_challenge_answer_unique` ON `session_upgrade_challenge` (`answer`);--> statement-breakpoint CREATE UNIQUE INDEX `user_email_unique` ON `user` (`email`); \ No newline at end of file diff --git a/drizzle/meta/0000_snapshot.json b/drizzle/meta/0000_snapshot.json index d8c1013..57c4a6a 100644 --- a/drizzle/meta/0000_snapshot.json +++ b/drizzle/meta/0000_snapshot.json @@ -1,7 +1,7 @@ { "version": "6", "dialect": "sqlite", - "id": "929c6bca-d0c0-4899-afc6-a0a498226f28", + "id": "c518e1b4-38f8-4c8e-bdc9-64152ab456d8", "prevId": "00000000-0000-0000-0000-000000000000", "tables": { "client": { @@ -147,8 +147,8 @@ "notNull": true, "autoincrement": false }, - "challenge": { - "name": "challenge", + "answer": { + "name": "answer", "type": "text", "primaryKey": false, "notNull": true, @@ -167,21 +167,13 @@ "primaryKey": false, "notNull": true, "autoincrement": false - }, - "is_used": { - "name": "is_used", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": false } }, "indexes": { - "user_client_challenge_challenge_unique": { - "name": "user_client_challenge_challenge_unique", + "user_client_challenge_answer_unique": { + "name": "user_client_challenge_answer_unique", "columns": [ - "challenge" + "answer" ], "isUnique": true } @@ -212,6 +204,21 @@ ], "onDelete": "no action", "onUpdate": "no action" + }, + "user_client_challenge_user_id_client_id_user_client_user_id_client_id_fk": { + "name": "user_client_challenge_user_id_client_id_user_client_user_id_client_id_fk", + "tableFrom": "user_client_challenge", + "tableTo": "user_client", + "columnsFrom": [ + "user_id", + "client_id" + ], + "columnsTo": [ + "user_id", + "client_id" + ], + "onDelete": "no action", + "onUpdate": "no action" } }, "compositePrimaryKeys": {}, @@ -656,8 +663,8 @@ }, "uniqueConstraints": {} }, - "refresh_token": { - "name": "refresh_token", + "session": { + "name": "session", "columns": { "id": { "name": "id", @@ -680,17 +687,38 @@ "notNull": false, "autoincrement": false }, - "expires_at": { - "name": "expires_at", + "created_at": { + "name": "created_at", "type": "integer", "primaryKey": false, "notNull": true, "autoincrement": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used_by_ip": { + "name": "last_used_by_ip", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_used_by_user_agent": { + "name": "last_used_by_user_agent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false } }, "indexes": { - "refresh_token_user_id_client_id_unique": { - "name": "refresh_token_user_id_client_id_unique", + "session_user_id_client_id_unique": { + "name": "session_user_id_client_id_unique", "columns": [ "user_id", "client_id" @@ -699,9 +727,9 @@ } }, "foreignKeys": { - "refresh_token_user_id_user_id_fk": { - "name": "refresh_token_user_id_user_id_fk", - "tableFrom": "refresh_token", + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", "tableTo": "user", "columnsFrom": [ "user_id" @@ -712,9 +740,9 @@ "onDelete": "no action", "onUpdate": "no action" }, - "refresh_token_client_id_client_id_fk": { - "name": "refresh_token_client_id_client_id_fk", - "tableFrom": "refresh_token", + "session_client_id_client_id_fk": { + "name": "session_client_id_client_id_fk", + "tableFrom": "session", "tableTo": "client", "columnsFrom": [ "client_id" @@ -729,8 +757,8 @@ "compositePrimaryKeys": {}, "uniqueConstraints": {} }, - "token_upgrade_challenge": { - "name": "token_upgrade_challenge", + "session_upgrade_challenge": { + "name": "session_upgrade_challenge", "columns": { "id": { "name": "id", @@ -739,8 +767,8 @@ "notNull": true, "autoincrement": false }, - "refresh_token_id": { - "name": "refresh_token_id", + "session_id": { + "name": "session_id", "type": "text", "primaryKey": false, "notNull": true, @@ -753,8 +781,8 @@ "notNull": true, "autoincrement": false }, - "challenge": { - "name": "challenge", + "answer": { + "name": "answer", "type": "text", "primaryKey": false, "notNull": true, @@ -773,32 +801,31 @@ "primaryKey": false, "notNull": true, "autoincrement": false - }, - "is_used": { - "name": "is_used", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": false } }, "indexes": { - "token_upgrade_challenge_challenge_unique": { - "name": "token_upgrade_challenge_challenge_unique", + "session_upgrade_challenge_session_id_unique": { + "name": "session_upgrade_challenge_session_id_unique", "columns": [ - "challenge" + "session_id" + ], + "isUnique": true + }, + "session_upgrade_challenge_answer_unique": { + "name": "session_upgrade_challenge_answer_unique", + "columns": [ + "answer" ], "isUnique": true } }, "foreignKeys": { - "token_upgrade_challenge_refresh_token_id_refresh_token_id_fk": { - "name": "token_upgrade_challenge_refresh_token_id_refresh_token_id_fk", - "tableFrom": "token_upgrade_challenge", - "tableTo": "refresh_token", + "session_upgrade_challenge_session_id_session_id_fk": { + "name": "session_upgrade_challenge_session_id_session_id_fk", + "tableFrom": "session_upgrade_challenge", + "tableTo": "session", "columnsFrom": [ - "refresh_token_id" + "session_id" ], "columnsTo": [ "id" @@ -806,9 +833,9 @@ "onDelete": "no action", "onUpdate": "no action" }, - "token_upgrade_challenge_client_id_client_id_fk": { - "name": "token_upgrade_challenge_client_id_client_id_fk", - "tableFrom": "token_upgrade_challenge", + "session_upgrade_challenge_client_id_client_id_fk": { + "name": "session_upgrade_challenge_client_id_client_id_fk", + "tableFrom": "session_upgrade_challenge", "tableTo": "client", "columnsFrom": [ "client_id" diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index b2615a0..62c9f38 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -5,8 +5,8 @@ { "idx": 0, "version": "6", - "when": 1736170919561, - "tag": "0000_handy_captain_marvel", + "when": 1736637983139, + "tag": "0000_spooky_lady_bullseye", "breakpoints": true } ] diff --git a/src/lib/hooks/callApi.ts b/src/lib/hooks/callApi.ts index c08b97d..1699ec2 100644 --- a/src/lib/hooks/callApi.ts +++ b/src/lib/hooks/callApi.ts @@ -1,35 +1,11 @@ -export const refreshToken = async (fetchInternal = fetch) => { - return await fetchInternal("/api/auth/refreshToken", { method: "POST" }); +export const callGetApi = async (input: RequestInfo, fetchInternal = fetch) => { + return await fetchInternal(input); }; -const callApi = async (input: RequestInfo, init?: RequestInit, fetchInternal = fetch) => { - let res = await fetchInternal(input, init); - if (res.status === 401) { - res = await refreshToken(); - if (!res.ok) { - return res; - } - res = await fetchInternal(input, init); - } - return res; -}; - -export const callGetApi = async (input: RequestInfo, fetchInternal?: typeof fetch) => { - return await callApi(input, undefined, fetchInternal); -}; - -export const callPostApi = async ( - input: RequestInfo, - payload?: T, - fetchInternal?: typeof fetch, -) => { - return await callApi( - input, - { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: payload ? JSON.stringify(payload) : undefined, - }, - fetchInternal, - ); +export const callPostApi = async (input: RequestInfo, payload?: T, fetchInternal = fetch) => { + return await fetchInternal(input, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: payload ? JSON.stringify(payload) : undefined, + }); }; diff --git a/src/lib/server/middlewares/authenticate.ts b/src/lib/server/middlewares/authenticate.ts index f484578..8880f1a 100644 --- a/src/lib/server/middlewares/authenticate.ts +++ b/src/lib/server/middlewares/authenticate.ts @@ -1,11 +1,9 @@ import { error, redirect, type Handle } from "@sveltejs/kit"; import { authenticate, AuthenticationError } from "$lib/server/modules/auth"; -const whitelist = ["/auth/login", "/api/auth/login"]; - export const authenticateMiddleware: Handle = async ({ event, resolve }) => { const { pathname, search } = event.url; - if (whitelist.some((path) => pathname.startsWith(path))) { + if (pathname === "/api/auth/login") { return await resolve(event); } @@ -19,7 +17,9 @@ export const authenticateMiddleware: Handle = async ({ event, resolve }) => { event.locals.session = await authenticate(sessionIdSigned, ip, userAgent); } catch (e) { if (e instanceof AuthenticationError) { - if (pathname.startsWith("/api")) { + if (pathname === "/auth/login") { + return await resolve(event); + } else if (pathname.startsWith("/api")) { error(e.status, e.message); } else { redirect(302, "/auth/login?redirect=" + encodeURIComponent(pathname + search)); diff --git a/src/lib/server/modules/mek.ts b/src/lib/server/modules/mek.ts index 0019ce0..d65ef0a 100644 --- a/src/lib/server/modules/mek.ts +++ b/src/lib/server/modules/mek.ts @@ -17,7 +17,7 @@ export const verifyClientEncMekSig = async ( ) => { const userClient = await getUserClientWithDetails(userId, clientId); if (!userClient) { - error(500, "Invalid access token"); + error(500, "Invalid session id"); } const data = JSON.stringify({ version, key: encMek }); diff --git a/src/lib/server/services/client.ts b/src/lib/server/services/client.ts index cc706e1..b5b0209 100644 --- a/src/lib/server/services/client.ts +++ b/src/lib/server/services/client.ts @@ -98,7 +98,7 @@ export const verifyUserClient = async ( export const getUserClientStatus = async (userId: number, clientId: number) => { const userClient = await getUserClient(userId, clientId); if (!userClient) { - error(500, "Invalid access token"); + error(500, "Invalid session id"); } return { diff --git a/src/lib/services/auth.ts b/src/lib/services/auth.ts index f784b03..03d445a 100644 --- a/src/lib/services/auth.ts +++ b/src/lib/services/auth.ts @@ -1,37 +1,30 @@ +import { callPostApi } from "$lib/hooks"; import { encodeToBase64, decryptChallenge, signMessage } from "$lib/modules/crypto"; import type { - TokenUpgradeRequest, - TokenUpgradeResponse, - TokenUpgradeVerifyRequest, + SessionUpgradeRequest, + SessionUpgradeResponse, + SessionUpgradeVerifyRequest, } from "$lib/server/schemas"; -export const requestTokenUpgrade = async ( +export const requestSessionUpgrade = async ( encryptKeyBase64: string, decryptKey: CryptoKey, verifyKeyBase64: string, signKey: CryptoKey, ) => { - let res = await fetch("/api/auth/upgradeToken", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - encPubKey: encryptKeyBase64, - sigPubKey: verifyKeyBase64, - } satisfies TokenUpgradeRequest), + let res = await callPostApi("/api/auth/upgradeSession", { + encPubKey: encryptKeyBase64, + sigPubKey: verifyKeyBase64, }); if (!res.ok) return false; - const { challenge }: TokenUpgradeResponse = await res.json(); + const { challenge }: SessionUpgradeResponse = await res.json(); const answer = await decryptChallenge(challenge, decryptKey); const answerSig = await signMessage(answer, signKey); - res = await fetch("/api/auth/upgradeToken/verify", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - answer: encodeToBase64(answer), - answerSig: encodeToBase64(answerSig), - } satisfies TokenUpgradeVerifyRequest), + res = await callPostApi("/api/auth/upgradeSession/verify", { + answer: encodeToBase64(answer), + answerSig: encodeToBase64(answerSig), }); return res.ok; }; diff --git a/src/routes/(fullscreen)/auth/login/+page.server.ts b/src/routes/(fullscreen)/auth/login/+page.server.ts index da7da9c..935dd9d 100644 --- a/src/routes/(fullscreen)/auth/login/+page.server.ts +++ b/src/routes/(fullscreen)/auth/login/+page.server.ts @@ -1,11 +1,10 @@ import { redirect } from "@sveltejs/kit"; import type { PageServerLoad } from "./$types"; -export const load: PageServerLoad = async ({ url, cookies }) => { +export const load: PageServerLoad = async ({ locals, url }) => { const redirectPath = url.searchParams.get("redirect") || "/home"; - const accessToken = cookies.get("accessToken"); - if (accessToken) { + if (locals.session) { redirect(302, redirectPath); } diff --git a/src/routes/(fullscreen)/auth/login/+page.svelte b/src/routes/(fullscreen)/auth/login/+page.svelte index 1ebb66b..4014368 100644 --- a/src/routes/(fullscreen)/auth/login/+page.svelte +++ b/src/routes/(fullscreen)/auth/login/+page.svelte @@ -1,12 +1,10 @@ diff --git a/src/routes/(fullscreen)/auth/login/service.ts b/src/routes/(fullscreen)/auth/login/service.ts index 4e6145d..2d267e1 100644 --- a/src/routes/(fullscreen)/auth/login/service.ts +++ b/src/routes/(fullscreen)/auth/login/service.ts @@ -1,21 +1,18 @@ +import { callPostApi } from "$lib/hooks"; import { exportRSAKeyToBase64 } from "$lib/modules/crypto"; import type { LoginRequest } from "$lib/server/schemas"; -import { requestTokenUpgrade as requestTokenUpgradeInternal } from "$lib/services/auth"; +import { requestSessionUpgrade as requestSessionUpgradeInternal } from "$lib/services/auth"; import { requestClientRegistration } from "$lib/services/key"; import type { ClientKeys } from "$lib/stores"; export { requestMasterKeyDownload } from "$lib/services/key"; export const requestLogin = async (email: string, password: string) => { - const res = await fetch("/api/auth/login", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ email, password } satisfies LoginRequest), - }); + const res = await callPostApi("/api/auth/login", { email, password }); return res.ok; }; -export const requestTokenUpgrade = async ({ +export const requestSessionUpgrade = async ({ encryptKey, decryptKey, signKey, @@ -23,12 +20,12 @@ export const requestTokenUpgrade = async ({ }: ClientKeys) => { const encryptKeyBase64 = await exportRSAKeyToBase64(encryptKey); const verifyKeyBase64 = await exportRSAKeyToBase64(verifyKey); - if (await requestTokenUpgradeInternal(encryptKeyBase64, decryptKey, verifyKeyBase64, signKey)) { + if (await requestSessionUpgradeInternal(encryptKeyBase64, decryptKey, verifyKeyBase64, signKey)) { return true; } if (await requestClientRegistration(encryptKeyBase64, decryptKey, verifyKeyBase64, signKey)) { - return await requestTokenUpgradeInternal( + return await requestSessionUpgradeInternal( encryptKeyBase64, decryptKey, verifyKeyBase64, diff --git a/src/routes/(fullscreen)/key/export/+page.svelte b/src/routes/(fullscreen)/key/export/+page.svelte index a2bca6d..3ce209f 100644 --- a/src/routes/(fullscreen)/key/export/+page.svelte +++ b/src/routes/(fullscreen)/key/export/+page.svelte @@ -10,7 +10,7 @@ serializeClientKeys, requestClientRegistration, storeClientKeys, - requestTokenUpgrade, + requestSessionUpgrade, requestInitialMasterKeyRegistration, } from "./service"; @@ -59,14 +59,14 @@ await storeClientKeys($clientKeyStore); if ( - !(await requestTokenUpgrade( + !(await requestSessionUpgrade( data.encryptKeyBase64, $clientKeyStore.decryptKey, data.verifyKeyBase64, $clientKeyStore.signKey, )) ) - throw new Error("Failed to upgrade token"); + throw new Error("Failed to upgrade session"); if ( !(await requestInitialMasterKeyRegistration(data.masterKeyWrapped, $clientKeyStore.signKey)) diff --git a/src/routes/(fullscreen)/key/export/service.ts b/src/routes/(fullscreen)/key/export/service.ts index 45de8d9..b02dd09 100644 --- a/src/routes/(fullscreen)/key/export/service.ts +++ b/src/routes/(fullscreen)/key/export/service.ts @@ -4,7 +4,7 @@ import { signMasterKeyWrapped } from "$lib/modules/crypto"; import type { InitialMasterKeyRegisterRequest } from "$lib/server/schemas"; import type { ClientKeys } from "$lib/stores"; -export { requestTokenUpgrade } from "$lib/services/auth"; +export { requestSessionUpgrade } from "$lib/services/auth"; export { requestClientRegistration } from "$lib/services/key"; type SerializedKeyPairs = {