Skip to content

Commit

Permalink
feat: unconditionally decorate request with jwtDecode (#297)
Browse files Browse the repository at this point in the history
  • Loading branch information
maslennikov authored Jun 29, 2023
1 parent a920699 commit 5611e1f
Show file tree
Hide file tree
Showing 7 changed files with 235 additions and 181 deletions.
17 changes: 11 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -432,10 +432,13 @@ fastify.get("/", async (request, reply) => {

### `namespace`

To define multiple JWT validators on the same routes, you may use the `namespace` option.
You can combine this with custom names for `jwtVerify` and `jwtSign`.
To define multiple JWT validators on the same routes, you may use the
`namespace` option. You can combine this with custom names for `jwtVerify`,
`jwtDecode`, and `jwtSign`.

When you omit the `jwtVerify` and `jwtSign` options, the default function name will be `<namespace>JwtVerify` and `<namespace>JwtSign`.
When you omit the `jwtVerify`, `jwtDecode`, or `jwtSign` options, the default
function name will be `<namespace>JwtVerify`, `<namespace>JwtDecode` and
`<namespace>JwtSign` correspondingly.

#### Example with namespace

Expand All @@ -445,12 +448,16 @@ const fastify = require('fastify')
fastify.register(jwt, {
secret: 'test',
namespace: 'security',
// will decorate request with `securityVerify`, `securitySign`,
// and default `securityJwtDecode` since no custom alias provided
jwtVerify: 'securityVerify',
jwtSign: 'securitySign'
})

fastify.register(jwt, {
secret: 'fastify',
// will decorate request with default `airDropJwtVerify`, `airDropJwtSign`,
// and `airDropJwtDecode` since no custom aliases provided
namespace: 'airDrop'
})

Expand Down Expand Up @@ -642,9 +649,7 @@ For your convenience, `request.jwtVerify()` will look for the token in the cooki
### request.jwtDecode([options,] callback)
Decode a JWT without verifying
As of 3.2.0, decorated when `options.jwtDecode` is truthy. Will become non-conditionally decorated in 4.0.0. This avoid breaking change that would effect fastify-auth0-verify.
Decode a JWT without verifying.
`options` must be an `Object` and can contain `verify` and `decode` options.
Expand Down
2 changes: 1 addition & 1 deletion jwt.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ declare namespace fastifyJwt {
decodedToken: {[k: string]: any}
) => boolean | Promise<boolean> | SignPayloadType | Promise<SignPayloadType>
formatUser?: (payload: SignPayloadType) => UserType
jwtDecode?: boolean | string
jwtDecode?: string
namespace?: string
jwtVerify?: string
jwtSign?: string
Expand Down
70 changes: 38 additions & 32 deletions jwt.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ const messages = {
authorizationTokenUnsigned: 'Unsigned authorization token'
}

function isString (x) {
return Object.prototype.toString.call(x) === '[object String]'
}

function wrapStaticSecretInCallback (secret) {
return function (request, payload, cb) {
return cb(null, secret)
Expand Down Expand Up @@ -54,13 +58,39 @@ function convertTemporalProps (options, isVerifyOptions) {
return formatedOptions
}

function fastifyJwt (fastify, options, next) {
if (!options.secret) {
return next(new Error('missing secret'))
function validateOptions (options) {
assert(options.secret, 'missing secret')
assert(!options.options, 'options prefix is deprecated')

assert(!options.jwtVerify || isString(options.jwtVerify), 'Invalid options.jwtVerify')
assert(!options.jwtDecode || isString(options.jwtDecode), 'Invalid options.jwtDecode')
assert(!options.jwtSign || isString(options.jwtSign), 'Invalid options.jwtSign')

if (
options.sign &&
options.sign.algorithm &&
options.sign.algorithm.includes('RS') &&
(typeof options.secret === 'string' ||
options.secret instanceof Buffer)
) {
throw new Error('RSA Signatures set as Algorithm in the options require a private and public key to be set as the secret')
}
if (
options.sign &&
options.sign.algorithm &&
options.sign.algorithm.includes('ES') &&
(typeof options.secret === 'string' ||
options.secret instanceof Buffer)
) {
throw new Error('ECDSA Signatures set as Algorithm in the options require a private and public key to be set as the secret')
}
}

if (options.options) {
return next(new Error('options prefix is deprecated'))
function fastifyJwt (fastify, options, next) {
try {
validateOptions(options)
} catch (e) {
return next(e)
}

const {
Expand Down Expand Up @@ -119,25 +149,6 @@ function fastifyJwt (fastify, options, next) {
const BadRequestError = createError('FST_JWT_BAD_REQUEST', messagesOptions.badRequestErrorMessage, 400)
const BadCookieRequestError = createError('FST_JWT_BAD_COOKIE_REQUEST', messagesOptions.badCookieRequestErrorMessage, 400)

if (
signOptions &&
signOptions.algorithm &&
signOptions.algorithm.includes('RS') &&
(typeof secret === 'string' ||
secret instanceof Buffer)
) {
return next(new Error('RSA Signatures set as Algorithm in the options require a private and public key to be set as the secret'))
}
if (
signOptions &&
signOptions.algorithm &&
signOptions.algorithm.includes('ES') &&
(typeof secret === 'string' ||
secret instanceof Buffer)
) {
return next(new Error('ECDSA Signatures set as Algorithm in the options require a private and public key to be set as the secret'))
}

const jwtDecorator = {
decode,
options: {
Expand All @@ -156,6 +167,7 @@ function fastifyJwt (fastify, options, next) {
let jwtDecodeName = 'jwtDecode'
let jwtVerifyName = 'jwtVerify'
let jwtSignName = 'jwtSign'

if (namespace) {
if (!fastify.jwt) {
fastify.decorateRequest(decoratorName, null)
Expand All @@ -167,21 +179,15 @@ function fastifyJwt (fastify, options, next) {
}
fastify.jwt[namespace] = jwtDecorator

jwtDecodeName = jwtDecode ? (typeof jwtDecode === 'string' ? jwtDecode : 'jwtDecode') : `${namespace}JwtDecode`
jwtDecodeName = jwtDecode || `${namespace}JwtDecode`
jwtVerifyName = jwtVerify || `${namespace}JwtVerify`
jwtSignName = jwtSign || `${namespace}JwtSign`
} else {
fastify.decorateRequest(decoratorName, null)
fastify.decorate('jwt', jwtDecorator)
}

// Temporary conditional to prevent breaking changes by exposing `jwtDecode`,
// which already exists in fastify-auth0-verify.
// If jwtDecode has been requested, or plugin is configured to use a namespace.
// TODO Remove conditional when fastify-jwt >=4.x.x
if (jwtDecode || namespace) {
fastify.decorateRequest(jwtDecodeName, requestDecode)
}
fastify.decorateRequest(jwtDecodeName, requestDecode)
fastify.decorateRequest(jwtVerifyName, requestVerify)
fastify.decorateReply(jwtSignName, replySign)

Expand Down
119 changes: 5 additions & 114 deletions test/jwt.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ test('export', function (t) {
})

test('register', function (t) {
t.plan(20)
t.plan(17)

t.test('Expose jwt methods', function (t) {
t.plan(8)
Expand All @@ -49,39 +49,6 @@ test('register', function (t) {
}
})

fastify.get('/methods', function (request, reply) {
t.notOk(request.jwtDecode)
t.ok(request.jwtVerify)
t.ok(reply.jwtSign)
})

fastify.ready(function () {
t.ok(fastify.jwt.decode)
t.ok(fastify.jwt.options)
t.ok(fastify.jwt.sign)
t.ok(fastify.jwt.verify)
t.ok(fastify.jwt.cookie)
})

fastify.inject({
method: 'get',
url: '/methods'
})
})

t.test('Expose jwt methods - 3.x.x conditional jwtDecode', function (t) {
t.plan(8)

const fastify = Fastify()
fastify.register(jwt, {
secret: 'test',
cookie: {
cookieName: 'token',
signed: false
},
jwtDecode: true
})

fastify.get('/methods', function (request, reply) {
t.ok(request.jwtDecode)
t.ok(request.jwtVerify)
Expand Down Expand Up @@ -287,80 +254,6 @@ test('register', function (t) {
})
})

t.test('RS/ES algorithm in sign options and secret as string', function (t) {
t.plan(2)

t.test('RS algorithm (Must return an error)', function (t) {
t.plan(1)

const fastify = Fastify()
fastify.register(jwt, {
secret: 'test',
sign: {
algorithm: 'RS256',
aud: 'Some audience',
iss: 'Some issuer',
sub: 'Some subject'
}
}).ready(function (error) {
t.equal(error.message, 'RSA Signatures set as Algorithm in the options require a private and public key to be set as the secret')
})
})

t.test('ES algorithm (Must return an error)', function (t) {
t.plan(1)
const fastify = Fastify()
fastify.register(jwt, {
secret: 'test',
sign: {
algorithm: 'ES256',
aud: 'Some audience',
iss: 'Some issuer',
sub: 'Some subject'
}
}).ready(function (error) {
t.equal(error.message, 'ECDSA Signatures set as Algorithm in the options require a private and public key to be set as the secret')
})
})
})

t.test('RS/ES algorithm in sign options and secret as a Buffer', function (t) {
t.plan(2)

t.test('RS algorithm (Must return an error)', function (t) {
t.plan(1)

const fastify = Fastify()
fastify.register(jwt, {
secret: Buffer.from('some secret', 'base64'),
sign: {
algorithm: 'RS256',
aud: 'Some audience',
iss: 'Some issuer',
sub: 'Some subject'
}
}).ready(function (error) {
t.equal(error.message, 'RSA Signatures set as Algorithm in the options require a private and public key to be set as the secret')
})
})

t.test('ES algorithm (Must return an error)', function (t) {
t.plan(1)
const fastify = Fastify()
fastify.register(jwt, {
secret: Buffer.from('some secret', 'base64'),
sign: {
algorithm: 'ES256',
aud: 'Some audience',
iss: 'Some issuer',
sub: 'Some subject'
}
}).ready(function (error) {
t.equal(error.message, 'ECDSA Signatures set as Algorithm in the options require a private and public key to be set as the secret')
})
})
})

async function runWithSecret (t, secret) {
const fastify = Fastify()
fastify.register(jwt, { secret })
Expand Down Expand Up @@ -2718,7 +2611,7 @@ test('expose decode token for plugin extension', function (t) {
t.plan(3)

const fastify = Fastify()
fastify.register(jwt, { secret: 'test', jwtDecode: true })
fastify.register(jwt, { secret: 'test' })

fastify.post('/sign', async function (request, reply) {
const token = await reply.jwtSign(request.body)
Expand Down Expand Up @@ -2814,7 +2707,7 @@ test('support extended config contract', function (t) {
}

const fastify = Fastify()
fastify.register(jwt, { secret: 'test', jwtDecode: true })
fastify.register(jwt, { secret: 'test' })

fastify.post('/sign', async function (request, reply) {
const token = await reply.jwtSign(request.body, extConfig)
Expand Down Expand Up @@ -2955,8 +2848,7 @@ test('supporting time definitions for "maxAge", "expiresIn" and "notBefore"', as
},
decode: {
complete: true
},
jwtDecode: true
}
}

const oneDayInSeconds = 24 * 60 * 60
Expand Down Expand Up @@ -3066,8 +2958,7 @@ test('global user options should not be modified', async function (t) {
},
decode: {
complete: true
},
jwtDecode: true
}
}

const fastify = Fastify()
Expand Down
Loading

0 comments on commit 5611e1f

Please sign in to comment.