refresh token

This commit is contained in:
2026-07-03 21:42:37 -05:00
parent 2ba686debe
commit af4004040c
8 changed files with 797 additions and 869 deletions
+6 -6
View File
@@ -1266,9 +1266,9 @@
}
},
"node_modules/iconv-lite": {
"version": "0.7.2",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz",
"integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==",
"version": "0.7.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz",
"integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==",
"license": "MIT",
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3.0.0"
@@ -1809,9 +1809,9 @@
}
},
"node_modules/tsx": {
"version": "4.22.4",
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.22.4.tgz",
"integrity": "sha512-X8EX+XV4QR5xCsrgxaED954zTDfY8KqlDtskKEL0cHhyS/P8b4IFOvGDQpsC9Q1XnLq915wEfwwY/zzskCtmhg==",
"version": "4.23.0",
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.0.tgz",
"integrity": "sha512-eUdUIaCr963q2h5u3+QwvYp0+eqPvn+egeqZUm0hwERCqqx1E3kK5ehbGCvqSE5MQAULr67ww0cA3jKc3YkM1w==",
"dev": true,
"license": "MIT",
"dependencies": {
+1 -1
View File
@@ -80,7 +80,7 @@ export const users = pgTable("users", {
hashed_password: varchar({ length: 255 }).notNull(),
logged_in: boolean().notNull().default(false),
token: varchar({ length: 511 }),
refresh_token: varchar({ length: 511 }),
permissions: smallint().notNull().default(44),
},
(_) => [
+2 -3
View File
@@ -42,12 +42,11 @@ export function decodeJWT(token: string) {
return { header, payload, signature };
}
export function generateSignedJWT(userId: string, permissions: number): string {
export function generateSignedJWT(userId: string, permissions: number, secondsToExpire: number): string {
const signingSecret = process.env.JWT_SECRET!;
const now = Math.floor(Date.now() / 1000);
// fifteen minutes
const expires = now + (15 * 60);
const expires = now + secondsToExpire;
const header: JsonWebToken["header"] = {
alg: "HS256",
+23 -10
View File
@@ -1,6 +1,8 @@
import { Request, Response, NextFunction } from "express";
import JsonWebToken, { defaultJWT } from '../types/JsonWebToken.ts';
import JsonWebToken from '../types/JsonWebToken.ts';
import { verifySignature, decodeJWT } from '../helpers/jwt.ts';
import { refreshJWT } from "./refreshJWT.ts";
import { validateCall } from "./validateCall.ts";
export function loadJWT(
req: Request,
@@ -9,22 +11,30 @@ export function loadJWT(
) {
const authHeader = req.header("Authorization");
// If there is no jwt, proceed to request with default permissions
// If there is no jwt, fail
if (!authHeader) {
req.jwt = defaultJWT();
return res.status(401).json({
error: "Missing JWT"
});
} else {
// otherwise decode the jwt
const [scheme, token] = authHeader.split(" ");
if (scheme.toLowerCase() != "bearer") {
return res.status(401).json({
error: "Invalid JWT"
error: "Invalid Authorization Scheme"
});
};
if (token === "") {
return res.status(401).json({
error: "Missing JWT"
});
};
if (token.split(".").length !== 3) {
return res.status(401).json({
error: "Invalid JWT"
error: "Malformed JWT"
});
}
@@ -36,25 +46,28 @@ export function loadJWT(
decodedToken = decodeJWT(token);
} catch {
return res.status(401).json({
error: "Invalid JWT"
error: "Malformed JWT"
});
}
const validClaims: boolean =
decodedToken.payload.exp >= Math.floor(Date.now() / 1000) &&
decodedToken.header.alg === "HS256" &&
decodedToken.header.typ === "JWT";
if (validJWT && validClaims) {
const expired: boolean = decodedToken.payload.exp < Math.floor(Date.now() / 1000);
if (validJWT && validClaims && !expired) {
req.jwt = decodedToken;
} else if (validJWT && validClaims && expired) {
req.jwt = decodedToken;
return refreshJWT(req, res, next);
} else {
// maybe attempt token refresh here
return res.status(401).json({
error: "Invalid JWT"
});
}
}
next();
return validateCall(req, res, next);
};
+70
View File
@@ -0,0 +1,70 @@
import { Request, Response, NextFunction } from "express";
import { validateCall } from "./validateCall.ts";
import { drizzle } from 'drizzle-orm/node-postgres';
import { eq } from 'drizzle-orm';
import { users } from '../db/schema.ts';
import { decodeJWT, generateSignedJWT, verifySignature } from "../helpers/jwt.ts";
const db = drizzle(process.env.DATABASE_URL!);
export async function refreshJWT(
req: Request,
res: Response,
next: NextFunction
) {
// get related user from jwt
const potentialUsers = await db
.select()
.from(users)
.where(eq(users.id, req.jwt.payload.uid));
const user = potentialUsers[0];
if (user.refresh_token === null) {
return res.status(403).json({
error: "No Refresh Token"
});
}
const [header, payload, signature] = user.refresh_token.split(".");
const validJWT: boolean = verifySignature(header, payload, signature);
if (!validJWT) {
return res.status(403).json({
error: "How did an invalid token get in my database?"
});
};
let refreshToken;
try {
refreshToken = decodeJWT(user.refresh_token)
} catch {
return res.status(403).json({
error: "How did an invalid token get in my database?"
});
}
const validClaims: boolean =
refreshToken.header.alg === "HS256" &&
refreshToken.header.typ === "JWT";
const expired: boolean = refreshToken.payload.exp < Math.floor(Date.now() / 1000);
if (!validClaims || expired) {
return res.status(403).json({
error: "Expired Refresh Token"
});
}
const tempToExpire = 60 * 15;
const jwt = generateSignedJWT(user.id, user.permissions, tempToExpire)
res.json({ signed_jwt: jwt });
// Have to overwrite with new jwt
// Also I ain't try/catching cause I just made it
const parsedJWT = decodeJWT(jwt);
req.jwt = parsedJWT;
return validateCall(req, res, next);
};
+9
View File
@@ -0,0 +1,9 @@
import { Request, Response, NextFunction } from "express";
export function validateCall(
req: Request,
res: Response,
next: NextFunction
) {
next();
};
+22 -4
View File
@@ -32,14 +32,16 @@ authRouter.get("/login", async (req: Request, res: Response) => {
.update(`${presalt}${password}${postsalt}`)
.digest("base64url");
const user = await db
const potentialUsers = await db
.select()
.from(users)
.where(eq(users.username, username))
.limit(1);
const user = potentialUsers[0];
const calculatedBuff = Buffer.from(hashedPassword);
const storedBuff = Buffer.from(user[0].hashed_password);
const storedBuff = Buffer.from(user.hashed_password);
const match = calculatedBuff.length === storedBuff.length &&
crypto.timingSafeEqual(calculatedBuff, storedBuff);
@@ -47,9 +49,25 @@ authRouter.get("/login", async (req: Request, res: Response) => {
if (!match)
return res.status(401).json({ error: "401: failed to authenticate username/password" });
const jwt = generateSignedJWT(user[0].id, user[0].permissions)
// fifteen minutes
const tempToExpire = 60 * 15;
const jwt = generateSignedJWT(user.id, user.permissions, tempToExpire)
res.json({ signed_jwt: jwt });
// 7 days
const refreshToExpire = 60 * 60 * 24 * 7;
const refreshjwt = generateSignedJWT(user.id, user.permissions, refreshToExpire);
const resp = await db
.update(users)
.set({
refresh_token: refreshjwt
})
.where(eq(users.id, user.id));
res.json({
signed_jwt: jwt,
refresh_resp: resp
});
});
authRouter.get("/logout", async (req: Request, res: Response) => {
+664 -845
View File
File diff suppressed because it is too large Load Diff