api: bind session tokens to ip hash

This commit is contained in:
jj 2025-03-30 17:03:52 +00:00
parent bf5937e336
commit 1f768df4ec
No known key found for this signature in database
3 changed files with 22 additions and 16 deletions

View File

@ -175,7 +175,7 @@ export const runAPI = async (express, app, __dirname, isPrimary = true) => {
return fail(res, "error.api.auth.jwt.invalid"); return fail(res, "error.api.auth.jwt.invalid");
} }
if (!jwt.verify(token)) { if (!jwt.verify(token, getIP(req, 32))) {
return fail(res, "error.api.auth.jwt.invalid"); return fail(res, "error.api.auth.jwt.invalid");
} }
@ -221,7 +221,7 @@ export const runAPI = async (express, app, __dirname, isPrimary = true) => {
} }
try { try {
res.json(jwt.generate()); res.json(jwt.generate(getIP(req, 32)));
} catch { } catch {
return fail(res, "error.api.generic"); return fail(res, "error.api.generic");
} }

View File

@ -82,14 +82,13 @@ export function normalizeRequest(request) {
)); ));
} }
export function getIP(req) { export function getIP(req, prefix = 56) {
const strippedIP = req.ip.replace(/^::ffff:/, ''); const strippedIP = req.ip.replace(/^::ffff:/, '');
const ip = ipaddr.parse(strippedIP); const ip = ipaddr.parse(strippedIP);
if (ip.kind() === 'ipv4') { if (ip.kind() === 'ipv4') {
return strippedIP; return strippedIP;
} }
const prefix = 56;
const v6Bytes = ip.toByteArray(); const v6Bytes = ip.toByteArray();
v6Bytes.fill(0, prefix / 8); v6Bytes.fill(0, prefix / 8);

View File

@ -6,12 +6,19 @@ import { env } from "../config.js";
const toBase64URL = (b) => Buffer.from(b).toString("base64url"); const toBase64URL = (b) => Buffer.from(b).toString("base64url");
const fromBase64URL = (b) => Buffer.from(b, "base64url").toString(); const fromBase64URL = (b) => Buffer.from(b, "base64url").toString();
const makeHmac = (header, payload) => const makeHmac = (data) => {
createHmac("sha256", env.jwtSecret) return createHmac("sha256", env.jwtSecret)
.update(`${header}.${payload}`) .update(data)
.digest("base64url"); .digest("base64url");
}
const generate = () => { const sign = (header, payload) =>
makeHmac(`${header}.${payload}`);
const getIPHash = (ip) =>
makeHmac(ip).slice(0, 8);
const generate = (ip) => {
const exp = Math.floor(new Date().getTime() / 1000) + env.jwtLifetime; const exp = Math.floor(new Date().getTime() / 1000) + env.jwtLifetime;
const header = toBase64URL(JSON.stringify({ const header = toBase64URL(JSON.stringify({
@ -21,10 +28,11 @@ const generate = () => {
const payload = toBase64URL(JSON.stringify({ const payload = toBase64URL(JSON.stringify({
jti: nanoid(8), jti: nanoid(8),
sub: getIPHash(ip),
exp, exp,
})); }));
const signature = makeHmac(header, payload); const signature = sign(header, payload);
return { return {
token: `${header}.${payload}.${signature}`, token: `${header}.${payload}.${signature}`,
@ -32,7 +40,7 @@ const generate = () => {
}; };
} }
const verify = (jwt) => { const verify = (jwt, ip) => {
const [header, payload, signature] = jwt.split(".", 3); const [header, payload, signature] = jwt.split(".", 3);
const timestamp = Math.floor(new Date().getTime() / 1000); const timestamp = Math.floor(new Date().getTime() / 1000);
@ -40,17 +48,16 @@ const verify = (jwt) => {
return false; return false;
} }
const verifySignature = makeHmac(header, payload); const verifySignature = sign(header, payload);
if (verifySignature !== signature) { if (verifySignature !== signature) {
return false; return false;
} }
if (timestamp >= JSON.parse(fromBase64URL(payload)).exp) { const data = JSON.parse(fromBase64URL(payload));
return false;
}
return true; return getIPHash(ip) === data.sub
&& timestamp <= data.exp;
} }
export default { export default {