From 3fbb481b6f80a3520bd066286af816d47ce0e36d Mon Sep 17 00:00:00 2001 From: stringsplit <77242831+nbitzz@users.noreply.github.com> Date: Wed, 1 May 2024 19:48:27 +0000 Subject: [PATCH 1/4] initial commit !! NOT DONE --- package-lock.json | 9 +++++++++ package.json | 1 + src/server/lib/auth.ts | 18 +++++------------- src/server/lib/schemas/auth.ts | 24 ++++++++++++++++++++++++ src/server/lib/schemas/index.ts | 3 ++- 5 files changed, 41 insertions(+), 14 deletions(-) create mode 100644 src/server/lib/schemas/auth.ts diff --git a/package-lock.json b/package-lock.json index c21765a..91eb762 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,6 +23,7 @@ "express": "^4.18.1", "formidable": "^3.5.1", "hono": "^4.0.10", + "jose": "^5.2.4", "multer": "^1.4.5-lts.1", "node-fetch": "^3.3.2", "nodemailer": "^6.9.3", @@ -1083,6 +1084,14 @@ "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" }, + "node_modules/jose": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/jose/-/jose-5.2.4.tgz", + "integrity": "sha512-6ScbIk2WWCeXkmzF6bRPmEuaqy1m8SbsRFMa/FLrSCkGIhj8OLVG/IH+XHVmNMx/KUo8cVWEE6oKR4dJ+S0Rkg==", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/kleur": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", diff --git a/package.json b/package.json index af8bc38..a91293d 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "express": "^4.18.1", "formidable": "^3.5.1", "hono": "^4.0.10", + "jose": "^5.2.4", "multer": "^1.4.5-lts.1", "node-fetch": "^3.3.2", "nodemailer": "^6.9.3", diff --git a/src/server/lib/auth.ts b/src/server/lib/auth.ts index 783144e..fa3d572 100644 --- a/src/server/lib/auth.ts +++ b/src/server/lib/auth.ts @@ -2,22 +2,14 @@ import crypto from "crypto" import { getCookie } from "hono/cookie" import type { Context } from "hono" import { readFile, writeFile } from "fs/promises" +import { z } from "zod" +import * as jose from "jose" +import { AuthSchemas } from "./schemas/index.js" export let AuthTokens: AuthToken[] = [] export let AuthTokenTO: { [key: string]: NodeJS.Timeout } = {} -export const ValidTokenPermissions = [ - "user", // permissions to /auth/me, with email docked - "email", // adds email back to /auth/me - "private", // allows app to read private files - "upload", // allows an app to upload under an account - "manage", // allows an app to manage an account's files - "customize", // allows an app to change customization settings - "admin", // only available for accounts with admin - // gives an app access to all admin tools -] as const - -export type TokenType = "User" | "App" -export type TokenPermission = (typeof ValidTokenPermissions)[number] +export type TokenType = z.infer +export type TokenPermission = z.infer export interface AuthToken { account: string diff --git a/src/server/lib/schemas/auth.ts b/src/server/lib/schemas/auth.ts new file mode 100644 index 0000000..05d4c03 --- /dev/null +++ b/src/server/lib/schemas/auth.ts @@ -0,0 +1,24 @@ +import {z} from "zod" + +export const TokenType = z.enum(["App", "User"]) +export const TokenPermission = z.enum([ + "user", // permissions to /auth/me, with email docked + "email", // adds email back to /auth/me + "private", // allows app to read private files + "upload", // allows an app to upload under an account + "manage", // allows an app to manage an account's files + "customize", // allows an app to change customization settings + "admin", // only available for accounts with admin + // gives an app access to all admin tools +]) +const BaseToken = z.object({ + sub: z.string(), + purpose: TokenType +}) +export const JwtPayload = z.discriminatedUnion( + "purpose", + [ + BaseToken.extend({purpose: z.literal("User")}), + BaseToken.extend({purpose: z.literal("App"), permissions: z.array(TokenPermission).default(['user'])}) + ] +) \ No newline at end of file diff --git a/src/server/lib/schemas/index.ts b/src/server/lib/schemas/index.ts index 4d030ab..5689486 100644 --- a/src/server/lib/schemas/index.ts +++ b/src/server/lib/schemas/index.ts @@ -1,2 +1,3 @@ export * as AccountSchemas from "./accounts.js" -export * as FileSchemas from "./files.js" \ No newline at end of file +export * as FileSchemas from "./files.js" +export * as AuthSchemas from "./auth.js" \ No newline at end of file From c85de0e86af9b11c5fcd3b6f8fd17cae9e5be11b Mon Sep 17 00:00:00 2001 From: stringsplit <77242831+nbitzz@users.noreply.github.com> Date: Fri, 24 May 2024 18:49:54 +0000 Subject: [PATCH 2/4] prepare to switch --- src/server/lib/accounts.ts | 10 ++++++++++ src/server/lib/auth.ts | 11 +++++------ src/server/lib/schemas/auth.ts | 3 +-- src/server/routes/api/v1/account/access.ts | 3 +-- 4 files changed, 17 insertions(+), 10 deletions(-) diff --git a/src/server/lib/accounts.ts b/src/server/lib/accounts.ts index 970a067..55f371f 100644 --- a/src/server/lib/accounts.ts +++ b/src/server/lib/accounts.ts @@ -161,6 +161,16 @@ export namespace files { } } +export type AccountResolvable = Account | string | `@${string}` + +export function resolve(obj: AccountResolvable) { + return typeof obj == "object" + ? obj + : obj.startsWith("@") + ? getFromUsername(obj.slice(1)) + : getFromId(obj) +} + Db.read() .then(() => { if (!Db.data.find(e => e.admin)) { diff --git a/src/server/lib/auth.ts b/src/server/lib/auth.ts index e0cc5a2..2a9572c 100644 --- a/src/server/lib/auth.ts +++ b/src/server/lib/auth.ts @@ -6,6 +6,7 @@ import { z } from "zod" import { AuthSchemas } from "./schemas/index.js" import DbFile from "./dbfile.js" import * as jose from "jose" +import { AccountResolvable } from "./accounts.js" export let AuthTokenTO: { [key: string]: NodeJS.Timeout } = {} export type TokenPermission = z.infer @@ -15,17 +16,15 @@ export type AuthToken = z.infer export const Db = new DbFile("tokens", []) export function create( - id: string, + account: AccountResolvable, expire: number | null = 24 * 60 * 60 * 1000, type: TokenType = "User", - tokenPermissions?: TokenPermission[], - issuer?: string + tokenPermissions?: TokenPermission[] ) { let token = AuthSchemas.AuthToken.parse({ - account: id, - token: crypto.randomBytes(36).toString("hex"), + account, + id: crypto.randomUUID(), expire: typeof expire == "number" ? Date.now() + expire : null, - issuer, type, tokenPermissions: type != "User" ? tokenPermissions || ["user"] : undefined, diff --git a/src/server/lib/schemas/auth.ts b/src/server/lib/schemas/auth.ts index cfc1b22..e945167 100644 --- a/src/server/lib/schemas/auth.ts +++ b/src/server/lib/schemas/auth.ts @@ -35,7 +35,6 @@ export const AuthToken = z.discriminatedUnion("type",[ }), BaseAuthToken.extend({ type: z.literal("App"), - tokenPermissions: z.array(Scope).default(["user"]), - issuer: z.string() + tokenPermissions: z.array(Scope).default(["user"]) }) ]) \ No newline at end of file diff --git a/src/server/routes/api/v1/account/access.ts b/src/server/routes/api/v1/account/access.ts index 8014289..885f470 100644 --- a/src/server/routes/api/v1/account/access.ts +++ b/src/server/routes/api/v1/account/access.ts @@ -114,8 +114,7 @@ export default function (files: Files) { "ApiKey", params.scopes == "all" ? AuthSchemas.Scope.options - : Array.from(new Set(params.scopes)), - "monofile" + : Array.from(new Set(params.scopes)) ) return ctx.text(token) } From 594efc49e0c73ae3599c2730a55f04a270029aa0 Mon Sep 17 00:00:00 2001 From: stringsplit <77242831+nbitzz@users.noreply.github.com> Date: Fri, 24 May 2024 21:36:08 +0000 Subject: [PATCH 3/4] This builds but probably doesn't work yet --- .env.example | 4 +- src/server/lib/auth.ts | 86 +++++++++++++-------- src/server/lib/config.ts | 6 +- src/server/lib/middleware.ts | 60 ++++++++------ src/server/lib/schemas/auth.ts | 6 +- src/server/routes/api/v0/adminRoutes.ts | 8 +- src/server/routes/api/v0/authRoutes.ts | 35 ++++----- src/server/routes/api/v0/fileApiRoutes.ts | 6 +- src/server/routes/api/v0/primaryApi.ts | 14 +--- src/server/routes/api/v1/account/access.ts | 10 +-- src/server/routes/api/v1/account/index.ts | 10 +-- src/server/routes/api/v1/account/prove.ts | 2 +- src/server/routes/api/v1/file/index.ts | 4 +- src/server/routes/api/v1/file/individual.ts | 8 +- src/server/routes/api/v1/session.ts | 14 ++-- 15 files changed, 147 insertions(+), 126 deletions(-) diff --git a/.env.example b/.env.example index 76a2d76..6901dae 100644 --- a/.env.example +++ b/.env.example @@ -18,4 +18,6 @@ MAIL__PORT= MAIL__SECURE= MAIL__SEND_FROM= MAIL__USER= -MAIL__PASS= \ No newline at end of file +MAIL__PASS= + +JWT_SECRET= \ No newline at end of file diff --git a/src/server/lib/auth.ts b/src/server/lib/auth.ts index 2a9572c..f8f42cd 100644 --- a/src/server/lib/auth.ts +++ b/src/server/lib/auth.ts @@ -7,27 +7,35 @@ import { AuthSchemas } from "./schemas/index.js" import DbFile from "./dbfile.js" import * as jose from "jose" import { AccountResolvable } from "./accounts.js" +import config from "./config.js" export let AuthTokenTO: { [key: string]: NodeJS.Timeout } = {} -export type TokenPermission = z.infer +export type Scope = z.infer export type TokenType = z.infer export type AuthToken = z.infer +export type TokenResolvable = string | AuthToken export const Db = new DbFile("tokens", []) +export function resolve(token: TokenResolvable) { + let resolved = typeof token == "object" ? token : Db.data.find(e => e.id == token) + if (resolved && (resolved.expire == null || Date.now() < resolved.expire)) + return resolved +} + export function create( account: AccountResolvable, expire: number | null = 24 * 60 * 60 * 1000, type: TokenType = "User", - tokenPermissions?: TokenPermission[] + scopes?: Scope[] ) { let token = AuthSchemas.AuthToken.parse({ account, id: crypto.randomUUID(), expire: typeof expire == "number" ? Date.now() + expire : null, type, - tokenPermissions: - type != "User" ? tokenPermissions || ["user"] : undefined, + scopes: + type != "User" ? scopes || ["user"] : undefined }) Db.data.push(token) @@ -35,59 +43,75 @@ export function create( Db.save() - return token.token + return token } -export function tokenFor(ctx: Context) { - return ( - getCookie(ctx, "auth") || - (ctx.req.header("authorization")?.startsWith("Bearer ") + +export async function getJwtId(jwt: string) { + let result = await jose.jwtVerify(jwt, config.jwtSecret).catch(e => null) + return result ? result.payload.jti : undefined +} + +export function makeJwt(_token: TokenResolvable) { + let token = resolve(_token)! + let jwt = new jose.SignJWT({ + exp: token.expire || undefined, + sub: token.account, + jti: token.id, + ...(token.type != "User" ? { scope: token.scopes } : {}) + }) + + return jwt.sign(config.jwtSecret) +} + +export async function tokenFor(ctx: Context) { + let token = + getCookie(ctx, "auth") + || (ctx.req.header("authorization")?.startsWith("Bearer ") ? ctx.req.header("authorization")?.split(" ")[1] : undefined) - ) + if (!token) return + + let jti = await getJwtId(token) + return jti } -function getToken(token: string) { - return Db.data.find( - (e) => e.token == token && (e.expire == null || Date.now() < e.expire) - ) +export function validate(token: TokenResolvable) { + return resolve(token)?.account } -export function validate(token: string) { - return getToken(token)?.account +export function getType(token: TokenResolvable) { + return resolve(token)?.type } -export function getType(token: string): TokenType | undefined { - return getToken(token)?.type -} - -export function getPermissions(token: string): TokenPermission[] | undefined { - let tok = getToken(token) - if (tok && "tokenPermissions" in tok) - return tok.tokenPermissions +export function getScopes(token: TokenResolvable): Scope[] | undefined { + let tok = resolve(token) + if (tok && "scopes" in tok) + return tok.scopes } export function tokenTimer(token: AuthToken) { if (!token.expire) return if (Date.now() >= token.expire) { - invalidate(token.token) + invalidate(token) return } - AuthTokenTO[token.token] = setTimeout( - () => invalidate(token.token), + AuthTokenTO[token.id] = setTimeout( + () => invalidate(token), token.expire - Date.now() ) } -export function invalidate(token: string) { - if (AuthTokenTO[token]) { - clearTimeout(AuthTokenTO[token]) +export function invalidate(_token: TokenResolvable) { + let token = resolve(_token)! + if (AuthTokenTO[token.id]) { + clearTimeout(AuthTokenTO[token.id]) } Db.data.splice( - Db.data.findIndex((e) => e.token == token), + Db.data.indexOf(token), 1 ) Db.save() diff --git a/src/server/lib/config.ts b/src/server/lib/config.ts index e6103c0..42bbf9f 100644 --- a/src/server/lib/config.ts +++ b/src/server/lib/config.ts @@ -26,7 +26,9 @@ export interface Configuration { } user: string pass: string - } + }, + + jwtSecret: Buffer } export interface ClientConfiguration { @@ -72,4 +74,6 @@ export default { user: process.env.MAIL__USER, pass: process.env.MAIL__PASS, }, + + jwtSecret: Buffer.from(process.env.JWT_SECRET!) } as Configuration diff --git a/src/server/lib/middleware.ts b/src/server/lib/middleware.ts index f02ce9f..9c778ce 100644 --- a/src/server/lib/middleware.ts +++ b/src/server/lib/middleware.ts @@ -1,5 +1,5 @@ import * as Accounts from "./accounts.js" -import type { Context, Handler as RequestHandler } from "hono" +import type { Context, Hono, Handler as RequestHandler } from "hono" import ServeError from "../lib/errors.js" import * as auth from "./auth.js" import { setCookie } from "hono/cookie" @@ -9,8 +9,8 @@ import { codes } from "./codes.js" /** * @description Middleware which adds an account, if any, to ctx.get("account") */ -export const getAccount: RequestHandler = function (ctx, next) { - let account = Accounts.getFromToken(auth.tokenFor(ctx)!) +export const getAccount: RequestHandler = async function (ctx, next) { + let account = Accounts.getFromToken((await auth.tokenFor(ctx))!) if (account?.suspension) setCookie(ctx, "auth", "") ctx.set("account", account) @@ -22,10 +22,10 @@ export const getAccount: RequestHandler = function (ctx, next) { */ export const getTarget: RequestHandler = async (ctx, next) => { - let tok = auth.tokenFor(ctx) + let tok = await auth.tokenFor(ctx) let permissions if (tok && auth.getType(tok) != "User") - permissions = auth.getPermissions(tok) + permissions = auth.getScopes(tok) let actor = ctx.get("account") @@ -59,10 +59,10 @@ export const getTarget: RequestHandler = async (ctx, next) => { * @description Blocks routes with a target user set to the account performing the action from bot tokens which do not have the manage_account permission */ export const accountMgmtRoute: RequestHandler = async (ctx,next) => { - let tok = auth.tokenFor(ctx) + let tok = await auth.tokenFor(ctx) let permissions if (tok && auth.getType(tok) != "User") - permissions = auth.getPermissions(tok) + permissions = auth.getScopes(tok) if ( ( @@ -110,26 +110,27 @@ export const requiresAdmin: RequestHandler = function (ctx, next) { * @param tokenPermissions Permissions which your route requires. * @returns Express middleware */ -export const requiresPermissions = function ( - ...tokenPermissions: auth.TokenPermission[] +export const requiresScopes = function ( + ...wantsScopes: auth.Scope[] ): RequestHandler { - return function (ctx, next) { - let token = auth.tokenFor(ctx)! + return async function (ctx, next) { + let token = (await auth.tokenFor(ctx))! let type = auth.getType(token) if (type != "User") { - let permissions = auth.getPermissions(token) + let scopes = auth.getScopes(token) - if (!permissions) return ServeError(ctx, 403, "insufficient permissions") + if (!scopes) return ServeError(ctx, 403, "insufficient permissions") else { - for (let v of tokenPermissions) { - if (!permissions.includes(v as auth.TokenPermission)) { + for (let v of wantsScopes) { + if (!scopes.includes(v)) { return ServeError(ctx, 403, "insufficient permissions") } } - return next() } - } else return next() + } + + return next() } } @@ -137,8 +138,8 @@ export const requiresPermissions = function ( * @description Blocks requests based on whether or not the token being used to access the route is of type `User`. */ -export const noAPIAccess: RequestHandler = function (ctx, next) { - if (auth.getType(auth.tokenFor(ctx)!) == "App") +export const noAPIAccess: RequestHandler = async function (ctx, next) { + if (auth.getType((await auth.tokenFor(ctx))!) == "App") return ServeError(ctx, 403, "apps are not allowed to access this endpoint") else return next() } @@ -150,8 +151,8 @@ export const noAPIAccess: RequestHandler = function (ctx, next) { export const assertAPI = function ( condition: (ctx: Context) => boolean ): RequestHandler { - return function (ctx, next) { - let reqToken = auth.tokenFor(ctx)! + return async function (ctx, next) { + let reqToken = (await auth.tokenFor(ctx))! if ( auth.getType(reqToken) != "User" && condition(ctx) @@ -184,9 +185,9 @@ export function scheme(scheme: z.ZodTypeAny, transformer: (ctx: Context) => Prom // Not really middleware but a utility -export const login = (ctx: Context, account: string) => { +export const login = async (ctx: Context, account: string) => { let token = auth.create(account, 3 * 24 * 60 * 60 * 1000) - setCookie(ctx, "auth", token, { + setCookie(ctx, "auth", await auth.makeJwt(token), { path: "/", sameSite: "Strict", secure: true, @@ -205,4 +206,15 @@ export const verifyPoi = (user: string, poi?: string, wantsMfaPoi: boolean = fal poiCode.terminate() return true -} \ No newline at end of file +} + +export const mirror = (apiRoot: Hono, ctx: Context, url: string, init: Partial) => apiRoot.fetch( + new Request( + (new URL(url, ctx.req.raw.url)).href, + { + ...ctx.req.raw, + ...init + } + ), + ctx.env +) \ No newline at end of file diff --git a/src/server/lib/schemas/auth.ts b/src/server/lib/schemas/auth.ts index e945167..3e584bf 100644 --- a/src/server/lib/schemas/auth.ts +++ b/src/server/lib/schemas/auth.ts @@ -17,7 +17,7 @@ export const TokenType = z.enum([ const BaseAuthToken = z.object({ account: z.string(), - token: z.string(), + id: z.string(), expire: z.number() .nullable() .refine(e => e == null || e > Date.now(), "expiration must be after now"), @@ -31,10 +31,10 @@ export const AuthToken = z.discriminatedUnion("type",[ }), BaseAuthToken.extend({ type: z.literal("ApiKey"), - tokenPermissions: z.array(Scope).default(["user"]) + scopes: z.array(Scope).default(["user"]) }), BaseAuthToken.extend({ type: z.literal("App"), - tokenPermissions: z.array(Scope).default(["user"]) + scopes: z.array(Scope).default(["user"]) }) ]) \ No newline at end of file diff --git a/src/server/routes/api/v0/adminRoutes.ts b/src/server/routes/api/v0/adminRoutes.ts index 6610814..1a586b1 100644 --- a/src/server/routes/api/v0/adminRoutes.ts +++ b/src/server/routes/api/v0/adminRoutes.ts @@ -7,7 +7,7 @@ import { getAccount, requiresAccount, requiresAdmin, - requiresPermissions, + requiresScopes, } from "../../../lib/middleware.js" import Files from "../../../lib/files.js" @@ -20,7 +20,7 @@ adminRoutes .use(getAccount) .use(requiresAccount) .use(requiresAdmin) - .use(requiresPermissions("manage_server")) + .use(requiresScopes("manage_server")) export default function (files: Files) { adminRoutes.post("/reset", async (ctx) => { @@ -42,7 +42,7 @@ export default function (files: Files) { Accounts.password.set(targetAccount.id, body.password) auth.Db.data.filter((e) => e.account == targetAccount?.id).forEach( (v) => { - auth.invalidate(v.token) + auth.invalidate(v.id) } ) @@ -107,7 +107,7 @@ export default function (files: Files) { let accId = targetAccount.id auth.Db.data.filter((e) => e.account == accId).forEach((v) => { - auth.invalidate(v.token) + auth.invalidate(v.id) }) let cpl = () => diff --git a/src/server/routes/api/v0/authRoutes.ts b/src/server/routes/api/v0/authRoutes.ts index 432420d..8ba544a 100644 --- a/src/server/routes/api/v0/authRoutes.ts +++ b/src/server/routes/api/v0/authRoutes.ts @@ -5,9 +5,10 @@ import * as auth from "../../../lib/auth.js" import { sendMail } from "../../../lib/mail.js" import { getAccount, + login, noAPIAccess, requiresAccount, - requiresPermissions, + requiresScopes, } from "../../../lib/middleware.js" import { accountRatelimit } from "../../../lib/ratelimit.js" import config from "../../../lib/config.js" @@ -58,13 +59,7 @@ export default function (files: Files) { assign token */ - setCookie(ctx, "auth", auth.create(acc.id, 3 * 24 * 60 * 60 * 1000), { - path: "/", - sameSite: "Strict", - secure: true, - httpOnly: true, - maxAge: 3 * 24 * 60 * 60 * 1000, - }) + login(ctx, acc.id) return ctx.text("") }) @@ -116,11 +111,7 @@ export default function (files: Files) { assign token */ - setCookie( - ctx, - "auth", - auth.create(newAcc, 3 * 24 * 60 * 60 * 1000) - ) + login(ctx, newAcc) return ctx.text("") }) .catch(() => ServeError(ctx, 500, "internal server error")) @@ -138,7 +129,7 @@ export default function (files: Files) { authRoutes.post( "/dfv", requiresAccount, - requiresPermissions("manage_files"), + requiresScopes("manage_files"), // Used body-parser async (ctx) => { const body = await ctx.req.json() @@ -171,7 +162,7 @@ export default function (files: Files) { let accId = acc.id auth.Db.data.filter((e) => e.account == accId).forEach((v) => { - auth.invalidate(v.token) + auth.invalidate(v.id) }) let cpl = () => @@ -462,7 +453,7 @@ export default function (files: Files) { } if (typeof ctx.req.param("code") == "string" && vcode) { - setCookie(ctx, "auth", auth.create(vcode, 3 * 24 * 60 * 60 * 1000)) + login(ctx, vcode) let e = pwReset.get(vcode)?.expiry if (e) clearTimeout(e) pwReset.delete(vcode) @@ -491,7 +482,7 @@ export default function (files: Files) { Accounts.password.set(accId, body.password) auth.Db.data.filter((e) => e.account == accId).forEach((v) => { - auth.invalidate(v.token) + auth.invalidate(v.id) }) if (acc.email) { @@ -518,7 +509,7 @@ export default function (files: Files) { let accId = acc.id auth.Db.data.filter((e) => e.account == accId).forEach((v) => { - auth.invalidate(v.token) + auth.invalidate(v.id) }) return ctx.text("logged out all sessions") @@ -528,10 +519,10 @@ export default function (files: Files) { authRoutes.get( "/me", requiresAccount, - requiresPermissions("user"), + requiresScopes("user"), async (ctx) => { let acc = ctx.get("account") as Accounts.Account - let sessionToken = auth.tokenFor(ctx)! + let sessionToken = (await auth.tokenFor(ctx))! let accId = acc.id return ctx.json({ ...acc, @@ -542,12 +533,12 @@ export default function (files: Files) { (e.expire == null || e.expire > Date.now()) ).length, sessionExpires: auth.Db.data.find( - (e) => e.token == sessionToken + (e) => e.id == sessionToken )?.expire, password: undefined, email: auth.getType(sessionToken) == "User" || - auth.getPermissions(sessionToken)?.includes("email") + auth.getScopes(sessionToken)?.includes("email") ? acc.email : undefined, }) diff --git a/src/server/routes/api/v0/fileApiRoutes.ts b/src/server/routes/api/v0/fileApiRoutes.ts index dc4ab62..fc2976d 100644 --- a/src/server/routes/api/v0/fileApiRoutes.ts +++ b/src/server/routes/api/v0/fileApiRoutes.ts @@ -5,7 +5,7 @@ import Files from "../../../lib/files.js" import { getAccount, requiresAccount, - requiresPermissions, + requiresScopes, } from "../../../lib/middleware.js" export let fileApiRoutes = new Hono<{ @@ -20,7 +20,7 @@ export default function (files: Files) { fileApiRoutes.get( "/list", requiresAccount, - requiresPermissions("user"), + requiresScopes("user"), async (ctx) => { let acc = ctx.get("account") as Accounts.Account @@ -49,7 +49,7 @@ export default function (files: Files) { fileApiRoutes.post( "/manage", - requiresPermissions("manage_files"), + requiresScopes("manage_files"), async (ctx) => { let acc = ctx.get("account") as Accounts.Account const body = await ctx.req.json() diff --git a/src/server/routes/api/v0/primaryApi.ts b/src/server/routes/api/v0/primaryApi.ts index 1581880..04bc00e 100644 --- a/src/server/routes/api/v0/primaryApi.ts +++ b/src/server/routes/api/v0/primaryApi.ts @@ -4,7 +4,7 @@ import * as auth from "../../../lib/auth.js" import RangeParser, { type Range } from "range-parser" import ServeError from "../../../lib/errors.js" import Files, { WebError } from "../../../lib/files.js" -import { getAccount, requiresPermissions } from "../../../lib/middleware.js" +import { getAccount, mirror, requiresScopes } from "../../../lib/middleware.js" import {Readable} from "node:stream" import type {ReadableStream as StreamWebReadable} from "node:stream/web" import formidable from "formidable" @@ -37,17 +37,7 @@ export default function (files: Files, apiRoot: Hono) { primaryApi.get("/cpt/:fileId/*", fileReader(apiRoot)) primaryApi.post("/upload", async (ctx) => - apiRoot.fetch( - new Request( - (new URL( - `/api/v1/file`, ctx.req.raw.url)).href, - { - ...ctx.req.raw, - method: "PUT" - } - ), - ctx.env - ) + mirror(apiRoot, ctx, "/api/v1/file", {method: "PUT"}) ) return primaryApi diff --git a/src/server/routes/api/v1/account/access.ts b/src/server/routes/api/v1/account/access.ts index 885f470..201be99 100644 --- a/src/server/routes/api/v1/account/access.ts +++ b/src/server/routes/api/v1/account/access.ts @@ -16,7 +16,7 @@ import { login, noAPIAccess, requiresAccount, - requiresPermissions, + requiresScopes, scheme, } from "../../../../lib/middleware.js" import ServeError from "../../../../lib/errors.js" @@ -41,7 +41,7 @@ function getTargetToken(ctx: Context) { return auth.Db.data.find( e => e.account == ctx.get("target").id - && e.token == ctx.req.param("token") + && e.id == ctx.req.param("token") ) } @@ -79,7 +79,7 @@ export default function (files: Files) { && ctx.get("parsedScheme").has(e.type) ) - targets.forEach(e => auth.invalidate(e.token)) + targets.forEach(e => auth.invalidate(e.id)) return ctx.text(`deleted ${targets.length} tokens`) } @@ -90,7 +90,7 @@ export default function (files: Files) { }) router.delete("/:token", async (ctx) => { - auth.invalidate(ctx.get("targetToken").token) + auth.invalidate(ctx.get("targetToken").id) return ctx.text(`deleted token ${ctx.req.param("token")}`) }) @@ -116,7 +116,7 @@ export default function (files: Files) { ? AuthSchemas.Scope.options : Array.from(new Set(params.scopes)) ) - return ctx.text(token) + return ctx.text(await auth.makeJwt(token.id)) } ) diff --git a/src/server/routes/api/v1/account/index.ts b/src/server/routes/api/v1/account/index.ts index b94c067..3e77194 100644 --- a/src/server/routes/api/v1/account/index.ts +++ b/src/server/routes/api/v1/account/index.ts @@ -17,7 +17,7 @@ import { login, noAPIAccess, requiresAccount, - requiresPermissions, + requiresScopes, scheme, verifyPoi, } from "../../../../lib/middleware.js" @@ -190,7 +190,7 @@ const validators: { if (params.suspension) auth.Db.data .filter(e => e.account == target.id) - .forEach(e => auth.invalidate(e.token)) + .forEach(e => auth.invalidate(e.id)) return params.suspension || undefined } }, @@ -338,7 +338,7 @@ export default function (files: Files) { return ServeError(ctx, 403, "invalid proof of identity provided") auth.Db.data.filter((e) => e.account == target?.id).forEach((token) => { - auth.invalidate(token.token) + auth.invalidate(token.id) }) await Accounts.deleteAccount(target.id) @@ -357,14 +357,14 @@ export default function (files: Files) { router.get("/:user", async (ctx) => { let acc = ctx.get("target") - let sessionToken = auth.tokenFor(ctx)! + let sessionToken = (await auth.tokenFor(ctx))! return ctx.json({ ...acc, password: undefined, email: auth.getType(sessionToken) == "User" || - auth.getPermissions(sessionToken)?.includes("email") + auth.getScopes(sessionToken)?.includes("email") ? acc.email : undefined, activeSessions: auth.Db.data.filter( diff --git a/src/server/routes/api/v1/account/prove.ts b/src/server/routes/api/v1/account/prove.ts index 05a1894..340a6ff 100644 --- a/src/server/routes/api/v1/account/prove.ts +++ b/src/server/routes/api/v1/account/prove.ts @@ -16,7 +16,7 @@ import { login, noAPIAccess, requiresAccount, - requiresPermissions, + requiresScopes, requiresTarget, scheme, } from "../../../../lib/middleware.js" diff --git a/src/server/routes/api/v1/file/index.ts b/src/server/routes/api/v1/file/index.ts index 9333b09..bf254a9 100644 --- a/src/server/routes/api/v1/file/index.ts +++ b/src/server/routes/api/v1/file/index.ts @@ -4,7 +4,7 @@ import * as auth from "../../../../lib/auth.js" import RangeParser, { type Range } from "range-parser" import ServeError from "../../../../lib/errors.js" import Files, { WebError } from "../../../../lib/files.js" -import { getAccount, requiresAccount, requiresPermissions, scheme } from "../../../../lib/middleware.js" +import { getAccount, requiresAccount, requiresScopes, scheme } from "../../../../lib/middleware.js" import {Readable} from "node:stream" import type {ReadableStream as StreamWebReadable} from "node:stream/web" import formidable from "formidable" @@ -28,7 +28,7 @@ export default function(files: Files) { router.on( ["PUT", "POST"], "/", - requiresPermissions("manage_files"), + requiresScopes("manage_files"), (ctx) => { return new Promise((resolve,reject) => { ctx.env.incoming.removeAllListeners("data") // remove hono's buffering diff --git a/src/server/routes/api/v1/file/individual.ts b/src/server/routes/api/v1/file/individual.ts index 8856ef2..279ee08 100644 --- a/src/server/routes/api/v1/file/individual.ts +++ b/src/server/routes/api/v1/file/individual.ts @@ -4,7 +4,7 @@ import * as auth from "../../../../lib/auth.js" import RangeParser, { type Range } from "range-parser" import ServeError from "../../../../lib/errors.js" import Files, { WebError } from "../../../../lib/files.js" -import { getAccount, requiresPermissions } from "../../../../lib/middleware.js" +import { getAccount, requiresScopes } from "../../../../lib/middleware.js" import {Readable} from "node:stream" import type {ReadableStream as StreamWebReadable} from "node:stream/web" import formidable from "formidable" @@ -50,10 +50,12 @@ export default function(files: Files, apiRoot: Hono) { return ServeError(ctx, 403, "you do not own this file") } + let token = (await auth.tokenFor(ctx))! + if ( - auth.getType(auth.tokenFor(ctx)!) != "User" && + auth.getType(token) != "User" && auth - .getPermissions(auth.tokenFor(ctx)!)! + .getScopes(token)! .includes("private") ) { return ServeError(ctx, 403, "insufficient permissions") diff --git a/src/server/routes/api/v1/session.ts b/src/server/routes/api/v1/session.ts index 7f21f5b..c23206d 100644 --- a/src/server/routes/api/v1/session.ts +++ b/src/server/routes/api/v1/session.ts @@ -58,17 +58,13 @@ export default function (files: Files) { return ctx.text("logged in") }) - router.get("/", requiresAccount, ctx => { - let sessionToken = auth.tokenFor(ctx) - return ctx.json({ - expiry: auth.Db.data.find( - (e) => e.token == sessionToken - )?.expire, - }) + router.get("/", requiresAccount, async ctx => { + let sessionToken = (await auth.tokenFor(ctx))! + return ctx.redirect(`/api/v1`) }) - router.delete("/", requiresAccount, (ctx) => { - auth.invalidate(auth.tokenFor(ctx)!) + router.delete("/", requiresAccount, async ctx => { + auth.invalidate((await auth.tokenFor(ctx))!) return ctx.text("logged out") }) From b130e4e2cbd319ea1dfca9e28c700d8a092903fa Mon Sep 17 00:00:00 2001 From: stringsplit <77242831+nbitzz@users.noreply.github.com> Date: Fri, 24 May 2024 21:17:05 -0700 Subject: [PATCH 4/4] fix a few implementations --- src/server/lib/accounts.ts | 28 ++++++++++------------ src/server/lib/auth.ts | 2 +- src/server/lib/middleware.ts | 27 ++++++++++----------- src/server/routes/api/v1/account/access.ts | 2 +- src/server/routes/api/v1/account/index.ts | 9 +++++-- src/server/routes/api/v1/session.ts | 6 ++--- 6 files changed, 38 insertions(+), 36 deletions(-) diff --git a/src/server/lib/accounts.ts b/src/server/lib/accounts.ts index 55f371f..1d10592 100644 --- a/src/server/lib/accounts.ts +++ b/src/server/lib/accounts.ts @@ -21,23 +21,21 @@ export type Account = z.infer * @returns A Promise which returns the new account's ID */ -export async function create(username:string,pwd:string,admin:boolean=false):Promise { - let accId = crypto.randomBytes(12).toString("hex") - - Db.data.push( - { - id: accId, - username: username, - password: password.hash(pwd), - files: [], - admin: admin, - defaultFileVisibility: "public", - settings: AccountSchemas.Settings.User.parse({}) - } - ) +export async function create(username:string,pwd:string,admin:boolean=false):Promise { + let acc: Account = { + id: crypto.randomUUID(), + username: username, + password: password.hash(pwd), + files: [], + admin: admin, + defaultFileVisibility: "public", + settings: AccountSchemas.Settings.User.parse({}) + } + Db.data.push(acc) await Db.save() - return accId + + return acc } /** diff --git a/src/server/lib/auth.ts b/src/server/lib/auth.ts index f8f42cd..722d3d0 100644 --- a/src/server/lib/auth.ts +++ b/src/server/lib/auth.ts @@ -59,7 +59,7 @@ export function makeJwt(_token: TokenResolvable) { sub: token.account, jti: token.id, ...(token.type != "User" ? { scope: token.scopes } : {}) - }) + }).setProtectedHeader({ alg: "HS256" }) return jwt.sign(config.jwtSecret) } diff --git a/src/server/lib/middleware.ts b/src/server/lib/middleware.ts index 9fbb29d..596645e 100644 --- a/src/server/lib/middleware.ts +++ b/src/server/lib/middleware.ts @@ -10,19 +10,18 @@ import { codes } from "./codes.js" * @description Middleware which adds an account, if any, to ctx.get("account") */ export const getAccount: RequestHandler = async function (ctx, next) { - let account = Accounts.getFromToken((await auth.tokenFor(ctx))!) + let uToken = (await auth.tokenFor(ctx))! + let account = Accounts.getFromToken(uToken) if (account?.suspension) - setCookie(ctx, "auth", "") + auth.invalidate(uToken) ctx.set("account", account) return next() } -export function resolveTarget(actor: Accounts.Account, targetString: string) { - return targetString == "me" +export function resolveTarget(actor: Accounts.Account, target: Accounts.AccountResolvable) { + return target == "me" ? actor - : targetString.startsWith("@") - ? Accounts.getFromUsername(targetString.slice(1)) - : Accounts.getFromId(targetString) + : Accounts.resolve(target) } /** @@ -36,7 +35,6 @@ export const getTarget: RequestHandler = async (ctx, next) => { permissions = auth.getScopes(tok) let actor = ctx.get("account") - let target = resolveTarget(actor, ctx.req.param("user")) if (!target) return ServeError(ctx, 404, "account does not exist") @@ -44,11 +42,12 @@ export const getTarget: RequestHandler = async (ctx, next) => { if (actor && ( ( target != actor // target is not the current account - && !actor?.admin // account is not admin - ) - || ( - actor?.admin // account is admin - && permissions && !permissions.includes("manage_server") // permissions does not include manage_server + && ( + !actor?.admin // account is not admin + || ( + permissions && !permissions.includes("manage_server") // account is admin but permissions does not include manage_server + ) + ) ) )) return ServeError(ctx, 403, "you cannot manage this user") @@ -188,7 +187,7 @@ export function scheme(scheme: z.ZodTypeAny, transformer: (ctx: Context) => Prom // Not really middleware but a utility -export const login = async (ctx: Context, account: string) => { +export const login = async (ctx: Context, account: Accounts.AccountResolvable) => { let token = auth.create(account, 3 * 24 * 60 * 60 * 1000) setCookie(ctx, "auth", await auth.makeJwt(token), { path: "/", diff --git a/src/server/routes/api/v1/account/access.ts b/src/server/routes/api/v1/account/access.ts index 201be99..7f6614c 100644 --- a/src/server/routes/api/v1/account/access.ts +++ b/src/server/routes/api/v1/account/access.ts @@ -90,7 +90,7 @@ export default function (files: Files) { }) router.delete("/:token", async (ctx) => { - auth.invalidate(ctx.get("targetToken").id) + auth.invalidate(ctx.get("targetToken")) return ctx.text(`deleted token ${ctx.req.param("token")}`) }) diff --git a/src/server/routes/api/v1/account/index.ts b/src/server/routes/api/v1/account/index.ts index 3e77194..9564da1 100644 --- a/src/server/routes/api/v1/account/index.ts +++ b/src/server/routes/api/v1/account/index.ts @@ -217,7 +217,12 @@ router.use(getAccount) router.on( ["GET","PATCH","DELETE"], "/:user", - requiresAccount, getTarget, accountMgmtRoute + requiresAccount, getTarget +) +router.on( + ["PATCH","DELETE"], + "/:user", + accountMgmtRoute ) function isMessage(object: any): object is Message { @@ -278,7 +283,7 @@ export default function (files: Files) { .then((account) => { if (!ctx.get("account")) login(ctx, account) - return ctx.text(account) + return ctx.text(account.id) }) .catch((e) => { console.error(e) diff --git a/src/server/routes/api/v1/session.ts b/src/server/routes/api/v1/session.ts index c23206d..4aa368d 100644 --- a/src/server/routes/api/v1/session.ts +++ b/src/server/routes/api/v1/session.ts @@ -12,6 +12,7 @@ import * as auth from "../../../lib/auth.js" import { getAccount, login, + mirror, requiresAccount, scheme } from "../../../lib/middleware.js" @@ -27,7 +28,7 @@ const router = new Hono<{ router.use(getAccount) -export default function (files: Files) { +export default function (files: Files, apiRoot: Hono) { router.post("/",scheme(z.object({ username: AccountSchemas.Username, password: AccountSchemas.StringPassword @@ -59,8 +60,7 @@ export default function (files: Files) { }) router.get("/", requiresAccount, async ctx => { - let sessionToken = (await auth.tokenFor(ctx))! - return ctx.redirect(`/api/v1`) + return ctx.json(auth.resolve((await auth.tokenFor(ctx))!)!) }) router.delete("/", requiresAccount, async ctx => {