diff --git a/api/src/config.js b/api/src/config.js index 866d8400..1fd35427 100644 --- a/api/src/config.js +++ b/api/src/config.js @@ -34,6 +34,8 @@ const env = { externalProxy: process.env.API_EXTERNAL_PROXY, turnstileSecret: process.env.TURNSTILE_SECRET, + jwtSecret: process.env.JWT_SECRET, + jwtLifetime: process.env.JWT_EXPIRY || 120, } export { diff --git a/api/src/core/api.js b/api/src/core/api.js index 27f87325..60fdf86c 100644 --- a/api/src/core/api.js +++ b/api/src/core/api.js @@ -13,10 +13,11 @@ import { languageCode } from "../misc/utils.js"; import { createResponse, normalizeRequest, getIP } from "../processing/request.js"; import { verifyStream, getInternalStream } from "../stream/manage.js"; import { randomizeCiphers } from "../misc/randomize-ciphers.js"; -import { verifyTurnstileToken } from "../misc/turnstile.js"; +import { verifyTurnstileToken } from "../security/turnstile.js"; import { extract } from "../processing/url.js"; import match from "../processing/match.js"; import stream from "../stream/stream.js"; +import jwt from "../security/jwt.js"; const git = { branch: await getBranch(), @@ -100,7 +101,7 @@ export function runAPI(express, app, __dirname) { }) app.use('/', express.json({ limit: 1024 })); - app.use('/', (err, _, res, next) => { + app.use('/post', (err, _, res, next) => { if (err) { const { status, body } = createResponse("error", { code: "error.body_invalid", @@ -114,6 +115,33 @@ export function runAPI(express, app, __dirname) { next(); }); + app.post("/session", async (req, res) => { + if (!env.turnstileSecret || !env.jwtSecret) { + return fail("error.api.auth.not_configured") + } + + const turnstileResponse = req.header("cf-turnstile-response"); + + if (!turnstileResponse) { + return fail("error.api.auth.turnstile.missing"); + } + + const turnstileResult = await verifyTurnstileToken( + turnstileResponse, + req.ip + ); + + if (!turnstileResult) { + return fail("error.api.auth.turnstile.invalid"); + } + + try { + res.json(jwt.generate()); + } catch { + return fail("error.api.generic"); + } + }); + app.post('/', async (req, res) => { const request = req.body; const lang = languageCode(req); @@ -123,6 +151,25 @@ export function runAPI(express, app, __dirname) { res.status(status).json(body); } + if (env.jwtSecret) { + const authorization = req.header("Authorization"); + if (!authorization) { + return fail("error.api.auth.jwt.missing"); + } + + if (!authorization.startsWith("Bearer ")) { + return fail("error.api.auth.jwt.invalid"); + } + + const verifyJwt = jwt.verify( + req.header("Authorization").split("Bearer ", 2)[1] + ); + + if (!verifyJwt) { + return fail("error.api.auth.jwt.invalid"); + } + } + if (!acceptRegex.test(req.header('Accept'))) { return fail('ErrorInvalidAcceptHeader'); } @@ -135,23 +182,6 @@ export function runAPI(express, app, __dirname) { return fail('ErrorNoLink'); } - if (env.turnstileSecret) { - const turnstileResponse = req.header("cf-turnstile-response"); - - if (!turnstileResponse) { - return fail("error.api.authentication"); - } - - const turnstileResult = await verifyTurnstileToken( - turnstileResponse, - req.ip - ); - - if (!turnstileResult) { - return fail("error.api.authentication"); - } - } - if (request.youtubeDubBrowserLang) { request.youtubeDubLang = lang; } diff --git a/api/src/security/jwt.js b/api/src/security/jwt.js new file mode 100644 index 00000000..d14f6a75 --- /dev/null +++ b/api/src/security/jwt.js @@ -0,0 +1,59 @@ +import { nanoid } from "nanoid"; +import { createHmac } from "crypto"; + +import { env } from "../config.js"; + +const toBase64URL = (b) => Buffer.from(b).toString("base64url"); +const fromBase64URL = (b) => Buffer.from(b, "base64url").toString(); + +const makeHmac = (header, payload) => + createHmac("sha256", env.jwtSecret) + .update(`${header}.${payload}`) + .digest("base64url"); + +export const generate = () => { + const exp = new Date().getTime() + env.jwtLifetime * 1000; + + const header = toBase64URL(JSON.stringify({ + alg: "HS256", + typ: "JWT" + })); + + const payload = toBase64URL(JSON.stringify({ + jti: nanoid(3), + exp, + })); + + const signature = makeHmac(header, payload); + + return { + token: `${header}.${payload}.${signature}`, + exp, + }; +} + +export const verify = (jwt) => { + const [header, payload, signature] = jwt.split(".", 3); + const timestamp = new Date().getTime(); + + if ([header, payload, signature].join('.') !== jwt) { + return false; + } + + const verifySignature = makeHmac(header, payload); + + if (verifySignature !== signature) { + return false; + } + + if (timestamp >= JSON.parse(fromBase64URL(payload)).exp) { + return false; + } + + return true; +} + +export default { + generate, + verify, +} diff --git a/api/src/misc/turnstile.js b/api/src/security/turnstile.js similarity index 100% rename from api/src/misc/turnstile.js rename to api/src/security/turnstile.js