├── .gitattributes ├── .github ├── dependabot.yml ├── stale.yml └── workflows │ └── ci.yml ├── .gitignore ├── .npmrc ├── LICENSE ├── README.md ├── auth.js ├── eslint.config.js ├── package.json ├── test ├── auth.test.js ├── example-async.js ├── example-async.test.js ├── example-composited.js ├── example-composited.test.js ├── example.js └── example.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 | 154 | # test db 155 | authdb 156 | authdb-async 157 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017-2024 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/auth 2 | 3 | [![CI](https://github.com/fastify/fastify-auth/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/fastify/fastify-auth/actions/workflows/ci.yml) 4 | [![NPM version](https://img.shields.io/npm/v/@fastify/auth.svg?style=flat)](https://www.npmjs.com/package/@fastify/auth) 5 | [![neostandard javascript style](https://img.shields.io/badge/code_style-neostandard-brightgreen?style=flat)](https://github.com/neostandard/neostandard) 6 | 7 | This module does not provide an authentication strategy but offers a fast utility to handle authentication and multiple strategies in routes without adding overhead. 8 | See a complete example [here](test/example.js). 9 | 10 | ## Install 11 | ``` 12 | npm i @fastify/auth 13 | ``` 14 | 15 | ### Compatibility 16 | | Plugin version | Fastify version | 17 | | ---------------|-----------------| 18 | | `>=5.x` | `^5.x` | 19 | | `>=3.x <5.x` | `^4.x` | 20 | | `>=1.x <3.x` | `^3.x` | 21 | | `^0.x` | `^2.x` | 22 | | `^0.x` | `^1.x` | 23 | 24 | 25 | Please note that if a Fastify version is out of support, then so are the corresponding versions of this plugin 26 | in the table above. 27 | See [Fastify's LTS policy](https://github.com/fastify/fastify/blob/main/docs/Reference/LTS.md) for more details. 28 | 29 | ## Usage 30 | `@fastify/auth` does not provide an authentication strategy; authentication strategies must be provided using a decorator or another plugin. 31 | 32 | The following example provides a straightforward implementation to demonstrate the usage of this module: 33 | ```js 34 | fastify 35 | .decorate('verifyJWTandLevel', function (request, reply, done) { 36 | // your validation logic 37 | done() // pass an error if the authentication fails 38 | }) 39 | .decorate('verifyUserAndPassword', function (request, reply, done) { 40 | // your validation logic 41 | done() // pass an error if the authentication fails 42 | }) 43 | .register(require('@fastify/auth')) 44 | .after(() => { 45 | fastify.route({ 46 | method: 'POST', 47 | url: '/auth-multiple', 48 | preHandler: fastify.auth([ 49 | fastify.verifyJWTandLevel, 50 | fastify.verifyUserAndPassword 51 | ]), 52 | handler: (req, reply) => { 53 | req.log.info('Auth route') 54 | reply.send({ hello: 'world' }) 55 | } 56 | }) 57 | }) 58 | ``` 59 | 60 | The default relationship of these customized authentication strategies is `or`, but `and` can also be used: 61 | ```js 62 | fastify 63 | .decorate('verifyAdmin', function (request, reply, done) { 64 | // your validation logic 65 | done() // pass an error if the authentication fails 66 | }) 67 | .decorate('verifyReputation', function (request, reply, done) { 68 | // your validation logic 69 | done() // pass an error if the authentication fails 70 | }) 71 | .register(require('@fastify/auth')) 72 | .after(() => { 73 | fastify.route({ 74 | method: 'POST', 75 | url: '/auth-multiple', 76 | preHandler: fastify.auth([ 77 | fastify.verifyAdmin, 78 | fastify.verifyReputation 79 | ], { 80 | relation: 'and' 81 | }), 82 | handler: (req, reply) => { 83 | req.log.info('Auth route') 84 | reply.send({ hello: 'world' }) 85 | } 86 | }) 87 | }) 88 | ``` 89 | 90 | For composite authentication, such as verifying user passwords and levels or meeting VIP criteria, use nested arrays. 91 | For example, the logic [(verifyUserPassword `and` verifyLevel) `or` (verifyVIP)] can be achieved with the following code: 92 | ```js 93 | fastify 94 | .decorate('verifyUserPassword', function (request, reply, done) { 95 | // your validation logic 96 | done() // pass an error if the authentication fails 97 | }) 98 | .decorate('verifyLevel', function (request, reply, done) { 99 | // your validation logic 100 | done() // pass an error if the authentication fails 101 | }) 102 | .decorate('verifyVIP', function (request, reply, done) { 103 | // your validation logic 104 | done() // pass an error if the authentication fails 105 | }) 106 | .register(require('@fastify/auth')) 107 | .after(() => { 108 | fastify.route({ 109 | method: 'POST', 110 | url: '/auth-multiple', 111 | preHandler: fastify.auth([ 112 | [fastify.verifyUserPassword, fastify.verifyLevel], // The arrays within an array have the opposite relation to the main (default) relation. 113 | fastify.verifyVIP 114 | ], { 115 | relation: 'or' // default relation 116 | }), 117 | handler: (req, reply) => { 118 | req.log.info('Auth route') 119 | reply.send({ hello: 'world' }) 120 | } 121 | }) 122 | }) 123 | ``` 124 | 125 | If the `relation` (`defaultRelation`) parameter is `and`, then the relation inside sub-arrays will be `or`. 126 | If the `relation` (`defaultRelation`) parameter is `or`, then the relation inside sub-arrays will be `and`. 127 | 128 | | auth code | resulting logical expression | 129 | | ------------- |:-------------:| 130 | | `fastify.auth([f1, f2, [f3, f4]], { relation: 'or' })` | `f1 OR f2 OR (f3 AND f4)` | 131 | | `fastify.auth([f1, f2, [f3, f4]], { relation: 'and' })` | `f1 AND f2 AND (f3 OR f4)` | 132 | 133 | 134 | The `defaultRelation` option can be used while registering the plugin to change the default `relation`: 135 | ```js 136 | fastify.register(require('@fastify/auth'), { defaultRelation: 'and'} ) 137 | ``` 138 | 139 | _For more examples, please check [`example-composited.js`](test/example-composited.js)_ 140 | 141 | This plugin supports `callback`s and `Promise`s returned by functions. Note that an `async` function **does not have** to call the `done` parameter, otherwise, the route handler linked to the auth methods [might be called multiple times](https://fastify.dev/docs/latest/Reference/Hooks/#respond-to-a-request-from-a-hook): 142 | ```js 143 | fastify 144 | .decorate('asyncVerifyJWTandLevel', async function (request, reply) { 145 | // your async validation logic 146 | await validation() 147 | // throws an error if the authentication fails 148 | }) 149 | .decorate('asyncVerifyUserAndPassword', function (request, reply) { 150 | // return a promise that throws an error if the authentication fails 151 | return myPromiseValidation() 152 | }) 153 | .register(require('@fastify/auth')) 154 | .after(() => { 155 | fastify.route({ 156 | method: 'POST', 157 | url: '/auth-multiple', 158 | preHandler: fastify.auth([ 159 | fastify.asyncVerifyJWTandLevel, 160 | fastify.asyncVerifyUserAndPassword 161 | ]), 162 | handler: (req, reply) => { 163 | req.log.info('Auth route') 164 | reply.send({ hello: 'world' }) 165 | } 166 | }) 167 | }) 168 | ``` 169 | 170 | 171 | Route definition should be done as [a plugin](https://github.com/fastify/fastify/blob/main/docs/Reference/Plugins.md) or within an `.after()` callback. For a complete example, see [example.js](test/example.js). 172 | 173 | `@fastify/auth` runs all authentication methods, allowing the request to continue if at least one succeeds; otherwise, it returns an error to the client. 174 | Any successful authentication stops `@fastify/auth` from trying the rest unless the `run: 'all'` parameter is provided: 175 | ```js 176 | fastify.route({ 177 | method: 'GET', 178 | url: '/run-all', 179 | preHandler: fastify.auth([ 180 | (request, reply, done) => { console.log('executed 1'); done() }, 181 | (request, reply, done) => { console.log('executed 2'); done() }, 182 | (request, reply, done) => { console.log('executed 3'); done(new Error('you are not authenticated')) }, 183 | (request, reply, done) => { console.log('executed 4'); done() }, 184 | (request, reply, done) => { console.log('executed 5'); done(new Error('you shall not pass')) } 185 | ], { run: 'all' }), 186 | handler: (req, reply) => { reply.send({ hello: 'world' }) } 187 | }) 188 | ``` 189 | This example shows all console logs and always replies with `401: you are not authenticated`. 190 | The `run` parameter is useful for adding business data read from auth tokens to the request. 191 | 192 | 193 | This plugin can be used at the route level as in the above example or at the hook level using the `preHandler` hook: 194 | ```js 195 | fastify.addHook('preHandler', fastify.auth([ 196 | fastify.verifyJWTandLevel, 197 | fastify.verifyUserAndPassword 198 | ])) 199 | 200 | fastify.route({ 201 | method: 'POST', 202 | url: '/auth-multiple', 203 | handler: (req, reply) => { 204 | req.log.info('Auth route') 205 | reply.send({ hello: 'world' }) 206 | } 207 | }) 208 | ``` 209 | 210 | The difference between the two approaches is that using the route-level `preHandler` function runs authentication for the selected route only, while using the `preHandler` hook runs authentication for all routes in the current plugin and its descendants. 211 | 212 | ## Security Considerations 213 | 214 | ### Hook selection 215 | 216 | In the [Fastify Lifecycle](https://fastify.dev/docs/latest/Reference/Lifecycle/), the `onRequest` and `preParsing` stages do not parse the payload, unlike the `preHandler` stage. Parsing the body can be a potential security risk, as it can be used for denial of service (DoS) attacks. Therefore, it is recommended to avoid parsing the body for unauthorized access. 217 | 218 | Using the `@fastify/auth` plugin in the `preHandler` hook can result in unnecessary memory allocation if a malicious user sends a large payload in the request body and the request is unauthorized. Fastify will parse the body, even though the request is not authorized, leading to unnecessary memory allocation. To avoid this, use an `onRequest` or `preParsing` hook for authentication if the method does not require the request body, such as `@fastify/jwt`, which expects authentication in the request header. 219 | 220 | For authentication methods that require the request body, such as sending a token in the body, use the `preHandler` hook. 221 | 222 | In mixed cases, you must use the `preHandler` hook. 223 | 224 | ## API 225 | 226 | ### Options 227 | 228 | *@fastify/auth* accepts the options object: 229 | 230 | ```js 231 | { 232 | defaultRelation: 'and' 233 | } 234 | ``` 235 | 236 | + `defaultRelation` (Default: `or`): The default relation between the functions. It can be either `or` or `and`. 237 | 238 | ## Acknowledgments 239 | 240 | This project is kindly sponsored by: 241 | - [LetzDoIt](https://www.letzdoitapp.com/) 242 | 243 | ## License 244 | 245 | Licensed under [MIT](./LICENSE). 246 | -------------------------------------------------------------------------------- /auth.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const fp = require('fastify-plugin') 4 | const reusify = require('reusify') 5 | 6 | const DEFAULT_RELATION = 'or' 7 | 8 | function fastifyAuth (fastify, opts, next) { 9 | if (opts.defaultRelation && opts.defaultRelation !== 'or' && opts.defaultRelation !== 'and') { 10 | return next(new Error("The value of default relation should be one of ['or', 'and']")) 11 | } else if (!opts.defaultRelation) { 12 | opts.defaultRelation = DEFAULT_RELATION 13 | } 14 | 15 | fastify.decorate('auth', auth(opts)) 16 | next() 17 | } 18 | 19 | function auth (pluginOptions) { 20 | return function (functions, opts) { 21 | if (!Array.isArray(functions)) { 22 | throw new TypeError('You must give an array of functions to the auth function') 23 | } 24 | if (!functions.length) { 25 | throw new Error('Missing auth functions') 26 | } 27 | 28 | const options = Object.assign({ 29 | relation: pluginOptions.defaultRelation, 30 | run: null 31 | }, opts) 32 | 33 | if (options.relation !== 'or' && options.relation !== 'and') { 34 | throw new Error('The value of options.relation should be one of [\'or\', \'and\']') 35 | } 36 | if (options.run && options.run !== 'all') { 37 | throw new Error('The value of options.run must be \'all\'') 38 | } 39 | 40 | for (let i = 0; i < functions.length; i++) { 41 | if (Array.isArray(functions[i]) === false) { 42 | functions[i] = functions[i].bind(this) 43 | } else { 44 | for (let j = 0; j < functions[i].length; j++) { 45 | if (Array.isArray(functions[i][j])) { 46 | throw new TypeError('Nesting sub-arrays is not supported') 47 | } 48 | functions[i][j] = functions[i][j].bind(this) 49 | } 50 | } 51 | } 52 | 53 | const instance = reusify(Auth) 54 | 55 | function _auth (request, reply, done) { 56 | const obj = instance.get() 57 | 58 | obj.request = request 59 | obj.reply = reply 60 | obj.done = done 61 | obj.functions = this.functions 62 | obj.options = this.options 63 | obj.i = 0 64 | obj.j = 0 65 | obj.currentError = null 66 | obj.skipFurtherErrors = false 67 | obj.skipFurtherArrayErrors = false 68 | 69 | obj.nextAuth() 70 | } 71 | 72 | return _auth.bind({ functions, options }) 73 | 74 | function Auth () { 75 | this.next = null 76 | this.i = 0 77 | this.j = 0 78 | this.functions = [] 79 | this.options = {} 80 | this.request = null 81 | this.reply = null 82 | this.done = null 83 | this.currentError = null 84 | this.skipFurtherErrors = false 85 | this.skipFurtherArrayErrors = false 86 | 87 | const that = this 88 | 89 | this.nextAuth = function nextAuth (err) { 90 | if (!that.skipFurtherErrors) that.currentError = err 91 | 92 | const func = that.functions[that.i++] 93 | if (!func) { 94 | return that.completeAuth() 95 | } 96 | 97 | if (!Array.isArray(func)) { 98 | that.processAuth(func, (err) => { 99 | if (that.options.run !== 'all') that.currentError = err 100 | 101 | if (that.options.relation === 'and') { 102 | if (err && that.options.run !== 'all') { 103 | that.completeAuth() 104 | } else { 105 | if (err && that.options.run === 'all' && !that.skipFurtherErrors) { 106 | that.skipFurtherErrors = true 107 | that.currentError = err 108 | } 109 | that.nextAuth(err) 110 | } 111 | } else { 112 | if (!err && that.options.run !== 'all') { 113 | that.completeAuth() 114 | } else { 115 | if (!err && that.options.run === 'all') { 116 | that.skipFurtherErrors = true 117 | that.currentError = null 118 | } 119 | that.nextAuth(err) 120 | } 121 | } 122 | }) 123 | } else { 124 | that.j = 0 125 | that.skipFurtherArrayErrors = false 126 | that.processAuthArray(func, (err) => { 127 | if (that.options.relation === 'and') { // sub-array relation is OR 128 | if (!err && that.options.run !== 'all') { 129 | that.nextAuth(err) 130 | } else { 131 | that.currentError = err 132 | that.nextAuth(err) 133 | } 134 | } else { // sub-array relation is AND 135 | if (err && that.options.run !== 'all') { 136 | that.currentError = err 137 | that.nextAuth(err) 138 | } else { 139 | if (!err && that.options.run !== 'all') { 140 | that.currentError = null 141 | return that.completeAuth() 142 | } 143 | that.nextAuth(err) 144 | } 145 | } 146 | }) 147 | } 148 | } 149 | 150 | this.processAuthArray = function processAuthArray (funcs, callback, err) { 151 | const func = funcs[that.j++] 152 | if (!func) return callback(err) 153 | 154 | that.processAuth(func, (err) => { 155 | if (that.options.relation === 'and') { // sub-array relation is OR 156 | if (!err && that.options.run !== 'all') { 157 | callback(err) 158 | } else { 159 | if (!err && that.options.run === 'all') { 160 | that.skipFurtherArrayErrors = true 161 | } 162 | that.processAuthArray(funcs, callback, that.skipFurtherArrayErrors ? null : err) 163 | } 164 | } else { // sub-array relation is AND 165 | if (err && that.options.run !== 'all') callback(err) 166 | else that.processAuthArray(funcs, callback, err) 167 | } 168 | }) 169 | } 170 | 171 | this.processAuth = function processAuth (func, callback) { 172 | try { 173 | const maybePromise = func(that.request, that.reply, callback) 174 | 175 | if (maybePromise && typeof maybePromise.then === 'function') { 176 | maybePromise.then(() => callback(null), callback) 177 | } 178 | } catch (err) { 179 | callback(err) 180 | } 181 | } 182 | 183 | this.completeAuth = function () { 184 | if (that.currentError && (!that.reply.raw.statusCode || that.reply.raw.statusCode < 400)) { 185 | that.reply.code(401) 186 | } else if (!that.currentError && that.reply.raw.statusCode && that.reply.raw.statusCode >= 400) { 187 | that.reply.code(200) 188 | } 189 | 190 | that.done(that.currentError) 191 | instance.release(that) 192 | } 193 | } 194 | } 195 | } 196 | 197 | module.exports = fp(fastifyAuth, { 198 | fastify: '5.x', 199 | name: '@fastify/auth' 200 | }) 201 | module.exports.default = fastifyAuth 202 | module.exports.fastifyAuth = fastifyAuth 203 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = require('neostandard')({ 4 | ignores: require('neostandard').resolveIgnoresFromGitignore(), 5 | ts: true 6 | }) 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@fastify/auth", 3 | "version": "5.0.2", 4 | "description": "Run multiple auth functions in Fastify", 5 | "repository": { 6 | "type": "git", 7 | "url": "git+https://github.com/fastify/fastify-auth.git" 8 | }, 9 | "bugs": { 10 | "url": "https://github.com/fastify/fastify-auth/issues" 11 | }, 12 | "homepage": "https://github.com/fastify/fastify-auth#readme", 13 | "funding": [ 14 | { 15 | "type": "github", 16 | "url": "https://github.com/sponsors/fastify" 17 | }, 18 | { 19 | "type": "opencollective", 20 | "url": "https://opencollective.com/fastify" 21 | } 22 | ], 23 | "main": "auth.js", 24 | "type": "commonjs", 25 | "types": "types/index.d.ts", 26 | "scripts": { 27 | "clean": "rimraf authdb", 28 | "lint": "eslint", 29 | "lint:fix": "eslint --fix", 30 | "test": "npm run test:unit && npm run test:typescript", 31 | "test:typescript": "tsd", 32 | "test:unit": "c8 --100 node --test" 33 | }, 34 | "keywords": [ 35 | "fastify", 36 | "auth", 37 | "speed" 38 | ], 39 | "author": "Tomas Della Vedova - @delvedor (http://delved.org)", 40 | "contributors": [ 41 | { 42 | "name": "Matteo Collina", 43 | "email": "hello@matteocollina.com" 44 | }, 45 | { 46 | "name": "Manuel Spigolon", 47 | "email": "behemoth89@gmail.com" 48 | }, 49 | { 50 | "name": "Aras Abbasi", 51 | "email": "aras.abbasi@gmail.com" 52 | }, 53 | { 54 | "name": "Frazer Smith", 55 | "email": "frazer.dev@icloud.com", 56 | "url": "https://github.com/fdawgs" 57 | } 58 | ], 59 | "license": "MIT", 60 | "devDependencies": { 61 | "@fastify/jwt": "^9.0.0", 62 | "@fastify/leveldb": "^6.0.0", 63 | "@fastify/pre-commit": "^2.1.0", 64 | "@fastify/type-provider-json-schema-to-ts": "^5.0.0", 65 | "@fastify/type-provider-typebox": "^5.0.0", 66 | "@types/node": "^22.0.0", 67 | "c8": "^10.1.2", 68 | "eslint": "^9.17.0", 69 | "fastify": "^5.0.0", 70 | "neostandard": "^0.12.0", 71 | "rimraf": "^6.0.1", 72 | "tsd": "^0.32.0" 73 | }, 74 | "dependencies": { 75 | "fastify-plugin": "^5.0.0", 76 | "reusify": "^1.0.4" 77 | }, 78 | "publishConfig": { 79 | "access": "public" 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /test/auth.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | const Fastify = require('fastify') 5 | const fastifyAuth = require('../auth') 6 | 7 | test('registering plugin with invalid default relation', (t, done) => { 8 | t.plan(2) 9 | 10 | const fastify = Fastify() 11 | fastify.register(fastifyAuth, { defaultRelation: 'auth' }) 12 | 13 | fastify.ready((err) => { 14 | t.assert.ok(err) 15 | t.assert.strictEqual(err.message, 'The value of default relation should be one of [\'or\', \'and\']') 16 | done() 17 | }) 18 | }) 19 | 20 | test('Clean status code through auth pipeline', (t, done) => { 21 | t.plan(3) 22 | 23 | const app = Fastify() 24 | app.register(fastifyAuth) 25 | .after(() => { 26 | app.addHook('preHandler', app.auth([failWithCode('one', 501), failWithCode('two', 502)])) 27 | app.get('/', (_req, res) => res.send(42)) 28 | }) 29 | 30 | app.inject({ 31 | method: 'GET', 32 | url: '/', 33 | query: { 34 | name: 'two' 35 | } 36 | }, (err, res) => { 37 | t.assert.ifError(err) 38 | t.assert.strictEqual(res.payload, '42') 39 | t.assert.strictEqual(res.statusCode, 200) 40 | done() 41 | }) 42 | }) 43 | 44 | test('defaultRelation: used when relation not specified', async (t) => { 45 | t.plan(2) 46 | 47 | const app = Fastify() 48 | await app.register(fastifyAuth, { defaultRelation: 'or' }) 49 | 50 | app.route({ 51 | method: 'GET', 52 | url: '/welcome', 53 | preHandler: app.auth([successWithCode('one', 200), failWithCode('two', 502)]), 54 | handler: async (_req, reply) => { 55 | console.log('fawzihandler1') 56 | await reply.send({ hello: 'welcome' }) 57 | } 58 | }) 59 | 60 | app.route({ 61 | method: 'GET', 62 | url: '/bye', 63 | preHandler: app.auth([failWithCode('one', 503), successWithCode('two', 200)], { relation: 'or' }), 64 | handler: (_req, reply) => { 65 | reply.send({ hello: 'bye' }) 66 | } 67 | }) 68 | 69 | const response = await app.inject({ 70 | method: 'GET', 71 | url: '/welcome' 72 | }) 73 | t.assert.strictEqual(response.statusCode, 502) 74 | 75 | const res = await app.inject({ 76 | method: 'GET', 77 | url: '/bye', 78 | query: { 79 | name: 'two' 80 | } 81 | }) 82 | t.assert.strictEqual(res.statusCode, 200) 83 | }) 84 | 85 | test('Options: non-array functions input', (t, done) => { 86 | t.plan(4) 87 | 88 | const app = Fastify() 89 | app.register(fastifyAuth).after(() => { 90 | try { 91 | app.addHook('preHandler', app.auth('bogus')) 92 | app.get('/', (_req, res) => res.send(42)) 93 | } catch (error) { 94 | t.assert.ok(error) 95 | t.assert.strictEqual(error.message, 'You must give an array of functions to the auth function') 96 | } 97 | }) 98 | 99 | app.inject({ 100 | method: 'GET', 101 | url: '/' 102 | }, (err, res) => { 103 | t.assert.ifError(err) 104 | t.assert.strictEqual(res.statusCode, 404) 105 | done() 106 | }) 107 | }) 108 | 109 | test('Options: empty array functions input', (t, done) => { 110 | t.plan(4) 111 | 112 | const app = Fastify() 113 | app.register(fastifyAuth).after(() => { 114 | try { 115 | app.addHook('preHandler', app.auth([])) 116 | app.get('/', (_req, res) => res.send(42)) 117 | } catch (error) { 118 | t.assert.ok(error) 119 | t.assert.strictEqual(error.message, 'Missing auth functions') 120 | } 121 | }) 122 | 123 | app.inject({ 124 | method: 'GET', 125 | url: '/' 126 | }, (err, res) => { 127 | t.assert.ifError(err) 128 | t.assert.strictEqual(res.statusCode, 404) 129 | done() 130 | }) 131 | }) 132 | 133 | test('Options: faulty relation', (t, done) => { 134 | t.plan(4) 135 | 136 | const app = Fastify() 137 | app.register(fastifyAuth).after(() => { 138 | try { 139 | app.addHook('preHandler', app.auth([successWithCode('one', 201)], { relation: 'foo' })) 140 | app.get('/', (_req, res) => res.send(42)) 141 | } catch (error) { 142 | t.assert.ok(error) 143 | t.assert.strictEqual(error.message, 'The value of options.relation should be one of [\'or\', \'and\']') 144 | } 145 | }) 146 | 147 | app.inject({ 148 | method: 'GET', 149 | url: '/' 150 | }, (err, res) => { 151 | t.assert.ifError(err) 152 | t.assert.strictEqual(res.statusCode, 404) 153 | done() 154 | }) 155 | }) 156 | 157 | test('Options: faulty run', (t, done) => { 158 | t.plan(4) 159 | 160 | const app = Fastify() 161 | app.register(fastifyAuth).after(() => { 162 | try { 163 | app.addHook('preHandler', app.auth([successWithCode('one', 201)], { run: 'foo' })) 164 | app.get('/', (_req, res) => res.send(42)) 165 | } catch (error) { 166 | t.assert.ok(error) 167 | t.assert.strictEqual(error.message, 'The value of options.run must be \'all\'') 168 | } 169 | }) 170 | 171 | app.inject({ 172 | method: 'GET', 173 | url: '/' 174 | }, (err, res) => { 175 | t.assert.ifError(err) 176 | t.assert.strictEqual(res.statusCode, 404) 177 | done() 178 | }) 179 | }) 180 | 181 | test('Avoid status code overwriting', (t, done) => { 182 | t.plan(3) 183 | 184 | const app = Fastify() 185 | app.register(fastifyAuth) 186 | .after(() => { 187 | app.addHook('preHandler', app.auth([successWithCode('one', 201), successWithCode('two', 202)])) 188 | app.get('/', (_req, res) => res.send(42)) 189 | }) 190 | 191 | app.inject({ 192 | method: 'GET', 193 | url: '/', 194 | query: { 195 | name: 'two' 196 | } 197 | }, (err, res) => { 198 | t.assert.ifError(err) 199 | t.assert.strictEqual(res.payload, '42') 200 | t.assert.strictEqual(res.statusCode, 202) 201 | done() 202 | }) 203 | }) 204 | 205 | test('Last win when all failures', (t, done) => { 206 | t.plan(2) 207 | 208 | const app = Fastify() 209 | app.register(fastifyAuth) 210 | .after(() => { 211 | app.addHook('preHandler', app.auth([failWithCode('one', 501), failWithCode('two', 502)])) 212 | app.get('/', (_req, res) => res.send(42)) 213 | }) 214 | 215 | app.inject({ 216 | method: 'GET', 217 | url: '/', 218 | query: { 219 | name: 'three' 220 | } 221 | }, (err, res) => { 222 | t.assert.ifError(err) 223 | t.assert.strictEqual(res.statusCode, 502) 224 | done() 225 | }) 226 | }) 227 | 228 | test('First success win', (t, done) => { 229 | t.plan(2) 230 | 231 | const app = Fastify() 232 | app.register(fastifyAuth) 233 | .after(() => { 234 | app.addHook('preHandler', app.auth([successWithCode('one', 201), successWithCode('two', 202)])) 235 | app.get('/', (_req, res) => res.send(42)) 236 | }) 237 | 238 | app.inject({ 239 | method: 'GET', 240 | url: '/', 241 | query: { 242 | name: 'two' 243 | } 244 | }, (err, res) => { 245 | t.assert.ifError(err) 246 | t.assert.strictEqual(res.statusCode, 202) 247 | done() 248 | }) 249 | }) 250 | 251 | function failWithCode (id, status) { 252 | return function (request, reply, done) { 253 | if (request.query.name === id) { 254 | done() 255 | } else { 256 | reply.code(status) 257 | done(new Error('query ' + id)) 258 | } 259 | } 260 | } 261 | 262 | function successWithCode (id, status) { 263 | return function (request, reply, done) { 264 | if (request.query.name === id) { 265 | reply.code(status) 266 | done() 267 | } else { 268 | done(new Error('query ' + id)) 269 | } 270 | } 271 | } 272 | -------------------------------------------------------------------------------- /test/example-async.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Fastify = require('fastify') 4 | 5 | function build (opts) { 6 | const fastify = Fastify(opts) 7 | 8 | fastify.register(require('@fastify/jwt'), { secret: 'supersecret' }) 9 | fastify.register(require('@fastify/leveldb'), { name: 'authdb-async' }) 10 | fastify.register(require('../auth')) 11 | fastify.register(routes) 12 | 13 | fastify.decorate('verifyJWTandLevel', verifyJWTandLevel) 14 | fastify.decorate('verifyUserAndPassword', verifyUserAndPassword) 15 | 16 | function verifyJWTandLevel (request, reply) { 17 | const jwt = this.jwt 18 | const level = this.level['authdb-async'] 19 | 20 | if (request.body?.failureWithReply) { 21 | reply.code(401).send({ error: 'Unauthorized' }) 22 | return Promise.reject(new Error()) 23 | } 24 | 25 | if (!request.raw.headers.auth) { 26 | return Promise.reject(new Error('Missing token header')) 27 | } 28 | 29 | return new Promise(function (resolve, reject) { 30 | jwt.verify(request.raw.headers.auth, function (err, decoded) { 31 | if (err) { return reject(err) }; 32 | resolve(decoded) 33 | }) 34 | }).then(function (decoded) { 35 | return level.get(decoded.user) 36 | .then(function (password) { 37 | if (!password || password !== decoded.password) { 38 | throw new Error('Token not valid') 39 | } 40 | }) 41 | }).catch(function (error) { 42 | request.log.error(error) 43 | throw new Error('Token not valid') 44 | }) 45 | } 46 | 47 | function verifyUserAndPassword (request, _reply, done) { 48 | const level = this.level['authdb-async'] 49 | 50 | level.get(request.body.user, onUser) 51 | 52 | function onUser (err, password) { 53 | if (err) { 54 | if (err.notFound) { 55 | return done(new Error('Password not valid')) 56 | } 57 | return done(err) 58 | } 59 | 60 | if (!password || password !== request.body.password) { 61 | return done(new Error('Password not valid')) 62 | } 63 | 64 | done() 65 | } 66 | } 67 | 68 | async function routes (fastify) { 69 | fastify.route({ 70 | method: 'POST', 71 | url: '/register', 72 | schema: { 73 | body: { 74 | type: 'object', 75 | properties: { 76 | user: { type: 'string' }, 77 | password: { type: 'string' } 78 | }, 79 | required: ['user', 'password'] 80 | } 81 | }, 82 | handler: (req, reply) => { 83 | req.log.info('Creating new user') 84 | fastify.level['authdb-async'].put(req.body.user, req.body.password, onPut) 85 | 86 | function onPut (err) { 87 | if (err) return reply.send(err) 88 | fastify.jwt.sign(req.body, onToken) 89 | } 90 | 91 | function onToken (err, token) { 92 | if (err) return reply.send(err) 93 | req.log.info('User created') 94 | reply.send({ token }) 95 | } 96 | } 97 | }) 98 | 99 | fastify.route({ 100 | method: 'GET', 101 | url: '/no-auth', 102 | handler: (req, reply) => { 103 | req.log.info('Auth free route') 104 | reply.send({ hello: 'world' }) 105 | } 106 | }) 107 | 108 | fastify.route({ 109 | method: 'GET', 110 | url: '/auth', 111 | preHandler: fastify.auth([fastify.verifyJWTandLevel]), 112 | handler: (req, reply) => { 113 | req.log.info('Auth route') 114 | reply.send({ hello: 'world' }) 115 | } 116 | }) 117 | 118 | fastify.route({ 119 | method: 'POST', 120 | url: '/auth-multiple', 121 | preHandler: fastify.auth([ 122 | fastify.verifyJWTandLevel, 123 | fastify.verifyUserAndPassword 124 | ]), 125 | handler: (req, reply) => { 126 | req.log.info('Auth route') 127 | reply.send({ hello: 'world' }) 128 | } 129 | }) 130 | } 131 | 132 | return fastify 133 | } 134 | 135 | module.exports = build 136 | -------------------------------------------------------------------------------- /test/example-async.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | const { rimrafSync } = require('rimraf') 5 | const build = require('./example-async') 6 | 7 | let fastify = null 8 | let token = null 9 | 10 | test.before(() => { 11 | rimrafSync('./authdb') 12 | fastify = build() 13 | }) 14 | 15 | test.after(async () => { 16 | await fastify.close() 17 | rimrafSync('./authdb') 18 | }) 19 | 20 | test('Route without auth', (t, done) => { 21 | t.plan(2) 22 | 23 | fastify.inject({ 24 | method: 'GET', 25 | url: '/no-auth' 26 | }, (err, res) => { 27 | t.assert.ifError(err) 28 | const payload = JSON.parse(res.payload) 29 | t.assert.deepStrictEqual(payload, { hello: 'world' }) 30 | done() 31 | }) 32 | }) 33 | 34 | test('Missing header', (t, done) => { 35 | t.plan(2) 36 | 37 | fastify.inject({ 38 | method: 'GET', 39 | url: '/auth', 40 | headers: {} 41 | }, (err, res) => { 42 | t.assert.ifError(err) 43 | const payload = JSON.parse(res.payload) 44 | t.assert.deepStrictEqual(payload, { 45 | error: 'Unauthorized', 46 | message: 'Missing token header', 47 | statusCode: 401 48 | }) 49 | done() 50 | }) 51 | }) 52 | 53 | test('Register user', (t, done) => { 54 | t.plan(3) 55 | 56 | fastify.inject({ 57 | method: 'POST', 58 | url: '/register', 59 | payload: { 60 | user: 'tomas', 61 | password: 'a-very-secure-one' 62 | } 63 | }, (err, res) => { 64 | t.assert.ifError(err) 65 | const payload = JSON.parse(res.payload) 66 | t.assert.strictEqual(res.statusCode, 200) 67 | token = payload.token 68 | t.assert.strictEqual(typeof payload.token, 'string') 69 | done() 70 | }) 71 | }) 72 | 73 | test('Auth successful', (t, done) => { 74 | t.plan(2) 75 | 76 | fastify.inject({ 77 | method: 'GET', 78 | url: '/auth', 79 | headers: { 80 | auth: token 81 | } 82 | }, (err, res) => { 83 | t.assert.ifError(err) 84 | const payload = JSON.parse(res.payload) 85 | t.assert.deepStrictEqual(payload, { hello: 'world' }) 86 | done() 87 | }) 88 | }) 89 | 90 | test('Auth successful (multiple)', (t, done) => { 91 | t.plan(2) 92 | 93 | fastify.inject({ 94 | method: 'POST', 95 | url: '/auth-multiple', 96 | payload: { 97 | user: 'tomas', 98 | password: 'a-very-secure-one' 99 | } 100 | }, (err, res) => { 101 | t.assert.ifError(err) 102 | const payload = JSON.parse(res.payload) 103 | t.assert.deepStrictEqual(payload, { hello: 'world' }) 104 | done() 105 | }) 106 | }) 107 | 108 | test('Auth not successful', (t, done) => { 109 | t.plan(2) 110 | 111 | fastify.inject({ 112 | method: 'GET', 113 | url: '/auth', 114 | headers: { 115 | auth: 'the winter is coming' 116 | } 117 | }, (err, res) => { 118 | t.assert.ifError(err) 119 | const payload = JSON.parse(res.payload) 120 | t.assert.deepStrictEqual(payload, { 121 | error: 'Unauthorized', 122 | message: 'Token not valid', 123 | statusCode: 401 124 | }) 125 | done() 126 | }) 127 | }) 128 | 129 | test('Auth not successful (multiple)', (t, done) => { 130 | t.plan(2) 131 | 132 | fastify.inject({ 133 | method: 'POST', 134 | url: '/auth-multiple', 135 | payload: { 136 | user: 'tomas', 137 | password: 'wrong!' 138 | } 139 | }, (err, res) => { 140 | t.assert.ifError(err) 141 | const payload = JSON.parse(res.payload) 142 | t.assert.deepStrictEqual(payload, { 143 | error: 'Unauthorized', 144 | message: 'Password not valid', 145 | statusCode: 401 146 | }) 147 | done() 148 | }) 149 | }) 150 | 151 | test('Failure with explicit reply', (t, done) => { 152 | t.plan(3) 153 | 154 | fastify.inject({ 155 | method: 'POST', 156 | url: '/auth-multiple', 157 | payload: { 158 | failureWithReply: true, 159 | user: 'tomas', 160 | password: 'wrong!' 161 | } 162 | }, (err, res) => { 163 | t.assert.ifError(err) 164 | const payload = JSON.parse(res.payload) 165 | t.assert.strictEqual(res.statusCode, 401) 166 | t.assert.deepStrictEqual(payload, { error: 'Unauthorized' }) 167 | done() 168 | }) 169 | }) 170 | -------------------------------------------------------------------------------- /test/example-composited.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Fastify = require('fastify') 4 | 5 | function build (opts) { 6 | const fastify = Fastify(opts) 7 | 8 | fastify 9 | .register(require('../auth')) 10 | .after(routes) 11 | 12 | fastify.decorate('verifyNumber', verifyNumber) 13 | fastify.decorate('verifyOdd', verifyOdd) 14 | fastify.decorate('verifyBig', verifyBig) 15 | fastify.decorate('verifyOddAsync', verifyOddAsync) 16 | fastify.decorate('verifyBigAsync', verifyBigAsync) 17 | 18 | function verifyNumber (request, _reply, done) { 19 | const n = request.body.n 20 | 21 | if (typeof (n) !== 'number') { 22 | request.number = false 23 | return done(new Error('type of `n` is not `number`')) 24 | } 25 | 26 | request.number = true 27 | return done() 28 | } 29 | 30 | function verifyOdd (request, _reply, done) { 31 | const n = request.body.n 32 | 33 | if (typeof (n) !== 'number' || n % 2 === 0) { 34 | request.odd = false 35 | return done(new Error('`n` is not odd')) 36 | } 37 | 38 | request.odd = true 39 | return done() 40 | } 41 | 42 | function verifyBig (request, _reply, done) { 43 | const n = request.body.n 44 | 45 | if (typeof (n) !== 'number' || n < 100) { 46 | request.big = false 47 | return done(new Error('`n` is not big')) 48 | } 49 | 50 | request.big = true 51 | return done() 52 | } 53 | 54 | function verifyOddAsync (request, reply) { 55 | request.verifyOddAsyncCalled = true 56 | return new Promise((resolve, reject) => { 57 | verifyOdd(request, reply, (err) => { 58 | if (err) reject(err) 59 | resolve() 60 | }) 61 | }) 62 | } 63 | 64 | function verifyBigAsync (request, reply) { 65 | request.verifyBigAsyncCalled = true 66 | return new Promise((resolve, reject) => { 67 | verifyBig(request, reply, (err) => { 68 | if (err) reject(err) 69 | resolve() 70 | }) 71 | }) 72 | } 73 | 74 | function routes () { 75 | fastify.route({ 76 | method: 'GET', 77 | url: '/', 78 | handler: (_req, reply) => { 79 | reply.send({ hello: 'world' }) 80 | } 81 | }) 82 | 83 | fastify.route({ 84 | method: 'POST', 85 | url: '/checkand', 86 | preHandler: fastify.auth([fastify.verifyNumber, fastify.verifyOdd], { relation: 'and' }), 87 | handler: (req, reply) => { 88 | req.log.info('Auth route') 89 | reply.send({ hello: 'world' }) 90 | } 91 | }) 92 | 93 | fastify.route({ 94 | method: 'POST', 95 | url: '/checkarrayand', 96 | preHandler: fastify.auth([[fastify.verifyNumber], [fastify.verifyOdd]], { relation: 'and' }), 97 | handler: (req, reply) => { 98 | req.log.info('Auth route') 99 | reply.send({ hello: 'world' }) 100 | } 101 | }) 102 | 103 | fastify.route({ 104 | method: 'POST', 105 | url: '/check-composite-and', 106 | preHandler: fastify.auth([fastify.verifyNumber, [fastify.verifyOdd, fastify.verifyBig]], { relation: 'and' }), 107 | handler: (req, reply) => { 108 | req.log.info('Auth route') 109 | reply.send({ hello: 'world' }) 110 | } 111 | }) 112 | 113 | fastify.route({ 114 | method: 'POST', 115 | url: '/check-composite-and-async', 116 | preHandler: fastify.auth([fastify.verifyNumber, [fastify.verifyOddAsync, fastify.verifyBigAsync]], { relation: 'and' }), 117 | handler: (req, reply) => { 118 | req.log.info('Auth route') 119 | reply.send({ hello: 'world' }) 120 | } 121 | }) 122 | 123 | fastify.route({ 124 | method: 'POST', 125 | url: '/check-composite-and-run-all', 126 | preHandler: fastify.auth([fastify.verifyNumber, [fastify.verifyOdd, fastify.verifyBig]], { relation: 'and', run: 'all' }), 127 | handler: (req, reply) => { 128 | req.log.info('Auth route') 129 | reply.send({ 130 | odd: req.odd, 131 | big: req.big, 132 | number: req.number 133 | }) 134 | } 135 | }) 136 | 137 | fastify.route({ 138 | method: 'POST', 139 | url: '/check-composite-or', 140 | preHandler: fastify.auth([fastify.verifyNumber, [fastify.verifyOdd, fastify.verifyBig]], { relation: 'or' }), 141 | handler: (req, reply) => { 142 | req.log.info('Auth route') 143 | reply.send({ hello: 'world' }) 144 | } 145 | }) 146 | 147 | fastify.route({ 148 | method: 'POST', 149 | url: '/check-two-sub-arrays-or', 150 | preHandler: fastify.auth([[fastify.verifyBigAsync], [fastify.verifyOddAsync]], { relation: 'or' }), 151 | handler: (req, reply) => { 152 | req.log.info('Auth route') 153 | reply.send({ hello: 'world' }) 154 | } 155 | }) 156 | 157 | fastify.route({ 158 | method: 'POST', 159 | url: '/check-two-sub-arrays-or-2', 160 | preHandler: fastify.auth([[fastify.verifyBigAsync], [fastify.verifyOddAsync]], { relation: 'or' }), 161 | handler: (req, reply) => { 162 | req.log.info('Auth route') 163 | reply.send({ 164 | verifyBigAsyncCalled: !!req.verifyBigAsyncCalled, 165 | verifyOddAsyncCalled: !!req.verifyOddAsyncCalled 166 | }) 167 | } 168 | }) 169 | 170 | fastify.route({ 171 | method: 'POST', 172 | url: '/check-composite-or-async', 173 | preHandler: fastify.auth([fastify.verifyNumber, [fastify.verifyOddAsync, fastify.verifyBigAsync]], { relation: 'or' }), 174 | handler: (req, reply) => { 175 | req.log.info('Auth route') 176 | reply.send({ hello: 'world' }) 177 | } 178 | }) 179 | 180 | fastify.route({ 181 | method: 'POST', 182 | url: '/check-composite-or-run-all', 183 | preHandler: fastify.auth([fastify.verifyNumber, [fastify.verifyOdd, fastify.verifyBig]], { relation: 'or', run: 'all' }), 184 | handler: (req, reply) => { 185 | req.log.info('Auth route') 186 | reply.send({ 187 | odd: req.odd, 188 | big: req.big, 189 | number: req.number 190 | }) 191 | } 192 | }) 193 | 194 | fastify.route({ 195 | method: 'POST', 196 | url: '/checkor', 197 | preHandler: fastify.auth([fastify.verifyOdd, fastify.verifyBig]), 198 | handler: (req, reply) => { 199 | req.log.info('Auth route') 200 | reply.send({ hello: 'world' }) 201 | } 202 | }) 203 | 204 | fastify.route({ 205 | method: 'POST', 206 | url: '/checkarrayor', 207 | preHandler: fastify.auth([[fastify.verifyOdd], [fastify.verifyBig]]), 208 | handler: (req, reply) => { 209 | req.log.info('Auth route') 210 | reply.send({ hello: 'world' }) 211 | } 212 | }) 213 | 214 | fastify.route({ 215 | method: 'POST', 216 | url: '/singleor', 217 | preHandler: fastify.auth([fastify.verifyOdd]), 218 | handler: (req, reply) => { 219 | req.log.info('Auth route') 220 | reply.send({ hello: 'world' }) 221 | } 222 | }) 223 | 224 | fastify.route({ 225 | method: 'POST', 226 | url: '/singlearrayor', 227 | preHandler: fastify.auth([[fastify.verifyOdd]]), 228 | handler: (req, reply) => { 229 | req.log.info('Auth route') 230 | reply.send({ hello: 'world' }) 231 | } 232 | }) 233 | 234 | fastify.route({ 235 | method: 'POST', 236 | url: '/singleand', 237 | preHandler: fastify.auth([fastify.verifyOdd], { relation: 'and' }), 238 | handler: (req, reply) => { 239 | req.log.info('Auth route') 240 | reply.send({ hello: 'world' }) 241 | } 242 | }) 243 | 244 | fastify.route({ 245 | method: 'POST', 246 | url: '/singlearrayand', 247 | preHandler: fastify.auth([[fastify.verifyOdd]], { relation: 'and' }), 248 | handler: (req, reply) => { 249 | req.log.info('Auth route') 250 | reply.send({ hello: 'world' }) 251 | } 252 | }) 253 | 254 | fastify.route({ 255 | method: 'POST', 256 | url: '/singlearraycheckand', 257 | preHandler: fastify.auth([[fastify.verifyNumber, fastify.verifyOdd]]), 258 | handler: (req, reply) => { 259 | req.log.info('Auth route') 260 | reply.send({ hello: 'world' }) 261 | } 262 | }) 263 | 264 | fastify.route({ 265 | method: 'POST', 266 | url: '/checkarrayorsingle', 267 | preHandler: fastify.auth([[fastify.verifyNumber, fastify.verifyOdd], fastify.verifyBig]), 268 | handler: (req, reply) => { 269 | req.log.info('Auth route') 270 | reply.send({ hello: 'world' }) 271 | } 272 | }) 273 | 274 | fastify.route({ 275 | method: 'POST', 276 | url: '/run-all-or', 277 | preHandler: fastify.auth([fastify.verifyOdd, fastify.verifyBig, fastify.verifyNumber], { run: 'all' }), 278 | handler: (req, reply) => { 279 | req.log.info('Auth route') 280 | reply.send({ 281 | odd: req.odd, 282 | big: req.big, 283 | number: req.number 284 | }) 285 | } 286 | }) 287 | 288 | fastify.route({ 289 | method: 'POST', 290 | url: '/run-all-and', 291 | preHandler: fastify.auth([fastify.verifyOdd, fastify.verifyBig, fastify.verifyNumber], { run: 'all', relation: 'and' }), 292 | handler: (req, reply) => { 293 | req.log.info('Auth route') 294 | reply.send({ 295 | odd: req.odd, 296 | big: req.big, 297 | number: req.number 298 | }) 299 | } 300 | }) 301 | } 302 | 303 | return fastify 304 | } 305 | 306 | module.exports = build 307 | -------------------------------------------------------------------------------- /test/example-composited.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | const build = require('./example-composited') 5 | 6 | let fastify = null 7 | 8 | test.after(async () => { 9 | await fastify.close() 10 | }) 11 | 12 | test.before(() => { 13 | fastify = build() 14 | }) 15 | 16 | test('And Relation success for single case', (t, done) => { 17 | t.plan(2) 18 | 19 | fastify.inject({ 20 | method: 'POST', 21 | url: '/singleand', 22 | payload: { 23 | n: 11 24 | } 25 | }, (err, res) => { 26 | t.assert.ifError(err) 27 | const payload = JSON.parse(res.payload) 28 | t.assert.deepStrictEqual(payload, { hello: 'world' }) 29 | done() 30 | }) 31 | }) 32 | 33 | test('And Relation failed for single case', (t, done) => { 34 | t.plan(2) 35 | 36 | fastify.inject({ 37 | method: 'POST', 38 | url: '/singleand', 39 | payload: { 40 | n: 10 41 | } 42 | }, (err, res) => { 43 | t.assert.ifError(err) 44 | const payload = JSON.parse(res.payload) 45 | t.assert.deepStrictEqual(payload, { 46 | error: 'Unauthorized', 47 | message: '`n` is not odd', 48 | statusCode: 401 49 | }) 50 | done() 51 | }) 52 | }) 53 | 54 | test('And Relation sucess for single [Array] case', (t, done) => { 55 | t.plan(2) 56 | 57 | fastify.inject({ 58 | method: 'POST', 59 | url: '/singlearrayand', 60 | payload: { 61 | n: 11 62 | } 63 | }, (err, res) => { 64 | t.assert.ifError(err) 65 | const payload = JSON.parse(res.payload) 66 | t.assert.deepStrictEqual(payload, { hello: 'world' }) 67 | done() 68 | }) 69 | }) 70 | 71 | test('And Relation failed for single [Array] case', (t, done) => { 72 | t.plan(2) 73 | 74 | fastify.inject({ 75 | method: 'POST', 76 | url: '/singlearrayand', 77 | payload: { 78 | n: 10 79 | } 80 | }, (err, res) => { 81 | t.assert.ifError(err) 82 | const payload = JSON.parse(res.payload) 83 | t.assert.deepStrictEqual(payload, { 84 | error: 'Unauthorized', 85 | message: '`n` is not odd', 86 | statusCode: 401 87 | }) 88 | done() 89 | }) 90 | }) 91 | 92 | test('Or Relation success for single case', (t, done) => { 93 | t.plan(2) 94 | 95 | fastify.inject({ 96 | method: 'POST', 97 | url: '/singleor', 98 | payload: { 99 | n: 11 100 | } 101 | }, (err, res) => { 102 | t.assert.ifError(err) 103 | const payload = JSON.parse(res.payload) 104 | t.assert.deepStrictEqual(payload, { hello: 'world' }) 105 | done() 106 | }) 107 | }) 108 | 109 | test('Or Relation failed for single case', (t, done) => { 110 | t.plan(2) 111 | 112 | fastify.inject({ 113 | method: 'POST', 114 | url: '/singleor', 115 | payload: { 116 | n: 10 117 | } 118 | }, (err, res) => { 119 | t.assert.ifError(err) 120 | const payload = JSON.parse(res.payload) 121 | t.assert.deepStrictEqual(payload, { 122 | error: 'Unauthorized', 123 | message: '`n` is not odd', 124 | statusCode: 401 125 | }) 126 | done() 127 | }) 128 | }) 129 | 130 | test('Or Relation success for single [Array] case', (t, done) => { 131 | t.plan(2) 132 | 133 | fastify.inject({ 134 | method: 'POST', 135 | url: '/singlearrayor', 136 | payload: { 137 | n: 11 138 | } 139 | }, (err, res) => { 140 | t.assert.ifError(err) 141 | const payload = JSON.parse(res.payload) 142 | t.assert.deepStrictEqual(payload, { hello: 'world' }) 143 | done() 144 | }) 145 | }) 146 | 147 | test('Or Relation failed for single [Array] case', (t, done) => { 148 | t.plan(2) 149 | 150 | fastify.inject({ 151 | method: 'POST', 152 | url: '/singlearrayor', 153 | payload: { 154 | n: 10 155 | } 156 | }, (err, res) => { 157 | t.assert.ifError(err) 158 | const payload = JSON.parse(res.payload) 159 | t.assert.deepStrictEqual(payload, { 160 | error: 'Unauthorized', 161 | message: '`n` is not odd', 162 | statusCode: 401 163 | }) 164 | done() 165 | }) 166 | }) 167 | 168 | test('And Relation failed for first check', (t, done) => { 169 | t.plan(2) 170 | 171 | fastify.inject({ 172 | method: 'POST', 173 | url: '/checkand', 174 | payload: { 175 | n: 'tomas' 176 | } 177 | }, (err, res) => { 178 | t.assert.ifError(err) 179 | const payload = JSON.parse(res.payload) 180 | t.assert.deepStrictEqual(payload, { 181 | error: 'Unauthorized', 182 | message: 'type of `n` is not `number`', 183 | statusCode: 401 184 | }) 185 | done() 186 | }) 187 | }) 188 | 189 | test('And Relation failed for first check', (t, done) => { 190 | t.plan(2) 191 | 192 | fastify.inject({ 193 | method: 'POST', 194 | url: '/checkand', 195 | payload: { 196 | m: 11 197 | } 198 | }, (err, res) => { 199 | t.assert.ifError(err) 200 | const payload = JSON.parse(res.payload) 201 | t.assert.deepStrictEqual(payload, { 202 | error: 'Unauthorized', 203 | message: 'type of `n` is not `number`', 204 | statusCode: 401 205 | }) 206 | done() 207 | }) 208 | }) 209 | 210 | test('And Relation failed for second check', (t, done) => { 211 | t.plan(2) 212 | 213 | fastify.inject({ 214 | method: 'POST', 215 | url: '/checkand', 216 | payload: { 217 | n: 10 218 | } 219 | }, (err, res) => { 220 | t.assert.ifError(err) 221 | const payload = JSON.parse(res.payload) 222 | t.assert.deepStrictEqual(payload, { 223 | error: 'Unauthorized', 224 | message: '`n` is not odd', 225 | statusCode: 401 226 | }) 227 | done() 228 | }) 229 | }) 230 | 231 | test('And Relation success', (t, done) => { 232 | t.plan(3) 233 | 234 | fastify.inject({ 235 | method: 'POST', 236 | url: '/checkand', 237 | payload: { 238 | n: 11 239 | } 240 | }, (err, res) => { 241 | t.assert.ifError(err) 242 | const payload = JSON.parse(res.payload) 243 | t.assert.deepStrictEqual(payload, { hello: 'world' }) 244 | t.assert.strictEqual(res.statusCode, 200) 245 | done() 246 | }) 247 | }) 248 | 249 | test('[Array] notation And Relation success', (t, done) => { 250 | t.plan(3) 251 | 252 | fastify.inject({ 253 | method: 'POST', 254 | url: '/checkarrayand', 255 | payload: { 256 | n: 11 257 | } 258 | }, (err, res) => { 259 | t.assert.ifError(err) 260 | const payload = JSON.parse(res.payload) 261 | t.assert.deepStrictEqual(payload, { hello: 'world' }) 262 | t.assert.strictEqual(res.statusCode, 200) 263 | done() 264 | }) 265 | }) 266 | 267 | test('And Relation with Or relation inside sub-array success', (t, done) => { 268 | t.plan(3) 269 | 270 | fastify.inject({ 271 | method: 'POST', 272 | url: '/check-composite-and', 273 | payload: { 274 | n: 11 275 | } 276 | }, (err, res) => { 277 | t.assert.ifError(err) 278 | const payload = JSON.parse(res.payload) 279 | t.assert.deepStrictEqual(payload, { hello: 'world' }) 280 | t.assert.strictEqual(res.statusCode, 200) 281 | done() 282 | }) 283 | }) 284 | 285 | test('And Relation with Or relation inside sub-array failed', (t, done) => { 286 | t.plan(2) 287 | 288 | fastify.inject({ 289 | method: 'POST', 290 | url: '/check-composite-and', 291 | payload: { 292 | n: 4 293 | } 294 | }, (err, res) => { 295 | t.assert.ifError(err) 296 | const payload = JSON.parse(res.payload) 297 | t.assert.deepStrictEqual(payload, { 298 | error: 'Unauthorized', 299 | message: '`n` is not big', 300 | statusCode: 401 301 | }) 302 | done() 303 | }) 304 | }) 305 | 306 | test('And Relation with Or relation inside sub-array with async functions success', (t, done) => { 307 | t.plan(3) 308 | 309 | fastify.inject({ 310 | method: 'POST', 311 | url: '/check-composite-and-async', 312 | payload: { 313 | n: 11 314 | } 315 | }, (err, res) => { 316 | t.assert.ifError(err) 317 | const payload = JSON.parse(res.payload) 318 | t.assert.deepStrictEqual(payload, { hello: 'world' }) 319 | t.assert.strictEqual(res.statusCode, 200) 320 | done() 321 | }) 322 | }) 323 | 324 | test('And Relation with Or relation inside sub-array with async functions failed', (t, done) => { 325 | t.plan(2) 326 | 327 | fastify.inject({ 328 | method: 'POST', 329 | url: '/check-composite-and-async', 330 | payload: { 331 | n: 4 332 | } 333 | }, (err, res) => { 334 | t.assert.ifError(err) 335 | const payload = JSON.parse(res.payload) 336 | t.assert.deepStrictEqual(payload, { 337 | error: 'Unauthorized', 338 | message: '`n` is not big', 339 | statusCode: 401 340 | }) 341 | done() 342 | }) 343 | }) 344 | 345 | test('Or Relation success under first case', (t, done) => { 346 | t.plan(3) 347 | 348 | fastify.inject({ 349 | method: 'POST', 350 | url: '/checkor', 351 | payload: { 352 | n: 1 353 | } 354 | }, (err, res) => { 355 | t.assert.ifError(err) 356 | const payload = JSON.parse(res.payload) 357 | t.assert.deepStrictEqual(payload, { hello: 'world' }) 358 | t.assert.strictEqual(res.statusCode, 200) 359 | done() 360 | }) 361 | }) 362 | 363 | test('[Array] notation Or Relation success under first case', (t, done) => { 364 | t.plan(3) 365 | 366 | fastify.inject({ 367 | method: 'POST', 368 | url: '/checkarrayor', 369 | payload: { 370 | n: 1 371 | } 372 | }, (err, res) => { 373 | t.assert.ifError(err) 374 | const payload = JSON.parse(res.payload) 375 | t.assert.deepStrictEqual(payload, { hello: 'world' }) 376 | t.assert.strictEqual(res.statusCode, 200) 377 | done() 378 | }) 379 | }) 380 | 381 | test('Or Relation success under second case', (t, done) => { 382 | t.plan(3) 383 | 384 | fastify.inject({ 385 | method: 'POST', 386 | url: '/checkor', 387 | payload: { 388 | n: 200 389 | } 390 | }, (err, res) => { 391 | t.assert.ifError(err) 392 | const payload = JSON.parse(res.payload) 393 | t.assert.deepStrictEqual(payload, { hello: 'world' }) 394 | t.assert.strictEqual(res.statusCode, 200) 395 | done() 396 | }) 397 | }) 398 | 399 | test('[Array] notation Or Relation success under second case', (t, done) => { 400 | t.plan(3) 401 | 402 | fastify.inject({ 403 | method: 'POST', 404 | url: '/checkarrayor', 405 | payload: { 406 | n: 200 407 | } 408 | }, (err, res) => { 409 | t.assert.ifError(err) 410 | const payload = JSON.parse(res.payload) 411 | t.assert.deepStrictEqual(payload, { hello: 'world' }) 412 | t.assert.strictEqual(res.statusCode, 200) 413 | done() 414 | }) 415 | }) 416 | 417 | test('Or Relation failed for both case', (t, done) => { 418 | t.plan(2) 419 | 420 | fastify.inject({ 421 | method: 'POST', 422 | url: '/checkor', 423 | payload: { 424 | n: 90 425 | } 426 | }, (err, res) => { 427 | t.assert.ifError(err) 428 | const payload = JSON.parse(res.payload) 429 | t.assert.deepStrictEqual(payload, { 430 | error: 'Unauthorized', 431 | message: '`n` is not big', 432 | statusCode: 401 433 | }) 434 | done() 435 | }) 436 | }) 437 | 438 | test('[Array] notation Or Relation failed for both case', (t, done) => { 439 | t.plan(2) 440 | 441 | fastify.inject({ 442 | method: 'POST', 443 | url: '/checkarrayor', 444 | payload: { 445 | n: 90 446 | } 447 | }, (err, res) => { 448 | t.assert.ifError(err) 449 | const payload = JSON.parse(res.payload) 450 | t.assert.deepStrictEqual(payload, { 451 | error: 'Unauthorized', 452 | message: '`n` is not big', 453 | statusCode: 401 454 | }) 455 | done() 456 | }) 457 | }) 458 | 459 | test('single [Array] And Relation success', (t, done) => { 460 | t.plan(2) 461 | 462 | fastify.inject({ 463 | method: 'POST', 464 | url: '/singlearraycheckand', 465 | payload: { 466 | n: 11 467 | } 468 | }, (err, res) => { 469 | t.assert.ifError(err) 470 | const payload = JSON.parse(res.payload) 471 | t.assert.deepStrictEqual(payload, { hello: 'world' }) 472 | done() 473 | }) 474 | }) 475 | 476 | test('single [Array] And Relation failed', (t, done) => { 477 | t.plan(2) 478 | 479 | fastify.inject({ 480 | method: 'POST', 481 | url: '/singlearraycheckand', 482 | payload: { 483 | n: 10 484 | } 485 | }, (err, res) => { 486 | t.assert.ifError(err) 487 | const payload = JSON.parse(res.payload) 488 | t.assert.deepStrictEqual(payload, { 489 | error: 'Unauthorized', 490 | message: '`n` is not odd', 491 | statusCode: 401 492 | }) 493 | done() 494 | }) 495 | }) 496 | 497 | test('Two sub-arrays Or Relation success', (t, done) => { 498 | t.plan(2) 499 | 500 | fastify.inject({ 501 | method: 'POST', 502 | url: '/check-two-sub-arrays-or', 503 | payload: { 504 | n: 11 505 | } 506 | }, (err, res) => { 507 | t.assert.ifError(err) 508 | const payload = JSON.parse(res.payload) 509 | t.assert.deepStrictEqual(payload, { hello: 'world' }) 510 | done() 511 | }) 512 | }) 513 | 514 | test('Two sub-arrays Or Relation called sequentially', (t, done) => { 515 | t.plan(2) 516 | 517 | fastify.inject({ 518 | method: 'POST', 519 | url: '/check-two-sub-arrays-or-2', 520 | payload: { 521 | n: 110 522 | } 523 | }, (err, res) => { 524 | t.assert.ifError(err) 525 | const payload = JSON.parse(res.payload) 526 | 527 | t.assert.deepStrictEqual(payload, { 528 | verifyBigAsyncCalled: true, 529 | verifyOddAsyncCalled: false 530 | }) 531 | done() 532 | }) 533 | }) 534 | 535 | test('Two sub-arrays Or Relation fail', (t, done) => { 536 | t.plan(2) 537 | 538 | fastify.inject({ 539 | method: 'POST', 540 | url: '/check-two-sub-arrays-or', 541 | payload: { 542 | n: 4 543 | } 544 | }, (err, res) => { 545 | t.assert.ifError(err) 546 | const payload = JSON.parse(res.payload) 547 | t.assert.deepStrictEqual(payload, { 548 | error: 'Unauthorized', 549 | message: '`n` is not odd', 550 | statusCode: 401 551 | }) 552 | done() 553 | }) 554 | }) 555 | 556 | test('[Array] notation & single case Or Relation success under first case', (t, done) => { 557 | t.plan(2) 558 | 559 | fastify.inject({ 560 | method: 'POST', 561 | url: '/checkarrayorsingle', 562 | payload: { 563 | n: 11 564 | } 565 | }, (err, res) => { 566 | t.assert.ifError(err) 567 | const payload = JSON.parse(res.payload) 568 | t.assert.deepStrictEqual(payload, { hello: 'world' }) 569 | done() 570 | }) 571 | }) 572 | 573 | test('[Array] notation & single case Or Relation success under second case', (t, done) => { 574 | t.plan(2) 575 | 576 | fastify.inject({ 577 | method: 'POST', 578 | url: '/checkarrayorsingle', 579 | payload: { 580 | n: 1002 581 | } 582 | }, (err, res) => { 583 | t.assert.ifError(err) 584 | const payload = JSON.parse(res.payload) 585 | t.assert.deepStrictEqual(payload, { hello: 'world' }) 586 | done() 587 | }) 588 | }) 589 | 590 | test('[Array] notation & single case Or Relation failed', (t, done) => { 591 | t.plan(2) 592 | 593 | fastify.inject({ 594 | method: 'POST', 595 | url: '/checkarrayorsingle', 596 | payload: { 597 | n: 2 598 | } 599 | }, (err, res) => { 600 | t.assert.ifError(err) 601 | const payload = JSON.parse(res.payload) 602 | t.assert.deepStrictEqual(payload, { 603 | error: 'Unauthorized', 604 | message: '`n` is not big', 605 | statusCode: 401 606 | }) 607 | done() 608 | }) 609 | }) 610 | 611 | test('And Relation with Or relation inside sub-array with run: all', (t, done) => { 612 | t.plan(2) 613 | 614 | fastify.inject({ 615 | method: 'POST', 616 | url: '/check-composite-and-run-all', 617 | payload: { 618 | n: 11 619 | } 620 | }, (err, res) => { 621 | t.assert.ifError(err) 622 | const payload = JSON.parse(res.payload) 623 | t.assert.deepStrictEqual(payload, { 624 | odd: true, 625 | big: false, 626 | number: true 627 | }) 628 | done() 629 | }) 630 | }) 631 | 632 | test('Or Relation with And relation inside sub-array with run: all', (t, done) => { 633 | t.plan(2) 634 | 635 | fastify.inject({ 636 | method: 'POST', 637 | url: '/check-composite-or-run-all', 638 | payload: { 639 | n: 110 640 | } 641 | }, (err, res) => { 642 | t.assert.ifError(err) 643 | const payload = JSON.parse(res.payload) 644 | t.assert.deepStrictEqual(payload, { 645 | odd: false, 646 | big: true, 647 | number: true 648 | }) 649 | done() 650 | }) 651 | }) 652 | 653 | test('Check run all line fail with AND', (t, done) => { 654 | t.plan(8) 655 | 656 | const fastify = build() 657 | 658 | fastify.after(() => { 659 | fastify.route({ 660 | method: 'GET', 661 | url: '/run-all-pipe', 662 | preHandler: fastify.auth([ 663 | (_request, _reply, done) => { t.assert.ok('executed 1'); done() }, 664 | (_request, _reply, done) => { t.assert.ok('executed 2'); done(new Error('second')) }, 665 | (_request, _reply, done) => { t.assert.ok('executed 3'); done() }, 666 | (_request, _reply, done) => { t.assert.ok('executed 4'); done() }, 667 | (_request, _reply, done) => { t.assert.ok('executed 5'); done(new Error('fifth')) } 668 | ], { relation: 'and', run: 'all' }), 669 | handler: (_req, reply) => { reply.send({ hello: 'world' }) } 670 | }) 671 | }) 672 | 673 | fastify.inject('/run-all-pipe', (err, res) => { 674 | t.assert.ifError(err) 675 | t.assert.strictEqual(res.statusCode, 401) 676 | const payload = JSON.parse(res.payload) 677 | t.assert.deepStrictEqual(payload, { 678 | error: 'Unauthorized', 679 | message: 'second', 680 | statusCode: 401 681 | }) 682 | done() 683 | }) 684 | }) 685 | 686 | test('Check run all line with AND', (t, done) => { 687 | t.plan(8) 688 | 689 | const fastify = build() 690 | 691 | fastify.after(() => { 692 | fastify.route({ 693 | method: 'GET', 694 | url: '/run-all-pipe', 695 | preHandler: fastify.auth([ 696 | (_request, _reply, done) => { t.assert.ok('executed 1'); done() }, 697 | (_request, _reply, done) => { t.assert.ok('executed 2'); done() }, 698 | (_request, _reply, done) => { t.assert.ok('executed 3'); done() }, 699 | (_request, _reply, done) => { t.assert.ok('executed 4'); done() }, 700 | (_request, _reply, done) => { t.assert.ok('executed 5'); done() } 701 | ], { relation: 'and', run: 'all' }), 702 | handler: (_req, reply) => { reply.send({ hello: 'world' }) } 703 | }) 704 | }) 705 | 706 | fastify.inject('/run-all-pipe', (err, res) => { 707 | t.assert.ifError(err) 708 | t.assert.strictEqual(res.statusCode, 200) 709 | const payload = JSON.parse(res.payload) 710 | t.assert.deepStrictEqual(payload, { hello: 'world' }) 711 | done() 712 | }) 713 | }) 714 | 715 | test('Check run all line with OR', (t, done) => { 716 | t.plan(8) 717 | 718 | const fastify = build() 719 | 720 | fastify.after(() => { 721 | fastify.route({ 722 | method: 'GET', 723 | url: '/run-all-pipe', 724 | preHandler: fastify.auth([ 725 | (_req, _reply, done) => { t.assert.ok('executed 1'); done(new Error('primo')) }, 726 | (_req, _reply, done) => { t.assert.ok('executed 2'); done(new Error('secondo')) }, 727 | (_req, _reply, done) => { t.assert.ok('executed 3'); done() }, 728 | (_req, _reply, done) => { t.assert.ok('executed 4'); done(new Error('quarto')) }, 729 | (_req, _reply, done) => { t.assert.ok('executed 5'); done() } 730 | ], { relation: 'or', run: 'all' }), 731 | handler: (_req, reply) => { reply.send({ hello: 'world' }) } 732 | }) 733 | }) 734 | 735 | fastify.inject('/run-all-pipe', (err, res) => { 736 | t.assert.ifError(err) 737 | t.assert.strictEqual(res.statusCode, 200) 738 | const payload = JSON.parse(res.payload) 739 | t.assert.deepStrictEqual(payload, { hello: 'world' }) 740 | done() 741 | }) 742 | }) 743 | 744 | test('Check run all fail line with OR', (t, done) => { 745 | t.plan(8) 746 | 747 | const fastify = build() 748 | 749 | fastify.after(() => { 750 | fastify.route({ 751 | method: 'GET', 752 | url: '/run-all-pipe', 753 | preHandler: fastify.auth([ 754 | (_req, _reply, done) => { t.assert.ok('executed 1'); done(new Error('primo')) }, 755 | (_req, _reply, done) => { t.assert.ok('executed 2'); done(new Error('secondo')) }, 756 | (_req, _reply, done) => { t.assert.ok('executed 3'); done(new Error('terzo')) }, 757 | (_req, _reply, done) => { t.assert.ok('executed 4'); done(new Error('quarto')) }, 758 | (_req, _reply, done) => { t.assert.ok('executed 5'); done(new Error('quinto')) } 759 | ], { relation: 'or', run: 'all' }), 760 | handler: (_req, reply) => { reply.send({ hello: 'world' }) } 761 | }) 762 | }) 763 | 764 | fastify.inject('/run-all-pipe', (err, res) => { 765 | t.assert.ifError(err) 766 | t.assert.strictEqual(res.statusCode, 401) 767 | const payload = JSON.parse(res.payload) 768 | t.assert.deepStrictEqual(payload, { 769 | error: 'Unauthorized', 770 | message: 'quinto', 771 | statusCode: 401 772 | }) 773 | done() 774 | }) 775 | }) 776 | 777 | test('Ignore last status', (t, done) => { 778 | t.plan(5) 779 | 780 | const fastify = build() 781 | 782 | fastify.after(() => { 783 | fastify.route({ 784 | method: 'GET', 785 | url: '/run-all-status', 786 | preHandler: fastify.auth([ 787 | (_req, _reply, done) => { t.assert.ok('executed 1'); done() }, 788 | (_req, _reply, done) => { t.assert.ok('executed 2'); done(new Error('last')) } 789 | ], { relation: 'or', run: 'all' }), 790 | handler: (_req, reply) => { reply.send({ hello: 'world' }) } 791 | }) 792 | }) 793 | 794 | fastify.inject('/run-all-status', (err, res) => { 795 | t.assert.ifError(err) 796 | t.assert.strictEqual(res.statusCode, 200) 797 | const payload = JSON.parse(res.payload) 798 | t.assert.deepStrictEqual(payload, { hello: 'world' }) 799 | done() 800 | }) 801 | }) 802 | 803 | test('Or Relation run all', (t, done) => { 804 | t.plan(2) 805 | 806 | fastify.inject({ 807 | method: 'POST', 808 | url: '/run-all-or', 809 | payload: { 810 | n: 11 811 | } 812 | }, (err, res) => { 813 | t.assert.ifError(err) 814 | const payload = JSON.parse(res.payload) 815 | t.assert.deepStrictEqual(payload, { 816 | odd: true, 817 | big: false, 818 | number: true 819 | }) 820 | done() 821 | }) 822 | }) 823 | 824 | test('Or Relation run all fail', (t, done) => { 825 | t.plan(2) 826 | 827 | fastify.inject({ 828 | method: 'POST', 829 | url: '/run-all-or', 830 | payload: { 831 | n: 'foo' 832 | } 833 | }, (err, res) => { 834 | t.assert.ifError(err) 835 | const payload = JSON.parse(res.payload) 836 | t.assert.deepStrictEqual(payload, { 837 | error: 'Unauthorized', 838 | message: 'type of `n` is not `number`', 839 | statusCode: 401 840 | }) 841 | done() 842 | }) 843 | }) 844 | 845 | test('Nested sub-arrays not supported', (t, done) => { 846 | t.plan(1) 847 | try { 848 | fastify.auth([[fastify.verifyBig, [fastify.verifyNumber]]]) 849 | } catch (err) { 850 | t.assert.deepStrictEqual(err.message, 'Nesting sub-arrays is not supported') 851 | done() 852 | } 853 | }) 854 | 855 | test('And Relation run all', (t, done) => { 856 | t.plan(2) 857 | 858 | fastify.inject({ 859 | method: 'POST', 860 | url: '/run-all-and', 861 | payload: { 862 | n: 101 863 | } 864 | }, (err, res) => { 865 | t.assert.ifError(err) 866 | const payload = JSON.parse(res.payload) 867 | t.assert.deepStrictEqual(payload, { 868 | odd: true, 869 | big: true, 870 | number: true 871 | }) 872 | done() 873 | }) 874 | }) 875 | 876 | test('Clean status code settle by user', (t, done) => { 877 | t.plan(5) 878 | 879 | const fastify = build() 880 | 881 | fastify.after(() => { 882 | fastify.route({ 883 | method: 'GET', 884 | url: '/run-all-status', 885 | preHandler: fastify.auth([ 886 | (_req, _reply, done) => { t.assert.ok('executed 1'); done() }, 887 | (_req, reply, done) => { t.assert.ok('executed 2'); reply.code(400); done(new Error('last')) } 888 | ], { relation: 'or', run: 'all' }), 889 | handler: (_req, reply) => { reply.send({ hello: 'world' }) } 890 | }) 891 | }) 892 | 893 | fastify.inject('/run-all-status', (err, res) => { 894 | t.assert.ifError(err) 895 | t.assert.strictEqual(res.statusCode, 200) 896 | const payload = JSON.parse(res.payload) 897 | t.assert.deepStrictEqual(payload, { hello: 'world' }) 898 | done() 899 | }) 900 | }) 901 | -------------------------------------------------------------------------------- /test/example.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | /* 3 | Register a user: 4 | 5 | curl -i 'http://127.0.0.1:3000/register' -H 'content-type: application/json' --data '{"user": "myuser","password":"mypass"}' 6 | Will return: 7 | {"token":"YOUR_JWT_TOKEN"} 8 | 9 | The application then: 10 | 1. generates a JWT token (from 'supersecret') and adds to the response headers 11 | 1. inserts user in the leveldb 12 | 13 | Check it's all working by using one or the other auth mechanisms: 14 | 1. Auth using username and password (you can also use JWT on this endpoint) 15 | curl 'http://127.0.0.1:3000/auth-multiple' -H 'content-type: application/json' --data '{"user": "myuser","password":"mypass"}' 16 | {"hello":"world"} 17 | 18 | 1. Auth using JWT token 19 | curl -i 'http://127.0.0.1:3000/auth' -H 'content-type: application/json' -H "auth: YOUR_JWT_TOKEN" 20 | */ 21 | 22 | const Fastify = require('fastify') 23 | 24 | function build (opts) { 25 | const fastify = Fastify(opts) 26 | 27 | fastify.register(require('@fastify/jwt'), { secret: 'supersecret' }) 28 | fastify.register(require('@fastify/leveldb'), { name: 'authdb' }) 29 | fastify.register(require('../auth')) // just 'fastify-auth' IRL 30 | fastify.after(routes) 31 | 32 | fastify.decorate('verifyJWTandLevelDB', verifyJWTandLevelDB) 33 | fastify.decorate('verifyUserAndPassword', verifyUserAndPassword) 34 | 35 | function verifyJWTandLevelDB (request, reply, done) { 36 | const jwt = this.jwt 37 | const level = this.level.authdb 38 | 39 | if (request.body && request.body.failureWithReply) { 40 | reply.code(401).send({ error: 'Unauthorized' }) 41 | return done(new Error()) 42 | } 43 | 44 | if (!request.raw.headers.auth) { 45 | return done(new Error('Missing token header')) 46 | } 47 | 48 | jwt.verify(request.raw.headers.auth, onVerify) 49 | 50 | function onVerify (err, decoded) { 51 | if (err || !decoded.user || !decoded.password) { 52 | return done(new Error('Token not valid')) 53 | } 54 | 55 | level.get(decoded.user, onUser) 56 | 57 | function onUser (err, password) { 58 | if (err) { 59 | if (err.notFound) { 60 | return done(new Error('Token not valid')) 61 | } 62 | return done(err) 63 | } 64 | 65 | if (!password || password !== decoded.password) { 66 | return done(new Error('Token not valid')) 67 | } 68 | 69 | done() 70 | } 71 | } 72 | } 73 | 74 | function verifyUserAndPassword (request, _reply, done) { 75 | const level = this.level.authdb 76 | 77 | if (!request.body || !request.body.user) { 78 | return done(new Error('Missing user in request body')) 79 | } 80 | 81 | level.get(request.body.user, onUser) 82 | 83 | function onUser (err, password) { 84 | if (err) { 85 | if (err.notFound) { 86 | return done(new Error('Password not valid')) 87 | } 88 | return done(err) 89 | } 90 | 91 | if (!password || password !== request.body.password) { 92 | return done(new Error('Password not valid')) 93 | } 94 | 95 | done() 96 | } 97 | } 98 | 99 | function routes () { 100 | fastify.route({ 101 | method: 'POST', 102 | url: '/register', 103 | schema: { 104 | body: { 105 | type: 'object', 106 | properties: { 107 | user: { type: 'string' }, 108 | password: { type: 'string' } 109 | }, 110 | required: ['user', 'password'] 111 | } 112 | }, 113 | handler: (req, reply) => { 114 | req.log.info('Creating new user') 115 | fastify.level.authdb.put(req.body.user, req.body.password, onPut) 116 | 117 | function onPut (err) { 118 | if (err) return reply.send(err) 119 | fastify.jwt.sign(req.body, onToken) 120 | } 121 | 122 | function onToken (err, token) { 123 | if (err) return reply.send(err) 124 | req.log.info('User created') 125 | reply.send({ token }) 126 | } 127 | } 128 | }) 129 | 130 | fastify.route({ 131 | method: 'GET', 132 | url: '/no-auth', 133 | handler: (req, reply) => { 134 | req.log.info('Auth free route') 135 | reply.send({ hello: 'world' }) 136 | } 137 | }) 138 | 139 | fastify.route({ 140 | method: 'GET', 141 | url: '/auth', 142 | preHandler: fastify.auth([fastify.verifyJWTandLevelDB]), 143 | handler: (req, reply) => { 144 | req.log.info('Auth route') 145 | reply.send({ hello: 'world' }) 146 | } 147 | }) 148 | 149 | fastify.route({ 150 | method: 'POST', 151 | url: '/auth-multiple', 152 | preHandler: fastify.auth([ 153 | // Only one of these has to pass 154 | fastify.verifyJWTandLevelDB, 155 | fastify.verifyUserAndPassword 156 | ]), 157 | handler: (req, reply) => { 158 | req.log.info('Auth route') 159 | reply.send({ hello: 'world' }) 160 | } 161 | }) 162 | } 163 | 164 | return fastify 165 | } 166 | 167 | module.exports = build 168 | -------------------------------------------------------------------------------- /test/example.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | const { rimrafSync } = require('rimraf') 5 | const build = require('./example') 6 | 7 | let fastify = null 8 | let token = null 9 | 10 | test.before(() => { 11 | rimrafSync('./authdb') 12 | fastify = build() 13 | }) 14 | 15 | test.after(async () => { 16 | await fastify.close() 17 | rimrafSync('./authdb') 18 | }) 19 | 20 | test('Route without auth', (t, done) => { 21 | t.plan(2) 22 | 23 | fastify.inject({ 24 | method: 'GET', 25 | url: '/no-auth' 26 | }, (err, res) => { 27 | t.assert.ifError(err) 28 | const payload = JSON.parse(res.payload) 29 | t.assert.deepStrictEqual(payload, { hello: 'world' }) 30 | done() 31 | }) 32 | }) 33 | 34 | test('Missing header', (t, done) => { 35 | t.plan(2) 36 | 37 | fastify.inject({ 38 | method: 'GET', 39 | url: '/auth', 40 | headers: {} 41 | }, (err, res) => { 42 | t.assert.ifError(err) 43 | const payload = JSON.parse(res.payload) 44 | t.assert.deepStrictEqual(payload, { 45 | error: 'Unauthorized', 46 | message: 'Missing token header', 47 | statusCode: 401 48 | }) 49 | done() 50 | }) 51 | }) 52 | 53 | test('Register user', (t, done) => { 54 | t.plan(3) 55 | 56 | fastify.inject({ 57 | method: 'POST', 58 | url: '/register', 59 | payload: { 60 | user: 'tomas', 61 | password: 'a-very-secure-one' 62 | } 63 | }, (err, res) => { 64 | t.assert.ifError(err) 65 | const payload = JSON.parse(res.payload) 66 | t.assert.strictEqual(res.statusCode, 200) 67 | token = payload.token 68 | t.assert.strictEqual(typeof payload.token, 'string') 69 | done() 70 | }) 71 | }) 72 | 73 | test('Auth successful', (t, done) => { 74 | t.plan(2) 75 | 76 | fastify.inject({ 77 | method: 'GET', 78 | url: '/auth', 79 | headers: { 80 | auth: token 81 | } 82 | }, (err, res) => { 83 | t.assert.ifError(err) 84 | const payload = JSON.parse(res.payload) 85 | t.assert.deepStrictEqual(payload, { hello: 'world' }) 86 | done() 87 | }) 88 | }) 89 | 90 | test('Auth not successful', (t, done) => { 91 | t.plan(2) 92 | 93 | fastify.inject({ 94 | method: 'GET', 95 | url: '/auth', 96 | headers: { 97 | auth: 'the winter is coming' 98 | } 99 | }, (err, res) => { 100 | t.assert.ifError(err) 101 | const payload = JSON.parse(res.payload) 102 | t.assert.deepStrictEqual(payload, { 103 | code: 'FAST_JWT_MALFORMED', 104 | error: 'Unauthorized', 105 | message: 'The token is malformed.', 106 | statusCode: 401 107 | }) 108 | done() 109 | }) 110 | }) 111 | 112 | test('Auth successful (multiple)', (t, done) => { 113 | t.plan(2) 114 | 115 | fastify.inject({ 116 | method: 'POST', 117 | url: '/auth-multiple', 118 | payload: { 119 | user: 'tomas', 120 | password: 'a-very-secure-one' 121 | } 122 | }, (err, res) => { 123 | t.assert.ifError(err) 124 | const payload = JSON.parse(res.payload) 125 | t.assert.deepStrictEqual(payload, { hello: 'world' }) 126 | done() 127 | }) 128 | }) 129 | 130 | test('Auth not successful (multiple)', (t, done) => { 131 | t.plan(2) 132 | 133 | fastify.inject({ 134 | method: 'POST', 135 | url: '/auth-multiple', 136 | payload: { 137 | user: 'tomas', 138 | password: 'wrong!' 139 | } 140 | }, (err, res) => { 141 | t.assert.ifError(err) 142 | const payload = JSON.parse(res.payload) 143 | t.assert.deepStrictEqual(payload, { 144 | error: 'Unauthorized', 145 | message: 'Password not valid', 146 | statusCode: 401 147 | }) 148 | done() 149 | }) 150 | }) 151 | 152 | test('Failure with missing user', (t, done) => { 153 | t.plan(2) 154 | 155 | fastify.inject({ 156 | method: 'POST', 157 | url: '/auth-multiple', 158 | payload: { 159 | password: 'wrong!' 160 | } 161 | }, (err, res) => { 162 | t.assert.ifError(err) 163 | const payload = JSON.parse(res.payload) 164 | t.assert.deepStrictEqual(payload, { 165 | error: 'Unauthorized', 166 | message: 'Missing user in request body', 167 | statusCode: 401 168 | }) 169 | done() 170 | }) 171 | }) 172 | 173 | test('Failure with explicit reply', (t, done) => { 174 | t.plan(3) 175 | 176 | fastify.inject({ 177 | method: 'POST', 178 | url: '/auth-multiple', 179 | payload: { 180 | failureWithReply: true, 181 | user: 'tomas', 182 | password: 'wrong!' 183 | } 184 | }, (err, res) => { 185 | t.assert.ifError(err) 186 | const payload = JSON.parse(res.payload) 187 | t.assert.strictEqual(res.statusCode, 401) 188 | t.assert.deepStrictEqual(payload, { error: 'Unauthorized' }) 189 | done() 190 | }) 191 | }) 192 | -------------------------------------------------------------------------------- /types/index.d.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ContextConfigDefault, 3 | FastifyInstance, 4 | FastifyPluginCallback, 5 | FastifyReply, 6 | FastifyRequest, 7 | FastifySchema, 8 | RouteGenericInterface, 9 | preHandlerHookHandler 10 | } from 'fastify' 11 | 12 | declare module 'fastify' { 13 | interface FastifyInstance { 14 | auth< 15 | Request extends FastifyRequest = FastifyRequest, 16 | Reply extends FastifyReply = FastifyReply 17 | >( 18 | functions: fastifyAuth.FastifyAuthFunction[] | (fastifyAuth.FastifyAuthFunction | fastifyAuth.FastifyAuthFunction[])[], 19 | options?: { 20 | relation?: fastifyAuth.FastifyAuthRelation; 21 | run?: 'all'; 22 | } 23 | ): preHandlerHookHandler; 24 | } 25 | } 26 | 27 | type FastifyAuth = FastifyPluginCallback 28 | 29 | declare namespace fastifyAuth { 30 | export type FastifyAuthRelation = 'and' | 'or' 31 | 32 | export type FastifyAuthFunction< 33 | Request extends FastifyRequest = FastifyRequest, 34 | Reply extends FastifyReply = FastifyReply 35 | > = ( 36 | this: FastifyInstance, 37 | request: Request, 38 | reply: Reply, 39 | done: (error?: Error) => void 40 | ) => void 41 | 42 | /** 43 | * @link [`fastify-auth` options documentation](https://github.com/fastify/fastify-auth#options) 44 | */ 45 | export interface FastifyAuthPluginOptions { 46 | /** 47 | * The default relation between the functions. It can be either `or` or `and`. 48 | * 49 | * @default 'or' 50 | */ 51 | defaultRelation?: FastifyAuthRelation; 52 | } 53 | 54 | export const fastifyAuth: FastifyAuth 55 | export { fastifyAuth as default } 56 | } 57 | 58 | declare function fastifyAuth (...params: Parameters): ReturnType 59 | export = fastifyAuth 60 | -------------------------------------------------------------------------------- /types/index.test-d.ts: -------------------------------------------------------------------------------- 1 | import fastify, { FastifyInstance, FastifyReply, FastifyRequest, preHandlerHookHandler } from 'fastify' 2 | import fastifyAuth from '..' 3 | import { expectType } from 'tsd' 4 | import { TypeBoxTypeProvider } from '@fastify/type-provider-typebox' 5 | import { JsonSchemaToTsProvider } from '@fastify/type-provider-json-schema-to-ts' 6 | 7 | const app = fastify() 8 | 9 | type Done = (error?: Error) => void 10 | 11 | app.register(fastifyAuth).after((_err) => { 12 | app.auth([ 13 | (request, reply, done) => { 14 | expectType(request) 15 | expectType(reply) 16 | expectType(done) 17 | }, 18 | ], { relation: 'or' }) 19 | app.auth([ 20 | (request, reply, done) => { 21 | expectType(request) 22 | expectType(reply) 23 | expectType(done) 24 | }, 25 | ], { run: 'all' }) 26 | app.auth([ 27 | (request, reply, done) => { 28 | expectType(request) 29 | expectType(reply) 30 | expectType(done) 31 | }, 32 | ]) 33 | app.auth([ 34 | function () { 35 | expectType(this) 36 | }, 37 | ]) 38 | const auth = app.auth([() => {}]) 39 | expectType(auth) 40 | app.get('/secret', { preHandler: auth }, () => {}) 41 | app.get('/private', { preHandler: [auth] }, () => {}) 42 | }) 43 | 44 | const typebox = fastify().withTypeProvider() 45 | typebox.register(fastifyAuth) 46 | typebox.route({ 47 | method: 'GET', 48 | url: '/', 49 | preHandler: typebox.auth([]), 50 | handler: () => {} 51 | }) 52 | 53 | const jsonSchemaToTS = fastify().withTypeProvider() 54 | jsonSchemaToTS.register(fastifyAuth) 55 | jsonSchemaToTS.route({ 56 | method: 'GET', 57 | url: '/', 58 | preHandler: jsonSchemaToTS.auth([]), 59 | handler: () => {} 60 | }) 61 | 62 | declare module 'fastify' { 63 | interface FastifyRequest { 64 | identity: { actorId: string }; 65 | } 66 | 67 | interface FastifyInstance { 68 | authenticate: (request: FastifyRequest) => Promise; 69 | } 70 | } 71 | 72 | export const usersMutationAccessPolicy = 73 | (fastify: FastifyInstance) => 74 | async ( 75 | request: FastifyRequest<{ 76 | Params: { userId: string } 77 | }> 78 | ): Promise => { 79 | const { actorId } = request.identity 80 | const isOwner = actorId === request.params.userId 81 | 82 | if (isOwner) { 83 | return 84 | } 85 | 86 | fastify.log.warn('Actor should not be able to see this route') 87 | 88 | throw new Error(request.params.userId) 89 | } 90 | 91 | async function usersController (fastify: FastifyInstance): Promise { 92 | fastify.patch<{ 93 | Params: { userId: string }; 94 | Body: { name: string }; 95 | }>( 96 | '/:userId', 97 | { 98 | onRequest: fastify.auth([ 99 | usersMutationAccessPolicy(fastify), 100 | ]), 101 | }, 102 | async () => ({ success: true }) 103 | ) 104 | } 105 | await usersController(app) 106 | 107 | async function usersControllerV2 (fastify: FastifyInstance): Promise { 108 | fastify.patch<{ 109 | Params: { userId: string }; 110 | Body: { name: string }; 111 | }>( 112 | '/:userId', 113 | { 114 | onRequest: usersMutationAccessPolicy(fastify), 115 | }, 116 | async () => ({ success: true }) 117 | ) 118 | } 119 | await usersControllerV2(app) 120 | --------------------------------------------------------------------------------