refresh token
This commit is contained in:
Generated
+6
-6
@@ -1266,9 +1266,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/iconv-lite": {
|
"node_modules/iconv-lite": {
|
||||||
"version": "0.7.2",
|
"version": "0.7.3",
|
||||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz",
|
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz",
|
||||||
"integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==",
|
"integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
||||||
@@ -1809,9 +1809,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/tsx": {
|
"node_modules/tsx": {
|
||||||
"version": "4.22.4",
|
"version": "4.23.0",
|
||||||
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.22.4.tgz",
|
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.0.tgz",
|
||||||
"integrity": "sha512-X8EX+XV4QR5xCsrgxaED954zTDfY8KqlDtskKEL0cHhyS/P8b4IFOvGDQpsC9Q1XnLq915wEfwwY/zzskCtmhg==",
|
"integrity": "sha512-eUdUIaCr963q2h5u3+QwvYp0+eqPvn+egeqZUm0hwERCqqx1E3kK5ehbGCvqSE5MQAULr67ww0cA3jKc3YkM1w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ export const users = pgTable("users", {
|
|||||||
hashed_password: varchar({ length: 255 }).notNull(),
|
hashed_password: varchar({ length: 255 }).notNull(),
|
||||||
logged_in: boolean().notNull().default(false),
|
logged_in: boolean().notNull().default(false),
|
||||||
|
|
||||||
token: varchar({ length: 511 }),
|
refresh_token: varchar({ length: 511 }),
|
||||||
permissions: smallint().notNull().default(44),
|
permissions: smallint().notNull().default(44),
|
||||||
},
|
},
|
||||||
(_) => [
|
(_) => [
|
||||||
|
|||||||
@@ -42,12 +42,11 @@ export function decodeJWT(token: string) {
|
|||||||
return { header, payload, signature };
|
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 signingSecret = process.env.JWT_SECRET!;
|
||||||
|
|
||||||
const now = Math.floor(Date.now() / 1000);
|
const now = Math.floor(Date.now() / 1000);
|
||||||
// fifteen minutes
|
const expires = now + secondsToExpire;
|
||||||
const expires = now + (15 * 60);
|
|
||||||
|
|
||||||
const header: JsonWebToken["header"] = {
|
const header: JsonWebToken["header"] = {
|
||||||
alg: "HS256",
|
alg: "HS256",
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { Request, Response, NextFunction } from "express";
|
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 { verifySignature, decodeJWT } from '../helpers/jwt.ts';
|
||||||
|
import { refreshJWT } from "./refreshJWT.ts";
|
||||||
|
import { validateCall } from "./validateCall.ts";
|
||||||
|
|
||||||
export function loadJWT(
|
export function loadJWT(
|
||||||
req: Request,
|
req: Request,
|
||||||
@@ -9,22 +11,30 @@ export function loadJWT(
|
|||||||
) {
|
) {
|
||||||
const authHeader = req.header("Authorization");
|
const authHeader = req.header("Authorization");
|
||||||
|
|
||||||
// If there is no jwt, proceed to request with default permissions
|
// If there is no jwt, fail
|
||||||
if (!authHeader) {
|
if (!authHeader) {
|
||||||
req.jwt = defaultJWT();
|
return res.status(401).json({
|
||||||
|
error: "Missing JWT"
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
// otherwise decode the jwt
|
// otherwise decode the jwt
|
||||||
const [scheme, token] = authHeader.split(" ");
|
const [scheme, token] = authHeader.split(" ");
|
||||||
|
|
||||||
if (scheme.toLowerCase() != "bearer") {
|
if (scheme.toLowerCase() != "bearer") {
|
||||||
return res.status(401).json({
|
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) {
|
if (token.split(".").length !== 3) {
|
||||||
return res.status(401).json({
|
return res.status(401).json({
|
||||||
error: "Invalid JWT"
|
error: "Malformed JWT"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,25 +46,28 @@ export function loadJWT(
|
|||||||
decodedToken = decodeJWT(token);
|
decodedToken = decodeJWT(token);
|
||||||
} catch {
|
} catch {
|
||||||
return res.status(401).json({
|
return res.status(401).json({
|
||||||
error: "Invalid JWT"
|
error: "Malformed JWT"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const validClaims: boolean =
|
const validClaims: boolean =
|
||||||
decodedToken.payload.exp >= Math.floor(Date.now() / 1000) &&
|
|
||||||
decodedToken.header.alg === "HS256" &&
|
decodedToken.header.alg === "HS256" &&
|
||||||
decodedToken.header.typ === "JWT";
|
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;
|
req.jwt = decodedToken;
|
||||||
|
} else if (validJWT && validClaims && expired) {
|
||||||
|
req.jwt = decodedToken;
|
||||||
|
return refreshJWT(req, res, next);
|
||||||
} else {
|
} else {
|
||||||
// maybe attempt token refresh here
|
|
||||||
return res.status(401).json({
|
return res.status(401).json({
|
||||||
error: "Invalid JWT"
|
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}`)
|
.update(`${presalt}${password}${postsalt}`)
|
||||||
.digest("base64url");
|
.digest("base64url");
|
||||||
|
|
||||||
const user = await db
|
const potentialUsers = await db
|
||||||
.select()
|
.select()
|
||||||
.from(users)
|
.from(users)
|
||||||
.where(eq(users.username, username))
|
.where(eq(users.username, username))
|
||||||
.limit(1);
|
.limit(1);
|
||||||
|
|
||||||
|
const user = potentialUsers[0];
|
||||||
|
|
||||||
const calculatedBuff = Buffer.from(hashedPassword);
|
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 &&
|
const match = calculatedBuff.length === storedBuff.length &&
|
||||||
crypto.timingSafeEqual(calculatedBuff, storedBuff);
|
crypto.timingSafeEqual(calculatedBuff, storedBuff);
|
||||||
@@ -47,9 +49,25 @@ authRouter.get("/login", async (req: Request, res: Response) => {
|
|||||||
if (!match)
|
if (!match)
|
||||||
return res.status(401).json({ error: "401: failed to authenticate username/password" });
|
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) => {
|
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