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 = /.(.*)$/
|
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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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, {
|
||||||
|
|
|
@ -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
|
|
@ -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")
|
|
@ -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,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 {
|
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 } })
|
||||||
req.headers["authorization"] = ""
|
if (authToken == null) {
|
||||||
res.status(201).json({
|
res.sendStatus(401)
|
||||||
message: "You are now logged out",
|
} else {
|
||||||
token,
|
authToken.expiredReason = reason
|
||||||
reason
|
authToken.save()
|
||||||
|
authToken.destroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
req.headers["authorization"] = ""
|
||||||
|
res.status(201).json({
|
||||||
|
message: "You are now logged out",
|
||||||
|
token,
|
||||||
|
reason
|
||||||
|
})
|
||||||
})
|
})
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
next(e)
|
next(e)
|
||||||
|
|
Loading…
Reference in a new issue