From 7e305429b438d18eb1738615aeb8d8df2e13e6c4 Mon Sep 17 00:00:00 2001 From: Divlo Date: Mon, 29 Aug 2022 17:26:43 +0000 Subject: [PATCH] feat: make JWT refreshTokens more secure Don't store the token itself in the database, store a UUID, and when refreshing the accessToken, verify the token and verify that in the payload there is a corresponding UUID stored in the database --- .commitlintrc.json | 8 +++- src/models/RefreshToken.ts | 4 +- src/models/User.ts | 4 ++ .../users/refresh-token/__test__/post.test.ts | 10 ++--- src/services/users/refresh-token/post.ts | 24 ++++++------ .../users/signout/__test__/post.test.ts | 12 +++++- src/services/users/signout/post.ts | 37 ++++++++++++------- src/tools/utils/jwtToken.ts | 13 ++++++- 8 files changed, 75 insertions(+), 37 deletions(-) diff --git a/.commitlintrc.json b/.commitlintrc.json index d3d7f0c..43640f7 100644 --- a/.commitlintrc.json +++ b/.commitlintrc.json @@ -1 +1,7 @@ -{ "extends": ["@commitlint/config-conventional"] } +{ + "extends": ["@commitlint/config-conventional"], + "rules": { + "body-max-length": [0, "always"], + "body-max-line-length": [0, "always"] + } +} diff --git a/src/models/RefreshToken.ts b/src/models/RefreshToken.ts index fc2faad..ea476b1 100644 --- a/src/models/RefreshToken.ts +++ b/src/models/RefreshToken.ts @@ -6,7 +6,7 @@ import { date, id } from './utils.js' export const refreshTokensSchema = { id, - token: Type.String(), + token: Type.String({ format: 'uuid' }), createdAt: date.createdAt, updatedAt: date.updatedAt, userId: id @@ -15,7 +15,7 @@ export const refreshTokensSchema = { export const refreshTokenExample: RefreshToken = { id: 1, userId: userExample.id, - token: 'sometoken', + token: 'sometokenUUID', createdAt: new Date(), updatedAt: new Date() } diff --git a/src/models/User.ts b/src/models/User.ts index f331d1f..967294b 100644 --- a/src/models/User.ts +++ b/src/models/User.ts @@ -10,6 +10,10 @@ export interface UserJWT { currentStrategy: AuthenticationStrategy } +export interface UserRefreshJWT extends UserJWT { + tokenUUID: string +} + export interface UserRequest { current: User currentStrategy: AuthenticationStrategy diff --git a/src/services/users/refresh-token/__test__/post.test.ts b/src/services/users/refresh-token/__test__/post.test.ts index d0c5018..eef9c35 100644 --- a/src/services/users/refresh-token/__test__/post.test.ts +++ b/src/services/users/refresh-token/__test__/post.test.ts @@ -1,5 +1,6 @@ import tap from 'tap' import sinon from 'sinon' +import jwt from 'jsonwebtoken' import { application } from '../../../../application.js' import { authenticateUserTest } from '../../../../__test__/utils/authenticateUserTest.js' @@ -13,8 +14,7 @@ await tap.test('POST /users/refresh-token', async (t) => { }) await t.test('succeeds', async (t) => { - const { accessToken, refreshToken, refreshTokenStubValue } = - await authenticateUserTest() + const { refreshToken, refreshTokenStubValue } = await authenticateUserTest() sinon.stub(prisma, 'refreshToken').value({ ...refreshTokenStubValue, findFirst: async () => { @@ -28,9 +28,6 @@ await tap.test('POST /users/refresh-token', async (t) => { const response = await application.inject({ method: 'POST', url: '/users/refresh-token', - headers: { - authorization: `Bearer ${accessToken}` - }, payload: { refreshToken } }) const responseJson = response.json() @@ -62,6 +59,9 @@ await tap.test('POST /users/refresh-token', async (t) => { return refreshTokenExample } }) + sinon.stub(jwt, 'verify').value(() => { + throw new Error('Invalid token') + }) const response = await application.inject({ method: 'POST', url: '/users/refresh-token', diff --git a/src/services/users/refresh-token/post.ts b/src/services/users/refresh-token/post.ts index 7f7d972..d5db0ac 100644 --- a/src/services/users/refresh-token/post.ts +++ b/src/services/users/refresh-token/post.ts @@ -9,7 +9,7 @@ import { jwtSchema, expiresIn } from '../../../tools/utils/jwtToken.js' -import { UserJWT } from '../../../models/User.js' +import { UserRefreshJWT } from '../../../models/User.js' import { JWT_REFRESH_SECRET } from '../../../tools/configurations/index.js' const bodyPostRefreshTokenSchema = Type.Object({ @@ -43,20 +43,20 @@ export const postRefreshTokenUser: FastifyPluginAsync = async (fastify) => { schema: postRefreshTokenSchema, handler: async (request, reply) => { const { refreshToken } = request.body - const foundRefreshToken = await prisma.refreshToken.findFirst({ - where: { token: refreshToken } - }) - if (foundRefreshToken == null) { - throw fastify.httpErrors.forbidden() - } try { - const userJWT = jwt.verify( - foundRefreshToken.token, + const userRefreshJWT = jwt.verify( + refreshToken, JWT_REFRESH_SECRET - ) as UserJWT + ) as UserRefreshJWT + const foundRefreshToken = await prisma.refreshToken.findFirst({ + where: { token: userRefreshJWT.tokenUUID } + }) + if (foundRefreshToken == null) { + throw fastify.httpErrors.forbidden() + } const accessToken = generateAccessToken({ - id: userJWT.id, - currentStrategy: userJWT.currentStrategy + id: userRefreshJWT.id, + currentStrategy: userRefreshJWT.currentStrategy }) reply.statusCode = 200 return { diff --git a/src/services/users/signout/__test__/post.test.ts b/src/services/users/signout/__test__/post.test.ts index 0d1dcbc..2f490d3 100644 --- a/src/services/users/signout/__test__/post.test.ts +++ b/src/services/users/signout/__test__/post.test.ts @@ -1,9 +1,11 @@ import tap from 'tap' import sinon from 'sinon' +import jwt from 'jsonwebtoken' import { application } from '../../../../application.js' import prisma from '../../../../tools/database/prisma.js' import { refreshTokenExample } from '../../../../models/RefreshToken.js' +import { UserRefreshJWT } from '../../../../models/User.js' await tap.test('POST /users/signout', async (t) => { t.afterEach(() => { @@ -17,10 +19,18 @@ await tap.test('POST /users/signout', async (t) => { }, delete: async () => {} }) + sinon.stub(jwt, 'verify').value(() => { + const value: UserRefreshJWT = { + id: 1, + tokenUUID: refreshTokenExample.token, + currentStrategy: 'Local' + } + return value + }) const response = await application.inject({ method: 'POST', url: '/users/signout', - payload: { refreshToken: refreshTokenExample.token } + payload: { refreshToken: 'jwt token' } }) t.equal(response.statusCode, 200) }) diff --git a/src/services/users/signout/post.ts b/src/services/users/signout/post.ts index 2a5e6a2..604476b 100644 --- a/src/services/users/signout/post.ts +++ b/src/services/users/signout/post.ts @@ -1,12 +1,15 @@ import { Static, Type } from '@sinclair/typebox' import { FastifyPluginAsync, FastifySchema } from 'fastify' +import jwt from 'jsonwebtoken' import prisma from '../../../tools/database/prisma.js' import { fastifyErrors } from '../../../models/utils.js' -import { refreshTokensSchema } from '../../../models/RefreshToken.js' +import { JWT_REFRESH_SECRET } from '../../../tools/configurations/index.js' +import { UserRefreshJWT } from '../../../models/User.js' +import { jwtSchema } from '../../../tools/utils/jwtToken.js' const bodyPostSignoutSchema = Type.Object({ - refreshToken: refreshTokensSchema.token + refreshToken: jwtSchema.refreshToken }) type BodyPostSignoutSchemaType = Static @@ -32,21 +35,27 @@ export const postSignoutUser: FastifyPluginAsync = async (fastify) => { schema: postSignoutSchema, handler: async (request, reply) => { const { refreshToken } = request.body - const token = await prisma.refreshToken.findFirst({ - where: { - token: refreshToken + try { + const userRefreshJWT = jwt.verify( + refreshToken, + JWT_REFRESH_SECRET + ) as UserRefreshJWT + const foundRefreshToken = await prisma.refreshToken.findFirst({ + where: { token: userRefreshJWT.tokenUUID } + }) + if (foundRefreshToken == null) { + throw fastify.httpErrors.notFound() } - }) - if (token == null) { + await prisma.refreshToken.delete({ + where: { + id: foundRefreshToken.id + } + }) + reply.statusCode = 200 + return {} + } catch { throw fastify.httpErrors.notFound() } - await prisma.refreshToken.delete({ - where: { - id: token.id - } - }) - reply.statusCode = 200 - return {} } }) } diff --git a/src/tools/utils/jwtToken.ts b/src/tools/utils/jwtToken.ts index 54a83c9..35a44ff 100644 --- a/src/tools/utils/jwtToken.ts +++ b/src/tools/utils/jwtToken.ts @@ -1,3 +1,5 @@ +import { randomUUID } from 'node:crypto' + import { Type } from '@sinclair/typebox' import jwt from 'jsonwebtoken' import ms from 'ms' @@ -34,9 +36,16 @@ export const generateAccessToken = (user: UserJWT): string => { } export const generateRefreshToken = async (user: UserJWT): Promise => { - const refreshToken = jwt.sign(user, JWT_REFRESH_SECRET) + const tokenUUID = randomUUID() + const refreshToken = jwt.sign( + { + ...user, + tokenUUID + }, + JWT_REFRESH_SECRET + ) await prisma.refreshToken.create({ - data: { token: refreshToken, userId: user.id } + data: { token: tokenUUID, userId: user.id } }) return refreshToken }