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 = /.(.*)$/
export function middleware(req: NextRequest) {
export function middleware(req: NextRequest, event: NextFetchEvent) {
const pathname = req.nextUrl.pathname
const signedIn = req.cookies['drift-token']
const getURL = (pageName: string) => new URL(`/${pageName}`, req.url).href
@ -19,6 +19,20 @@ export function middleware(req: NextRequest) {
const resp = NextResponse.redirect(getURL(''));
resp.clearCookie('drift-token');
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
}

View file

@ -1,3 +1,4 @@
import { AuthToken } from "@lib/models/AuthToken"
import { NextFunction, Request, Response } from "express"
import * as jwt from "jsonwebtoken"
import config from "../config"
@ -11,7 +12,7 @@ export interface UserJwtRequest extends Request {
user?: User
}
export default function authenticateToken(
export default async function authenticateToken(
req: UserJwtRequest,
res: Response,
next: NextFunction
@ -21,6 +22,17 @@ export default function authenticateToken(
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) => {
if (err) return res.sendStatus(403)
const userObj = await UserModel.findByPk(user.id, {

View file

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

View file

@ -3,7 +3,7 @@ import { DataTypes } from "sequelize"
import type { Migration } from "../database"
export const up: Migration = async ({ context: queryInterface }) =>
queryInterface.createTable("JWTDenyLists", {
queryInterface.createTable("AuthTokens", {
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
@ -13,8 +13,9 @@ export const up: Migration = async ({ context: queryInterface }) =>
token: {
type: DataTypes.STRING
},
reason: {
type: DataTypes.STRING
expiredReason: {
type: DataTypes.STRING,
allowNull: true
},
createdAt: {
type: DataTypes.DATE
@ -23,9 +24,20 @@ export const up: Migration = async ({ context: queryInterface }) =>
type: DataTypes.DATE
},
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 }) =>
queryInterface.dropTable("JWTDenyLists")
queryInterface.dropTable("AuthTokens")

View file

@ -1,11 +1,12 @@
import { Router } from "express"
import { genSalt, hash, compare } from "bcryptjs"
import { User } from "@lib/models/User"
import { JWTDenyList } from "@lib/models/JWTDenyList"
import { AuthToken } from "@lib/models/AuthToken"
import { sign, verify } from "jsonwebtoken"
import config from "@lib/config"
import jwt from "@lib/middleware/jwt"
import { celebrate, Joi } from "celebrate"
import secretKey from "@lib/middleware/secret-key"
const NO_EMPTY_SPACE_REGEX = /^\S*$/
@ -71,7 +72,7 @@ auth.post(
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 })
} catch (e) {
@ -109,7 +110,7 @@ auth.post(
const password_valid = await compare(req.body.password, user.password)
if (password_valid) {
const token = generateAccessToken(user.id)
const token = generateAccessToken(user)
res.status(200).json({ token: token, userId: user.id })
} else {
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) => {
@ -146,28 +158,38 @@ 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 {
const authHeader = req.headers["authorization"]
const token = authHeader?.split(" ")[1]
let reason = ""
if (token == null) return res.sendStatus(401)
verify(token, config.jwt_secret, (err: any, user: any) => {
if (err) return res.sendStatus(403)
if (user) {
reason = "Manually revoked"
} else {
verify(token, config.jwt_secret, async (err: any, user: any) => {
if (err) {
reason = "Token expired"
} else if (user) {
reason = "User signed out"
} else {
reason = "Unknown"
}
})
const denylist = await new JWTDenyList({ token, reason })
await denylist.save()
req.headers["authorization"] = ""
res.status(201).json({
message: "You are now logged out",
token,
reason
// find and destroy the AuthToken + set the reason
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"] = ""
res.status(201).json({
message: "You are now logged out",
token,
reason
})
})
} catch (e) {
next(e)