server/client: replace JWTDenyList model with AuthToken, update middleware and routes

This commit is contained in:
Max Leiter 2022-04-06 09:08:51 -07:00
parent b6439858df
commit c6f89a28ad
No known key found for this signature in database
GPG key ID: A3512F2F2F17EBDA
5 changed files with 100 additions and 30 deletions

View file

@ -1,8 +1,8 @@
import { NextRequest, NextResponse } from 'next/server' import { NextFetchEvent, NextRequest, NextResponse } from 'next/server'
const PUBLIC_FILE = /.(.*)$/ const PUBLIC_FILE = /.(.*)$/
export function middleware(req: NextRequest) { export function middleware(req: NextRequest, event: NextFetchEvent) {
const pathname = req.nextUrl.pathname const pathname = req.nextUrl.pathname
const signedIn = req.cookies['drift-token'] const signedIn = req.cookies['drift-token']
const getURL = (pageName: string) => new URL(`/${pageName}`, req.url).href const getURL = (pageName: string) => new URL(`/${pageName}`, req.url).href
@ -19,6 +19,20 @@ export function middleware(req: NextRequest) {
const resp = NextResponse.redirect(getURL('')); const resp = NextResponse.redirect(getURL(''));
resp.clearCookie('drift-token'); resp.clearCookie('drift-token');
resp.clearCookie('drift-userid'); resp.clearCookie('drift-userid');
const signoutPromise = new Promise((resolve) => {
fetch(`${process.env.API_URL}/auth/signout`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${signedIn}`,
'x-secret-key': process.env.SECRET_KEY || '',
},
})
.then(() => {
resolve(true)
})
})
event.waitUntil(signoutPromise)
return resp return resp
} }

View file

@ -1,3 +1,4 @@
import { AuthToken } from "@lib/models/AuthToken"
import { NextFunction, Request, Response } from "express" import { NextFunction, Request, Response } from "express"
import * as jwt from "jsonwebtoken" import * as jwt from "jsonwebtoken"
import config from "../config" import config from "../config"
@ -11,7 +12,7 @@ export interface UserJwtRequest extends Request {
user?: User user?: User
} }
export default function authenticateToken( export default async function authenticateToken(
req: UserJwtRequest, req: UserJwtRequest,
res: Response, res: Response,
next: NextFunction next: NextFunction
@ -21,6 +22,17 @@ export default function authenticateToken(
if (token == null) return res.sendStatus(401) if (token == null) return res.sendStatus(401)
const authToken = await AuthToken.findOne({ where: { token: token } })
if (authToken == null) {
return res.sendStatus(401)
}
if (authToken.deletedAt) {
return res.sendStatus(401).json({
message: "Token is no longer valid",
})
}
jwt.verify(token, config.jwt_secret, async (err: any, user: any) => { jwt.verify(token, config.jwt_secret, async (err: any, user: any) => {
if (err) return res.sendStatus(403) if (err) return res.sendStatus(403)
const userObj = await UserModel.findByPk(user.id, { const userObj = await UserModel.findByPk(user.id, {

View file

@ -8,11 +8,14 @@ import {
CreatedAt, CreatedAt,
UpdatedAt, UpdatedAt,
DeletedAt, DeletedAt,
Unique Unique,
BelongsTo,
ForeignKey
} from "sequelize-typescript" } from "sequelize-typescript"
import { User } from "./User"
@Table @Table
export class JWTDenyList extends Model { export class AuthToken extends Model {
@IsUUID(4) @IsUUID(4)
@PrimaryKey @PrimaryKey
@Unique @Unique
@ -25,8 +28,15 @@ export class JWTDenyList extends Model {
@Column @Column
token!: string token!: string
@BelongsTo(() => User, "userId")
user!: User
@ForeignKey(() => User)
@Column @Column
reason!: string userId!: number
@Column
expiredReason?: string
@CreatedAt @CreatedAt
@Column @Column

View file

@ -3,7 +3,7 @@ import { DataTypes } from "sequelize"
import type { Migration } from "../database" import type { Migration } from "../database"
export const up: Migration = async ({ context: queryInterface }) => export const up: Migration = async ({ context: queryInterface }) =>
queryInterface.createTable("JWTDenyLists", { queryInterface.createTable("AuthTokens", {
id: { id: {
type: DataTypes.UUID, type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4, defaultValue: DataTypes.UUIDV4,
@ -13,8 +13,9 @@ export const up: Migration = async ({ context: queryInterface }) =>
token: { token: {
type: DataTypes.STRING type: DataTypes.STRING
}, },
reason: { expiredReason: {
type: DataTypes.STRING type: DataTypes.STRING,
allowNull: true
}, },
createdAt: { createdAt: {
type: DataTypes.DATE type: DataTypes.DATE
@ -23,9 +24,20 @@ export const up: Migration = async ({ context: queryInterface }) =>
type: DataTypes.DATE type: DataTypes.DATE
}, },
deletedAt: { deletedAt: {
type: DataTypes.DATE type: DataTypes.DATE,
allowNull: true
},
userId: {
type: DataTypes.UUID,
allowNull: false,
references: {
model: "users",
key: "id"
},
onUpdate: "CASCADE",
onDelete: "CASCADE"
} }
}) })
export const down: Migration = async ({ context: queryInterface }) => export const down: Migration = async ({ context: queryInterface }) =>
queryInterface.dropTable("JWTDenyLists") queryInterface.dropTable("AuthTokens")

View file

@ -1,11 +1,12 @@
import { Router } from "express" import { Router } from "express"
import { genSalt, hash, compare } from "bcryptjs" import { genSalt, hash, compare } from "bcryptjs"
import { User } from "@lib/models/User" import { User } from "@lib/models/User"
import { JWTDenyList } from "@lib/models/JWTDenyList" import { AuthToken } from "@lib/models/AuthToken"
import { sign, verify } from "jsonwebtoken" import { sign, verify } from "jsonwebtoken"
import config from "@lib/config" import config from "@lib/config"
import jwt from "@lib/middleware/jwt" import jwt from "@lib/middleware/jwt"
import { celebrate, Joi } from "celebrate" import { celebrate, Joi } from "celebrate"
import secretKey from "@lib/middleware/secret-key"
const NO_EMPTY_SPACE_REGEX = /^\S*$/ const NO_EMPTY_SPACE_REGEX = /^\S*$/
@ -71,7 +72,7 @@ auth.post(
const created_user = await User.create(user) const created_user = await User.create(user)
const token = generateAccessToken(created_user.id) const token = generateAccessToken(created_user)
res.status(201).json({ token: token, userId: created_user.id }) res.status(201).json({ token: token, userId: created_user.id })
} catch (e) { } catch (e) {
@ -109,7 +110,7 @@ auth.post(
const password_valid = await compare(req.body.password, user.password) const password_valid = await compare(req.body.password, user.password)
if (password_valid) { if (password_valid) {
const token = generateAccessToken(user.id) const token = generateAccessToken(user)
res.status(200).json({ token: token, userId: user.id }) res.status(200).json({ token: token, userId: user.id })
} else { } else {
throw errorToThrow throw errorToThrow
@ -132,8 +133,19 @@ auth.get("/requires-passcode", async (req, res, next) => {
} }
}) })
function generateAccessToken(id: string) {
return sign({ id: id }, config.jwt_secret, { expiresIn: "2d" }) /**
* Creates an access token, stores it in AuthToken table, and returns it
*/
function generateAccessToken(user: User) {
const token = sign({ id: user.id }, config.jwt_secret, { expiresIn: "2d" })
const authToken = new AuthToken({
userId: user.id,
token: token
})
authToken.save()
return token
} }
auth.get("/verify-token", jwt, async (req, res, next) => { auth.get("/verify-token", jwt, async (req, res, next) => {
@ -146,29 +158,39 @@ auth.get("/verify-token", jwt, async (req, res, next) => {
} }
}) })
auth.post("/signout", jwt, async (req, res, next) => { auth.post("/signout", secretKey, async (req, res, next) => {
try { try {
const authHeader = req.headers["authorization"] const authHeader = req.headers["authorization"]
const token = authHeader?.split(" ")[1] const token = authHeader?.split(" ")[1]
let reason = "" let reason = ""
if (token == null) return res.sendStatus(401) if (token == null) return res.sendStatus(401)
verify(token, config.jwt_secret, (err: any, user: any) => { verify(token, config.jwt_secret, async (err: any, user: any) => {
if (err) return res.sendStatus(403) if (err) {
if (user) {
reason = "Manually revoked"
} else {
reason = "Token expired" reason = "Token expired"
} else if (user) {
reason = "User signed out"
} else {
reason = "Unknown"
} }
})
const denylist = await new JWTDenyList({ token, reason }) // find and destroy the AuthToken + set the reason
await denylist.save() const authToken = await AuthToken.findOne({ where: { token: token } })
if (authToken == null) {
res.sendStatus(401)
} else {
authToken.expiredReason = reason
authToken.save()
authToken.destroy()
}
req.headers["authorization"] = "" req.headers["authorization"] = ""
res.status(201).json({ res.status(201).json({
message: "You are now logged out", message: "You are now logged out",
token, token,
reason reason
}) })
})
} catch (e) { } catch (e) {
next(e) next(e)
} }