UNPKG

fastify-jwt-jwks

Version:
1,112 lines (952 loc) 36.7 kB
'use strict' const { describe, test, before, after, beforeEach, afterEach, mock } = require('node:test') const { readFileSync } = require('fs') const path = require('path') const fastify = require('fastify') const { createSigner } = require('fast-jwt') const nock = require('nock') /* How to regenerate the keys for RS256: ssh-keygen -t rsa -b 4096 -m PEM -f private.key openssl req -x509 -new -key private.key -out public.key -subj "/CN=unused" */ const jwks = { keys: [ { alg: 'RS512', kid: 'KEY', x5c: ['UNUSED'] }, { alg: 'RS256', kid: 'KEY', x5c: [ `MIIFAzCCAuugAwIBAgIUYqKCXKygI2fvcK43voYleb27xYgwDQYJKoZIhvcNAQEL BQAwETEPMA0GA1UEAwwGdW51c2VkMB4XDTIxMTIwNjA4NDIxOFoXDTIyMDEwNTA4 NDIxOFowETEPMA0GA1UEAwwGdW51c2VkMIICIjANBgkqhkiG9w0BAQEFAAOCAg8A MIICCgKCAgEA4xLWpT1v6ZiQNp+seqlCBZCZESEt7HVWt+D5rxcQfqOKy0OUvONn 83N8Q2SybuJ7StD+S3pIm3SWqZXV6N369iJLM+DIyDa4/81NGNdsm6z9X9KTr44v uVvljw4h8CbXUSPFdt4uvn0E+RybXfqsPNgFY21KeQZEruIJl/q3V3TvpdvpbFhg 0+7+piPwTS/oODP1ocY+oMutavrqdL0BWfwKSw/IVMH0PzhSyd28Yn5e98XHw7og oDZgF5RYaNKKK/L5waU7KYI8bQwZ72v+qBhBKiC68ZaA9wGZlvNw08/IdE6zP5AY 4Mpcpd0BK7NC+R6HXlqcqp+Fgrn/3c/+nyPcNTH/O40LOLlxGG1d66utUPl5oatY XIcH55GHrrXw5l31tQPxMT44B8FFtv2VAxYuXPzIbnMOlYJK4yu9n0j3PpN/rDWD Ki7k9bLCNB26NOuwqdUrcpIBtbv/pqgFnOgbZVQfudsT9sGeNP5m6luT6KM/bZ3Z ljyL1t1Skrtlym6LPAg7cNtfzN2wQfZGhOWraYT/qgkZbNsfaNxaLscrdxHwlvi/ 5ObBGMNK33Dz1uY4rlan/fD/6wSUBKel7UlPq636/WTR/FYlttshp3RVD0nlAZEm BYP5VfOfWsiXxYbVEnHyBUX6sS8RAtMwX3/qAbc6+2e/ymnRhyfZDcECAwEAAaNT MFEwHQYDVR0OBBYEFMHvQkKUefNH3fepeNVVbGcWQAGlMB8GA1UdIwQYMBaAFMHv QkKUefNH3fepeNVVbGcWQAGlMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEL BQADggIBAFbN5uDPyWg4vttGpihOrYszC5o172TOw/Tmp4ggtltLexJKSXd5UKVP MD2oXJB1WW6YTae5hZSBcXUJ+Gmu54V7Ge2Lcv19zQkKu5OhJD0cn6L51s8iMdzP 5yvZRgM00+Pdzizl/NkZgSE/b6W9zEE4ZmhPa8aLKjKxQlv42HAUyFAqHiiPzOpq +vDZPTz4lxnERfXnF4eVSMmkyB2f0T3ilIg+Mjwbe2m749FanVCse3E5cgPJVFYl h2bs5/pb7rVfkRNt89IW7icZZGkqHn88y0EksjawF4O2eX5mCgEBM7/TCAWR84qW OOhZzwxJh68NlzRfuvNqTLQrVdP0xQNFY3b7gWDRf6vqc7KGJr2cwqDsKXFQqqp0 IgA9Tfd8FNIgTnsR+RvybYQHcg60Vd4HlzxWqVs/d7baZLUIi4alFkBFQyuV0jAt jXg+kbow83jsg57ZcIxdFD/2RZj34TCTvsoDuhZEgqgHZs07HfNbDRcQ195A8D3t ax0dsIii8tCkffEyzRwmFgcGHBh+2CvH0/p5Sn8RdBqamjNgko7QqrYNMRMP3I71 lXoKOhH7jk9Nis2d2i+ktNy0IMQdWsV75FP+yE3CWTl10bMvCvccg0B1dVmxAbDZ h7b8BjRiGIgwqVjdclzAy0sVMZHquiFvoiE78n5rndcI9jtzx0Ub`.trim() ] }, { kty: 'RSA', use: 'sig', kid: 'KEY', x5c: [ `MIIFAzCCAuugAwIBAgIUYqKCXKygI2fvcK43voYleb27xYgwDQYJKoZIhvcNAQEL BQAwETEPMA0GA1UEAwwGdW51c2VkMB4XDTIxMTIwNjA4NDIxOFoXDTIyMDEwNTA4 NDIxOFowETEPMA0GA1UEAwwGdW51c2VkMIICIjANBgkqhkiG9w0BAQEFAAOCAg8A MIICCgKCAgEA4xLWpT1v6ZiQNp+seqlCBZCZESEt7HVWt+D5rxcQfqOKy0OUvONn 83N8Q2SybuJ7StD+S3pIm3SWqZXV6N369iJLM+DIyDa4/81NGNdsm6z9X9KTr44v uVvljw4h8CbXUSPFdt4uvn0E+RybXfqsPNgFY21KeQZEruIJl/q3V3TvpdvpbFhg 0+7+piPwTS/oODP1ocY+oMutavrqdL0BWfwKSw/IVMH0PzhSyd28Yn5e98XHw7og oDZgF5RYaNKKK/L5waU7KYI8bQwZ72v+qBhBKiC68ZaA9wGZlvNw08/IdE6zP5AY 4Mpcpd0BK7NC+R6HXlqcqp+Fgrn/3c/+nyPcNTH/O40LOLlxGG1d66utUPl5oatY XIcH55GHrrXw5l31tQPxMT44B8FFtv2VAxYuXPzIbnMOlYJK4yu9n0j3PpN/rDWD Ki7k9bLCNB26NOuwqdUrcpIBtbv/pqgFnOgbZVQfudsT9sGeNP5m6luT6KM/bZ3Z ljyL1t1Skrtlym6LPAg7cNtfzN2wQfZGhOWraYT/qgkZbNsfaNxaLscrdxHwlvi/ 5ObBGMNK33Dz1uY4rlan/fD/6wSUBKel7UlPq636/WTR/FYlttshp3RVD0nlAZEm BYP5VfOfWsiXxYbVEnHyBUX6sS8RAtMwX3/qAbc6+2e/ymnRhyfZDcECAwEAAaNT MFEwHQYDVR0OBBYEFMHvQkKUefNH3fepeNVVbGcWQAGlMB8GA1UdIwQYMBaAFMHv QkKUefNH3fepeNVVbGcWQAGlMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEL BQADggIBAFbN5uDPyWg4vttGpihOrYszC5o172TOw/Tmp4ggtltLexJKSXd5UKVP MD2oXJB1WW6YTae5hZSBcXUJ+Gmu54V7Ge2Lcv19zQkKu5OhJD0cn6L51s8iMdzP 5yvZRgM00+Pdzizl/NkZgSE/b6W9zEE4ZmhPa8aLKjKxQlv42HAUyFAqHiiPzOpq +vDZPTz4lxnERfXnF4eVSMmkyB2f0T3ilIg+Mjwbe2m749FanVCse3E5cgPJVFYl h2bs5/pb7rVfkRNt89IW7icZZGkqHn88y0EksjawF4O2eX5mCgEBM7/TCAWR84qW OOhZzwxJh68NlzRfuvNqTLQrVdP0xQNFY3b7gWDRf6vqc7KGJr2cwqDsKXFQqqp0 IgA9Tfd8FNIgTnsR+RvybYQHcg60Vd4HlzxWqVs/d7baZLUIi4alFkBFQyuV0jAt jXg+kbow83jsg57ZcIxdFD/2RZj34TCTvsoDuhZEgqgHZs07HfNbDRcQ195A8D3t ax0dsIii8tCkffEyzRwmFgcGHBh+2CvH0/p5Sn8RdBqamjNgko7QqrYNMRMP3I71 lXoKOhH7jk9Nis2d2i+ktNy0IMQdWsV75FP+yE3CWTl10bMvCvccg0B1dVmxAbDZ h7b8BjRiGIgwqVjdclzAy0sVMZHquiFvoiE78n5rndcI9jtzx0Ub`.trim() ] } ] } const generateToken = (options, payload) => { const signSync = createSigner(options) return signSync(payload) } const tokens = { hs256Valid: generateToken( { key: 'secret', noTimestamp: true }, { admin: true, name: 'John Doe', sub: '1234567890' } ), hs256ValidWithIssuer: generateToken( { key: 'secret', noTimestamp: true, iss: 'https://localhost/' }, { admin: true, name: 'John Doe', sub: '1234567890' } ), hs256ValidWithProvidedIssuer: generateToken( { key: 'secret', noTimestamp: true, iss: 'foo' }, { admin: true, name: 'John Doe', sub: '1234567890' } ), hs256ValidWithAudience: generateToken( { key: 'secret', noTimestamp: true, aud: 'foo' }, { admin: true, name: 'John Doe', sub: '1234567890' } ), hs256ValidWithDomainAsAudience: generateToken( { key: 'secret', noTimestamp: true, aud: 'https://localhost/', iss: 'https://localhost/' }, { admin: true, name: 'John Doe', sub: '1234567890' } ), hs256InvalidSignature: generateToken( { key: 'secret', noTimestamp: true }, { admin: true, name: 'John Doe', sub: '1234567890' } ) + '-INVALID', rs256Valid: generateToken( { key: readFileSync(`${path.join(__dirname, 'keys')}/private.key`, 'utf8'), noTimestamp: true, iss: 'https://localhost/', kid: 'KEY' }, { admin: true, name: 'John Doe', sub: '1234567890' } ), rs256ValidWithAudience: generateToken( { key: readFileSync(`${path.join(__dirname, 'keys')}/private.key`, 'utf8'), noTimestamp: true, iss: 'https://localhost/', aud: 'foo', kid: 'KEY' }, { admin: true, name: 'John Doe', sub: '1234567890' } ), rs256ValidWithDomainAsAudience: generateToken( { key: readFileSync(`${path.join(__dirname, 'keys')}/private.key`, 'utf8'), noTimestamp: true, iss: 'https://localhost/', aud: 'https://localhost/', kid: 'KEY' }, { admin: true, name: 'John Doe', sub: '1234567890' } ), rs256InvalidSignature: generateToken( { key: readFileSync(`${path.join(__dirname, 'keys')}/private.key`, 'utf8'), noTimestamp: true, iss: 'https://localhost/', kid: 'KEY' }, { admin: true, name: 'John Doe', sub: '1234567890' } ) + '-INVALID', rs256MissingKey: 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IkFOT1RIRVItS0VZIn0.eyJwYXlsb2FkIjp7InN1YiI6IjEyMzQ1Njc4OTAiLCJuYW1lIjoiSm9obiBEb2UiLCJhZG1pbiI6dHJ1ZSwiaXNzIjoiaHR0cHM6Ly9sb2NhbGhvc3QvIiwiYXVkIjoiaHR0cHM6Ly9sb2NhbGhvc3QvIn19.jZrn8F1RClAbb4P1JJR0XJ0KTw0U7DqQEd098AQhxjojb-6BfGwxABn-hIrFeQhDPs1-RtzCfoRJ0WvA40UoqAPf071gdlB5FFq95lUO_9B8XXby0ueUe-RdlqMkP3HvukLLFhQW481zBEVAyp8xSz-P1LsYHk6avCA1lAGMKZoh6FOsoE-cyBMKF0koc2MWUPvu6BYr48gyX50QKBr_yrSdfLgQj67tcMicvESddwZX1ggr7eF4ZeHXVZV_F_AMkOywiEkiS4EvC2gywNJkbIz3eLqsQFYYzUhMsQfu5x-YfSw3-pmEtw7SQZ-QeP2zs1sZP0tcJJ03ya-dcG1E7IindR1eAoji6CYtRElF0DMsIgV-Cd6NB1Vx5R-Le15MROuvArGisJKOlHYf79g1-1hWC5LAtQ0eAR5gkeRRX6UjUL_kCMVtf69qed74mq-nA4P2BNW72CL9SzjPwmNeUVfGdui10NLMt9QAs8jcYksgeMiMoQW6NVvsc9ptKmynmTJzCEP1s-Jgv0erMIIe5_mU9YnihZHJ19dL7BDvg0YV_tP3i6vRXqJsYBx43YPKMwiI5OKRSregfRLvq66JSlL7k2hfIVRLhJc-tvaxoeewDJc1qksc-qgsBWwQ7lVpQlj_mBbmzujXmj99nQJfqpV9iPS5WPPCbtJTeTlXcP8', unsupportedAlgorithm: 'eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.nZU_gPcMXkWpkCUpJceSxS7lSickF0tTImHhAR949Z-Nt69LgW8G6lid-mqd9B579tYM8C4FN2jdhR2VRMsjtA', expired: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTQxNjIzOTAyMiwiZXhwIjoxNTExMjM5MDIyfQ.zTxEk-Z5qfP4n7U4jMU1gRvQEpl4HhnYTniTeQpaMkI', invalidAudience: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImF1ZCI6ImJhciJ9.Z0NHItgQv74Ce9re2q9qca_ifOn_cgndSaKBENZfN7M', invalidIssuer: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlzcyI6ImJhciJ9.jG2FWFY709fd9ooB4NgU1YpPmT4gp_Ig8JisFZAOBS0' } async function buildServer(options) { const server = fastify() await server.register(require('../'), options) await server.register(require('@fastify/cookie')) server.get('/verify', { preValidation: server.authenticate }, req => { return req.user }) server.get('/decode', async req => { return { regular: await req.jwtDecode(), full: await req.jwtDecode({ decode: { complete: true } }) } }) await server.listen({ port: 0 }) return server } describe('Options parsing', function () { test('should enable RS256 when jwksUrl is present', async function (t) { const server = await buildServer({ jwksUrl: 'https://localhost/.well-known/jwks.json' }) t.assert.deepStrictEqual(server.jwtJwks.verify.algorithms, ['RS256']) server.close() }) test('should enable HS256 when the secret is present', async function (t) { const server = await buildServer({ secret: 'secret' }) t.assert.deepStrictEqual(server.jwtJwks.verify.algorithms, ['HS256']) server.close() }) test('should enable both algorithms if both options are present', async function (t) { const server = await buildServer({ jwksUrl: 'https://localhost/.well-known/jwks.json', secret: 'secret' }) t.assert.deepStrictEqual(server.jwtJwks.verify.algorithms, ['RS256', 'HS256']) server.close() }) test('should complain if neither jwksUrl or secret are present', async function (t) { await t.assert.rejects( () => buildServer(), new Error('Please provide at least one of the "jwksUrl" or "secret" options.') ) }) test('should complain if forbidden options are present', async function (t) { await t.assert.rejects( () => buildServer({ algorithms: 'whatever' }), new Error('Option "algorithms" is not supported.') ) }) }) describe('JWT token decoding', function () { let server before(async function () { server = await buildServer({ secret: 'secret' }) }) after(() => server.close()) test('should decode a JWT token', async function (t) { const response = await server.inject({ method: 'GET', url: '/decode', headers: { Authorization: `Bearer ${tokens.hs256Valid}` } }) t.assert.deepStrictEqual(response.statusCode, 200) t.assert.deepStrictEqual(response.json(), { regular: { admin: true, name: 'John Doe', sub: '1234567890' }, full: { header: { alg: 'HS256', typ: 'JWT' }, input: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhZG1pbiI6dHJ1ZSwibmFtZSI6IkpvaG4gRG9lIiwic3ViIjoiMTIzNDU2Nzg5MCJ9', payload: { admin: true, name: 'John Doe', sub: '1234567890' }, signature: 'eNK_fimsCW3Q-meOXyc_dnZHubl2D4eZkIcn6llniCk' } }) }) test('should complain if the HTTP Authorization header is missing', async function (t) { const response = await server.inject({ method: 'GET', url: '/decode' }) t.assert.deepStrictEqual(response.statusCode, 401) t.assert.deepStrictEqual(response.json(), { code: 'FST_JWT_NO_AUTHORIZATION_IN_HEADER', statusCode: 401, error: 'Unauthorized', message: 'No Authorization was found in request.headers' }) }) test('should complain if the HTTP Authorization header is in the wrong format', async function (t) { const response = await server.inject({ method: 'GET', url: '/decode', headers: { Authorization: 'FOO' } }) t.assert.deepStrictEqual(response.statusCode, 401) t.assert.deepStrictEqual(response.json(), { code: 'FST_JWT_NO_AUTHORIZATION_IN_HEADER', statusCode: 401, error: 'Unauthorized', message: 'No Authorization was found in request.headers' }) }) }) describe('JWT cookie token decoding', function () { let server before(async function () { server = await buildServer({ secret: 'secret', token: 'token', cookie: { cookieName: 'token' } }) }) after(() => server.close()) test('should decode a JWT token from cookie', async function (t) { const response = await server.inject({ method: 'GET', url: '/decode', cookies: { token: tokens.hs256Valid } }) t.assert.deepStrictEqual(response.statusCode, 200) t.assert.deepStrictEqual(response.json(), { regular: { admin: true, name: 'John Doe', sub: '1234567890' }, full: { header: { alg: 'HS256', typ: 'JWT' }, input: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhZG1pbiI6dHJ1ZSwibmFtZSI6IkpvaG4gRG9lIiwic3ViIjoiMTIzNDU2Nzg5MCJ9', payload: { admin: true, name: 'John Doe', sub: '1234567890' }, signature: 'eNK_fimsCW3Q-meOXyc_dnZHubl2D4eZkIcn6llniCk' } }) }) test('should complain if the JWT token cookie is missing', async function (t) { const response = await server.inject({ method: 'GET', url: '/decode', cookies: { foo: 'bar' } }) t.assert.deepStrictEqual(response.statusCode, 401) t.assert.deepStrictEqual(response.json(), { code: 'FST_JWT_NO_AUTHORIZATION_IN_COOKIE', statusCode: 401, error: 'Unauthorized', message: 'No Authorization was found in request.cookies' }) }) test('should complain if the JWT token cookie is in the wrong format', async function (t) { const response = await server.inject({ method: 'GET', url: '/decode', cookies: { foo: 'bar' } }) t.assert.deepStrictEqual(response.statusCode, 401) t.assert.deepStrictEqual(response.json(), { code: 'FST_JWT_NO_AUTHORIZATION_IN_COOKIE', statusCode: 401, error: 'Unauthorized', message: 'No Authorization was found in request.cookies' }) }) }) describe('Format decoded token', function () { let server before(async function () { server = await buildServer({ secret: 'secret', token: 'token', cookie: { cookieName: 'token' }, formatUser: user => { return { sub: user.sub, username: user.name, admin: user.admin } } }) }) after(() => server.close()) test('should format verified user', async function (t) { const response = await server.inject({ method: 'GET', url: '/verify', headers: { Authorization: `Bearer ${tokens.hs256Valid}` } }) t.assert.deepStrictEqual(response.statusCode, 200) t.assert.deepStrictEqual(response.json(), { sub: '1234567890', username: 'John Doe', admin: true }) }) }) describe('HS256 JWT token validation', function () { let server beforeEach(async function () { server = await buildServer({ secret: 'secret' }) }) afterEach(() => server.close()) test('should make the token information available through request.user', async function (t) { const response = await server.inject({ method: 'GET', url: '/verify', headers: { Authorization: `Bearer ${tokens.hs256Valid}` } }) t.assert.deepStrictEqual(response.statusCode, 200) t.assert.deepStrictEqual(response.json(), { sub: '1234567890', name: 'John Doe', admin: true }) }) test('should make the complete token information available through request.user', async function (t) { await server.close() server = await buildServer({ secret: 'secret', complete: true }) const token = tokens.hs256Valid const response = await server.inject({ method: 'GET', url: '/verify', headers: { Authorization: `Bearer ${token}` } }) t.assert.deepStrictEqual(response.statusCode, 200) t.assert.deepStrictEqual(response.json(), { header: { alg: 'HS256', typ: 'JWT' }, input: token.split('.').slice(0, 2).join('.'), payload: { sub: '1234567890', name: 'John Doe', admin: true }, signature: 'eNK_fimsCW3Q-meOXyc_dnZHubl2D4eZkIcn6llniCk' }) }) test('should validate the issuer', async function (t) { await server.close() server = await buildServer({ jwksUrl: 'localhost', secret: 'secret' }) const response = await server.inject({ method: 'GET', url: '/verify', headers: { Authorization: `Bearer ${tokens.hs256ValidWithIssuer}` } }) t.assert.deepStrictEqual(response.statusCode, 200) t.assert.deepStrictEqual(response.json(), { sub: '1234567890', name: 'John Doe', admin: true, iss: 'https://localhost/' }) }) test('should validate provided issuer', async function (t) { await server.close() server = await buildServer({ jwksUrl: 'localhost', secret: 'secret', issuer: 'foo' }) const response = await server.inject({ method: 'GET', url: '/verify', headers: { Authorization: `Bearer ${tokens.hs256ValidWithProvidedIssuer}` } }) t.assert.deepStrictEqual(response.statusCode, 200) t.assert.deepStrictEqual(response.json(), { sub: '1234567890', name: 'John Doe', admin: true, iss: 'foo' }) }) test('should validate multiple issuers', async function (t) { await server.close() server = await buildServer({ jwksUrl: 'localhost', secret: 'secret', issuer: ['bar', 'foo', 'blah'] }) const response = await server.inject({ method: 'GET', url: '/verify', headers: { Authorization: `Bearer ${tokens.hs256ValidWithProvidedIssuer}` } }) t.assert.deepStrictEqual(response.statusCode, 200) t.assert.deepStrictEqual(response.json(), { sub: '1234567890', name: 'John Doe', admin: true, iss: 'foo' }) }) test('should validate the audience', async function (t) { await server.close() server = await buildServer({ audience: 'foo', secret: 'secret' }) const response = await server.inject({ method: 'GET', url: '/verify', headers: { Authorization: `Bearer ${tokens.hs256ValidWithAudience}` } }) t.assert.deepStrictEqual(response.statusCode, 200) t.assert.deepStrictEqual(response.json(), { sub: '1234567890', name: 'John Doe', admin: true, aud: 'foo' }) }) test('should validate the audience using the jwksUrl', async function (t) { await server.close() server = await buildServer({ jwksUrl: 'localhost', audience: true, secret: 'secret' }) const response = await server.inject({ method: 'GET', url: '/verify', headers: { Authorization: `Bearer ${tokens.hs256ValidWithDomainAsAudience}` } }) t.assert.deepStrictEqual(response.statusCode, 200) t.assert.deepStrictEqual(response.json(), { sub: '1234567890', name: 'John Doe', admin: true, iss: 'https://localhost/', aud: 'https://localhost/' }) }) test('should reject an invalid signature', async function (t) { const response = await server.inject({ method: 'GET', url: '/verify', headers: { Authorization: `Bearer ${tokens.hs256InvalidSignature}` } }) t.assert.deepStrictEqual(response.statusCode, 401) t.assert.deepStrictEqual(response.json(), { code: 'FST_JWT_AUTHORIZATION_TOKEN_INVALID', statusCode: 401, error: 'Unauthorized', message: 'Authorization token is invalid: The token signature is invalid.' }) }) }) describe('RS256 JWT token validation', function () { let server beforeEach(async function () { server = await buildServer({ jwksUrl: 'https://localhost/.well-known/jwks.json' }) }) afterEach(() => server.close()) beforeEach(function () { nock.disableNetConnect() nock('https://localhost/').get('/.well-known/jwks.json').reply(200, jwks) }) afterEach(() => { nock.cleanAll() nock.enableNetConnect() }) test('should make the token informations available through request.user', async function (t) { const response = await server.inject({ method: 'GET', url: '/verify', headers: { Authorization: `Bearer ${tokens.rs256Valid}` } }) t.assert.deepStrictEqual(response.statusCode, 200) t.assert.deepStrictEqual(response.json(), { sub: '1234567890', name: 'John Doe', admin: true, iss: 'https://localhost/' }) }) test('should make the complete token information available through request.user', async function (t) { await server.close() server = await buildServer({ jwksUrl: 'https://localhost/.well-known/jwks.json', complete: true }) const token = tokens.rs256Valid const response = await server.inject({ method: 'GET', url: '/verify', headers: { Authorization: `Bearer ${token}` } }) t.assert.deepStrictEqual(response.statusCode, 200) t.assert.deepStrictEqual(response.json(), { header: { alg: 'RS256', kid: 'KEY', typ: 'JWT' }, payload: { sub: '1234567890', name: 'John Doe', admin: true, iss: 'https://localhost/' }, input: token.split('.').slice(0, 2).join('.'), signature: 'HYgGxrwl3vthMChCy44eg-VK0x_SR-mf6761VI9jNk9rMqKZmFcabE7dVUA_hCKFXyj7VL7bJ09i3PxYFkj78PMz28B9hZz_h4ntVuafPmDL9FCHvW91oZTJRhosNor2yyUFcx6ijfu6WeUTZRtQdBqvcAgtKutNl9H0Q0wff-Jn10ViiFJTEmiaC-XhoZFjZQee7_bS7mOZtJCZeH69D_CWrCf4I-N2nl8U1sVHp-H0fRCc5D5SvlIhCsIXYJoFDRAuTtRvwrXXVPlIPugCeJ8l91S-GbIEEUejDCE8JPW9bEGfKoAFBiIbnRBSb4hKEbdFUqWHk-5_YOLzvPnq57vlCB8yeC10exEgiSeSb74tXGZyB4z540Mjt-2k9O9t7Uz1ICDZHvrYLUN2wzlSKqSucOvr5YpH8y-iLaWqAQeiR2b6w0u_c9kMEgzCAaobJp4QxjGkKHfYNmUFlV1uoY5_I2CBls-ICr0_E9PicMBnddg_JG8KabqAmZObCrkM5WRxSPPNLTElmw80MACxFqgaKxsMg-6uqmgTwy9ie9TjYVVdL1pdxWWaLDhzpDN1mmdTuIazfnSaib7PnzgPPgHlN7TnSCmCnYzffAg-i2Fz8JOhiK50mF86hc8n6em6K7cbVLm0nQcA4249D88Um9KBs8AoPXov8HGAS4Khwhk' }) }) test('should validate the audience', async function (t) { await server.close() server = await buildServer({ jwksUrl: 'https://localhost/.well-known/jwks.json', audience: 'foo' }) const response = await server.inject({ method: 'GET', url: '/verify', headers: { Authorization: `Bearer ${tokens.rs256ValidWithAudience}` } }) t.assert.deepStrictEqual(response.statusCode, 200) t.assert.deepStrictEqual(response.json(), { sub: '1234567890', name: 'John Doe', admin: true, iss: 'https://localhost/', aud: 'foo' }) }) test('should validate the audience using the jwksUrl', async function (t) { await server.close() server = await buildServer({ jwksUrl: 'https://localhost/.well-known/jwks.json', audience: true, secret: 'secret' }) const response = await server.inject({ method: 'GET', url: '/verify', headers: { Authorization: `Bearer ${tokens.rs256ValidWithDomainAsAudience}` } }) t.assert.deepStrictEqual(response.statusCode, 200) t.assert.deepStrictEqual(response.json(), { sub: '1234567890', name: 'John Doe', admin: true, iss: 'https://localhost/', aud: 'https://localhost/' }) }) test('should validate with multiple audiences', async function (t) { await server.close() server = await buildServer({ jwksUrl: 'https://localhost/.well-known/jwks.json', audience: ['https://otherhost/', 'foo', 'https://somehost/'], secret: 'secret' }) const response = await server.inject({ method: 'GET', url: '/verify', headers: { Authorization: `Bearer ${tokens.rs256ValidWithAudience}` } }) t.assert.deepStrictEqual(response.statusCode, 200) t.assert.deepStrictEqual(response.json(), { sub: '1234567890', name: 'John Doe', admin: true, iss: 'https://localhost/', aud: 'foo' }) }) test('should reject an invalid signature', async function (t) { const response = await server.inject({ method: 'GET', url: '/verify', headers: { Authorization: `Bearer ${tokens.rs256InvalidSignature}` } }) t.assert.deepStrictEqual(response.statusCode, 401) t.assert.deepStrictEqual(response.json(), { code: 'FST_JWT_AUTHORIZATION_TOKEN_INVALID', statusCode: 401, error: 'Unauthorized', message: 'Authorization token is invalid: The token signature is invalid.' }) }) test('should reject an invalid token', async function (t) { const response = await server.inject({ method: 'GET', url: '/verify', headers: { Authorization: `Bearer ${tokens.hs256Valid}` } }) t.assert.deepStrictEqual(response.statusCode, 401) t.assert.deepStrictEqual(response.json(), { statusCode: 401, error: 'Unauthorized', message: 'Unsupported token.' }) }) test('should reject a token when is not possible to retrieve the JWK set due to a HTTP error', async function (t) { nock.cleanAll() nock('https://localhost/').get('/.well-known/jwks.json').reply(404, { error: 'Not found.' }) const response = await server.inject({ method: 'GET', url: '/verify', headers: { Authorization: `Bearer ${tokens.rs256Valid}` } }) t.assert.deepStrictEqual(response.statusCode, 500) t.assert.deepStrictEqual(response.json(), { statusCode: 500, error: 'Internal Server Error', message: 'Unable to get the JWS due to a HTTP error: [HTTP 404] {"error":"Not found."}' }) }) test("should reject a token when the retrieved JWT set doesn't have the required key", async function (t) { const response = await server.inject({ method: 'GET', url: '/verify', headers: { Authorization: `Bearer ${tokens.rs256MissingKey}` } }) t.assert.deepStrictEqual(response.statusCode, 401) t.assert.deepStrictEqual(response.json(), { statusCode: 401, error: 'Unauthorized', message: 'Missing Key: Public key must be provided' }) }) test('should reject a token when the retrieved JWT set returns an invalid key', async function (t) { nock.cleanAll() nock('https://localhost/') .get('/.well-known/jwks.json') .reply(200, { keys: [ { alg: 'RS256', kid: 'KEY', x5c: ['FOO'] } ] }) const response = await server.inject({ method: 'GET', url: '/verify', headers: { Authorization: `Bearer ${tokens.rs256Valid}` } }) t.assert.deepStrictEqual(response.statusCode, 401) t.assert.deepStrictEqual(response.json(), { code: 'FST_JWT_AUTHORIZATION_TOKEN_INVALID', statusCode: 401, error: 'Unauthorized', message: 'Authorization token is invalid: Unsupported PEM public key.' }) }) test('should reject a token when is not possible to retrieve the JWK set due to a generic error', async function (t) { nock.cleanAll() nock.enableNetConnect() const response = await server.inject({ method: 'GET', url: '/verify', headers: { Authorization: `Bearer ${tokens.rs256Valid}` } }) t.assert.deepStrictEqual(response.statusCode, 500) t.assert.deepStrictEqual(response.json(), { message: 'fetch failed', statusCode: 500, error: 'Internal Server Error' }) }) test('should cache the key and not it the well-known URL more than once', async function (t) { let response response = await server.inject({ method: 'GET', url: '/verify', headers: { Authorization: `Bearer ${tokens.rs256Valid}` } }) t.assert.deepStrictEqual(response.statusCode, 200) response = await server.inject({ method: 'GET', url: '/verify', headers: { Authorization: `Bearer ${tokens.rs256Valid}` } }) t.assert.deepStrictEqual(response.statusCode, 200) }) test('should correctly get the key again from the well-known URL if cache expired', async function (t) { await server.close() server = await buildServer({ jwksUrl: 'https://localhost/.well-known/jwks.json', secret: 'secret', secretsTtl: 10 }) let response response = await server.inject({ method: 'GET', url: '/verify', headers: { Authorization: `Bearer ${tokens.rs256Valid}` } }) t.assert.deepStrictEqual(response.statusCode, 200) await new Promise(resolve => setTimeout(resolve, 20)) response = await server.inject({ method: 'GET', url: '/verify', headers: { Authorization: `Bearer ${tokens.rs256Valid}` } }) const body = response.json() t.assert.deepStrictEqual(response.statusCode, 404) t.assert.deepStrictEqual(body.error, 'Not Found') t.assert.deepStrictEqual(body.statusCode, 404) t.assert.match(body.message, /Nock: No match for request/) }) test('should not cache the key if cache was disabled', async function (t) { await server.close() server = await buildServer({ jwksUrl: 'https://localhost/.well-known/jwks.json', secret: 'secret', secretsTtl: 0 }) let response response = await server.inject({ method: 'GET', url: '/verify', headers: { Authorization: `Bearer ${tokens.rs256Valid}` } }) t.assert.deepStrictEqual(response.statusCode, 200) response = await server.inject({ method: 'GET', url: '/verify', headers: { Authorization: `Bearer ${tokens.rs256Valid}` } }) const body = response.json() t.assert.deepStrictEqual(response.statusCode, 404) t.assert.deepStrictEqual(body.error, 'Not Found') t.assert.deepStrictEqual(body.statusCode, 404) t.assert.match(body.message, /Nock: No match for request/) }) test('should not try to get the key twice when using caching if a previous attempt failed', async function (t) { let response response = await server.inject({ method: 'GET', url: '/verify', headers: { Authorization: `Bearer ${tokens.rs256MissingKey}` } }) t.assert.deepStrictEqual(response.statusCode, 401) t.assert.deepStrictEqual(response.json(), { statusCode: 401, error: 'Unauthorized', message: 'Missing Key: Public key must be provided' }) response = await server.inject({ method: 'GET', url: '/verify', headers: { Authorization: `Bearer ${tokens.rs256MissingKey}` } }) t.assert.deepStrictEqual(response.statusCode, 401) t.assert.deepStrictEqual(response.json(), { statusCode: 401, error: 'Unauthorized', message: 'Missing Key: Public key must be provided' }) }) }) describe('Server configured with the namespace option', function () { let server after(() => server.close()) test('decorates the server with the correct function names', async function (t) { server = await buildServer({ secret: 'secret', namespace: 'test' }) // @fastify/jwt decorators t.assert.deepStrictEqual(server.hasRequestDecorator('jwtDecode'), false) t.assert.deepStrictEqual(server.hasRequestDecorator('jwtVerify'), false) t.assert.deepStrictEqual(server.hasRequestDecorator('testJwtDecode'), true) t.assert.deepStrictEqual(server.hasRequestDecorator('testJwtVerify'), true) // fastify-jwt-jwks decorators t.assert.deepStrictEqual(server.hasDecorator('authenticate'), false) t.assert.deepStrictEqual(server.hasDecorator('jwtJwks'), false) t.assert.deepStrictEqual(server.hasRequestDecorator('jwtJwks'), false) t.assert.deepStrictEqual(server.hasRequestDecorator('jwtJwksSecretsCache'), false) t.assert.deepStrictEqual(server.hasDecorator('testAuthenticate'), true) t.assert.deepStrictEqual(server.hasDecorator('testJwtJwks'), true) t.assert.deepStrictEqual(server.hasRequestDecorator('testJwtJwks'), true) t.assert.deepStrictEqual(server.hasRequestDecorator('testJwtJwksSecretsCache'), true) }) }) describe('General error handling', function () { let server beforeEach(async function () { server = await buildServer({ secret: 'secret' }) }) afterEach(() => server.close()) test('should complain if the HTTP Authorization header is missing', async function (t) { const response = await server.inject({ method: 'GET', url: '/verify' }) t.assert.deepStrictEqual(response.statusCode, 401) t.assert.deepStrictEqual(response.json(), { statusCode: 401, error: 'Unauthorized', message: 'Missing Authorization HTTP header.' }) }) test('should complain if the HTTP Authorization header is in the wrong format', async function (t) { const response = await server.inject({ method: 'GET', url: '/verify', headers: { Authorization: 'FOO' } }) t.assert.deepStrictEqual(response.statusCode, 401) t.assert.deepStrictEqual(response.json(), { statusCode: 401, error: 'Unauthorized', message: 'Missing Authorization HTTP header.' }) }) test('should complain if the JWT token is malformed', async function (t) { const response = await server.inject({ method: 'GET', url: '/verify', headers: { Authorization: 'Bearer FOO' } }) t.assert.deepStrictEqual(response.statusCode, 401) t.assert.deepStrictEqual(response.json(), { code: 'FST_JWT_AUTHORIZATION_TOKEN_INVALID', statusCode: 401, error: 'Unauthorized', message: 'Authorization token is invalid: The token is malformed.' }) }) test('should complain if the JWT token has an unsupported algorithm', async function (t) { const response = await server.inject({ method: 'GET', url: '/verify', headers: { Authorization: `Bearer ${tokens.unsupportedAlgorithm}` } }) t.assert.deepStrictEqual(response.statusCode, 401) t.assert.deepStrictEqual(response.json(), { statusCode: 401, error: 'Unauthorized', message: 'The token algorithm is invalid.' }) }) test('should complain if the JWT token has expired', async function (t) { const response = await server.inject({ method: 'GET', url: '/verify', headers: { Authorization: `Bearer ${tokens.expired}` } }) t.assert.deepStrictEqual(response.statusCode, 401) t.assert.deepStrictEqual(response.json(), { statusCode: 401, error: 'Unauthorized', message: 'Expired token.' }) }) test('should complain if the JWT token has an invalid issuer', async function (t) { await server.close() server = await buildServer({ jwksUrl: 'foo', secret: 'secret' }) const response = await server.inject({ method: 'GET', url: '/verify', headers: { Authorization: `Bearer ${tokens.invalidIssuer}` } }) t.assert.deepStrictEqual(response.statusCode, 401) t.assert.deepStrictEqual(response.json(), { code: 'FST_JWT_AUTHORIZATION_TOKEN_INVALID', statusCode: 401, error: 'Unauthorized', message: 'Authorization token is invalid: The iss claim value is not allowed.' }) }) test('should complain if the JWT token has an invalid audience', async function (t) { await server.close() server = await buildServer({ audience: 'foo', secret: 'secret' }) const response = await server.inject({ method: 'GET', url: '/verify', headers: { Authorization: `Bearer ${tokens.invalidAudience}` } }) t.assert.deepStrictEqual(response.statusCode, 401) t.assert.deepStrictEqual(response.json(), { code: 'FST_JWT_AUTHORIZATION_TOKEN_INVALID', statusCode: 401, error: 'Unauthorized', message: 'Authorization token is invalid: The aud claim value is not allowed.' }) }) }) describe('Cleanup', function () { test('should close the cache when the server stops', function (t, done) { const NodeCache = require('node-cache') const mockClose = mock.fn() NodeCache.prototype.close = mockClose buildServer({ secret: 'secret' }).then(server => { server.close(() => { t.assert.deepStrictEqual(mockClose.mock.callCount(), 1) done() }) }, done) }) })