백엔드에서 JWT가 아닌 세션 ID 기반으로 인증하도록 변경

This commit is contained in:
static
2025-01-12 07:28:38 +09:00
parent 0bdf990dae
commit 1a86c8d9e0
42 changed files with 487 additions and 624 deletions

View File

@@ -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=

View File

@@ -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}

View File

@@ -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",

94
pnpm-lock.yaml generated
View File

@@ -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: {}

14
src/app.d.ts vendored
View File

@@ -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;
};
}
}
}

View File

@@ -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);

View File

@@ -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) {

View File

@@ -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),

View File

@@ -1,5 +1,5 @@
export * from "./client";
export * from "./file";
export * from "./mek";
export * from "./token";
export * from "./session";
export * from "./user";

View File

@@ -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),
});

View File

@@ -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()));
};

View File

@@ -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()));
};

View File

@@ -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",
};

View File

@@ -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;

View File

@@ -0,0 +1,2 @@
export { default as authenticate } from "./authenticate";
export { default as setAgentInfo } from "./setAgentInfo";

View File

@@ -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;

View File

@@ -1,90 +1,127 @@
import { error, type Cookies } from "@sveltejs/kit";
import jwt from "jsonwebtoken";
import { error } from "@sveltejs/kit";
import { getUserClient } from "$lib/server/db/client";
import { IntegrityError } from "$lib/server/db/error";
import { createSession, refreshSession } from "$lib/server/db/session";
import env from "$lib/server/loadenv";
import { issueSessionId, verifySessionId } from "$lib/server/modules/crypto";
type TokenPayload =
| {
type: "access";
userId: number;
clientId?: number;
}
| {
type: "refresh";
jti: string;
};
export enum TokenError {
EXPIRED,
INVALID,
interface Session {
sessionId: string;
userId: number;
clientId?: number;
}
type Permission = "pendingClient" | "activeClient";
interface ClientSession extends Session {
clientId: number;
}
export const issueToken = (payload: TokenPayload) => {
return jwt.sign(payload, env.jwt.secret, {
expiresIn: (payload.type === "access" ? env.jwt.accessExp : env.jwt.refreshExp) / 1000,
});
export class AuthenticationError extends Error {
constructor(
public status: 400 | 401,
message: string,
) {
super(message);
this.name = "AuthenticationError";
}
}
export const startSession = async (userId: number, ip: string, userAgent: string) => {
const { sessionId, sessionIdSigned } = await issueSessionId(32, env.session.secret);
await createSession(userId, null, sessionId, ip, userAgent);
return sessionIdSigned;
};
export const verifyToken = (token: string) => {
export const authenticate = async (sessionIdSigned: string, ip: string, userAgent: string) => {
const sessionId = verifySessionId(sessionIdSigned, env.session.secret);
if (!sessionId) {
throw new AuthenticationError(400, "Invalid session id");
}
try {
return jwt.verify(token, env.jwt.secret) as TokenPayload;
} catch (error) {
if (error instanceof jwt.TokenExpiredError) {
return TokenError.EXPIRED;
const { userId, clientId } = await refreshSession(sessionId, ip, userAgent);
return {
id: sessionId,
userId,
clientId: clientId ?? undefined,
};
} catch (e) {
if (e instanceof IntegrityError && e.message === "Session not found") {
throw new AuthenticationError(401, "Invalid session id");
}
return TokenError.INVALID;
throw e;
}
};
export const authenticate = (cookies: Cookies) => {
const accessToken = cookies.get("accessToken");
if (!accessToken) {
error(401, "Access token not found");
}
const tokenPayload = verifyToken(accessToken);
if (tokenPayload === TokenError.EXPIRED) {
error(401, "Access token expired");
} else if (tokenPayload === TokenError.INVALID || tokenPayload.type !== "access") {
error(401, "Invalid access token");
}
return {
userId: tokenPayload.userId,
clientId: tokenPayload.clientId,
};
};
export async function authorize(locals: App.Locals, requiredPermission: "any"): Promise<Session>;
export async function authorize(
cookies: Cookies,
locals: App.Locals,
requiredPermission: "notClient",
): Promise<Session>;
export async function authorize(
locals: App.Locals,
requiredPermission: "anyClient",
): Promise<ClientSession>;
export async function authorize(
locals: App.Locals,
requiredPermission: "pendingClient",
): Promise<{ userId: number; clientId: number }>;
): Promise<ClientSession>;
export async function authorize(
cookies: Cookies,
locals: App.Locals,
requiredPermission: "activeClient",
): Promise<{ userId: number; clientId: number }>;
): Promise<ClientSession>;
export async function authorize(
cookies: Cookies,
requiredPermission: Permission,
): Promise<{ userId: number; clientId?: number }> {
const tokenPayload = authenticate(cookies);
const { userId, clientId } = tokenPayload;
const userClient = clientId ? await getUserClient(userId, clientId) : undefined;
locals: App.Locals,
requiredPermission: "any" | "notClient" | "anyClient" | "pendingClient" | "activeClient",
): Promise<Session> {
if (!locals.session) {
error(500, "Unauthenticated");
}
const { id: sessionId, userId, clientId } = locals.session;
switch (requiredPermission) {
case "pendingClient":
if (!userClient || userClient.state !== "pending") {
case "any":
break;
case "notClient":
if (clientId) {
error(403, "Forbidden");
}
return tokenPayload;
case "activeClient":
if (!userClient || userClient.state !== "active") {
break;
case "anyClient":
if (!clientId) {
error(403, "Forbidden");
}
return tokenPayload;
break;
case "pendingClient": {
if (!clientId) {
error(403, "Forbidden");
}
const userClient = await getUserClient(userId, clientId);
if (!userClient) {
error(500, "Invalid session id");
} else if (userClient.state !== "pending") {
error(403, "Forbidden");
}
break;
}
case "activeClient": {
if (!clientId) {
error(403, "Forbidden");
}
const userClient = await getUserClient(userId, clientId);
if (!userClient) {
error(500, "Invalid session id");
} else if (userClient.state !== "active") {
error(403, "Forbidden");
}
break;
}
}
return { sessionId, userId, clientId };
}

View File

@@ -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;
}
};

View File

@@ -6,19 +6,19 @@ export const loginRequest = z.object({
});
export type LoginRequest = z.infer<typeof loginRequest>;
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<typeof tokenUpgradeRequest>;
export type SessionUpgradeRequest = z.infer<typeof sessionUpgradeRequest>;
export const tokenUpgradeResponse = z.object({
export const sessionUpgradeResponse = z.object({
challenge: z.string().base64().nonempty(),
});
export type TokenUpgradeResponse = z.infer<typeof tokenUpgradeResponse>;
export type SessionUpgradeResponse = z.infer<typeof sessionUpgradeResponse>;
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<typeof tokenUpgradeVerifyRequest>;
export type SessionUpgradeVerifyRequest = z.infer<typeof sessionUpgradeVerifyRequest>;

View File

@@ -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;
}

View File

@@ -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",
});

View File

@@ -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" } });
};

View File

@@ -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" } });
};

View File

@@ -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));
};

View File

@@ -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" } });
};

View File

@@ -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));
};

View File

@@ -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" } });
};

View File

@@ -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));
};

View File

@@ -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));
};

View File

@@ -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" } });
};

View File

@@ -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({

View File

@@ -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({

View File

@@ -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({

View File

@@ -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({

View File

@@ -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");

View File

@@ -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({

View File

@@ -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({

View File

@@ -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({

View File

@@ -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({

View File

@@ -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");

View File

@@ -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({

View File

@@ -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");