├── .gitattributes ├── .github ├── dependabot.yml ├── stale.yml └── workflows │ └── ci.yml ├── .gitignore ├── .npmrc ├── LICENSE ├── Readme.md ├── eslint.config.js ├── examples └── example.js ├── index.js ├── lib ├── authenticate.js ├── compare.js ├── errors.js └── verify-bearer-auth-factory.js ├── package.json ├── test ├── decorate-with-logger.test.js ├── decorate.test.js ├── hooks.test.js ├── integration.test.js ├── spec-compliance-invalid.test.js ├── spec-compliance-rfc-6749.test.js ├── spec-compliance-rfc-6750.test.js └── verify-bearer-auth-factory.test.js └── types ├── index.d.ts └── index.test-d.ts /.gitattributes: -------------------------------------------------------------------------------- 1 | # Set default behavior to automatically convert line endings 2 | * text=auto eol=lf 3 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "monthly" 7 | open-pull-requests-limit: 10 8 | 9 | - package-ecosystem: "npm" 10 | directory: "/" 11 | schedule: 12 | interval: "monthly" 13 | open-pull-requests-limit: 10 14 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Number of days of inactivity before an issue becomes stale 2 | daysUntilStale: 15 3 | # Number of days of inactivity before a stale issue is closed 4 | daysUntilClose: 7 5 | # Issues with these labels will never be considered stale 6 | exemptLabels: 7 | - "discussion" 8 | - "feature request" 9 | - "bug" 10 | - "help wanted" 11 | - "plugin suggestion" 12 | - "good first issue" 13 | # Label to use when marking an issue as stale 14 | staleLabel: stale 15 | # Comment to post when marking an issue as stale. Set to `false` to disable 16 | markComment: > 17 | This issue has been automatically marked as stale because it has not had 18 | recent activity. It will be closed if no further activity occurs. Thank you 19 | for your contributions. 20 | # Comment to post when closing a stale issue. Set to `false` to disable 21 | closeComment: false 22 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - next 8 | - 'v*' 9 | paths-ignore: 10 | - 'docs/**' 11 | - '*.md' 12 | pull_request: 13 | paths-ignore: 14 | - 'docs/**' 15 | - '*.md' 16 | 17 | permissions: 18 | contents: read 19 | 20 | jobs: 21 | test: 22 | permissions: 23 | contents: write 24 | pull-requests: write 25 | uses: fastify/workflows/.github/workflows/plugins-ci.yml@v5 26 | with: 27 | license-check: true 28 | lint: true 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # Docusaurus cache and generated files 108 | .docusaurus 109 | 110 | # Serverless directories 111 | .serverless/ 112 | 113 | # FuseBox cache 114 | .fusebox/ 115 | 116 | # DynamoDB Local files 117 | .dynamodb/ 118 | 119 | # TernJS port file 120 | .tern-port 121 | 122 | # Stores VSCode versions used for testing VSCode extensions 123 | .vscode-test 124 | 125 | # yarn v2 126 | .yarn/cache 127 | .yarn/unplugged 128 | .yarn/build-state.yml 129 | .yarn/install-state.gz 130 | .pnp.* 131 | 132 | # Vim swap files 133 | *.swp 134 | 135 | # macOS files 136 | .DS_Store 137 | 138 | # Clinic 139 | .clinic 140 | 141 | # lock files 142 | bun.lockb 143 | package-lock.json 144 | pnpm-lock.yaml 145 | yarn.lock 146 | 147 | # editor files 148 | .vscode 149 | .idea 150 | 151 | #tap files 152 | .tap/ 153 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) The Fastify Team 4 | 5 | The Fastify team members are listed at https://github.com/fastify/fastify#team 6 | and in the README file. 7 | 8 | Permission is hereby granted, free of charge, to any person obtaining a copy 9 | of this software and associated documentation files (the "Software"), to deal 10 | in the Software without restriction, including without limitation the rights 11 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | copies of the Software, and to permit persons to whom the Software is 13 | furnished to do so, subject to the following conditions: 14 | 15 | The above copyright notice and this permission notice shall be included in all 16 | copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | SOFTWARE. 25 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # @fastify/bearer-auth 2 | 3 | [![CI](https://github.com/fastify/fastify-bearer-auth/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/fastify/fastify-bearer-auth/actions/workflows/ci.yml) 4 | [![npm version](https://img.shields.io/npm/v/@fastify/bearer-auth)](https://www.npmjs.com/package/@fastify/bearer-auth) 5 | [![neostandard javascript style](https://img.shields.io/badge/code_style-neostandard-brightgreen?style=flat)](https://github.com/neostandard/neostandard) 6 | 7 | *@fastify/bearer-auth* provides a simple Bearer auth request hook for the [Fastify][fastify] 8 | web framework. 9 | 10 | [fastify]: https://fastify.dev/ 11 | 12 | 13 | ## Install 14 | ``` 15 | npm i @fastify/bearer-auth 16 | ``` 17 | 18 | ### Compatibility 19 | | Plugin version | Fastify version | 20 | | ---------------|-----------------| 21 | | `^10.x` | `^5.x` | 22 | | `^8.x` | `^4.x` | 23 | | `^5.x` | `^3.x` | 24 | | `^4.x` | `^2.x` | 25 | | `^1.x` | `^1.x` | 26 | 27 | 28 | Please note that if a Fastify version is out of support, then so are the corresponding versions of this plugin 29 | in the table above. 30 | See [Fastify's LTS policy](https://github.com/fastify/fastify/blob/main/docs/Reference/LTS.md) for more details. 31 | 32 | ## Example 33 | 34 | ```js 35 | 'use strict' 36 | 37 | const fastify = require('fastify')() 38 | const bearerAuthPlugin = require('@fastify/bearer-auth') 39 | const keys = new Set(['a-super-secret-key', 'another-super-secret-key']) 40 | 41 | fastify.register(bearerAuthPlugin, {keys}) 42 | fastify.get('/foo', (req, reply) => { 43 | reply.send({authenticated: true}) 44 | }) 45 | 46 | fastify.listen({port: 8000}, (err) => { 47 | if (err) { 48 | fastify.log.error(err.message) 49 | process.exit(1) 50 | } 51 | fastify.log.info('http://127.0.0.1:8000/foo') 52 | }) 53 | ``` 54 | 55 | ## API 56 | 57 | *@fastify/bearer-auth* exports a standard [Fastify plugin](https://github.com/fastify/fastify-plugin). 58 | This allows registering the plugin within scoped paths, so some paths can be protected 59 | by the plugin while others are not. See the [Fastify](https://fastify.dev/docs/latest) 60 | documentation and examples for more details. 61 | 62 | When registering the plugin a configuration object must be specified: 63 | 64 | * `keys`: A `Set` or array with valid keys of type `string` (required) 65 | * `function errorResponse (err) {}`: Method must synchronously return the content body to be 66 | sent to the client (optional) 67 | * `contentType`: If the content to be sent is anything other than 68 | `application/json`, then the `contentType` property must be set (optional) 69 | * `bearerType`: String specifying the Bearer string (optional) 70 | * `specCompliance`: 71 | Plugin spec compliance. Accepts either 72 | [`rfc6749`](https://datatracker.ietf.org/doc/html/rfc6749) or 73 | [`rfc6750`](https://datatracker.ietf.org/doc/html/rfc6750). 74 | Defaults to `rfc6750`. 75 | * `rfc6749` is about the generic OAuth2.0 protocol, which allows the token type to be case-insensitive 76 | * `rfc6750` is about the Bearer Token Usage, which forces the token type to be an exact match 77 | * `function auth (key, req) {}` : This function tests if `key` is a valid token. It must return 78 | `true` if accepted or `false` if rejected. The function may also return a promise that resolves 79 | to one of these values. If the function returns or resolves to any other value, rejects, or throws, 80 | an HTTP status of `500` will be sent. `req` is the Fastify request object. If `auth` is a function, 81 | `keys` will be ignored. If `auth` is not a function or `undefined`, `keys` will be used 82 | * `addHook`: Accepts a boolean, `'onRequest'`, or `'preParsing'` (optional, defaults to `'onRequest'`): 83 | * `true` registers an `onRequest` hook 84 | * `'onRequest'` and `'preParsing'` registers their respective hooks 85 | * `false` will not register a hook, and the `fastify.verifyBearerAuth` and `fastify.verifyBearerAuthFactory` decorators will be exposed 86 | * `verifyErrorLogLevel`: An optional string specifying the log level for verification errors. 87 | It must be a valid log level supported by Fastify, or an exception will be thrown when 88 | registering the plugin. By default, this option is set to `error` 89 | 90 | The default configuration object is: 91 | 92 | ```js 93 | { 94 | keys: new Set(), 95 | contentType: undefined, 96 | bearerType: 'Bearer', 97 | specCompliance: 'rfc6750', 98 | errorResponse: (err) => { 99 | return {error: err.message} 100 | }, 101 | auth: undefined, 102 | addHook: true 103 | } 104 | ``` 105 | 106 | The plugin registers a standard Fastify [onRequest hook][onrequesthook] to inspect the request's 107 | headers for an `authorization` header in the format `bearer key`. The `key` is matched against 108 | the configured `keys` object using a [constant time algorithm](https://en.wikipedia.org/wiki/Time_complexity#Constant_time) 109 | to prevent [timing-attacks](https://snyk.io/blog/node-js-timing-attack-ccc-ctf/). If the 110 | `authorization` header is missing, malformed, or the `key` does not validate, a 401 response 111 | is sent with a `{error: message}` body, and no further request processing is performed. 112 | 113 | [onrequesthook]: https://github.com/fastify/fastify/blob/main/docs/Reference/Hooks.md#onrequest 114 | 115 | ## Integration with `@fastify/auth` 116 | 117 | This plugin can integrate with `@fastify/auth` by following this example: 118 | 119 | ```js 120 | const fastify = require('fastify')() 121 | const auth = require('@fastify/auth') 122 | const bearerAuthPlugin = require('@fastify/bearer-auth') 123 | const keys = new Set(['a-super-secret-key', 'another-super-secret-key']) 124 | 125 | async function server() { 126 | 127 | await fastify 128 | .register(auth) 129 | .register(bearerAuthPlugin, { addHook: false, keys, verifyErrorLogLevel: 'debug' }) 130 | .decorate('allowAnonymous', function (req, reply, done) { 131 | if (req.headers.authorization) { 132 | return done(Error('not anonymous')) 133 | } 134 | return done() 135 | }) 136 | 137 | fastify.route({ 138 | method: 'GET', 139 | url: '/multiauth', 140 | preHandler: fastify.auth([ 141 | fastify.allowAnonymous, 142 | fastify.verifyBearerAuth 143 | ]), 144 | handler: function (_, reply) { 145 | reply.send({ hello: 'world' }) 146 | } 147 | }) 148 | 149 | await fastify.listen({port: 8000}) 150 | } 151 | 152 | server() 153 | ``` 154 | 155 | Passing `{ addHook: false }` in the options causes the `verifyBearerAuth` hook to invoke 156 | `done(someError)` instead of immediately replying on error (`reply.send(someError)`). This allows 157 | `fastify.auth` to continue with the next authentication scheme in the hook list. 158 | Setting `{ verifyErrorLogLevel: 'debug' }` in the options makes `@fastify/bearer-auth` emit 159 | all verification error logs at the `debug` level. If `verifyBearerAuth` is the last hook in the list, 160 | `fastify.auth` will reply with `Unauthorized`. 161 | 162 | ## License 163 | 164 | Licensed under [MIT](./LICENSE). 165 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = require('neostandard')({ 4 | ignores: require('neostandard').resolveIgnoresFromGitignore(), 5 | ts: true 6 | }) 7 | -------------------------------------------------------------------------------- /examples/example.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const fastify = require('fastify')({ 4 | }) 5 | const bearerAuthPlugin = require('..') 6 | const keys = new Set(['key']) 7 | 8 | fastify.register(bearerAuthPlugin, { keys }) 9 | fastify.get('/foo', (_req, reply) => { 10 | reply.send({ authenticated: true }) 11 | }) 12 | 13 | fastify.listen({ port: 8000 }, (err) => { 14 | if (err) { 15 | fastify.log.error(err.message) 16 | process.exit(1) 17 | } 18 | fastify.log.info('http://127.0.0.1:8000/foo') 19 | }) 20 | 21 | // Missing Header 22 | // autocannon http://127.0.0.1:8000/foo 23 | // Invalid Bearer Type 24 | // autocannon -H authorization='Beaver key' http://127.0.0.1:8000/foo 25 | // Invalid Key 26 | // autocannon -H authorization='Bearer invalid' http://127.0.0.1:8000/foo 27 | // Valid Request 28 | // autocannon -H authorization='Bearer key' http://127.0.0.1:8000/foo 29 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const fp = require('fastify-plugin') 4 | const verifyBearerAuthFactory = require('./lib/verify-bearer-auth-factory') 5 | const { FST_BEARER_AUTH_INVALID_HOOK, FST_BEARER_AUTH_INVALID_LOG_LEVEL } = require('./lib/errors') 6 | 7 | /** 8 | * Hook type limited to 'onRequest' and 'preParsing' to protect against DoS attacks. 9 | * @see {@link https://github.com/fastify/fastify-auth?tab=readme-ov-file#hook-selection | fastify-auth hook selection} 10 | */ 11 | const validHooks = new Set(['onRequest', 'preParsing']) 12 | 13 | function fastifyBearerAuth (fastify, options, done) { 14 | options = { verifyErrorLogLevel: 'error', ...options } 15 | if (options.addHook === true || options.addHook == null) { 16 | options.addHook = 'onRequest' 17 | } 18 | 19 | if ( 20 | Object.hasOwn(fastify.log, 'error') === false || 21 | typeof fastify.log.error !== 'function' 22 | ) { 23 | options.verifyErrorLogLevel = null 24 | } 25 | 26 | if ( 27 | options.verifyErrorLogLevel != null && 28 | ( 29 | typeof options.verifyErrorLogLevel !== 'string' || 30 | Object.hasOwn(fastify.log, options.verifyErrorLogLevel) === false || 31 | typeof fastify.log[options.verifyErrorLogLevel] !== 'function' 32 | ) 33 | ) { 34 | done(new FST_BEARER_AUTH_INVALID_LOG_LEVEL(options.verifyErrorLogLevel)) 35 | } 36 | 37 | try { 38 | if (options.addHook) { 39 | if (!validHooks.has(options.addHook)) { 40 | done(new FST_BEARER_AUTH_INVALID_HOOK()) 41 | } 42 | 43 | if (options.addHook === 'preParsing') { 44 | const verifyBearerAuth = verifyBearerAuthFactory(options) 45 | fastify.addHook('preParsing', (request, reply, _payload, done) => { 46 | verifyBearerAuth(request, reply, done) 47 | }) 48 | } else { 49 | fastify.addHook(options.addHook, verifyBearerAuthFactory(options)) 50 | } 51 | } else { 52 | fastify.decorate('verifyBearerAuthFactory', verifyBearerAuthFactory) 53 | fastify.decorate('verifyBearerAuth', verifyBearerAuthFactory(options)) 54 | } 55 | done() 56 | } catch (err) { 57 | done(err) 58 | } 59 | } 60 | 61 | module.exports = fp(fastifyBearerAuth, { 62 | fastify: '5.x', 63 | name: '@fastify/bearer-auth' 64 | }) 65 | module.exports.default = fastifyBearerAuth 66 | module.exports.fastifyBearerAuth = fastifyBearerAuth 67 | -------------------------------------------------------------------------------- /lib/authenticate.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const compare = require('./compare') 4 | 5 | module.exports = function authenticate (keys, key) { 6 | const b = Buffer.from(key) 7 | return keys.findIndex((a) => compare(a, b)) !== -1 8 | } 9 | -------------------------------------------------------------------------------- /lib/compare.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const crypto = require('node:crypto') 4 | 5 | // perform constant-time comparison to prevent timing attacks 6 | module.exports = function compare (a, b) { 7 | if (a.length !== b.length) { 8 | // Delay return with cryptographically secure timing check. 9 | crypto.timingSafeEqual(a, a) 10 | return false 11 | } 12 | 13 | return crypto.timingSafeEqual(a, b) 14 | } 15 | -------------------------------------------------------------------------------- /lib/errors.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { createError } = require('@fastify/error') 4 | 5 | const FST_BEARER_AUTH_INVALID_KEYS_OPTION_TYPE = createError('FST_BEARER_AUTH_INVALID_KEYS_OPTION_TYPE', 'options.keys has to be an Array or a Set') 6 | const FST_BEARER_AUTH_INVALID_HOOK = createError('FST_BEARER_AUTH_INVALID_HOOK', 'options.addHook must be either "onRequest" or "preParsing"') 7 | const FST_BEARER_AUTH_INVALID_LOG_LEVEL = createError('FST_BEARER_AUTH_INVALID_LOG_LEVEL', 'fastify.log does not have level \'%s\'') 8 | const FST_BEARER_AUTH_KEYS_OPTION_INVALID_KEY_TYPE = createError('FST_BEARER_AUTH_KEYS_OPTION_INVALID_KEY_TYPE', 'options.keys has to contain only string entries') 9 | const FST_BEARER_AUTH_INVALID_SPEC = createError('FST_BEARER_AUTH_INVALID_SPEC', 'options.specCompliance has to be set to \'rfc6750\' or \'rfc6749\'') 10 | const FST_BEARER_AUTH_MISSING_AUTHORIZATION_HEADER = createError('FST_BEARER_AUTH_MISSING_AUTHORIZATION_HEADER', 'missing authorization header', 401) 11 | const FST_BEARER_AUTH_INVALID_AUTHORIZATION_HEADER = createError('FST_BEARER_AUTH_INVALID_AUTHORIZATION_HEADER', 'invalid authorization header', 401) 12 | 13 | module.exports = { 14 | FST_BEARER_AUTH_INVALID_KEYS_OPTION_TYPE, 15 | FST_BEARER_AUTH_INVALID_HOOK, 16 | FST_BEARER_AUTH_INVALID_LOG_LEVEL, 17 | FST_BEARER_AUTH_KEYS_OPTION_INVALID_KEY_TYPE, 18 | FST_BEARER_AUTH_INVALID_SPEC, 19 | FST_BEARER_AUTH_MISSING_AUTHORIZATION_HEADER, 20 | FST_BEARER_AUTH_INVALID_AUTHORIZATION_HEADER 21 | } 22 | -------------------------------------------------------------------------------- /lib/verify-bearer-auth-factory.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const authenticate = require('./authenticate') 4 | const { 5 | FST_BEARER_AUTH_INVALID_KEYS_OPTION_TYPE, 6 | FST_BEARER_AUTH_KEYS_OPTION_INVALID_KEY_TYPE, 7 | FST_BEARER_AUTH_INVALID_SPEC, 8 | FST_BEARER_AUTH_MISSING_AUTHORIZATION_HEADER, 9 | FST_BEARER_AUTH_INVALID_AUTHORIZATION_HEADER 10 | } = require('./errors') 11 | 12 | const validSpecs = new Set([ 13 | 'rfc6749', 14 | 'rfc6750' 15 | ]) 16 | 17 | const defaultOptions = { 18 | keys: [], 19 | auth: undefined, 20 | errorResponse (err) { 21 | return { error: err.message } 22 | }, 23 | contentType: undefined, 24 | bearerType: 'Bearer', 25 | specCompliance: 'rfc6750' 26 | } 27 | 28 | module.exports = function verifyBearerAuthFactory (options) { 29 | const _options = { ...defaultOptions, ...options } 30 | if (_options.keys instanceof Set) { 31 | _options.keys = Array.from(_options.keys) 32 | } else if (Array.isArray(_options.keys)) { 33 | // create unique array of keys 34 | _options.keys = Array.from(new Set(_options.keys)) 35 | } else { 36 | throw new FST_BEARER_AUTH_INVALID_KEYS_OPTION_TYPE() 37 | } 38 | 39 | const { keys, errorResponse, contentType, bearerType, specCompliance, auth, addHook = true, verifyErrorLogLevel = 'error' } = _options 40 | 41 | if (validSpecs.has(specCompliance) === false) { 42 | throw new FST_BEARER_AUTH_INVALID_SPEC() 43 | } 44 | 45 | for (let i = 0, il = keys.length; i < il; ++i) { 46 | if (typeof keys[i] !== 'string') { 47 | throw new FST_BEARER_AUTH_KEYS_OPTION_INVALID_KEY_TYPE() 48 | } 49 | keys[i] = Buffer.from(keys[i]) 50 | } 51 | 52 | const bearerTypePrefixLength = bearerType.length + 1 53 | const bearerTypePrefix = specCompliance === 'rfc6750' 54 | ? bearerType + ' ' 55 | : bearerType.toLowerCase() + ' ' 56 | 57 | const verifyBearerType = specCompliance === 'rfc6750' 58 | ? function (authorizationHeader) { 59 | return authorizationHeader.substring(0, bearerTypePrefixLength) !== bearerTypePrefix 60 | } 61 | : function (authorizationHeader) { 62 | return authorizationHeader.substring(0, bearerTypePrefixLength).toLowerCase() !== bearerTypePrefix 63 | } 64 | 65 | function handleUnauthorized (request, reply, done, error) { 66 | if (verifyErrorLogLevel) request.log[verifyErrorLogLevel]('unauthorized: %s', error.message) 67 | if (contentType) reply.header('content-type', contentType) 68 | reply.code(401) 69 | if (!addHook) { 70 | done(error) 71 | return 72 | } 73 | reply.send(errorResponse(error)) 74 | } 75 | 76 | return function verifyBearerAuth (request, reply, done) { 77 | const authorizationHeader = request.raw.headers.authorization 78 | if (!authorizationHeader) { 79 | const error = new FST_BEARER_AUTH_MISSING_AUTHORIZATION_HEADER() 80 | return handleUnauthorized(request, reply, done, error) 81 | } 82 | 83 | if (verifyBearerType(authorizationHeader)) { 84 | const error = new FST_BEARER_AUTH_INVALID_AUTHORIZATION_HEADER() 85 | return handleUnauthorized(request, reply, done, error) 86 | } 87 | 88 | const key = authorizationHeader.substring(bearerTypePrefixLength).trim() 89 | let retVal = false 90 | // check if auth function is defined 91 | if (auth && auth instanceof Function) { 92 | try { 93 | retVal = auth(key, request) 94 | // catch any error from the user provided function 95 | } catch (err) { 96 | retVal = Promise.reject(err) 97 | } 98 | } else { 99 | // if auth is not defined use keys 100 | retVal = authenticate(keys, key) 101 | } 102 | 103 | // retVal contains the result of the auth function if defined or the 104 | // result of the key comparison. 105 | // retVal is enclosed in a Promise.resolve to allow auth to be a normal 106 | // function or an async function. If it returns a non-promise value it 107 | // will be converted to a resolving promise. If it returns a promise it 108 | // will be resolved. 109 | Promise.resolve(retVal).then((val) => { 110 | // if val is not truthy return 401 111 | if (val === false) { 112 | const error = new FST_BEARER_AUTH_INVALID_AUTHORIZATION_HEADER() 113 | handleUnauthorized(request, reply, done, error) 114 | return 115 | } 116 | if (val === true) { 117 | // if it fails down stream return the proper error 118 | try { 119 | done() 120 | } catch (err) { 121 | done(err) 122 | } 123 | return 124 | } 125 | const retErr = new Error('internal server error') 126 | reply.code(500) 127 | if (!addHook) return done(retErr) 128 | reply.send(errorResponse(retErr)) 129 | }).catch((err) => { 130 | const retErr = err instanceof Error ? err : Error(String(err)) 131 | reply.code(500) 132 | if (!addHook) return done(retErr) 133 | reply.send(errorResponse(retErr)) 134 | }) 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@fastify/bearer-auth", 3 | "version": "10.1.1", 4 | "description": "A Bearer authentication plugin for Fastify", 5 | "main": "index.js", 6 | "type": "commonjs", 7 | "types": "types/index.d.ts", 8 | "scripts": { 9 | "lint": "eslint", 10 | "lint:fix": "eslint --fix", 11 | "test": "npm run test:unit && npm run test:typescript", 12 | "test:typescript": "tsd", 13 | "test:unit": "c8 --100 node --test" 14 | }, 15 | "precommit": [ 16 | "lint", 17 | "test" 18 | ], 19 | "repository": { 20 | "type": "git", 21 | "url": "git+ssh://git@github.com/fastify/fastify-bearer-auth.git" 22 | }, 23 | "keywords": [ 24 | "fastify", 25 | "authentication" 26 | ], 27 | "author": "James Sumners ", 28 | "contributors": [ 29 | { 30 | "name": "Matteo Collina", 31 | "email": "hello@matteocollina.com" 32 | }, 33 | { 34 | "name": "Manuel Spigolon", 35 | "email": "behemoth89@gmail.com" 36 | }, 37 | { 38 | "name": "Aras Abbasi", 39 | "email": "aras.abbasi@gmail.com" 40 | }, 41 | { 42 | "name": "Frazer Smith", 43 | "email": "frazer.dev@icloud.com", 44 | "url": "https://github.com/fdawgs" 45 | } 46 | ], 47 | "license": "MIT", 48 | "bugs": { 49 | "url": "https://github.com/fastify/fastify-bearer-auth/issues" 50 | }, 51 | "homepage": "https://github.com/fastify/fastify-bearer-auth#readme", 52 | "funding": [ 53 | { 54 | "type": "github", 55 | "url": "https://github.com/sponsors/fastify" 56 | }, 57 | { 58 | "type": "opencollective", 59 | "url": "https://opencollective.com/fastify" 60 | } 61 | ], 62 | "devDependencies": { 63 | "@fastify/auth": "^5.0.0", 64 | "@fastify/pre-commit": "^2.1.0", 65 | "@types/node": "^22.0.0", 66 | "c8": "^10.1.2", 67 | "eslint": "^9.17.0", 68 | "fastify": "^5.0.0", 69 | "neostandard": "^0.12.0", 70 | "tsd": "^0.32.0" 71 | }, 72 | "dependencies": { 73 | "@fastify/error": "^4.0.0", 74 | "fastify-plugin": "^5.0.0" 75 | }, 76 | "pre-commit": [ 77 | "lint", 78 | "test" 79 | ], 80 | "publishConfig": { 81 | "access": "public" 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /test/decorate-with-logger.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | const stream = require('node:stream') 5 | const Fastify = require('fastify') 6 | const plugin = require('..') 7 | 8 | test('verifyBearerAuth with debug log', async (t) => { 9 | t.plan(5) 10 | 11 | const logs = [] 12 | const destination = new stream.Writable({ 13 | write: function (chunk, _encoding, next) { 14 | logs.push(JSON.parse(chunk)) 15 | next() 16 | } 17 | }) 18 | 19 | const fastify = Fastify({ logger: { level: 'debug', stream: destination } }) 20 | await fastify.register(plugin, { addHook: false, keys: new Set(['123456']), verifyErrorLogLevel: 'debug' }) 21 | 22 | fastify.get('/', { 23 | onRequest: [ 24 | fastify.verifyBearerAuth 25 | ] 26 | }, async (_request, _reply) => { 27 | return { message: 'ok' } 28 | }) 29 | 30 | await fastify.ready() 31 | 32 | t.assert.ok(fastify.verifyBearerAuth) 33 | t.assert.ok(fastify.verifyBearerAuthFactory) 34 | 35 | const response = await fastify.inject({ 36 | method: 'GET', 37 | path: '/', 38 | headers: { 39 | authorization: 'Bearer bad key' 40 | } 41 | }) 42 | 43 | // Debug level is equal to 20 so we search for an entry with a level of 20 44 | const failure = logs.find((entry) => entry.level && entry.level === 20) 45 | 46 | t.assert.strictEqual(failure.level, 20) 47 | t.assert.strictEqual(failure.msg, 'unauthorized: invalid authorization header') 48 | 49 | t.assert.strictEqual(response.statusCode, 401) 50 | }) 51 | 52 | test('register with invalid log level', async (t) => { 53 | t.plan(1) 54 | 55 | const invalidLogLevel = 'invalid' 56 | const fastify = Fastify({ logger: { level: 'error' } }) 57 | 58 | try { 59 | await fastify.register(plugin, { addHook: false, keys: new Set(['123456']), verifyErrorLogLevel: invalidLogLevel }) 60 | } catch (err) { 61 | t.assert.strictEqual(err.message, `fastify.log does not have level '${invalidLogLevel}'`) 62 | } 63 | }) 64 | -------------------------------------------------------------------------------- /test/decorate.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | const fastify = require('fastify')() 5 | const plugin = require('../') 6 | 7 | fastify.register(plugin, { addHook: false, keys: new Set(['123456']) }) 8 | 9 | test('verifyBearerAuth', async (t) => { 10 | t.plan(1) 11 | await fastify.ready() 12 | t.assert.ok(fastify.verifyBearerAuth) 13 | }) 14 | 15 | test('verifyBearerAuthFactory', async (t) => { 16 | t.plan(1) 17 | await fastify.ready() 18 | t.assert.ok(fastify.verifyBearerAuthFactory) 19 | }) 20 | 21 | test('verifyBearerAuthFactory', async (t) => { 22 | t.plan(2) 23 | await fastify.ready() 24 | const keys = { keys: new Set([123456]) } 25 | await t.assert.rejects( 26 | async () => fastify.verifyBearerAuthFactory(keys), 27 | (err) => { 28 | t.assert.strictEqual(err.message, 'options.keys has to contain only string entries') 29 | return true 30 | } 31 | ) 32 | }) 33 | -------------------------------------------------------------------------------- /test/hooks.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | const Fastify = require('fastify') 5 | const kFastifyContext = require('fastify/lib/symbols').kRouteContext 6 | const plugin = require('../') 7 | 8 | const keys = new Set(['123456']) 9 | const authorization = 'Bearer 123456' 10 | 11 | test('onRequest hook used by default', async (t) => { 12 | t.plan(9) 13 | const fastify = Fastify() 14 | fastify.register(plugin, { keys, addHook: undefined }).get('/test', (_req, res) => { 15 | res.send({ hello: 'world' }) 16 | }) 17 | 18 | fastify.addHook('onResponse', (request, _reply, done) => { 19 | t.assert.strictEqual(request[kFastifyContext].onError, null) 20 | t.assert.strictEqual(request[kFastifyContext].onRequest.length, 1) 21 | t.assert.strictEqual(request[kFastifyContext].onSend, null) 22 | t.assert.strictEqual(request[kFastifyContext].preHandler, null) 23 | t.assert.strictEqual(request[kFastifyContext].preParsing, null) 24 | t.assert.strictEqual(request[kFastifyContext].preSerialization, null) 25 | t.assert.strictEqual(request[kFastifyContext].preValidation, null) 26 | done() 27 | }) 28 | 29 | const response = await fastify.inject({ 30 | method: 'GET', 31 | url: '/test', 32 | headers: { 33 | authorization 34 | } 35 | }) 36 | t.assert.strictEqual(response.statusCode, 200) 37 | t.assert.deepStrictEqual(JSON.parse(response.body), { hello: 'world' }) 38 | }) 39 | 40 | test('preParsing hook used when specified', async (t) => { 41 | t.plan(9) 42 | const fastify = Fastify() 43 | fastify.register(plugin, { keys, addHook: 'preParsing' }).get('/test', (_req, res) => { 44 | res.send({ hello: 'world' }) 45 | }) 46 | 47 | fastify.addHook('onResponse', (request, _reply, done) => { 48 | t.assert.strictEqual(request[kFastifyContext].onError, null) 49 | t.assert.strictEqual(request[kFastifyContext].onRequest, null) 50 | t.assert.strictEqual(request[kFastifyContext].onSend, null) 51 | t.assert.strictEqual(request[kFastifyContext].preHandler, null) 52 | t.assert.strictEqual(request[kFastifyContext].preParsing.length, 1) 53 | t.assert.strictEqual(request[kFastifyContext].preSerialization, null) 54 | t.assert.strictEqual(request[kFastifyContext].preValidation, null) 55 | done() 56 | }) 57 | 58 | const response = await fastify.inject({ 59 | method: 'GET', 60 | url: '/test', 61 | headers: { 62 | authorization 63 | } 64 | }) 65 | t.assert.strictEqual(response.statusCode, 200) 66 | t.assert.deepStrictEqual(JSON.parse(response.body), { hello: 'world' }) 67 | }) 68 | 69 | test('onRequest hook used when specified', async (t) => { 70 | t.plan(9) 71 | const fastify = Fastify() 72 | fastify.register(plugin, { keys, addHook: 'onRequest' }).get('/test', (_req, res) => { 73 | res.send({ hello: 'world' }) 74 | }) 75 | 76 | fastify.addHook('onResponse', (request, _reply, done) => { 77 | t.assert.strictEqual(request[kFastifyContext].onError, null) 78 | t.assert.strictEqual(request[kFastifyContext].onRequest.length, 1) 79 | t.assert.strictEqual(request[kFastifyContext].onSend, null) 80 | t.assert.strictEqual(request[kFastifyContext].preHandler, null) 81 | t.assert.strictEqual(request[kFastifyContext].preParsing, null) 82 | t.assert.strictEqual(request[kFastifyContext].preSerialization, null) 83 | t.assert.strictEqual(request[kFastifyContext].preValidation, null) 84 | done() 85 | }) 86 | 87 | const response = await fastify.inject({ 88 | method: 'GET', 89 | url: '/test', 90 | headers: { 91 | authorization 92 | } 93 | }) 94 | t.assert.strictEqual(response.statusCode, 200) 95 | t.assert.deepStrictEqual(JSON.parse(response.body), { hello: 'world' }) 96 | }) 97 | 98 | test('error when invalid hook specified', async (t) => { 99 | t.plan(1) 100 | const fastify = Fastify() 101 | try { 102 | await fastify.register(plugin, { keys, addHook: 'onResponse' }) 103 | } catch (err) { 104 | t.assert.strictEqual(err.message, 'options.addHook must be either "onRequest" or "preParsing"') 105 | } 106 | }) 107 | -------------------------------------------------------------------------------- /test/integration.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | const fastify = require('fastify')() 5 | const plugin = require('../') 6 | 7 | fastify.register(plugin, { keys: new Set(['123456']) }) 8 | 9 | fastify.get('/test', (_req, res) => { 10 | res.send({ hello: 'world' }) 11 | }) 12 | 13 | test('success route succeeds', async (t) => { 14 | t.plan(2) 15 | const response = await fastify.inject({ 16 | method: 'GET', 17 | url: '/test', 18 | headers: { 19 | authorization: 'Bearer 123456' 20 | } 21 | }) 22 | t.assert.strictEqual(response.statusCode, 200) 23 | t.assert.deepStrictEqual(JSON.parse(response.body), { hello: 'world' }) 24 | }) 25 | 26 | test('invalid key route fails correctly', async (t) => { 27 | t.plan(2) 28 | const response = await fastify.inject({ 29 | method: 'GET', 30 | url: '/test', 31 | headers: { 32 | authorization: 'Bearer 987654' 33 | } 34 | }) 35 | t.assert.strictEqual(response.statusCode, 401) 36 | t.assert.strictEqual(JSON.parse(response.body).error, 'invalid authorization header') 37 | }) 38 | 39 | test('missing space between bearerType and key fails correctly', async (t) => { 40 | t.plan(2) 41 | const response = await fastify.inject({ 42 | method: 'GET', 43 | url: '/test', 44 | headers: { 45 | authorization: 'Bearer123456' 46 | } 47 | }) 48 | t.assert.strictEqual(response.statusCode, 401) 49 | t.assert.strictEqual(JSON.parse(response.body).error, 'invalid authorization header') 50 | }) 51 | 52 | test('missing header route fails correctly', async (t) => { 53 | t.plan(2) 54 | const response = await fastify.inject({ method: 'GET', url: '/test' }) 55 | t.assert.strictEqual(response.statusCode, 401) 56 | t.assert.strictEqual(JSON.parse(response.body).error, 'missing authorization header') 57 | }) 58 | 59 | test('integration with @fastify/auth', async () => { 60 | const fastify = require('fastify')() 61 | await fastify.register(plugin, { addHook: false, keys: new Set(['123456']) }) 62 | fastify.decorate('allowAnonymous', function (request, _, done) { 63 | if (!request.headers.authorization) { 64 | return done() 65 | } 66 | return done(new Error('not anonymous')) 67 | }) 68 | await fastify.register(require('@fastify/auth')) 69 | 70 | fastify.route({ 71 | method: 'GET', 72 | url: '/anonymous', 73 | preHandler: fastify.auth([ 74 | fastify.allowAnonymous, 75 | fastify.verifyBearerAuth 76 | ]), 77 | handler: function (_, reply) { 78 | reply.send({ hello: 'world' }) 79 | } 80 | }) 81 | 82 | await fastify.ready() 83 | 84 | await test('anonymous should pass', async (t) => { 85 | t.plan(2) 86 | const res = await fastify.inject({ method: 'GET', url: '/anonymous' }) 87 | t.assert.strictEqual(res.statusCode, 200) 88 | t.assert.strictEqual(JSON.parse(res.body).hello, 'world') 89 | }) 90 | 91 | await test('bearer auth should pass', async (t) => { 92 | t.plan(2) 93 | const res = await fastify.inject({ 94 | method: 'GET', 95 | url: '/anonymous', 96 | headers: { 97 | authorization: 'Bearer 123456' 98 | } 99 | }) 100 | t.assert.strictEqual(res.statusCode, 200) 101 | t.assert.strictEqual(JSON.parse(res.body).hello, 'world') 102 | }) 103 | 104 | await test('bearer auth should fail, so fastify.auth fails', async (t) => { 105 | t.plan(2) 106 | const res = await fastify.inject({ 107 | method: 'GET', 108 | url: '/anonymous', 109 | headers: { 110 | authorization: 'Bearer fail' 111 | } 112 | }) 113 | t.assert.strictEqual(res.statusCode, 401) 114 | t.assert.strictEqual(JSON.parse(res.body).error, 'Unauthorized') 115 | }) 116 | }) 117 | 118 | test('integration with @fastify/auth; not the last auth option', async () => { 119 | const fastify = require('fastify')() 120 | await fastify.register(plugin, { addHook: false, keys: new Set(['123456']) }) 121 | fastify.decorate('alwaysValidAuth', function (_request, _, done) { 122 | return done() 123 | }) 124 | await fastify.register(require('@fastify/auth')) 125 | 126 | fastify.route({ 127 | method: 'GET', 128 | url: '/bearer-first', 129 | preHandler: fastify.auth([ 130 | fastify.verifyBearerAuth, 131 | fastify.alwaysValidAuth 132 | ]), 133 | handler: function (_, reply) { 134 | reply.send({ hello: 'world' }) 135 | } 136 | }) 137 | 138 | await fastify.ready() 139 | 140 | await test('bearer auth should pass so fastify.auth should pass', async (t) => { 141 | t.plan(2) 142 | const res = await fastify.inject({ 143 | method: 'GET', 144 | url: '/bearer-first', 145 | headers: { 146 | authorization: 'Bearer 123456' 147 | } 148 | }) 149 | t.assert.strictEqual(res.statusCode, 200) 150 | t.assert.strictEqual(JSON.parse(res.body).hello, 'world') 151 | }) 152 | 153 | await test('bearer should fail but fastify.auth should pass', async (t) => { 154 | t.plan(2) 155 | const res = await fastify.inject({ 156 | method: 'GET', 157 | url: '/bearer-first', 158 | headers: { 159 | authorization: 'Bearer fail' 160 | } 161 | }) 162 | t.assert.strictEqual(res.statusCode, 200) 163 | t.assert.strictEqual(JSON.parse(res.body).hello, 'world') 164 | }) 165 | 166 | await test('bearer should fail but fastify.auth should pass', async (t) => { 167 | t.plan(2) 168 | const res = await fastify.inject({ 169 | method: 'GET', 170 | url: '/bearer-first', 171 | headers: {} 172 | }) 173 | t.assert.strictEqual(res.statusCode, 200) 174 | t.assert.strictEqual(JSON.parse(res.body).hello, 'world') 175 | }) 176 | }) 177 | -------------------------------------------------------------------------------- /test/spec-compliance-invalid.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | const Fastify = require('fastify') 5 | const plugin = require('../') 6 | 7 | test('throws FST_BEARER_AUTH_INVALID_SPEC when invalid value for specCompliance was used', async (t) => { 8 | t.plan(2) 9 | 10 | const fastify = Fastify() 11 | 12 | await t.assert.rejects( 13 | async () => fastify.register(plugin, { keys: new Set(['123456']), specCompliance: 'invalid' }), 14 | (err) => { 15 | t.assert.strictEqual(err.name, 'FastifyError') 16 | return true 17 | } 18 | ) 19 | }) 20 | -------------------------------------------------------------------------------- /test/spec-compliance-rfc-6749.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | const fastify = require('fastify')() 5 | const plugin = require('../') 6 | 7 | fastify.register(plugin, { keys: new Set(['123456']), specCompliance: 'rfc6749' }) 8 | 9 | fastify.get('/test', (_req, res) => { 10 | res.send({ hello: 'world' }) 11 | }) 12 | 13 | test('bearerType starting with capital letter', async (t) => { 14 | t.plan(2) 15 | 16 | const response = await fastify.inject({ 17 | method: 'GET', 18 | url: '/test', 19 | headers: { 20 | authorization: 'Bearer 123456' 21 | } 22 | }) 23 | 24 | t.assert.strictEqual(response.statusCode, 200) 25 | t.assert.deepStrictEqual(JSON.parse(response.body), { hello: 'world' }) 26 | }) 27 | 28 | test('bearerType all lowercase', async (t) => { 29 | t.plan(2) 30 | 31 | const response = await fastify.inject({ 32 | method: 'GET', 33 | url: '/test', 34 | headers: { 35 | authorization: 'bearer 123456' 36 | } 37 | }) 38 | 39 | t.assert.strictEqual(response.statusCode, 200) 40 | t.assert.deepStrictEqual(JSON.parse(response.body), { hello: 'world' }) 41 | }) 42 | 43 | test('bearerType all uppercase', async (t) => { 44 | t.plan(2) 45 | 46 | const response = await fastify.inject({ 47 | method: 'GET', 48 | url: '/test', 49 | headers: { 50 | authorization: 'Bearer 123456' 51 | } 52 | }) 53 | 54 | t.assert.strictEqual(response.statusCode, 200) 55 | t.assert.deepStrictEqual(JSON.parse(response.body), { hello: 'world' }) 56 | }) 57 | 58 | test('invalid key route fails correctly', async (t) => { 59 | t.plan(2) 60 | const response = await fastify.inject({ 61 | method: 'GET', 62 | url: '/test', 63 | headers: { 64 | authorization: 'bearer 987654' 65 | } 66 | }) 67 | 68 | t.assert.strictEqual(response.statusCode, 401) 69 | t.assert.strictEqual(JSON.parse(response.body).error, 'invalid authorization header') 70 | }) 71 | 72 | test('missing space between bearerType and key fails correctly', async (t) => { 73 | t.plan(2) 74 | 75 | const response = await fastify.inject({ 76 | method: 'GET', 77 | url: '/test', 78 | headers: { 79 | authorization: 'bearer123456' 80 | } 81 | }) 82 | t.assert.strictEqual(response.statusCode, 401) 83 | t.assert.strictEqual(JSON.parse(response.body).error, 'invalid authorization header') 84 | }) 85 | -------------------------------------------------------------------------------- /test/spec-compliance-rfc-6750.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | const fastify = require('fastify')() 5 | const plugin = require('../') 6 | 7 | fastify.register(plugin, { keys: new Set(['123456']), specCompliance: 'rfc6750' }) 8 | 9 | fastify.get('/test', (_req, res) => { 10 | res.send({ hello: 'world' }) 11 | }) 12 | 13 | test('bearerType starting with capital letter', async (t) => { 14 | t.plan(2) 15 | 16 | const response = await fastify.inject({ 17 | method: 'GET', 18 | url: '/test', 19 | headers: { 20 | authorization: 'Bearer 123456' 21 | } 22 | }) 23 | 24 | t.assert.strictEqual(response.statusCode, 200) 25 | t.assert.deepStrictEqual(JSON.parse(response.body), { hello: 'world' }) 26 | }) 27 | 28 | test('bearerType all lowercase', async (t) => { 29 | t.plan(2) 30 | 31 | const response = await fastify.inject({ 32 | method: 'GET', 33 | url: '/test', 34 | headers: { 35 | authorization: 'bearer 123456' 36 | } 37 | }) 38 | 39 | t.assert.strictEqual(response.statusCode, 401) 40 | t.assert.strictEqual(JSON.parse(response.body).error, 'invalid authorization header') 41 | }) 42 | 43 | test('bearerType all uppercase', async (t) => { 44 | t.plan(2) 45 | 46 | const response = await fastify.inject({ 47 | method: 'GET', 48 | url: '/test', 49 | headers: { 50 | authorization: 'Bearer 123456' 51 | } 52 | }) 53 | 54 | t.assert.strictEqual(response.statusCode, 200) 55 | t.assert.deepStrictEqual(JSON.parse(response.body), { hello: 'world' }) 56 | }) 57 | 58 | test('invalid key route fails correctly', async (t) => { 59 | t.plan(2) 60 | const response = await fastify.inject({ 61 | method: 'GET', 62 | url: '/test', 63 | headers: { 64 | authorization: 'bearer 987654' 65 | } 66 | }) 67 | 68 | t.assert.strictEqual(response.statusCode, 401) 69 | t.assert.strictEqual(JSON.parse(response.body).error, 'invalid authorization header') 70 | }) 71 | 72 | test('missing space between bearerType and key fails correctly', async (t) => { 73 | t.plan(2) 74 | 75 | const response = await fastify.inject({ 76 | method: 'GET', 77 | url: '/test', 78 | headers: { 79 | authorization: 'bearer123456' 80 | } 81 | }) 82 | t.assert.strictEqual(response.statusCode, 401) 83 | t.assert.strictEqual(JSON.parse(response.body).error, 'invalid authorization header') 84 | }) 85 | -------------------------------------------------------------------------------- /test/verify-bearer-auth-factory.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | const noop = () => { } 5 | const verifyBearerAuthFactory = require('../lib/verify-bearer-auth-factory') 6 | const key = '123456789012354579814' 7 | const keys = { keys: new Set([key]) } 8 | 9 | test('hook rejects for missing header', (t) => { 10 | t.plan(2) 11 | 12 | const request = { 13 | log: { error: noop }, 14 | raw: { headers: {} } 15 | } 16 | const response = { 17 | code: () => response, 18 | send 19 | } 20 | 21 | function send (body) { 22 | t.assert.ok(body.error) 23 | t.assert.strictEqual(body.error, 'missing authorization header') 24 | } 25 | 26 | const hook = verifyBearerAuthFactory() 27 | hook(request, response) 28 | }) 29 | 30 | test('hook rejects for missing header with custom content type', (t) => { 31 | t.plan(6) 32 | 33 | const CUSTOM_CONTENT_TYPE = 'text/fastify' 34 | const request = { 35 | log: { error: noop }, 36 | raw: { headers: {} } 37 | } 38 | const response = { 39 | code: () => response, 40 | header, 41 | send 42 | } 43 | function header (key, value) { 44 | t.assert.ok(key) 45 | t.assert.ok(value) 46 | t.assert.strictEqual(key, 'content-type') 47 | t.assert.strictEqual(value, CUSTOM_CONTENT_TYPE) 48 | } 49 | function send (body) { 50 | t.assert.ok(body.error) 51 | t.assert.strictEqual(body.error, 'missing authorization header') 52 | } 53 | 54 | const hook = verifyBearerAuthFactory({ contentType: CUSTOM_CONTENT_TYPE }) 55 | hook(request, response) 56 | }) 57 | 58 | test('hook rejects for wrong bearer type but same string length as `bearer`', (t) => { 59 | t.plan(2) 60 | 61 | const request = { 62 | log: { error: noop }, 63 | raw: { headers: { authorization: `reraeB ${key}` } } 64 | } 65 | const response = { 66 | code: () => response, 67 | send 68 | } 69 | 70 | function send (body) { 71 | t.assert.ok(body.error) 72 | t.assert.strictEqual(body.error, 'invalid authorization header') 73 | } 74 | 75 | const hook = verifyBearerAuthFactory() 76 | hook(request, response) 77 | }) 78 | 79 | test('hook rejects for wrong bearer type', (t) => { 80 | t.plan(2) 81 | 82 | const request = { 83 | log: { error: noop }, 84 | raw: { headers: { authorization: `fake-bearer ${key}` } } 85 | } 86 | const response = { 87 | code: () => response, 88 | send 89 | } 90 | 91 | function send (body) { 92 | t.assert.ok(body.error) 93 | t.assert.strictEqual(body.error, 'invalid authorization header') 94 | } 95 | 96 | const hook = verifyBearerAuthFactory() 97 | hook(request, response) 98 | }) 99 | 100 | test('hook rejects for wrong alternate Bearer', (t) => { 101 | t.plan(2) 102 | 103 | const bearerAlt = 'BearerAlt' 104 | const keysAlt = { keys: new Set([key]), bearerType: bearerAlt } 105 | const request = { 106 | log: { error: noop }, 107 | raw: { 108 | headers: { authorization: `tlAreraeB ${key}` } 109 | } 110 | } 111 | const response = { 112 | code: () => response, 113 | send 114 | } 115 | 116 | function send (body) { 117 | t.assert.ok(body.error) 118 | t.assert.strictEqual(body.error, 'invalid authorization header') 119 | } 120 | 121 | const hook = verifyBearerAuthFactory(keysAlt) 122 | hook(request, response) 123 | }) 124 | 125 | test('hook rejects header without bearer prefix', (t) => { 126 | t.plan(2) 127 | 128 | const request = { 129 | log: { error: noop }, 130 | raw: { 131 | headers: { authorization: key } 132 | } 133 | } 134 | const response = { 135 | code: () => response, 136 | send 137 | } 138 | 139 | function send (body) { 140 | t.assert.ok(body.error) 141 | t.assert.strictEqual(body.error, 'invalid authorization header') 142 | } 143 | 144 | const hook = verifyBearerAuthFactory(keys) 145 | hook(request, response) 146 | }) 147 | 148 | test('hook rejects malformed header', (t) => { 149 | t.plan(2) 150 | 151 | const request = { 152 | log: { error: noop }, 153 | raw: { 154 | headers: { authorization: `bearerr ${key}` } 155 | } 156 | } 157 | const response = { 158 | code: () => response, 159 | send 160 | } 161 | 162 | function send (body) { 163 | t.assert.ok(body.error) 164 | t.assert.strictEqual(body.error, 'invalid authorization header') 165 | } 166 | 167 | const hook = verifyBearerAuthFactory(keys) 168 | hook(request, response) 169 | }) 170 | 171 | test('hook accepts correct header', (t) => { 172 | t.plan(1) 173 | 174 | const request = { 175 | log: { error: noop }, 176 | raw: { 177 | headers: { authorization: `Bearer ${key}` } 178 | } 179 | } 180 | const response = { 181 | code: () => response, 182 | send 183 | } 184 | 185 | function send (body) { 186 | t.assert.ifError(body) 187 | } 188 | 189 | const hook = verifyBearerAuthFactory(keys) 190 | hook(request, response, () => { 191 | t.assert.ok(true) 192 | }) 193 | }) 194 | 195 | test('hook accepts correct header and alternate Bearer', (t) => { 196 | t.plan(1) 197 | 198 | const bearerAlt = 'BearerAlt' 199 | const keysAlt = { keys: new Set([key]), bearerType: bearerAlt } 200 | const request = { 201 | log: { error: noop }, 202 | raw: { 203 | headers: { authorization: `BearerAlt ${key}` } 204 | } 205 | } 206 | const response = { 207 | code: () => response, 208 | send 209 | } 210 | 211 | function send (body) { 212 | t.assert.ifError(body) 213 | } 214 | 215 | const hook = verifyBearerAuthFactory(keysAlt) 216 | hook(request, response, () => { 217 | t.assert.ok(true) 218 | }) 219 | }) 220 | 221 | test('hook throws if header misses at least one space after bearerType', (t) => { 222 | t.plan(2) 223 | 224 | const request = { 225 | log: { error: noop }, 226 | raw: { 227 | headers: { authorization: `Bearer${key}` } 228 | } 229 | } 230 | const response = { 231 | code: () => response, 232 | send 233 | } 234 | 235 | function send (body) { 236 | t.assert.ok(body.error) 237 | t.assert.strictEqual(body.error, 'invalid authorization header') 238 | } 239 | 240 | const hook = verifyBearerAuthFactory(keys) 241 | hook(request, response) 242 | }) 243 | 244 | test('hook accepts correct header with extra padding', (t) => { 245 | t.plan(1) 246 | 247 | const request = { 248 | log: { error: noop }, 249 | raw: { 250 | headers: { authorization: `Bearer ${key} ` } 251 | } 252 | } 253 | const response = { 254 | code: () => response, 255 | send 256 | } 257 | 258 | function send (body) { 259 | t.assert.ifError(body) 260 | } 261 | 262 | const hook = verifyBearerAuthFactory(keys) 263 | hook(request, response, () => { 264 | t.assert.ok(true) 265 | }) 266 | }) 267 | 268 | test('hook accepts correct header with auth function (promise)', (t) => { 269 | t.plan(2) 270 | const auth = function (val) { 271 | t.assert.strictEqual(val, key, 'wrong argument') 272 | return Promise.resolve(true) 273 | } 274 | const request = { 275 | log: { error: noop }, 276 | raw: { 277 | headers: { authorization: `Bearer ${key}` } 278 | } 279 | } 280 | const response = { 281 | code: () => response, 282 | send 283 | } 284 | 285 | function send (_body) { 286 | t.assert.ifError('should not happen') 287 | } 288 | 289 | const hook = verifyBearerAuthFactory({ auth }) 290 | hook(request, response, () => { 291 | t.assert.ok(true) 292 | }) 293 | }) 294 | 295 | test('hook accepts correct header with auth function (non-promise)', (t) => { 296 | t.plan(2) 297 | const auth = function (val) { 298 | t.assert.strictEqual(val, key, 'wrong argument') 299 | return true 300 | } 301 | const request = { 302 | log: { error: noop }, 303 | raw: { 304 | headers: { authorization: `Bearer ${key}` } 305 | } 306 | } 307 | const response = { 308 | code: () => response, 309 | send 310 | } 311 | 312 | function send (_body) { 313 | t.assert.ifError('should not happen') 314 | } 315 | 316 | const hook = verifyBearerAuthFactory({ auth }) 317 | hook(request, response, () => { 318 | t.assert.ok(true) 319 | }) 320 | }) 321 | 322 | test('hook rejects wrong token with keys', (t) => { 323 | t.plan(2) 324 | 325 | const request = { 326 | log: { error: noop }, 327 | raw: { 328 | headers: { authorization: 'Bearer abcdedfg' } 329 | } 330 | } 331 | const response = { 332 | code: () => response, 333 | send 334 | } 335 | 336 | function send (body) { 337 | t.assert.ok(body.error) 338 | t.assert.strictEqual(body.error, 'invalid authorization header') 339 | } 340 | 341 | const hook = verifyBearerAuthFactory(keys) 342 | hook(request, response, () => { 343 | t.assert.ifError('should not accept') 344 | }) 345 | }) 346 | 347 | test('hook rejects wrong token with custom content type', (t) => { 348 | t.plan(6) 349 | 350 | const CUSTOM_CONTENT_TYPE = 'text/fastify' 351 | const request = { 352 | log: { error: noop }, 353 | raw: { 354 | headers: { authorization: 'Bearer abcdefg' } 355 | } 356 | } 357 | const response = { 358 | code: () => response, 359 | header, 360 | send 361 | } 362 | function header (key, value) { 363 | t.assert.ok(key) 364 | t.assert.ok(value) 365 | t.assert.strictEqual(key, 'content-type') 366 | t.assert.strictEqual(value, CUSTOM_CONTENT_TYPE) 367 | } 368 | function send (body) { 369 | t.assert.ok(body.error) 370 | t.assert.strictEqual(body.error, 'invalid authorization header') 371 | } 372 | 373 | const hook = verifyBearerAuthFactory({ ...keys, contentType: CUSTOM_CONTENT_TYPE }) 374 | hook(request, response) 375 | }) 376 | 377 | test('hook rejects wrong token with auth function', (t) => { 378 | t.plan(5) 379 | 380 | const request = { 381 | log: { error: noop }, 382 | raw: { 383 | headers: { authorization: 'Bearer abcdefg' } 384 | } 385 | } 386 | 387 | const auth = function (val, req) { 388 | t.assert.strictEqual(req, request) 389 | t.assert.strictEqual(val, 'abcdefg', 'wrong argument') 390 | return false 391 | } 392 | 393 | const response = { 394 | code: (status) => { 395 | t.assert.strictEqual(401, status) 396 | return response 397 | }, 398 | send 399 | } 400 | 401 | function send (body) { 402 | t.assert.ok(body.error) 403 | t.assert.strictEqual(body.error, 'invalid authorization header') 404 | } 405 | 406 | const hook = verifyBearerAuthFactory({ auth }) 407 | hook(request, response, () => { 408 | t.fail('should not accept') 409 | }) 410 | }) 411 | 412 | test('hook rejects wrong token with function (resolved promise)', (t) => { 413 | t.plan(4) 414 | 415 | const auth = function (val) { 416 | t.assert.strictEqual(val, 'abcdefg', 'wrong argument') 417 | return Promise.resolve(false) 418 | } 419 | 420 | const request = { 421 | log: { error: noop }, 422 | raw: { 423 | headers: { authorization: 'Bearer abcdefg' } 424 | } 425 | } 426 | const response = { 427 | code: (status) => { 428 | t.assert.strictEqual(401, status) 429 | return response 430 | }, 431 | send 432 | } 433 | 434 | function send (body) { 435 | t.assert.ok(body.error) 436 | t.assert.strictEqual(body.error, 'invalid authorization header') 437 | } 438 | 439 | const hook = verifyBearerAuthFactory({ auth }) 440 | hook(request, response, () => { 441 | t.fail('should not accept') 442 | }) 443 | }) 444 | 445 | test('hook rejects with 500 when functions fails', (t) => { 446 | t.plan(4) 447 | 448 | const auth = function (val) { 449 | t.assert.strictEqual(val, 'abcdefg', 'wrong argument') 450 | throw Error('failing') 451 | } 452 | 453 | const request = { 454 | log: { error: noop }, 455 | raw: { 456 | headers: { authorization: 'Bearer abcdefg' } 457 | } 458 | } 459 | const response = { 460 | code: (status) => { 461 | t.assert.strictEqual(500, status) 462 | return response 463 | }, 464 | send 465 | } 466 | 467 | function send (body) { 468 | t.assert.ok(body.error) 469 | t.assert.strictEqual(body.error, 'failing') 470 | } 471 | 472 | const hook = verifyBearerAuthFactory({ auth }) 473 | hook(request, response, () => { 474 | t.fail('should not accept') 475 | }) 476 | }) 477 | 478 | test('hook rejects with 500 when promise rejects', (t) => { 479 | t.plan(4) 480 | 481 | const auth = function (val) { 482 | t.assert.strictEqual(val, 'abcdefg', 'wrong argument') 483 | return Promise.reject(Error('failing')) 484 | } 485 | 486 | const request = { 487 | log: { error: noop }, 488 | raw: { 489 | headers: { authorization: 'Bearer abcdefg' } 490 | } 491 | } 492 | const response = { 493 | code: (status) => { 494 | t.assert.strictEqual(500, status) 495 | return response 496 | }, 497 | send 498 | } 499 | 500 | function send (body) { 501 | t.assert.ok(body.error) 502 | t.assert.strictEqual(body.error, 'failing') 503 | } 504 | 505 | const hook = verifyBearerAuthFactory({ auth }) 506 | hook(request, response, () => { 507 | t.assert.ifError('should not accept') 508 | }) 509 | }) 510 | 511 | test('hook rejects with 500 when promise rejects with non Error', (t) => { 512 | t.plan(4) 513 | 514 | const auth = function (val) { 515 | t.assert.strictEqual(val, 'abcdefg', 'wrong argument') 516 | return Promise.reject('failing') // eslint-disable-line 517 | } 518 | 519 | const request = { 520 | log: { error: noop }, 521 | raw: { 522 | headers: { authorization: 'Bearer abcdefg' } 523 | } 524 | } 525 | const response = { 526 | code: (status) => { 527 | t.assert.strictEqual(500, status) 528 | return response 529 | }, 530 | send 531 | } 532 | 533 | function send (body) { 534 | t.assert.ok(body.error) 535 | t.assert.strictEqual(body.error, 'failing') 536 | } 537 | 538 | const hook = verifyBearerAuthFactory({ auth }) 539 | hook(request, response, () => { 540 | t.assert.ifError('should not accept') 541 | }) 542 | }) 543 | 544 | test('hook returns proper error for valid key but failing callback', (t) => { 545 | t.plan(4) 546 | 547 | const request = { 548 | log: { error: noop }, 549 | raw: { 550 | headers: { authorization: `Bearer ${key}` } 551 | } 552 | } 553 | const response = { 554 | code: (status) => { 555 | t.assert.strictEqual(500, status) 556 | return response 557 | }, 558 | send 559 | } 560 | 561 | function send (body) { 562 | t.assert.ok(body.error) 563 | t.assert.strictEqual(body.error, 'foo!') 564 | } 565 | 566 | const hook = verifyBearerAuthFactory(keys) 567 | hook(request, response, (err) => { 568 | if (err) { 569 | t.assert.ok(err) 570 | } 571 | throw new Error('foo!') 572 | }) 573 | }) 574 | 575 | test('hook rejects with 500 when functions returns non-boolean', (t) => { 576 | t.plan(4) 577 | 578 | const auth = function (val) { 579 | t.assert.strictEqual(val, 'abcdefg', 'wrong argument') 580 | return 'foobar' 581 | } 582 | 583 | const request = { 584 | log: { error: noop }, 585 | raw: { 586 | headers: { authorization: 'Bearer abcdefg' } 587 | } 588 | } 589 | const response = { 590 | code: (status) => { 591 | t.assert.strictEqual(500, status) 592 | return response 593 | }, 594 | send 595 | } 596 | 597 | function send (body) { 598 | t.assert.ok(body.error) 599 | t.assert.strictEqual(body.error, 'internal server error') 600 | } 601 | 602 | const hook = verifyBearerAuthFactory({ auth }) 603 | hook(request, response, () => { 604 | t.assert.ifError('should not accept') 605 | }) 606 | }) 607 | 608 | test('hook rejects with 500 when promise resolves to non-boolean', (t) => { 609 | t.plan(4) 610 | 611 | const auth = function (val) { 612 | t.assert.strictEqual(val, 'abcdefg', 'wrong argument') 613 | return Promise.resolve('abcde') 614 | } 615 | 616 | const request = { 617 | log: { error: noop }, 618 | raw: { 619 | headers: { authorization: 'Bearer abcdefg' } 620 | } 621 | } 622 | const response = { 623 | code: (status) => { 624 | t.assert.strictEqual(500, status) 625 | return response 626 | }, 627 | send 628 | } 629 | 630 | function send (body) { 631 | t.assert.ok(body.error) 632 | t.assert.strictEqual(body.error, 'internal server error') 633 | } 634 | 635 | const hook = verifyBearerAuthFactory({ auth }) 636 | hook(request, response, () => { 637 | t.assert.ifError('should not accept') 638 | }) 639 | }) 640 | 641 | test('hook rejects with 500 when functions returns non-boolean (addHook: false)', (t) => { 642 | t.plan(4) 643 | 644 | const auth = function (val) { 645 | t.assert.strictEqual(val, 'abcdefg', 'wrong argument') 646 | return 'foobar' 647 | } 648 | 649 | const request = { 650 | log: { error: noop }, 651 | raw: { 652 | headers: { authorization: 'Bearer abcdefg' } 653 | } 654 | } 655 | const response = { 656 | code: (status) => { 657 | t.assert.strictEqual(500, status) 658 | return response 659 | } 660 | } 661 | 662 | const hook = verifyBearerAuthFactory({ auth, addHook: false }) 663 | hook(request, response, (err) => { 664 | t.assert.ok(err) 665 | t.assert.strictEqual(err.message, 'internal server error') 666 | }) 667 | }) 668 | 669 | test('hook rejects with 500 when promise rejects (addHook: false)', (t) => { 670 | t.plan(4) 671 | 672 | const auth = function (val) { 673 | t.assert.strictEqual(val, 'abcdefg', 'wrong argument') 674 | return Promise.reject(Error('failing')) 675 | } 676 | 677 | const request = { 678 | log: { error: noop }, 679 | raw: { 680 | headers: { authorization: 'Bearer abcdefg' } 681 | } 682 | } 683 | const response = { 684 | code: (status) => { 685 | t.assert.strictEqual(500, status) 686 | return response 687 | } 688 | } 689 | 690 | const hook = verifyBearerAuthFactory({ auth, addHook: false }) 691 | hook(request, response, (err) => { 692 | t.assert.ok(err) 693 | t.assert.strictEqual(err.message, 'failing') 694 | }) 695 | }) 696 | 697 | test('options.keys can be an Array', (t) => { 698 | t.plan(1) 699 | 700 | const request = { 701 | log: { error: noop }, 702 | raw: { 703 | headers: { authorization: `Bearer ${key}` } 704 | } 705 | } 706 | const response = { 707 | code: () => response, 708 | send 709 | } 710 | 711 | function send (body) { 712 | t.assert.ifError(body) 713 | } 714 | 715 | const hook = verifyBearerAuthFactory({ keys: [key] }) 716 | hook(request, response, () => { 717 | t.assert.ok(true) 718 | }) 719 | }) 720 | 721 | test('options.keys throws if not an Array and not a Set', (t) => { 722 | t.plan(1) 723 | 724 | t.assert.throws(() => verifyBearerAuthFactory({ keys: true })) 725 | }) 726 | -------------------------------------------------------------------------------- /types/index.d.ts: -------------------------------------------------------------------------------- 1 | import { 2 | FastifyRequest, 3 | FastifyReply, 4 | FastifyPluginCallback 5 | } from 'fastify' 6 | import { FastifyError } from '@fastify/error' 7 | 8 | declare interface FastifyBearerAuthErrors { 9 | FST_BEARER_AUTH_INVALID_KEYS_OPTION_TYPE: FastifyError 10 | FST_BEARER_AUTH_INVALID_HOOK: FastifyError 11 | FST_BEARER_AUTH_INVALID_LOG_LEVEL: FastifyError 12 | FST_BEARER_AUTH_KEYS_OPTION_INVALID_KEY_TYPE: FastifyError 13 | FST_BEARER_AUTH_INVALID_SPEC: FastifyError 14 | FST_BEARER_AUTH_MISSING_AUTHORIZATION_HEADER: FastifyError 15 | FST_BEARER_AUTH_INVALID_AUTHORIZATION_HEADER: FastifyError 16 | } 17 | 18 | declare module 'fastify' { 19 | interface FastifyInstance { 20 | verifyBearerAuthFactory?: fastifyBearerAuth.verifyBearerAuthFactory 21 | verifyBearerAuth?: fastifyBearerAuth.verifyBearerAuth 22 | } 23 | } 24 | 25 | type FastifyBearerAuth = FastifyPluginCallback 26 | 27 | declare namespace fastifyBearerAuth { 28 | export interface FastifyBearerAuthOptions { 29 | keys: Set | string[]; 30 | auth?: (key: string, req: FastifyRequest) => boolean | Promise | undefined; 31 | errorResponse?: (err: Error) => { error: string }; 32 | contentType?: string | undefined; 33 | bearerType?: string; 34 | specCompliance?: 'rfc6749' | 'rfc6750'; 35 | addHook?: boolean | 'onRequest' | 'preParsing' | undefined; 36 | verifyErrorLogLevel?: string; 37 | } 38 | 39 | export type verifyBearerAuth = (request: FastifyRequest, reply: FastifyReply, done: (err?: Error) => void) => void 40 | export type verifyBearerAuthFactory = (options: fastifyBearerAuth.FastifyBearerAuthOptions) => verifyBearerAuth 41 | 42 | export const fastifyBearerAuth: FastifyBearerAuth 43 | export const FastifyBearerAuthErrors: FastifyBearerAuthErrors 44 | export { fastifyBearerAuth as default } 45 | } 46 | 47 | declare function fastifyBearerAuth (...params: Parameters): ReturnType 48 | export = fastifyBearerAuth 49 | -------------------------------------------------------------------------------- /types/index.test-d.ts: -------------------------------------------------------------------------------- 1 | import fastify, { FastifyRequest } from 'fastify' 2 | import { FastifyError } from '@fastify/error' 3 | import { expectAssignable, expectType } from 'tsd' 4 | import bearerAuth, { FastifyBearerAuthErrors, FastifyBearerAuthOptions, verifyBearerAuth, verifyBearerAuthFactory } from '..' 5 | 6 | const pluginOptions: FastifyBearerAuthOptions = { 7 | keys: new Set(['foo']), 8 | auth: (_key: string, _req: FastifyRequest) => { return true }, 9 | errorResponse: (err: Error) => { return { error: err.message } }, 10 | contentType: '', 11 | bearerType: '', 12 | addHook: false 13 | } 14 | 15 | const pluginOptionsAuthPromise: FastifyBearerAuthOptions = { 16 | keys: new Set(['foo']), 17 | auth: (_key: string, _req: FastifyRequest) => { return Promise.resolve(true) }, 18 | errorResponse: (err: Error) => { return { error: err.message } }, 19 | contentType: '', 20 | bearerType: '', 21 | addHook: 'onRequest' 22 | } 23 | 24 | const pluginOptionsKeyArray: FastifyBearerAuthOptions = { 25 | keys: ['foo'], 26 | auth: (_key: string, _req: FastifyRequest) => { return Promise.resolve(true) }, 27 | errorResponse: (err: Error) => { return { error: err.message } }, 28 | contentType: '', 29 | bearerType: '' 30 | } 31 | 32 | const pluginOptionsUndefined: FastifyBearerAuthOptions = { 33 | keys: ['foo'], 34 | auth: undefined, 35 | errorResponse: (err: Error) => { return { error: err.message } }, 36 | contentType: undefined, 37 | bearerType: undefined, 38 | addHook: undefined 39 | } 40 | 41 | expectAssignable<{ 42 | keys: Set | string[]; 43 | auth?: (key: string, req: FastifyRequest) => boolean | Promise | undefined; 44 | errorResponse?: (err: Error) => { error: string }; 45 | contentType?: string | undefined; 46 | bearerType?: string; 47 | }>(pluginOptions) 48 | 49 | expectAssignable<{ 50 | keys: Set | string[]; 51 | auth?: (key: string, req: FastifyRequest) => boolean | Promise | undefined; 52 | errorResponse?: (err: Error) => { error: string }; 53 | contentType?: string | undefined; 54 | bearerType?: string; 55 | addHook?: boolean | 'onRequest' | 'preParsing' | undefined; 56 | }>(pluginOptionsKeyArray) 57 | 58 | expectAssignable<{ 59 | keys: Set | string[]; 60 | auth?: (key: string, req: FastifyRequest) => boolean | Promise | undefined; 61 | errorResponse?: (err: Error) => { error: string }; 62 | contentType?: string | undefined; 63 | bearerType?: string; 64 | addHook?: boolean | 'onRequest' | 'preParsing' | undefined; 65 | }>(pluginOptionsUndefined) 66 | 67 | expectAssignable<{ 68 | keys: Set | string[]; 69 | auth?: (key: string, req: FastifyRequest) => boolean | Promise | undefined; 70 | errorResponse?: (err: Error) => { error: string }; 71 | contentType?: string | undefined; 72 | bearerType?: string; 73 | }>(pluginOptionsAuthPromise) 74 | 75 | expectAssignable<{ 76 | keys: Set | string[]; 77 | auth?: (key: string, req: FastifyRequest) => boolean | Promise | undefined; 78 | errorResponse?: (err: Error) => { error: string }; 79 | contentType?: string | undefined; 80 | bearerType?: string; 81 | specCompliance?: 'rfc6749' | 'rfc6750'; 82 | verifyErrorLogLevel?: string; 83 | }>(pluginOptionsAuthPromise) 84 | 85 | fastify().register(bearerAuth, pluginOptions) 86 | fastify().register(bearerAuth, pluginOptionsAuthPromise) 87 | fastify().register(bearerAuth, pluginOptionsKeyArray) 88 | 89 | expectType(fastify().verifyBearerAuth) 90 | expectType(fastify().verifyBearerAuthFactory) 91 | 92 | expectType(FastifyBearerAuthErrors.FST_BEARER_AUTH_INVALID_KEYS_OPTION_TYPE) 93 | expectType(FastifyBearerAuthErrors.FST_BEARER_AUTH_INVALID_HOOK) 94 | expectType(FastifyBearerAuthErrors.FST_BEARER_AUTH_INVALID_LOG_LEVEL) 95 | expectType(FastifyBearerAuthErrors.FST_BEARER_AUTH_KEYS_OPTION_INVALID_KEY_TYPE) 96 | expectType(FastifyBearerAuthErrors.FST_BEARER_AUTH_INVALID_SPEC) 97 | expectType(FastifyBearerAuthErrors.FST_BEARER_AUTH_MISSING_AUTHORIZATION_HEADER) 98 | expectType(FastifyBearerAuthErrors.FST_BEARER_AUTH_INVALID_AUTHORIZATION_HEADER) 99 | --------------------------------------------------------------------------------