Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -405,6 +405,37 @@ async function validateToken(request, decodedToken) {
}
```

#### validateDecoded

`validateDecoded` allows you to validate the decoded payload of the JWT before it is considered trusted. This is useful for custom validation logic such as checking specific claims, types, or applying JSON Schema-based validations.

This function can be **synchronous** or return a **Promise**. If it throws or rejects, Fastify will return a `400 Bad Request` with the error message.

```js
fastify.register(jwt, {
secret: 'supersecret',
validateDecoded: (payload) => {
if (!payload.isVerified) {
throw new Error('User is not verified')
}
}
})
```

You can also use an async function:
```js
fastify.register(jwt, {
secret: 'supersecret',
validateDecoded: async (payload) => {
const isAllowed = await checkInDatabase(payload.userId)
if (!isAllowed) {
throw new Error('Not allowed')
}
}
})
```


### `formatUser`

#### Example with formatted user
Expand Down Expand Up @@ -909,6 +940,22 @@ fastify.get('/', async (request, reply) => {

```

### Token Payload Validation (`validateDecoded`)

You can use the `validateDecoded` option to validate the decoded payload after the token is verified but before the request is processed.

Example:
```js
fastify.register(require('@fastify/jwt'), {
secret: 'supersecret',
validateDecoded: async (payload) => {
if (!payload.role || payload.role !== 'admin') {
throw new Error('Token must include admin role')
}
}
})
```

## Acknowledgments

This project is kindly sponsored by:
Expand Down
19 changes: 19 additions & 0 deletions jwt.js
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ function fastifyJwt (fastify, options, next) {
sign: initialSignOptions = {},
trusted,
decoratorName = 'user',
validateDecoded,
// TODO: disable on next major
// enable errorCacheTTL to prevent breaking change
verify: initialVerifyOptions = { errorCacheTTL: 600000 },
Expand Down Expand Up @@ -148,6 +149,7 @@ function fastifyJwt (fastify, options, next) {
, 401)
const BadRequestError = createError('FST_JWT_BAD_REQUEST', messagesOptions.badRequestErrorMessage, 400)
const BadCookieRequestError = createError('FST_JWT_BAD_COOKIE_REQUEST', messagesOptions.badCookieRequestErrorMessage, 400)
const AuthorizationTokenValidationError = createError('FST_JWT_VALIDATION_FAILED', 'Token payload validation failed', 400)

const jwtDecorator = {
decode,
Expand Down Expand Up @@ -512,6 +514,23 @@ function fastifyJwt (fastify, options, next) {
return wrapError(error, callback)
}
},
function validateClaims (result, callback) {
if (!validateDecoded) return callback(null, result)

try {
const maybePromise = validateDecoded(result)

if (maybePromise?.then) {
maybePromise
.then(() => callback(null, result))
.catch(err => callback(new AuthorizationTokenValidationError(err.message)))
} else {
callback(null, result)
}
} catch (err) {
callback(new AuthorizationTokenValidationError(err.message))
}
},
function checkIfIsTrusted (result, callback) {
if (!trusted) {
callback(null, result)
Expand Down
139 changes: 139 additions & 0 deletions test/validate-decoded-option.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
'use strict'

const { test } = require('node:test')
const assert = require('node:assert')
const Fastify = require('fastify')
const jwt = require('../jwt')

test('validateDecoded option - success case', async (t) => {
const fastify = Fastify()
fastify.register(jwt, {
secret: 'supersecret',
validateDecoded: (payload) => {
assert.equal(payload.foo, 'bar')
}
})

fastify.get('/protected', {
handler: async (request, reply) => {
await request.jwtVerify()
return { user: request.user }
}
})

await fastify.ready()

const token = fastify.jwt.sign({ foo: 'bar' })

const response = await fastify.inject({
method: 'GET',
url: '/protected',
headers: {
Authorization: `Bearer ${token}`
}
})

assert.equal(response.statusCode, 200)

const body = JSON.parse(response.body)
assert.equal(body.user.foo, 'bar')
assert.ok(body.user.iat)
})

test('validateDecoded option - should throw and block access', async (t) => {
const fastify = Fastify()
fastify.register(jwt, {
secret: 'supersecret',
validateDecoded: (payload) => {
if (!payload.admin) throw new Error('Unauthorized')
}
})

fastify.get('/admin', {
handler: async (request, reply) => {
await request.jwtVerify()
return { user: request.user }
}
})

await fastify.ready()

const token = fastify.jwt.sign({ foo: 'bar' })

const response = await fastify.inject({
method: 'GET',
url: '/admin',
headers: {
Authorization: `Bearer ${token}`
}
})

assert.equal(response.statusCode, 400)
assert.match(response.body, /Unauthorized/)
})

test('validateDecoded option - async function', async (t) => {
const fastify = Fastify()
fastify.register(jwt, {
secret: 'supersecret',
validateDecoded: async (payload) => {
if (!payload.verified) throw new Error('Not verified')
}
})

fastify.get('/async-check', {
handler: async (request, reply) => {
await request.jwtVerify()
return { user: request.user }
}
})

await fastify.ready()

const token = fastify.jwt.sign({ verified: true })

const response = await fastify.inject({
method: 'GET',
url: '/async-check',
headers: {
Authorization: `Bearer ${token}`
}
})

assert.equal(response.statusCode, 200)

const body = JSON.parse(response.body)
assert.equal(body.user.verified, true)
assert.ok(body.user.iat)
})
test('validateDecoded - returns 400 with validation failure', async (t) => {
const fastify = Fastify()
fastify.register(jwt, {
secret: 'supersecret',
validateDecoded: (payload) => {
throw new Error('Missing required claim')
}
})

fastify.get('/protected', {
handler: async (request, reply) => {
await request.jwtVerify()
return { user: request.user }
}
})

await fastify.ready()

const token = fastify.jwt.sign({ foo: 'bar' })

const response = await fastify.inject({
method: 'GET',
url: '/protected',
headers: {
Authorization: `Bearer ${token}`
}
})

assert.equal(response.statusCode, 400)
assert.match(response.body, /Missing required claim/)
})
1 change: 1 addition & 0 deletions types/jwt.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ declare namespace fastifyJwt {
decodedToken: { [k: string]: any }
) => boolean | Promise<boolean> | SignPayloadType | Promise<SignPayloadType>
formatUser?: (payload: SignPayloadType) => UserType
validateDecoded?: (payload: Record<string, any>) => void | Promise<void>
jwtDecode?: string
namespace?: string
jwtVerify?: string
Expand Down