server/client: replace JWTDenyList model with AuthToken, update middleware and routes
This commit is contained in:
parent
b6439858df
commit
c6f89a28ad
5 changed files with 100 additions and 30 deletions
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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, {
|
||||
|
|
|
@ -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
|
|
@ -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")
|
|
@ -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,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 {
|
||||
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()
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue