From 1a86c8d9e01777341083a2635ccd1238f7288f3e Mon Sep 17 00:00:00 2001 From: static Date: Sun, 12 Jan 2025 07:28:38 +0900 Subject: [PATCH] =?UTF-8?q?=EB=B0=B1=EC=97=94=EB=93=9C=EC=97=90=EC=84=9C?= =?UTF-8?q?=20JWT=EA=B0=80=20=EC=95=84=EB=8B=8C=20=EC=84=B8=EC=85=98=20ID?= =?UTF-8?q?=20=EA=B8=B0=EB=B0=98=EC=9C=BC=EB=A1=9C=20=EC=9D=B8=EC=A6=9D?= =?UTF-8?q?=ED=95=98=EB=8F=84=EB=A1=9D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.example | 7 +- docker-compose.yaml | 7 +- package.json | 2 - pnpm-lock.yaml | 94 ----------- src/app.d.ts | 14 +- src/hooks.server.ts | 30 +--- src/lib/server/db/error.ts | 8 +- src/lib/server/db/schema/client.ts | 2 +- src/lib/server/db/schema/index.ts | 2 +- .../server/db/schema/{token.ts => session.ts} | 21 ++- src/lib/server/db/session.ts | 124 ++++++++++++++ src/lib/server/db/token.ts | 133 --------------- src/lib/server/loadenv.ts | 11 +- src/lib/server/middlewares/authenticate.ts | 34 ++++ src/lib/server/middlewares/index.ts | 2 + src/lib/server/middlewares/setAgentInfo.ts | 18 ++ src/lib/server/modules/auth.ts | 159 +++++++++++------- src/lib/server/modules/crypto.ts | 33 +++- src/lib/server/schemas/auth.ts | 12 +- src/lib/server/services/auth.ts | 153 ++++------------- src/routes/api/auth/login/+server.ts | 14 +- src/routes/api/auth/logout/+server.ts | 13 +- src/routes/api/auth/refreshToken/+server.ts | 23 --- src/routes/api/auth/upgradeSession/+server.ts | 26 +++ .../api/auth/upgradeSession/verify/+server.ts | 16 ++ src/routes/api/auth/upgradeToken/+server.ts | 25 --- .../api/auth/upgradeToken/verify/+server.ts | 33 ---- src/routes/api/client/list/+server.ts | 12 +- src/routes/api/client/register/+server.ts | 11 +- .../api/client/register/verify/+server.ts | 11 +- src/routes/api/client/status/+server.ts | 12 +- src/routes/api/directory/[id]/+server.ts | 4 +- .../api/directory/[id]/delete/+server.ts | 4 +- .../api/directory/[id]/rename/+server.ts | 4 +- src/routes/api/directory/create/+server.ts | 4 +- src/routes/api/file/[id]/+server.ts | 4 +- src/routes/api/file/[id]/delete/+server.ts | 4 +- src/routes/api/file/[id]/download/+server.ts | 4 +- src/routes/api/file/[id]/rename/+server.ts | 4 +- src/routes/api/file/upload/+server.ts | 4 +- src/routes/api/mek/list/+server.ts | 4 +- .../api/mek/register/initial/+server.ts | 9 +- 42 files changed, 487 insertions(+), 624 deletions(-) rename src/lib/server/db/schema/{token.ts => session.ts} (52%) create mode 100644 src/lib/server/db/session.ts delete mode 100644 src/lib/server/db/token.ts create mode 100644 src/lib/server/middlewares/authenticate.ts create mode 100644 src/lib/server/middlewares/index.ts create mode 100644 src/lib/server/middlewares/setAgentInfo.ts delete mode 100644 src/routes/api/auth/refreshToken/+server.ts create mode 100644 src/routes/api/auth/upgradeSession/+server.ts create mode 100644 src/routes/api/auth/upgradeSession/verify/+server.ts delete mode 100644 src/routes/api/auth/upgradeToken/+server.ts delete mode 100644 src/routes/api/auth/upgradeToken/verify/+server.ts diff --git a/.env.example b/.env.example index c0eef8e..128bd9f 100644 --- a/.env.example +++ b/.env.example @@ -1,10 +1,9 @@ # Required environment variables -JWT_SECRET= +SESSION_SECRET= # Optional environment variables DATABASE_URL= -JWT_ACCESS_TOKEN_EXPIRES= -JWT_REFRESH_TOKEN_EXPIRES= +SESSION_EXPIRES= USER_CLIENT_CHALLENGE_EXPIRES= -TOKEN_UPGRADE_CHALLENGE_EXPIRES= +SESSION_UPGRADE_CHALLENGE_EXPIRES= LIBRARY_PATH= diff --git a/docker-compose.yaml b/docker-compose.yaml index ffe08ab..aecd8c8 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -8,11 +8,10 @@ services: environment: # ArkVault - DATABASE_URL=/app/data/database.sqlite - - JWT_SECRET=${JWT_SECRET:?} # Required - - JWT_ACCESS_TOKEN_EXPIRES - - JWT_REFRESH_TOKEN_EXPIRES + - SESSION_SECRET=${SESSION_SECRET:?} # Required + - SESSION_EXPIRES - USER_CLIENT_CHALLENGE_EXPIRES - - TOKEN_UPGRADE_CHALLENGE_EXPIRES + - SESSION_UPGRADE_CHALLENGE_EXPIRES - LIBRARY_PATH=/app/data/library # SvelteKit - ADDRESS_HEADER=${TRUST_PROXY:+X-Forwarded-For} diff --git a/package.json b/package.json index c15ca72..41cbbde 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,6 @@ "@sveltejs/vite-plugin-svelte": "^4.0.4", "@types/better-sqlite3": "^7.6.12", "@types/file-saver": "^2.0.7", - "@types/jsonwebtoken": "^9.0.7", "@types/ms": "^0.7.34", "@types/node-schedule": "^2.1.7", "autoprefixer": "^10.4.20", @@ -53,7 +52,6 @@ "argon2": "^0.41.1", "better-sqlite3": "^11.7.2", "drizzle-orm": "^0.33.0", - "jsonwebtoken": "^9.0.2", "ms": "^2.1.3", "node-schedule": "^2.1.1", "uuid": "^11.0.4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4e3c1fe..e6e96c5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -17,9 +17,6 @@ importers: drizzle-orm: specifier: ^0.33.0 version: 0.33.0(@types/better-sqlite3@7.6.12)(better-sqlite3@11.7.2) - jsonwebtoken: - specifier: ^9.0.2 - version: 9.0.2 ms: specifier: ^2.1.3 version: 2.1.3 @@ -54,9 +51,6 @@ importers: '@types/file-saver': specifier: ^2.0.7 version: 2.0.7 - '@types/jsonwebtoken': - specifier: ^9.0.7 - version: 9.0.7 '@types/ms': specifier: ^0.7.34 version: 0.7.34 @@ -854,9 +848,6 @@ packages: '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} - '@types/jsonwebtoken@9.0.7': - resolution: {integrity: sha512-ugo316mmTYBl2g81zDFnZ7cfxlut3o+/EQdaP7J8QN2kY6lJ22hmQYCK5EHcJHbrW+dkCGSCPgbG8JtYj6qSrg==} - '@types/ms@0.7.34': resolution: {integrity: sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==} @@ -1016,9 +1007,6 @@ packages: engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true - buffer-equal-constant-time@1.0.1: - resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} - buffer-from@1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} @@ -1228,9 +1216,6 @@ packages: eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} - ecdsa-sig-formatter@1.0.11: - resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} - electron-to-chromium@1.5.79: resolution: {integrity: sha512-nYOxJNxQ9Om4EC88BE4pPoNI8xwSFf8pU/BAeOl4Hh/b/i6V4biTAzwV7pXi3ARKeoYO5JZKMIXTryXSVer5RA==} @@ -1555,16 +1540,6 @@ packages: json-stable-stringify-without-jsonify@1.0.1: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} - jsonwebtoken@9.0.2: - resolution: {integrity: sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==} - engines: {node: '>=12', npm: '>=6'} - - jwa@1.4.1: - resolution: {integrity: sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==} - - jws@3.2.2: - resolution: {integrity: sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==} - keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} @@ -1604,30 +1579,9 @@ packages: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} - lodash.includes@4.3.0: - resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==} - - lodash.isboolean@3.0.3: - resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==} - - lodash.isinteger@4.0.4: - resolution: {integrity: sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==} - - lodash.isnumber@3.0.3: - resolution: {integrity: sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==} - - lodash.isplainobject@4.0.6: - resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==} - - lodash.isstring@4.0.1: - resolution: {integrity: sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==} - lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} - lodash.once@4.1.1: - resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==} - long-timeout@0.1.1: resolution: {integrity: sha512-BFRuQUqc7x2NWxfJBCyUrN8iYUYznzL9JROmRz1gZ6KlOIgmoD+njPVbb+VNn2nGMKggMsK79iUNErillsrx7w==} @@ -2809,10 +2763,6 @@ snapshots: '@types/json-schema@7.0.15': {} - '@types/jsonwebtoken@9.0.7': - dependencies: - '@types/node': 22.10.5 - '@types/ms@0.7.34': {} '@types/node-schedule@2.1.7': @@ -3001,8 +2951,6 @@ snapshots: node-releases: 2.0.19 update-browserslist-db: 1.1.2(browserslist@4.24.4) - buffer-equal-constant-time@1.0.1: {} - buffer-from@1.1.2: {} buffer@5.7.1: @@ -3108,10 +3056,6 @@ snapshots: eastasianwidth@0.2.0: {} - ecdsa-sig-formatter@1.0.11: - dependencies: - safe-buffer: 5.2.1 - electron-to-chromium@1.5.79: {} emoji-regex@8.0.0: {} @@ -3499,30 +3443,6 @@ snapshots: json-stable-stringify-without-jsonify@1.0.1: {} - jsonwebtoken@9.0.2: - dependencies: - jws: 3.2.2 - lodash.includes: 4.3.0 - lodash.isboolean: 3.0.3 - lodash.isinteger: 4.0.4 - lodash.isnumber: 3.0.3 - lodash.isplainobject: 4.0.6 - lodash.isstring: 4.0.1 - lodash.once: 4.1.1 - ms: 2.1.3 - semver: 7.6.3 - - jwa@1.4.1: - dependencies: - buffer-equal-constant-time: 1.0.1 - ecdsa-sig-formatter: 1.0.11 - safe-buffer: 5.2.1 - - jws@3.2.2: - dependencies: - jwa: 1.4.1 - safe-buffer: 5.2.1 - keyv@4.5.4: dependencies: json-buffer: 3.0.1 @@ -3555,22 +3475,8 @@ snapshots: dependencies: p-locate: 5.0.0 - lodash.includes@4.3.0: {} - - lodash.isboolean@3.0.3: {} - - lodash.isinteger@4.0.4: {} - - lodash.isnumber@3.0.3: {} - - lodash.isplainobject@4.0.6: {} - - lodash.isstring@4.0.1: {} - lodash.merge@4.6.2: {} - lodash.once@4.1.1: {} - long-timeout@0.1.1: {} lru-cache@10.4.3: {} diff --git a/src/app.d.ts b/src/app.d.ts index 0904582..bfe1252 100644 --- a/src/app.d.ts +++ b/src/app.d.ts @@ -5,11 +5,15 @@ import "unplugin-icons/types/svelte"; declare global { namespace App { - // interface Error {} - // interface Locals {} - // interface PageData {} - // interface PageState {} - // interface Platform {} + interface Locals { + ip: string; + userAgent: string; + session?: { + id: string; + userId: number; + clientId?: number; + }; + } } } diff --git a/src/hooks.server.ts b/src/hooks.server.ts index e237845..9dac88c 100644 --- a/src/hooks.server.ts +++ b/src/hooks.server.ts @@ -1,34 +1,22 @@ -import { redirect, type ServerInit, type Handle } from "@sveltejs/kit"; +import type { ServerInit } from "@sveltejs/kit"; +import { sequence } from "@sveltejs/kit/hooks"; import schedule from "node-schedule"; import { cleanupExpiredUserClientChallenges } from "$lib/server/db/client"; import { migrateDB } from "$lib/server/db/drizzle"; import { - cleanupExpiredRefreshTokens, - cleanupExpiredTokenUpgradeChallenges, -} from "$lib/server/db/token"; + cleanupExpiredSessions, + cleanupExpiredSessionUpgradeChallenges, +} from "$lib/server/db/session"; +import { authenticate, setAgentInfo } from "$lib/server/middlewares"; export const init: ServerInit = () => { migrateDB(); schedule.scheduleJob("0 * * * *", () => { cleanupExpiredUserClientChallenges(); - cleanupExpiredRefreshTokens(); - cleanupExpiredTokenUpgradeChallenges(); + cleanupExpiredSessions(); + cleanupExpiredSessionUpgradeChallenges(); }); }; -export const handle: Handle = async ({ event, resolve }) => { - if (["/api", "/auth"].some((path) => event.url.pathname.startsWith(path))) { - return await resolve(event); - } - - const accessToken = event.cookies.get("accessToken"); - if (accessToken) { - return await resolve(event); - } else { - redirect( - 302, - "/auth/login?redirect=" + encodeURIComponent(event.url.pathname + event.url.search), - ); - } -}; +export const handle = sequence(setAgentInfo, authenticate); diff --git a/src/lib/server/db/error.ts b/src/lib/server/db/error.ts index 7644800..beadb6f 100644 --- a/src/lib/server/db/error.ts +++ b/src/lib/server/db/error.ts @@ -1,4 +1,6 @@ type IntegrityErrorMessages = + // Challenge + | "Challenge already registered" // Client | "Public key(s) already registered" | "User client already exists" @@ -9,9 +11,9 @@ type IntegrityErrorMessages = // MEK | "MEK already registered" | "Inactive MEK version" - // Token - | "Refresh token not found" - | "Refresh token already registered"; + // Session + | "Session not found" + | "Session already exists"; export class IntegrityError extends Error { constructor(public message: IntegrityErrorMessages) { diff --git a/src/lib/server/db/schema/client.ts b/src/lib/server/db/schema/client.ts index 437695d..eacd9c9 100644 --- a/src/lib/server/db/schema/client.ts +++ b/src/lib/server/db/schema/client.ts @@ -39,7 +39,7 @@ export const userClientChallenge = sqliteTable("user_client_challenge", { clientId: integer("client_id") .notNull() .references(() => client.id), - answer: text("challenge").notNull().unique(), // Base64 + answer: text("answer").notNull().unique(), // Base64 allowedIp: text("allowed_ip").notNull(), expiresAt: integer("expires_at", { mode: "timestamp_ms" }).notNull(), isUsed: integer("is_used", { mode: "boolean" }).notNull().default(false), diff --git a/src/lib/server/db/schema/index.ts b/src/lib/server/db/schema/index.ts index 4344b00..41fd4fe 100644 --- a/src/lib/server/db/schema/index.ts +++ b/src/lib/server/db/schema/index.ts @@ -1,5 +1,5 @@ export * from "./client"; export * from "./file"; export * from "./mek"; -export * from "./token"; +export * from "./session"; export * from "./user"; diff --git a/src/lib/server/db/schema/token.ts b/src/lib/server/db/schema/session.ts similarity index 52% rename from src/lib/server/db/schema/token.ts rename to src/lib/server/db/schema/session.ts index 72106d7..5f2129d 100644 --- a/src/lib/server/db/schema/token.ts +++ b/src/lib/server/db/schema/session.ts @@ -2,31 +2,34 @@ import { sqliteTable, text, integer, unique } from "drizzle-orm/sqlite-core"; import { client } from "./client"; import { user } from "./user"; -export const refreshToken = sqliteTable( - "refresh_token", +export const session = sqliteTable( + "session", { - id: text("id").primaryKey(), + id: text("id").notNull().primaryKey(), userId: integer("user_id") .notNull() .references(() => user.id), clientId: integer("client_id").references(() => client.id), - expiresAt: integer("expires_at", { mode: "timestamp_ms" }).notNull(), // Only used for cleanup + createdAt: integer("created_at", { mode: "timestamp_ms" }).notNull(), + lastUsedAt: integer("last_used_at", { mode: "timestamp_ms" }).notNull(), + lastUsedByIp: text("last_used_by_ip"), + lastUsedByUserAgent: text("last_used_by_user_agent"), }, (t) => ({ unq: unique().on(t.userId, t.clientId), }), ); -export const tokenUpgradeChallenge = sqliteTable("token_upgrade_challenge", { +export const sessionUpgradeChallenge = sqliteTable("session_upgrade_challenge", { id: integer("id").primaryKey(), - refreshTokenId: text("refresh_token_id") + sessionId: text("session_id") .notNull() - .references(() => refreshToken.id), + .references(() => session.id) + .unique(), clientId: integer("client_id") .notNull() .references(() => client.id), - answer: text("challenge").notNull().unique(), // Base64 + answer: text("answer").notNull().unique(), // Base64 allowedIp: text("allowed_ip").notNull(), expiresAt: integer("expires_at", { mode: "timestamp_ms" }).notNull(), - isUsed: integer("is_used", { mode: "boolean" }).notNull().default(false), }); diff --git a/src/lib/server/db/session.ts b/src/lib/server/db/session.ts new file mode 100644 index 0000000..276090a --- /dev/null +++ b/src/lib/server/db/session.ts @@ -0,0 +1,124 @@ +import { SqliteError } from "better-sqlite3"; +import { and, eq, gt, lte, isNull } from "drizzle-orm"; +import env from "$lib/server/loadenv"; +import db from "./drizzle"; +import { IntegrityError } from "./error"; +import { session, sessionUpgradeChallenge } from "./schema"; + +export const createSession = async ( + userId: number, + clientId: number | null, + sessionId: string, + ip: string | null, + userAgent: string | null, +) => { + try { + const now = new Date(); + await db.insert(session).values({ + id: sessionId, + userId, + clientId, + createdAt: now, + lastUsedAt: now, + lastUsedByIp: ip, + lastUsedByUserAgent: userAgent, + }); + } catch (e) { + if (e instanceof SqliteError && e.code === "SQLITE_CONSTRAINT_UNIQUE") { + throw new IntegrityError("Session already exists"); + } + throw e; + } +}; + +export const refreshSession = async ( + sessionId: string, + ip: string | null, + userAgent: string | null, +) => { + const now = new Date(); + const sessions = await db + .update(session) + .set({ + lastUsedAt: now, + lastUsedByIp: ip, + lastUsedByUserAgent: userAgent, + }) + .where( + and( + eq(session.id, sessionId), + gt(session.lastUsedAt, new Date(now.getTime() - env.session.exp)), + ), + ) + .returning({ userId: session.userId, clientId: session.clientId }); + if (!sessions[0]) { + throw new IntegrityError("Session not found"); + } + return sessions[0]; +}; + +export const upgradeSession = async (sessionId: string, clientId: number) => { + const res = await db + .update(session) + .set({ clientId }) + .where(and(eq(session.id, sessionId), isNull(session.clientId))); + if (res.changes === 0) { + throw new IntegrityError("Session not found"); + } +}; + +export const deleteSession = async (sessionId: string) => { + await db.delete(session).where(eq(session.id, sessionId)); +}; + +export const cleanupExpiredSessions = async () => { + await db.delete(session).where(lte(session.lastUsedAt, new Date(Date.now() - env.session.exp))); +}; + +export const registerSessionUpgradeChallenge = async ( + sessionId: string, + clientId: number, + answer: string, + allowedIp: string, + expiresAt: Date, +) => { + try { + await db.insert(sessionUpgradeChallenge).values({ + sessionId, + clientId, + answer, + allowedIp, + expiresAt, + }); + } catch (e) { + if (e instanceof SqliteError && e.code === "SQLITE_CONSTRAINT_UNIQUE") { + throw new IntegrityError("Challenge already registered"); + } + throw e; + } +}; + +export const consumeSessionUpgradeChallenge = async ( + sessionId: string, + answer: string, + ip: string, +) => { + const challenges = await db + .delete(sessionUpgradeChallenge) + .where( + and( + eq(sessionUpgradeChallenge.sessionId, sessionId), + eq(sessionUpgradeChallenge.answer, answer), + eq(sessionUpgradeChallenge.allowedIp, ip), + gt(sessionUpgradeChallenge.expiresAt, new Date()), + ), + ) + .returning({ clientId: sessionUpgradeChallenge.clientId }); + return challenges[0] ?? null; +}; + +export const cleanupExpiredSessionUpgradeChallenges = async () => { + await db + .delete(sessionUpgradeChallenge) + .where(lte(sessionUpgradeChallenge.expiresAt, new Date())); +}; diff --git a/src/lib/server/db/token.ts b/src/lib/server/db/token.ts deleted file mode 100644 index 25bf1de..0000000 --- a/src/lib/server/db/token.ts +++ /dev/null @@ -1,133 +0,0 @@ -import { SqliteError } from "better-sqlite3"; -import { and, eq, gt, lte } from "drizzle-orm"; -import env from "$lib/server/loadenv"; -import db from "./drizzle"; -import { IntegrityError } from "./error"; -import { refreshToken, tokenUpgradeChallenge } from "./schema"; - -const expiresAt = () => new Date(Date.now() + env.jwt.refreshExp); - -export const registerRefreshToken = async ( - userId: number, - clientId: number | null, - tokenId: string, -) => { - try { - await db.insert(refreshToken).values({ - id: tokenId, - userId, - clientId, - expiresAt: expiresAt(), - }); - } catch (e) { - if (e instanceof SqliteError && e.code === "SQLITE_CONSTRAINT_UNIQUE") { - throw new IntegrityError("Refresh token already registered"); - } - throw e; - } -}; - -export const getRefreshToken = async (tokenId: string) => { - const tokens = await db.select().from(refreshToken).where(eq(refreshToken.id, tokenId)).limit(1); - return tokens[0] ?? null; -}; - -export const rotateRefreshToken = async (oldTokenId: string, newTokenId: string) => { - await db.transaction( - async (tx) => { - await tx - .delete(tokenUpgradeChallenge) - .where(eq(tokenUpgradeChallenge.refreshTokenId, oldTokenId)); - - const res = await tx - .update(refreshToken) - .set({ - id: newTokenId, - expiresAt: expiresAt(), - }) - .where(eq(refreshToken.id, oldTokenId)); - if (res.changes === 0) { - throw new IntegrityError("Refresh token not found"); - } - }, - { behavior: "exclusive" }, - ); -}; - -export const upgradeRefreshToken = async ( - oldTokenId: string, - newTokenId: string, - clientId: number, -) => { - await db.transaction( - async (tx) => { - await tx - .delete(tokenUpgradeChallenge) - .where(eq(tokenUpgradeChallenge.refreshTokenId, oldTokenId)); - - const res = await tx - .update(refreshToken) - .set({ - id: newTokenId, - clientId, - expiresAt: expiresAt(), - }) - .where(eq(refreshToken.id, oldTokenId)); - if (res.changes === 0) { - throw new IntegrityError("Refresh token not found"); - } - }, - { behavior: "exclusive" }, - ); -}; - -export const revokeRefreshToken = async (tokenId: string) => { - await db.delete(refreshToken).where(eq(refreshToken.id, tokenId)); -}; - -export const cleanupExpiredRefreshTokens = async () => { - await db.delete(refreshToken).where(lte(refreshToken.expiresAt, new Date())); -}; - -export const registerTokenUpgradeChallenge = async ( - tokenId: string, - clientId: number, - answer: string, - allowedIp: string, - expiresAt: Date, -) => { - await db.insert(tokenUpgradeChallenge).values({ - refreshTokenId: tokenId, - clientId, - answer, - allowedIp, - expiresAt, - }); -}; - -export const getTokenUpgradeChallenge = async (answer: string, ip: string) => { - const challenges = await db - .select() - .from(tokenUpgradeChallenge) - .where( - and( - eq(tokenUpgradeChallenge.answer, answer), - eq(tokenUpgradeChallenge.allowedIp, ip), - gt(tokenUpgradeChallenge.expiresAt, new Date()), - eq(tokenUpgradeChallenge.isUsed, false), - ), - ) - .limit(1); - return challenges[0] ?? null; -}; - -export const markTokenUpgradeChallengeAsUsed = async (id: number) => { - await db - .update(tokenUpgradeChallenge) - .set({ isUsed: true }) - .where(eq(tokenUpgradeChallenge.id, id)); -}; - -export const cleanupExpiredTokenUpgradeChallenges = async () => { - await db.delete(tokenUpgradeChallenge).where(lte(tokenUpgradeChallenge.expiresAt, new Date())); -}; diff --git a/src/lib/server/loadenv.ts b/src/lib/server/loadenv.ts index a57eff8..01e442a 100644 --- a/src/lib/server/loadenv.ts +++ b/src/lib/server/loadenv.ts @@ -3,19 +3,18 @@ import { building } from "$app/environment"; import { env } from "$env/dynamic/private"; if (!building) { - if (!env.JWT_SECRET) throw new Error("JWT_SECRET is not set"); + if (!env.SESSION_SECRET) throw new Error("SESSION_SECRET not set"); } export default { databaseUrl: env.DATABASE_URL || "local.db", - jwt: { - secret: env.JWT_SECRET, - accessExp: ms(env.JWT_ACCESS_TOKEN_EXPIRES || "5m"), - refreshExp: ms(env.JWT_REFRESH_TOKEN_EXPIRES || "14d"), + session: { + secret: env.SESSION_SECRET!, + exp: ms(env.SESSION_EXPIRES || "14d"), }, challenge: { userClientExp: ms(env.USER_CLIENT_CHALLENGE_EXPIRES || "5m"), - tokenUpgradeExp: ms(env.TOKEN_UPGRADE_CHALLENGE_EXPIRES || "5m"), + sessionUpgradeExp: ms(env.SESSION_UPGRADE_CHALLENGE_EXPIRES || "5m"), }, libraryPath: env.LIBRARY_PATH || "library", }; diff --git a/src/lib/server/middlewares/authenticate.ts b/src/lib/server/middlewares/authenticate.ts new file mode 100644 index 0000000..f484578 --- /dev/null +++ b/src/lib/server/middlewares/authenticate.ts @@ -0,0 +1,34 @@ +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))) { + return await resolve(event); + } + + try { + const sessionIdSigned = event.cookies.get("sessionId"); + if (!sessionIdSigned) { + throw new AuthenticationError(401, "Session id not found"); + } + + const { ip, userAgent } = event.locals; + event.locals.session = await authenticate(sessionIdSigned, ip, userAgent); + } catch (e) { + if (e instanceof AuthenticationError) { + if (pathname.startsWith("/api")) { + error(e.status, e.message); + } else { + redirect(302, "/auth/login?redirect=" + encodeURIComponent(pathname + search)); + } + } + throw e; + } + + return await resolve(event); +}; + +export default authenticateMiddleware; diff --git a/src/lib/server/middlewares/index.ts b/src/lib/server/middlewares/index.ts new file mode 100644 index 0000000..4af122e --- /dev/null +++ b/src/lib/server/middlewares/index.ts @@ -0,0 +1,2 @@ +export { default as authenticate } from "./authenticate"; +export { default as setAgentInfo } from "./setAgentInfo"; diff --git a/src/lib/server/middlewares/setAgentInfo.ts b/src/lib/server/middlewares/setAgentInfo.ts new file mode 100644 index 0000000..d272f2a --- /dev/null +++ b/src/lib/server/middlewares/setAgentInfo.ts @@ -0,0 +1,18 @@ +import { error, type Handle } from "@sveltejs/kit"; + +export const setAgentInfoMiddleware: Handle = async ({ event, resolve }) => { + const ip = event.getClientAddress(); + const userAgent = event.request.headers.get("User-Agent"); + if (!ip) { + error(500, "IP address not found"); + } else if (!userAgent) { + error(400, "User agent not found"); + } + + event.locals.ip = ip; + event.locals.userAgent = userAgent; + + return await resolve(event); +}; + +export default setAgentInfoMiddleware; diff --git a/src/lib/server/modules/auth.ts b/src/lib/server/modules/auth.ts index 37248ed..4e03783 100644 --- a/src/lib/server/modules/auth.ts +++ b/src/lib/server/modules/auth.ts @@ -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; export async function authorize( - cookies: Cookies, + locals: App.Locals, + requiredPermission: "notClient", +): Promise; + +export async function authorize( + locals: App.Locals, + requiredPermission: "anyClient", +): Promise; + +export async function authorize( + locals: App.Locals, requiredPermission: "pendingClient", -): Promise<{ userId: number; clientId: number }>; +): Promise; export async function authorize( - cookies: Cookies, + locals: App.Locals, requiredPermission: "activeClient", -): Promise<{ userId: number; clientId: number }>; +): Promise; 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 { + 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 }; } diff --git a/src/lib/server/modules/crypto.ts b/src/lib/server/modules/crypto.ts index de3dbf4..6bd7898 100644 --- a/src/lib/server/modules/crypto.ts +++ b/src/lib/server/modules/crypto.ts @@ -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; + } +}; diff --git a/src/lib/server/schemas/auth.ts b/src/lib/server/schemas/auth.ts index 10c8fcc..91858c9 100644 --- a/src/lib/server/schemas/auth.ts +++ b/src/lib/server/schemas/auth.ts @@ -6,19 +6,19 @@ export const loginRequest = z.object({ }); export type LoginRequest = z.infer; -export const tokenUpgradeRequest = z.object({ +export const sessionUpgradeRequest = z.object({ encPubKey: z.string().base64().nonempty(), sigPubKey: z.string().base64().nonempty(), }); -export type TokenUpgradeRequest = z.infer; +export type SessionUpgradeRequest = z.infer; -export const tokenUpgradeResponse = z.object({ +export const sessionUpgradeResponse = z.object({ challenge: z.string().base64().nonempty(), }); -export type TokenUpgradeResponse = z.infer; +export type SessionUpgradeResponse = z.infer; -export const tokenUpgradeVerifyRequest = z.object({ +export const sessionUpgradeVerifyRequest = z.object({ answer: z.string().base64().nonempty(), answerSig: z.string().base64().nonempty(), }); -export type TokenUpgradeVerifyRequest = z.infer; +export type SessionUpgradeVerifyRequest = z.infer; diff --git a/src/lib/server/services/auth.ts b/src/lib/server/services/auth.ts index 36a3c5a..c3fee31 100644 --- a/src/lib/server/services/auth.ts +++ b/src/lib/server/services/auth.ts @@ -1,131 +1,49 @@ import { error } from "@sveltejs/kit"; import argon2 from "argon2"; -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 { IntegrityError } from "$lib/server/db/error"; import { - registerRefreshToken, - getRefreshToken, - rotateRefreshToken, - upgradeRefreshToken, - revokeRefreshToken, - registerTokenUpgradeChallenge, - getTokenUpgradeChallenge, - markTokenUpgradeChallengeAsUsed, -} from "$lib/server/db/token"; -import { issueToken, verifyToken, TokenError } from "$lib/server/modules/auth"; + upgradeSession, + deleteSession, + registerSessionUpgradeChallenge, + consumeSessionUpgradeChallenge, +} from "$lib/server/db/session"; +import { getUserByEmail } from "$lib/server/db/user"; +import env from "$lib/server/loadenv"; +import { startSession } 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 }); - - try { - await registerRefreshToken(userId, clientId ?? null, jti); - return token; - } catch (e) { - if (e instanceof IntegrityError && e.message === "Refresh token already registered") { - error(409, "Already logged in"); - } - throw e; - } -}; - -export const login = async (email: string, password: string) => { +export const login = async (email: string, password: string, ip: string, userAgent: 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, "Invalid refresh token"); - } - - 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(); - try { - await rotateRefreshToken(oldJti, newJti); - return { - accessToken: issueAccessToken(userId, clientId), - refreshToken: issueToken({ type: "refresh", jti: newJti }), - }; + return { sessionIdSigned: await startSession(user.id, ip, userAgent) }; } catch (e) { - if (e instanceof IntegrityError && e.message === "Refresh token not found") { - error(500, "Invalid refresh token"); + if (e instanceof IntegrityError && e.message === "Session already exists") { + error(403, "Already logged in"); } throw e; } }; -const expiresAt = () => new Date(Date.now() + env.challenge.tokenUpgradeExp); - -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 logout = async (sessionId: string) => { + await deleteSession(sessionId); }; -export const createTokenUpgradeChallenge = async ( - refreshToken: string, +export const createSessionUpgradeChallenge = async ( + sessionId: string, + userId: number, 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) { @@ -134,29 +52,29 @@ export const createTokenUpgradeChallenge = async ( error(403, "Unregistered client"); } - return { challenge: await createChallenge(ip, jti, client.id, encPubKey) }; + const { answer, challenge } = await generateChallenge(32, encPubKey); + await registerSessionUpgradeChallenge( + sessionId, + client.id, + answer.toString("base64"), + ip, + new Date(Date.now() + env.challenge.sessionUpgradeExp), + ); + + return { challenge: challenge.toString("base64") }; }; -export const upgradeToken = async ( - refreshToken: string, +export const verifySessionUpgradeChallenge = async ( + sessionId: string, ip: string, answer: string, answerSig: string, ) => { - const { jti: oldJti, userId, clientId } = await verifyRefreshToken(refreshToken); - if (clientId) { - error(403, "Forbidden"); - } - - const challenge = await getTokenUpgradeChallenge(answer, ip); + const challenge = await consumeSessionUpgradeChallenge(sessionId, answer, ip); if (!challenge) { error(403, "Invalid challenge answer"); - } else if (challenge.refreshTokenId !== oldJti) { - error(403, "Forbidden"); } - await markTokenUpgradeChallengeAsUsed(challenge.id); - const client = await getClient(challenge.clientId); if (!client) { error(500, "Invalid challenge answer"); @@ -165,15 +83,10 @@ export const upgradeToken = async ( } try { - const newJti = uuidv4(); - await upgradeRefreshToken(oldJti, newJti, client.id); - return { - accessToken: issueAccessToken(userId, client.id), - refreshToken: issueToken({ type: "refresh", jti: newJti }), - }; + await upgradeSession(sessionId, client.id); } catch (e) { - if (e instanceof IntegrityError && e.message === "Refresh token not found") { - error(500, "Invalid refresh token"); + if (e instanceof IntegrityError && e.message === "Session not found") { + error(500, "Invalid challenge answer"); } throw e; } diff --git a/src/routes/api/auth/login/+server.ts b/src/routes/api/auth/login/+server.ts index 479f561..d748f6a 100644 --- a/src/routes/api/auth/login/+server.ts +++ b/src/routes/api/auth/login/+server.ts @@ -4,20 +4,16 @@ import { loginRequest } from "$lib/server/schemas"; import { login } from "$lib/server/services/auth"; import type { RequestHandler } from "./$types"; -export const POST: RequestHandler = async ({ request, cookies }) => { +export const POST: RequestHandler = async ({ locals, request, cookies }) => { const zodRes = loginRequest.safeParse(await request.json()); if (!zodRes.success) error(400, "Invalid request body"); const { email, password } = zodRes.data; - const { accessToken, refreshToken } = await login(email, password); - cookies.set("accessToken", accessToken, { + const { sessionIdSigned } = await login(email, password, locals.ip, locals.userAgent); + cookies.set("sessionId", sessionIdSigned, { path: "/", - maxAge: env.jwt.accessExp / 1000, - sameSite: "strict", - }); - cookies.set("refreshToken", refreshToken, { - path: "/api/auth", - maxAge: env.jwt.refreshExp / 1000, + maxAge: env.session.exp / 1000, + secure: true, sameSite: "strict", }); diff --git a/src/routes/api/auth/logout/+server.ts b/src/routes/api/auth/logout/+server.ts index f9f0ea6..b5c1f11 100644 --- a/src/routes/api/auth/logout/+server.ts +++ b/src/routes/api/auth/logout/+server.ts @@ -1,14 +1,13 @@ -import { error, text } from "@sveltejs/kit"; +import { text } from "@sveltejs/kit"; +import { authorize } from "$lib/server/modules/auth"; import { logout } from "$lib/server/services/auth"; import type { RequestHandler } from "./$types"; -export const POST: RequestHandler = async ({ cookies }) => { - const token = cookies.get("refreshToken"); - if (!token) error(401, "Refresh token not found"); +export const POST: RequestHandler = async ({ locals, cookies }) => { + const { sessionId } = await authorize(locals, "any"); - await logout(token); - cookies.delete("accessToken", { path: "/" }); - cookies.delete("refreshToken", { path: "/api/auth" }); + await logout(sessionId); + cookies.delete("sessionId", { path: "/" }); return text("Logged out", { headers: { "Content-Type": "text/plain" } }); }; diff --git a/src/routes/api/auth/refreshToken/+server.ts b/src/routes/api/auth/refreshToken/+server.ts deleted file mode 100644 index 374fd8c..0000000 --- a/src/routes/api/auth/refreshToken/+server.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { error, text } from "@sveltejs/kit"; -import env from "$lib/server/loadenv"; -import { refreshToken as doRefreshToken } from "$lib/server/services/auth"; -import type { RequestHandler } from "./$types"; - -export const POST: RequestHandler = async ({ cookies }) => { - const token = cookies.get("refreshToken"); - if (!token) error(401, "Refresh token not found"); - - const { accessToken, refreshToken } = await doRefreshToken(token); - cookies.set("accessToken", accessToken, { - path: "/", - maxAge: env.jwt.accessExp / 1000, - sameSite: "strict", - }); - cookies.set("refreshToken", refreshToken, { - path: "/api/auth", - maxAge: env.jwt.refreshExp / 1000, - sameSite: "strict", - }); - - return text("Token refreshed", { headers: { "Content-Type": "text/plain" } }); -}; diff --git a/src/routes/api/auth/upgradeSession/+server.ts b/src/routes/api/auth/upgradeSession/+server.ts new file mode 100644 index 0000000..760f4c0 --- /dev/null +++ b/src/routes/api/auth/upgradeSession/+server.ts @@ -0,0 +1,26 @@ +import { error, json } from "@sveltejs/kit"; +import { authorize } from "$lib/server/modules/auth"; +import { + sessionUpgradeRequest, + sessionUpgradeResponse, + type SessionUpgradeResponse, +} from "$lib/server/schemas"; +import { createSessionUpgradeChallenge } from "$lib/server/services/auth"; +import type { RequestHandler } from "./$types"; + +export const POST: RequestHandler = async ({ locals, request }) => { + const { sessionId, userId } = await authorize(locals, "notClient"); + + const zodRes = sessionUpgradeRequest.safeParse(await request.json()); + if (!zodRes.success) error(400, "Invalid request body"); + const { encPubKey, sigPubKey } = zodRes.data; + + const { challenge } = await createSessionUpgradeChallenge( + sessionId, + userId, + locals.ip, + encPubKey, + sigPubKey, + ); + return json(sessionUpgradeResponse.parse({ challenge } satisfies SessionUpgradeResponse)); +}; diff --git a/src/routes/api/auth/upgradeSession/verify/+server.ts b/src/routes/api/auth/upgradeSession/verify/+server.ts new file mode 100644 index 0000000..82cb315 --- /dev/null +++ b/src/routes/api/auth/upgradeSession/verify/+server.ts @@ -0,0 +1,16 @@ +import { error, text } from "@sveltejs/kit"; +import { authorize } from "$lib/server/modules/auth"; +import { sessionUpgradeVerifyRequest } from "$lib/server/schemas"; +import { verifySessionUpgradeChallenge } from "$lib/server/services/auth"; +import type { RequestHandler } from "./$types"; + +export const POST: RequestHandler = async ({ locals, request }) => { + const { sessionId } = await authorize(locals, "notClient"); + + const zodRes = sessionUpgradeVerifyRequest.safeParse(await request.json()); + if (!zodRes.success) error(400, "Invalid request body"); + const { answer, answerSig } = zodRes.data; + + await verifySessionUpgradeChallenge(sessionId, locals.ip, answer, answerSig); + return text("Session upgraded", { headers: { "Content-Type": "text/plain" } }); +}; diff --git a/src/routes/api/auth/upgradeToken/+server.ts b/src/routes/api/auth/upgradeToken/+server.ts deleted file mode 100644 index cb09582..0000000 --- a/src/routes/api/auth/upgradeToken/+server.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { error, json } from "@sveltejs/kit"; -import { - tokenUpgradeRequest, - tokenUpgradeResponse, - type TokenUpgradeResponse, -} from "$lib/server/schemas"; -import { createTokenUpgradeChallenge } from "$lib/server/services/auth"; -import type { RequestHandler } from "./$types"; - -export const POST: RequestHandler = async ({ request, cookies, getClientAddress }) => { - const token = cookies.get("refreshToken"); - if (!token) error(401, "Refresh token not found"); - - const zodRes = tokenUpgradeRequest.safeParse(await request.json()); - if (!zodRes.success) error(400, "Invalid request body"); - const { encPubKey, sigPubKey } = zodRes.data; - - const { challenge } = await createTokenUpgradeChallenge( - token, - getClientAddress(), - encPubKey, - sigPubKey, - ); - return json(tokenUpgradeResponse.parse({ challenge } satisfies TokenUpgradeResponse)); -}; diff --git a/src/routes/api/auth/upgradeToken/verify/+server.ts b/src/routes/api/auth/upgradeToken/verify/+server.ts deleted file mode 100644 index eb78286..0000000 --- a/src/routes/api/auth/upgradeToken/verify/+server.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { error, text } from "@sveltejs/kit"; -import env from "$lib/server/loadenv"; -import { tokenUpgradeVerifyRequest } from "$lib/server/schemas"; -import { upgradeToken } from "$lib/server/services/auth"; -import type { RequestHandler } from "./$types"; - -export const POST: RequestHandler = async ({ request, cookies, getClientAddress }) => { - const token = cookies.get("refreshToken"); - if (!token) error(401, "Refresh token not found"); - - const zodRes = tokenUpgradeVerifyRequest.safeParse(await request.json()); - if (!zodRes.success) error(400, "Invalid request body"); - const { answer, answerSig } = zodRes.data; - - const { accessToken, refreshToken } = await upgradeToken( - token, - getClientAddress(), - answer, - answerSig, - ); - cookies.set("accessToken", accessToken, { - path: "/", - maxAge: env.jwt.accessExp / 1000, - sameSite: "strict", - }); - cookies.set("refreshToken", refreshToken, { - path: "/api/auth", - maxAge: env.jwt.refreshExp / 1000, - sameSite: "strict", - }); - - return text("Token upgraded", { headers: { "Content-Type": "text/plain" } }); -}; diff --git a/src/routes/api/client/list/+server.ts b/src/routes/api/client/list/+server.ts index 5354ece..78193ee 100644 --- a/src/routes/api/client/list/+server.ts +++ b/src/routes/api/client/list/+server.ts @@ -1,15 +1,11 @@ -import { error, json } from "@sveltejs/kit"; -import { authenticate } from "$lib/server/modules/auth"; +import { json } from "@sveltejs/kit"; +import { authorize } from "$lib/server/modules/auth"; import { clientListResponse, type ClientListResponse } from "$lib/server/schemas"; import { getUserClientList } from "$lib/server/services/client"; import type { RequestHandler } from "./$types"; -export const GET: RequestHandler = async ({ cookies }) => { - const { userId, clientId } = authenticate(cookies); - if (!clientId) { - error(403, "Forbidden"); - } - +export const GET: RequestHandler = async ({ locals }) => { + const { userId } = await authorize(locals, "anyClient"); const { userClients } = await getUserClientList(userId); return json(clientListResponse.parse({ clients: userClients } satisfies ClientListResponse)); }; diff --git a/src/routes/api/client/register/+server.ts b/src/routes/api/client/register/+server.ts index 0a6e5a0..d6aa4ce 100644 --- a/src/routes/api/client/register/+server.ts +++ b/src/routes/api/client/register/+server.ts @@ -1,5 +1,5 @@ import { error, json } from "@sveltejs/kit"; -import { authenticate } from "$lib/server/modules/auth"; +import { authorize } from "$lib/server/modules/auth"; import { clientRegisterRequest, clientRegisterResponse, @@ -8,16 +8,13 @@ import { import { registerUserClient } from "$lib/server/services/client"; import type { RequestHandler } from "./$types"; -export const POST: RequestHandler = async ({ request, cookies, getClientAddress }) => { - const { userId, clientId } = authenticate(cookies); - if (clientId) { - error(403, "Forbidden"); - } +export const POST: RequestHandler = async ({ locals, request }) => { + const { userId } = await authorize(locals, "notClient"); const zodRes = clientRegisterRequest.safeParse(await request.json()); if (!zodRes.success) error(400, "Invalid request body"); const { encPubKey, sigPubKey } = zodRes.data; - const { challenge } = await registerUserClient(userId, getClientAddress(), encPubKey, sigPubKey); + const { challenge } = await registerUserClient(userId, locals.ip, encPubKey, sigPubKey); return json(clientRegisterResponse.parse({ challenge } satisfies ClientRegisterResponse)); }; diff --git a/src/routes/api/client/register/verify/+server.ts b/src/routes/api/client/register/verify/+server.ts index e48b454..32d5214 100644 --- a/src/routes/api/client/register/verify/+server.ts +++ b/src/routes/api/client/register/verify/+server.ts @@ -1,19 +1,16 @@ import { error, text } from "@sveltejs/kit"; -import { authenticate } from "$lib/server/modules/auth"; +import { authorize } from "$lib/server/modules/auth"; import { clientRegisterVerifyRequest } from "$lib/server/schemas"; import { verifyUserClient } from "$lib/server/services/client"; import type { RequestHandler } from "./$types"; -export const POST: RequestHandler = async ({ request, cookies, getClientAddress }) => { - const { userId, clientId } = authenticate(cookies); - if (clientId) { - error(403, "Forbidden"); - } +export const POST: RequestHandler = async ({ locals, request }) => { + const { userId } = await authorize(locals, "notClient"); const zodRes = clientRegisterVerifyRequest.safeParse(await request.json()); if (!zodRes.success) error(400, "Invalid request body"); const { answer, answerSig } = zodRes.data; - await verifyUserClient(userId, getClientAddress(), answer, answerSig); + await verifyUserClient(userId, locals.ip, answer, answerSig); return text("Client verified", { headers: { "Content-Type": "text/plain" } }); }; diff --git a/src/routes/api/client/status/+server.ts b/src/routes/api/client/status/+server.ts index a1e9cd8..a7ecc82 100644 --- a/src/routes/api/client/status/+server.ts +++ b/src/routes/api/client/status/+server.ts @@ -1,15 +1,11 @@ -import { error, json } from "@sveltejs/kit"; -import { authenticate } from "$lib/server/modules/auth"; +import { json } from "@sveltejs/kit"; +import { authorize } from "$lib/server/modules/auth"; import { clientStatusResponse, type ClientStatusResponse } from "$lib/server/schemas"; import { getUserClientStatus } from "$lib/server/services/client"; import type { RequestHandler } from "./$types"; -export const GET: RequestHandler = async ({ cookies }) => { - const { userId, clientId } = authenticate(cookies); - if (!clientId) { - error(403, "Forbidden"); - } - +export const GET: RequestHandler = async ({ locals }) => { + const { userId, clientId } = await authorize(locals, "anyClient"); const { state, isInitialMekNeeded } = await getUserClientStatus(userId, clientId); return json( clientStatusResponse.parse({ diff --git a/src/routes/api/directory/[id]/+server.ts b/src/routes/api/directory/[id]/+server.ts index 7cd1d09..1b50018 100644 --- a/src/routes/api/directory/[id]/+server.ts +++ b/src/routes/api/directory/[id]/+server.ts @@ -5,8 +5,8 @@ import { directoryInfoResponse, type DirectoryInfoResponse } from "$lib/server/s import { getDirectoryInformation } from "$lib/server/services/directory"; import type { RequestHandler } from "./$types"; -export const GET: RequestHandler = async ({ cookies, params }) => { - const { userId } = await authorize(cookies, "activeClient"); +export const GET: RequestHandler = async ({ locals, params }) => { + const { userId } = await authorize(locals, "activeClient"); const zodRes = z .object({ diff --git a/src/routes/api/directory/[id]/delete/+server.ts b/src/routes/api/directory/[id]/delete/+server.ts index c7777df..4873912 100644 --- a/src/routes/api/directory/[id]/delete/+server.ts +++ b/src/routes/api/directory/[id]/delete/+server.ts @@ -4,8 +4,8 @@ import { authorize } from "$lib/server/modules/auth"; import { deleteDirectory } from "$lib/server/services/directory"; import type { RequestHandler } from "./$types"; -export const POST: RequestHandler = async ({ cookies, params }) => { - const { userId } = await authorize(cookies, "activeClient"); +export const POST: RequestHandler = async ({ locals, params }) => { + const { userId } = await authorize(locals, "activeClient"); const zodRes = z .object({ diff --git a/src/routes/api/directory/[id]/rename/+server.ts b/src/routes/api/directory/[id]/rename/+server.ts index c951a9c..0d95e13 100644 --- a/src/routes/api/directory/[id]/rename/+server.ts +++ b/src/routes/api/directory/[id]/rename/+server.ts @@ -5,8 +5,8 @@ import { directoryRenameRequest } from "$lib/server/schemas"; import { renameDirectory } from "$lib/server/services/directory"; import type { RequestHandler } from "./$types"; -export const POST: RequestHandler = async ({ request, cookies, params }) => { - const { userId } = await authorize(cookies, "activeClient"); +export const POST: RequestHandler = async ({ locals, params, request }) => { + const { userId } = await authorize(locals, "activeClient"); const paramsZodRes = z .object({ diff --git a/src/routes/api/directory/create/+server.ts b/src/routes/api/directory/create/+server.ts index b31d15f..2af0c3c 100644 --- a/src/routes/api/directory/create/+server.ts +++ b/src/routes/api/directory/create/+server.ts @@ -4,8 +4,8 @@ import { directoryCreateRequest } from "$lib/server/schemas"; import { createDirectory } from "$lib/server/services/directory"; import type { RequestHandler } from "./$types"; -export const POST: RequestHandler = async ({ request, cookies }) => { - const { userId } = await authorize(cookies, "activeClient"); +export const POST: RequestHandler = async ({ locals, request }) => { + const { userId } = await authorize(locals, "activeClient"); const zodRes = directoryCreateRequest.safeParse(await request.json()); if (!zodRes.success) error(400, "Invalid request body"); diff --git a/src/routes/api/file/[id]/+server.ts b/src/routes/api/file/[id]/+server.ts index ceb8a0f..6007589 100644 --- a/src/routes/api/file/[id]/+server.ts +++ b/src/routes/api/file/[id]/+server.ts @@ -5,8 +5,8 @@ import { fileInfoResponse, type FileInfoResponse } from "$lib/server/schemas"; import { getFileInformation } from "$lib/server/services/file"; import type { RequestHandler } from "./$types"; -export const GET: RequestHandler = async ({ cookies, params }) => { - const { userId } = await authorize(cookies, "activeClient"); +export const GET: RequestHandler = async ({ locals, params }) => { + const { userId } = await authorize(locals, "activeClient"); const zodRes = z .object({ diff --git a/src/routes/api/file/[id]/delete/+server.ts b/src/routes/api/file/[id]/delete/+server.ts index 4cbf733..7baac25 100644 --- a/src/routes/api/file/[id]/delete/+server.ts +++ b/src/routes/api/file/[id]/delete/+server.ts @@ -4,8 +4,8 @@ import { authorize } from "$lib/server/modules/auth"; import { deleteFile } from "$lib/server/services/file"; import type { RequestHandler } from "./$types"; -export const POST: RequestHandler = async ({ cookies, params }) => { - const { userId } = await authorize(cookies, "activeClient"); +export const POST: RequestHandler = async ({ locals, params }) => { + const { userId } = await authorize(locals, "activeClient"); const zodRes = z .object({ diff --git a/src/routes/api/file/[id]/download/+server.ts b/src/routes/api/file/[id]/download/+server.ts index 58f915d..5040c73 100644 --- a/src/routes/api/file/[id]/download/+server.ts +++ b/src/routes/api/file/[id]/download/+server.ts @@ -4,8 +4,8 @@ import { authorize } from "$lib/server/modules/auth"; import { getFileStream } from "$lib/server/services/file"; import type { RequestHandler } from "./$types"; -export const GET: RequestHandler = async ({ cookies, params }) => { - const { userId } = await authorize(cookies, "activeClient"); +export const GET: RequestHandler = async ({ locals, params }) => { + const { userId } = await authorize(locals, "activeClient"); const zodRes = z .object({ diff --git a/src/routes/api/file/[id]/rename/+server.ts b/src/routes/api/file/[id]/rename/+server.ts index 46fd4b3..c6748a0 100644 --- a/src/routes/api/file/[id]/rename/+server.ts +++ b/src/routes/api/file/[id]/rename/+server.ts @@ -5,8 +5,8 @@ import { fileRenameRequest } from "$lib/server/schemas"; import { renameFile } from "$lib/server/services/file"; import type { RequestHandler } from "./$types"; -export const POST: RequestHandler = async ({ request, cookies, params }) => { - const { userId } = await authorize(cookies, "activeClient"); +export const POST: RequestHandler = async ({ locals, params, request }) => { + const { userId } = await authorize(locals, "activeClient"); const paramsZodRes = z .object({ diff --git a/src/routes/api/file/upload/+server.ts b/src/routes/api/file/upload/+server.ts index 2de4c9a..ac1ac51 100644 --- a/src/routes/api/file/upload/+server.ts +++ b/src/routes/api/file/upload/+server.ts @@ -4,8 +4,8 @@ import { fileUploadRequest } from "$lib/server/schemas"; import { uploadFile } from "$lib/server/services/file"; import type { RequestHandler } from "./$types"; -export const POST: RequestHandler = async ({ request, cookies }) => { - const { userId } = await authorize(cookies, "activeClient"); +export const POST: RequestHandler = async ({ locals, request }) => { + const { userId } = await authorize(locals, "activeClient"); const form = await request.formData(); const metadata = form.get("metadata"); diff --git a/src/routes/api/mek/list/+server.ts b/src/routes/api/mek/list/+server.ts index ccb9fa3..b3df9fe 100644 --- a/src/routes/api/mek/list/+server.ts +++ b/src/routes/api/mek/list/+server.ts @@ -4,8 +4,8 @@ import { masterKeyListResponse, type MasterKeyListResponse } from "$lib/server/s import { getClientMekList } from "$lib/server/services/mek"; import type { RequestHandler } from "./$types"; -export const GET: RequestHandler = async ({ cookies }) => { - const { userId, clientId } = await authorize(cookies, "activeClient"); +export const GET: RequestHandler = async ({ locals }) => { + const { userId, clientId } = await authorize(locals, "activeClient"); const { encMeks } = await getClientMekList(userId, clientId); return json( masterKeyListResponse.parse({ diff --git a/src/routes/api/mek/register/initial/+server.ts b/src/routes/api/mek/register/initial/+server.ts index ba959fb..bb761e2 100644 --- a/src/routes/api/mek/register/initial/+server.ts +++ b/src/routes/api/mek/register/initial/+server.ts @@ -1,14 +1,11 @@ import { error, text } from "@sveltejs/kit"; -import { authenticate } from "$lib/server/modules/auth"; +import { authorize } from "$lib/server/modules/auth"; import { initialMasterKeyRegisterRequest } from "$lib/server/schemas"; import { registerInitialActiveMek } from "$lib/server/services/mek"; import type { RequestHandler } from "./$types"; -export const POST: RequestHandler = async ({ request, cookies }) => { - const { userId, clientId } = authenticate(cookies); - if (!clientId) { - error(403, "Forbidden"); - } +export const POST: RequestHandler = async ({ locals, request }) => { + const { userId, clientId } = await authorize(locals, "pendingClient"); const zodRes = initialMasterKeyRegisterRequest.safeParse(await request.json()); if (!zodRes.success) error(400, "Invalid request body");