v3: fixed authorization bug with stale tokens, fixed private dump access bug, various small fixes across the app
Some checks failed
Build and Publish Docker Image / build-and-push (push) Failing after 20s
Some checks failed
Build and Publish Docker Image / build-and-push (push) Failing after 20s
This commit is contained in:
@@ -1,10 +1,28 @@
|
||||
import type { Context } from "@oak/oak";
|
||||
import { verifyJWT } from "./jwt.ts";
|
||||
import { APIErrorCode, APIException } from "../model/interfaces.ts";
|
||||
|
||||
/** Extracts the userId from an optional Bearer token. Returns null if absent or invalid. */
|
||||
/**
|
||||
* Extracts the userId from an optional Bearer token.
|
||||
*
|
||||
* Distinguishes *missing* credentials from *invalid* ones:
|
||||
* - No `Authorization: Bearer …` header (or a placeholder value such as the
|
||||
* `Bearer undefined` that the client emits when logged out) → anonymous (null).
|
||||
* - A real token that fails verification (expired / bad signature) → throws 401,
|
||||
* so a stale client session surfaces as an auth error it can react to, instead
|
||||
* of being silently downgraded to anonymous (which yields a misleading 404 on
|
||||
* private resources).
|
||||
*/
|
||||
export async function parseOptionalAuth(ctx: Context): Promise<string | null> {
|
||||
const authHeader = ctx.request.headers.get("Authorization");
|
||||
if (!authHeader?.startsWith("Bearer ")) return null;
|
||||
const payload = await verifyJWT(authHeader.substring(7));
|
||||
return payload?.userId ?? null;
|
||||
|
||||
const token = authHeader.substring(7).trim();
|
||||
if (!token || token === "undefined" || token === "null") return null;
|
||||
|
||||
const payload = await verifyJWT(token);
|
||||
if (!payload) {
|
||||
throw new APIException(APIErrorCode.UNAUTHORIZED, 401, "Invalid token");
|
||||
}
|
||||
return payload.userId;
|
||||
}
|
||||
|
||||
@@ -71,7 +71,7 @@ export async function createJWT(
|
||||
): Promise<string> {
|
||||
return await new SignJWT(payload)
|
||||
.setProtectedHeader({ alg: "HS256" })
|
||||
.setExpirationTime("24h")
|
||||
.setExpirationTime("7d")
|
||||
.sign(JWT_KEY);
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Router } from "@oak/oak";
|
||||
import { APIErrorCode, APIException } from "../model/interfaces.ts";
|
||||
import { getDump } from "../services/dump-service.ts";
|
||||
import { DUMPS_DIR } from "../lib/upload.ts";
|
||||
import { verifyJWT } from "../lib/jwt.ts";
|
||||
|
||||
const router = new Router({ prefix: "/api/files" });
|
||||
|
||||
@@ -30,7 +31,12 @@ router.get("/:dumpId", async (ctx) => {
|
||||
throw new APIException(APIErrorCode.BAD_REQUEST, 400, "Invalid dump ID");
|
||||
}
|
||||
|
||||
const dump = getDump(dumpId);
|
||||
// Media elements can't send an Authorization header, so the viewer's token
|
||||
// arrives as a query param (same as the WebSocket route). Needed to authorize
|
||||
// access to private dumps the requester owns.
|
||||
const token = ctx.request.url.searchParams.get("token");
|
||||
const payload = token ? await verifyJWT(token) : null;
|
||||
const dump = getDump(dumpId, payload?.userId);
|
||||
|
||||
if (dump.kind !== "file" || !dump.fileMime || !dump.fileName) {
|
||||
throw new APIException(
|
||||
|
||||
@@ -3,6 +3,7 @@ import { APIErrorCode, APIException } from "../model/interfaces.ts";
|
||||
import { getDump, getDumpThumbnailInfo } from "../services/dump-service.ts";
|
||||
import { serveUploadedFile } from "../lib/upload.ts";
|
||||
import { DUMPS_DIR, THUMBNAILS_DIR } from "../config.ts";
|
||||
import { verifyJWT } from "../lib/jwt.ts";
|
||||
|
||||
const router = new Router({ prefix: "/api/thumbnails" });
|
||||
|
||||
@@ -54,6 +55,12 @@ router.get("/:dumpId", async (ctx) => {
|
||||
throw new APIException(APIErrorCode.BAD_REQUEST, 400, "Invalid dump ID");
|
||||
}
|
||||
|
||||
// Token via query param (media elements can't send headers) — authorizes the
|
||||
// owner's private dumps and gates the custom-thumbnail branch below too.
|
||||
const token = ctx.request.url.searchParams.get("token");
|
||||
const payload = token ? await verifyJWT(token) : null;
|
||||
const dump = getDump(dumpId, payload?.userId);
|
||||
|
||||
const customThumb = getDumpThumbnailInfo(dumpId);
|
||||
if (customThumb) {
|
||||
const served = await serveUploadedFile(
|
||||
@@ -64,8 +71,6 @@ router.get("/:dumpId", async (ctx) => {
|
||||
if (served) return;
|
||||
}
|
||||
|
||||
const dump = getDump(dumpId);
|
||||
|
||||
if (dump.kind !== "file" || !dump.fileMime?.startsWith("video/")) {
|
||||
throw new APIException(
|
||||
APIErrorCode.NOT_FOUND,
|
||||
|
||||
Reference in New Issue
Block a user