refresh token
This commit is contained in:
Generated
+6
-6
@@ -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": {
|
||||
|
||||
@@ -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),
|
||||
},
|
||||
(_) => [
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
@@ -0,0 +1,9 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
|
||||
export function validateCall(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
next();
|
||||
};
|
||||
@@ -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) => {
|
||||
|
||||
Generated
+664
-845
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user