mirror of
https://github.com/kmc7468/arkvault.git
synced 2025-12-15 22:38:47 +00:00
백엔드에서 JWT가 아닌 세션 ID 기반으로 인증하도록 변경
This commit is contained in:
14
src/app.d.ts
vendored
14
src/app.d.ts
vendored
@@ -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;
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
export * from "./client";
|
||||
export * from "./file";
|
||||
export * from "./mek";
|
||||
export * from "./token";
|
||||
export * from "./session";
|
||||
export * from "./user";
|
||||
|
||||
@@ -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),
|
||||
});
|
||||
124
src/lib/server/db/session.ts
Normal file
124
src/lib/server/db/session.ts
Normal 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()));
|
||||
};
|
||||
@@ -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()));
|
||||
};
|
||||
@@ -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",
|
||||
};
|
||||
|
||||
34
src/lib/server/middlewares/authenticate.ts
Normal file
34
src/lib/server/middlewares/authenticate.ts
Normal 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;
|
||||
2
src/lib/server/middlewares/index.ts
Normal file
2
src/lib/server/middlewares/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { default as authenticate } from "./authenticate";
|
||||
export { default as setAgentInfo } from "./setAgentInfo";
|
||||
18
src/lib/server/middlewares/setAgentInfo.ts
Normal file
18
src/lib/server/middlewares/setAgentInfo.ts
Normal 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;
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
});
|
||||
|
||||
|
||||
@@ -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" } });
|
||||
};
|
||||
|
||||
@@ -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" } });
|
||||
};
|
||||
26
src/routes/api/auth/upgradeSession/+server.ts
Normal file
26
src/routes/api/auth/upgradeSession/+server.ts
Normal 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));
|
||||
};
|
||||
16
src/routes/api/auth/upgradeSession/verify/+server.ts
Normal file
16
src/routes/api/auth/upgradeSession/verify/+server.ts
Normal 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" } });
|
||||
};
|
||||
@@ -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));
|
||||
};
|
||||
@@ -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" } });
|
||||
};
|
||||
@@ -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));
|
||||
};
|
||||
|
||||
@@ -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));
|
||||
};
|
||||
|
||||
@@ -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" } });
|
||||
};
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user