├── .gitattributes ├── .github ├── dependabot.yml ├── stale.yml └── workflows │ └── ci.yml ├── .gitignore ├── .npmrc ├── LICENSE ├── README.md ├── eslint.config.js ├── example └── example.js ├── index.js ├── package.json ├── test └── index.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) 2018 Fastify 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # @fastify/basic-auth 2 | 3 | [![CI](https://github.com/fastify/fastify-basic-auth/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/fastify/fastify-basic-auth/actions/workflows/ci.yml) 4 | [![NPM version](https://img.shields.io/npm/v/@fastify/basic-auth.svg?style=flat)](https://www.npmjs.com/package/@fastify/basic-auth) 5 | [![neostandard javascript style](https://img.shields.io/badge/code_style-neostandard-brightgreen?style=flat)](https://github.com/neostandard/neostandard) 6 | 7 | A simple [Basic auth](https://datatracker.ietf.org/doc/html/rfc7617) plugin for Fastify. 8 | 9 | ## Install 10 | ``` 11 | npm i @fastify/basic-auth 12 | ``` 13 | 14 | ### Compatibility 15 | | Plugin version | Fastify version | 16 | | ---------------|-----------------| 17 | | `>=6.x` | `^5.x` | 18 | | `^4.x` | `^4.x` | 19 | | `>=1.x <4.x` | `^3.x` | 20 | | `^0.x` | `^2.x` | 21 | | `^0.x` | `^1.x` | 22 | 23 | 24 | Please note that if a Fastify version is out of support, then so are the corresponding versions of this plugin 25 | in the table above. 26 | See [Fastify's LTS policy](https://github.com/fastify/fastify/blob/main/docs/Reference/LTS.md) for more details. 27 | 28 | ## Usage 29 | This plugin decorates the fastify instance with a `basicAuth` function, which can be used inside any hook before a route handler, or with [`@fastify/auth`](https://github.com/fastify/fastify-auth). 30 | 31 | ```js 32 | const fastify = require('fastify')() 33 | const authenticate = {realm: 'Westeros'} 34 | fastify.register(require('@fastify/basic-auth'), { validate, authenticate }) 35 | // `this` inside validate is `fastify` 36 | function validate (username, password, req, reply, done) { 37 | if (username === 'Tyrion' && password === 'wine') { 38 | done() 39 | } else { 40 | done(new Error('Winter is coming')) 41 | } 42 | } 43 | 44 | fastify.after(() => { 45 | fastify.addHook('onRequest', fastify.basicAuth) 46 | 47 | fastify.get('/', (req, reply) => { 48 | reply.send({ hello: 'world' }) 49 | }) 50 | }) 51 | ``` 52 | 53 | Promises and *async/await* are supported as well! 54 | ```js 55 | const fastify = require('fastify')() 56 | const authenticate = {realm: 'Westeros'} 57 | fastify.register(require('@fastify/basic-auth'), { validate, authenticate }) 58 | async function validate (username, password, req, reply) { 59 | if (username !== 'Tyrion' || password !== 'wine') { 60 | return new Error('Winter is coming') 61 | } 62 | } 63 | ``` 64 | 65 | Use with `onRequest`: 66 | ```js 67 | const fastify = require('fastify')() 68 | const authenticate = {realm: 'Westeros'} 69 | fastify.register(require('@fastify/basic-auth'), { validate, authenticate }) 70 | async function validate (username, password, req, reply) { 71 | if (username !== 'Tyrion' || password !== 'wine') { 72 | return new Error('Winter is coming') 73 | } 74 | } 75 | 76 | fastify.after(() => { 77 | fastify.route({ 78 | method: 'GET', 79 | url: '/', 80 | onRequest: fastify.basicAuth, 81 | handler: async (req, reply) => { 82 | return { hello: 'world' } 83 | } 84 | }) 85 | }) 86 | ``` 87 | 88 | Use with [`@fastify/auth`](https://github.com/fastify/fastify-auth): 89 | ```js 90 | const fastify = require('fastify')() 91 | const authenticate = {realm: 'Westeros'} 92 | fastify.register(require('@fastify/auth')) 93 | fastify.register(require('@fastify/basic-auth'), { validate, authenticate }) 94 | async function validate (username, password, req, reply) { 95 | if (username !== 'Tyrion' || password !== 'wine') { 96 | return new Error('Winter is coming') 97 | } 98 | } 99 | 100 | fastify.after(() => { 101 | // use preHandler to authenticate all the routes 102 | fastify.addHook('preHandler', fastify.auth([fastify.basicAuth])) 103 | 104 | fastify.route({ 105 | method: 'GET', 106 | url: '/', 107 | // use onRequest to authenticate just this one 108 | onRequest: fastify.auth([fastify.basicAuth]), 109 | handler: async (req, reply) => { 110 | return { hello: 'world' } 111 | } 112 | }) 113 | }) 114 | ``` 115 | 116 | ### Custom error handler 117 | 118 | On failed authentication, `@fastify/basic-auth` calls the Fastify 119 | [generic error 120 | handler](https://fastify.dev/docs/latest/Reference/Server/#seterrorhandler) with an error 121 | and sets `err.statusCode` to `401`. 122 | 123 | To properly `401` errors: 124 | 125 | ```js 126 | fastify.setErrorHandler(function (err, req, reply) { 127 | if (err.statusCode === 401) { 128 | // This was unauthorized! Display the correct page/message 129 | reply.code(401).send({ was: 'unauthorized' }) 130 | return 131 | } 132 | reply.send(err) 133 | }) 134 | ``` 135 | 136 | ## Options 137 | 138 | ### `utf8` (optional, default: true) 139 | 140 | User-ids or passwords with non-US-ASCII characters may cause issues 141 | unless both parties agree on the encoding scheme. Setting `utf8` to 142 | true sends the 'charset' parameter to prefer "UTF-8", increasing the 143 | likelihood that clients will use that encoding. 144 | 145 | ### `strictCredentials` (optional, default: true) 146 | 147 | Setting `strictCredentials` to false allows additional whitespaces at 148 | the beginning, middle, and end of the authorization header. 149 | This is a fallback option to ensure the same behavior as `@fastify/basic-auth` 150 | version <=5.x. 151 | 152 | ### `validate` (required) 153 | 154 | The `validate` function is called on each request made 155 | and is passed the `username`, `password`, `req`, and `reply` 156 | parameters, in that order. An optional fifth parameter, `done`, can be 157 | used to signify a valid request when called with no arguments 158 | or an invalid request when called with an `Error` object. Alternatively, 159 | the `validate` function can return a promise, resolving for valid 160 | requests and rejecting for invalid. This can also be achieved using 161 | an `async/await` function, and throwing for invalid requests. 162 | 163 | See code above for examples. 164 | 165 | ### `authenticate` (optional, default: false) 166 | 167 | The `authenticate` option adds the [`WWW-Authenticate` header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/WWW-Authenticate) and can set the `realm` value. 168 | 169 | This can trigger client-side authentication interfaces, such as the browser authentication dialog. 170 | 171 | Setting `authenticate` to `true` adds the header `WWW-Authenticate: Basic`. When `false`, no header is added (default). 172 | 173 | When `proxyMode` is `true` it will use the [`Proxy-Authenticate`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Proxy-Authenticate) header instead. 174 | 175 | ```js 176 | fastify.register(require('@fastify/basic-auth'), { 177 | validate, 178 | authenticate: true // WWW-Authenticate: Basic 179 | }) 180 | 181 | fastify.register(require('@fastify/basic-auth'), { 182 | validate, 183 | proxyMode: true, 184 | authenticate: true // Proxy-Authenticate: Basic 185 | }) 186 | 187 | fastify.register(require('@fastify/basic-auth'), { 188 | validate, 189 | authenticate: false // no authenticate header, same as omitting authenticate option 190 | }) 191 | ``` 192 | 193 | The `authenticate` option can have a `realm` key when supplied as an object. 194 | If the `realm` key is supplied, it will be appended to the header value: 195 | 196 | ```js 197 | fastify.register(require('@fastify/basic-auth'), { 198 | validate, 199 | authenticate: {realm: 'example'} // WWW-Authenticate: Basic realm="example" 200 | }) 201 | ``` 202 | 203 | The `realm` key can also be a function: 204 | 205 | ```js 206 | fastify.register(require('@fastify/basic-auth'), { 207 | validate, 208 | authenticate: { 209 | realm(req) { 210 | return 'example' // WWW-Authenticate: Basic realm="example" 211 | } 212 | } 213 | }) 214 | ``` 215 | 216 | The `authenticate` object can include an optional `header` key to customize the header name, replacing the default `WWW-Authenticate`: 217 | 218 | ```js 219 | fastify.register(require('@fastify/basic-auth'), { 220 | validate, 221 | authenticate: { 222 | header: 'Proxy-Authenticate' // Proxy-Authenticate: Basic 223 | } 224 | }) 225 | ``` 226 | 227 | ### `proxyMode` Boolean (optional, default: false) 228 | 229 | Setting the `proxyMode` to `true` will make the plugin implement [HTTP proxy authentication](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Authentication#proxy_authentication), rather than resource authentication. In other words, the plugin will: 230 | 231 | - read credentials from the `Proxy-Authorization` header rather than `Authorization` 232 | - use `407` response status code instead of `401` to signal missing or invalid credentials 233 | - use the `Proxy-Authenticate` header rather than `WWW-Authenticate` if the `authenticate` option is set 234 | 235 | ### `header` String (optional) 236 | 237 | The `header` option specifies the header name to get credentials from for validation. If not specified it defaults to `Authorization` or `Proxy-Authorization` (according to the value of `proxyMode` option) 238 | 239 | ```js 240 | fastify.register(require('@fastify/basic-auth'), { 241 | validate, 242 | header: 'x-forwarded-authorization' 243 | }) 244 | ``` 245 | 246 | ## License 247 | 248 | Licensed under [MIT](./LICENSE). 249 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = require('neostandard')({ 4 | ignores: require('neostandard').resolveIgnoresFromGitignore(), 5 | ts: true 6 | }) 7 | -------------------------------------------------------------------------------- /example/example.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const fastify = require('fastify')() 4 | const crypto = require('node:crypto') 5 | const authenticate = { realm: 'Westeros' } 6 | 7 | const validUsername = 'Tyrion' 8 | const validPassword = 'wine' 9 | 10 | fastify.register(require('..'), { validate, authenticate }) 11 | 12 | // perform constant-time comparison to prevent timing attacks 13 | function compare (a, b) { 14 | a = Buffer.from(a) 15 | b = Buffer.from(b) 16 | if (a.length !== b.length) { 17 | // Delay return with cryptographically secure timing check. 18 | crypto.timingSafeEqual(a, a) 19 | return false 20 | } 21 | 22 | return crypto.timingSafeEqual(a, b) 23 | } 24 | 25 | // `this` inside validate is `fastify` 26 | function validate (username, password, _req, _reply, done) { 27 | let result = true 28 | result = compare(username, validUsername) && result 29 | result = compare(password, validPassword) && result 30 | if (result) { 31 | done() 32 | } else { 33 | done(new Error('Winter is coming')) 34 | } 35 | } 36 | 37 | fastify.after(() => { 38 | fastify.addHook('onRequest', fastify.basicAuth) 39 | 40 | fastify.get('/', (_req, reply) => { 41 | reply.send({ hello: 'world' }) 42 | }) 43 | }) 44 | 45 | const basicAuthCredentials = Buffer.from(`${validUsername}:${validPassword}`).toString('base64') 46 | console.log(`curl -H "authorization: Basic ${basicAuthCredentials}" http://localhost:3000`) 47 | fastify.listen({ port: 3000 }) 48 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const fp = require('fastify-plugin') 4 | const createError = require('@fastify/error') 5 | 6 | /** 7 | * HTTP provides a simple challenge-response authentication framework 8 | * that can be used by a server to challenge a client request and by a 9 | * client to provide authentication information. It uses a case- 10 | * insensitive token as a means to identify the authentication scheme, 11 | * followed by additional information necessary for achieving 12 | * authentication via that scheme. 13 | * 14 | * @see https://datatracker.ietf.org/doc/html/rfc7235#section-2.1 15 | * 16 | * The scheme name is "Basic". 17 | * @see https://datatracker.ietf.org/doc/html/rfc7617#section-2 18 | */ 19 | const authScheme = '(?:basic)' 20 | /** 21 | * The BWS rule is used where the grammar allows optional whitespace 22 | * only for historical reasons. A sender MUST NOT generate BWS in 23 | * messages. A recipient MUST parse for such bad whitespace and remove 24 | * it before interpreting the protocol element. 25 | * 26 | * @see https://datatracker.ietf.org/doc/html/rfc7230#section-3.2.3 27 | */ 28 | const BWS = '[ \t]' 29 | /** 30 | * The token68 syntax allows the 66 unreserved URI characters 31 | * ([RFC3986]), plus a few others, so that it can hold a base64, 32 | * base64url (URL and filename safe alphabet), base32, or base16 (hex) 33 | * encoding, with or without padding, but excluding whitespace 34 | * ([RFC4648]). 35 | * @see https://datatracker.ietf.org/doc/html/rfc7235#section-2.1 36 | */ 37 | const token68 = '([\\w.~+/-]+=*)' 38 | 39 | /** 40 | * @see https://datatracker.ietf.org/doc/html/rfc7235#appendix-C 41 | */ 42 | const credentialsStrictRE = new RegExp(`^${authScheme} ${token68}$`, 'i') 43 | 44 | const credentialsLaxRE = new RegExp(`^${BWS}*${authScheme}${BWS}+${token68}${BWS}*$`, 'i') 45 | 46 | /** 47 | * @see https://datatracker.ietf.org/doc/html/rfc5234#appendix-B.1 48 | */ 49 | // eslint-disable-next-line no-control-regex 50 | const controlRE = /[\x00-\x1F\x7F]/ 51 | 52 | /** 53 | * RegExp for basic auth user/pass 54 | * 55 | * user-pass = userid ":" password 56 | * userid = * 57 | * password = *TEXT 58 | */ 59 | 60 | const userPassRE = /^([^:]*):(.*)$/ 61 | 62 | async function fastifyBasicAuth (fastify, opts) { 63 | if (typeof opts.validate !== 'function') { 64 | throw new Error('Basic Auth: Missing validate function') 65 | } 66 | 67 | const strictCredentials = opts.strictCredentials ?? true 68 | const useUtf8 = opts.utf8 ?? true 69 | const charset = useUtf8 ? 'utf-8' : 'ascii' 70 | const authenticateHeader = getAuthenticateHeaders(opts.authenticate, useUtf8, opts.proxyMode) 71 | const header = opts.header?.toLowerCase() || (opts.proxyMode ? 'proxy-authorization' : 'authorization') 72 | const errorResponseCode = opts.proxyMode ? 407 : 401 73 | 74 | const MissingOrBadAuthorizationHeader = createError( 75 | 'FST_BASIC_AUTH_MISSING_OR_BAD_AUTHORIZATION_HEADER', 76 | 'Missing or bad formatted authorization header', 77 | errorResponseCode 78 | ) 79 | 80 | const credentialsRE = strictCredentials 81 | ? credentialsStrictRE 82 | : credentialsLaxRE 83 | 84 | const validate = opts.validate.bind(fastify) 85 | fastify.decorate('basicAuth', basicAuth) 86 | 87 | function basicAuth (req, reply, next) { 88 | const credentials = req.headers[header] 89 | 90 | if (typeof credentials !== 'string') { 91 | done(new MissingOrBadAuthorizationHeader()) 92 | return 93 | } 94 | 95 | // parse header 96 | const match = credentialsRE.exec(credentials) 97 | if (match === null) { 98 | done(new MissingOrBadAuthorizationHeader()) 99 | return 100 | } 101 | 102 | // decode user pass 103 | const credentialsDecoded = Buffer.from(match[1], 'base64').toString(charset) 104 | 105 | /** 106 | * The user-id and password MUST NOT contain any control characters (see 107 | * "CTL" in Appendix B.1 of [RFC5234]). 108 | * @see https://datatracker.ietf.org/doc/html/rfc7617#section-2 109 | */ 110 | if (controlRE.test(credentialsDecoded)) { 111 | done(new MissingOrBadAuthorizationHeader()) 112 | return 113 | } 114 | 115 | const userPass = userPassRE.exec(credentialsDecoded) 116 | if (userPass === null) { 117 | done(new MissingOrBadAuthorizationHeader()) 118 | return 119 | } 120 | 121 | const result = validate(userPass[1], userPass[2], req, reply, done) 122 | if (result && typeof result.then === 'function') { 123 | result.then(done, done) 124 | } 125 | 126 | function done (err) { 127 | if (err !== undefined) { 128 | // We set the status code to be `errorResponseCode` (normally 401) if it is not set 129 | if (!err.statusCode) { 130 | err.statusCode = errorResponseCode 131 | } 132 | 133 | if (err.statusCode === errorResponseCode) { 134 | const header = authenticateHeader(req) 135 | if (header) { 136 | reply.header(header[0], header[1]) 137 | } 138 | } 139 | next(err) 140 | } else { 141 | next() 142 | } 143 | } 144 | } 145 | } 146 | 147 | function getAuthenticateHeaders (authenticate, useUtf8, proxyMode) { 148 | const defaultHeaderName = proxyMode ? 'Proxy-Authenticate' : 'WWW-Authenticate' 149 | if (!authenticate) return () => false 150 | if (authenticate === true) { 151 | return useUtf8 152 | ? () => [defaultHeaderName, 'Basic charset="UTF-8"'] 153 | : () => [defaultHeaderName, 'Basic'] 154 | } 155 | if (typeof authenticate === 'object') { 156 | const realm = authenticate.realm 157 | const headerName = authenticate.header || defaultHeaderName 158 | switch (typeof realm) { 159 | case 'undefined': 160 | case 'boolean': 161 | return useUtf8 162 | ? () => [headerName, 'Basic charset="UTF-8"'] 163 | : () => [headerName, 'Basic'] 164 | case 'string': 165 | return useUtf8 166 | ? () => [headerName, `Basic realm="${realm}", charset="UTF-8"`] 167 | : () => [headerName, `Basic realm="${realm}"`] 168 | case 'function': 169 | return useUtf8 170 | ? (req) => [headerName, `Basic realm="${realm(req)}", charset="UTF-8"`] 171 | : (req) => [headerName, `Basic realm="${realm(req)}"`] 172 | } 173 | } 174 | 175 | throw new Error('Basic Auth: Invalid authenticate option') 176 | } 177 | 178 | module.exports = fp(fastifyBasicAuth, { 179 | fastify: '5.x', 180 | name: '@fastify/basic-auth' 181 | }) 182 | module.exports.default = fastifyBasicAuth 183 | module.exports.fastifyBasicAuth = fastifyBasicAuth 184 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@fastify/basic-auth", 3 | "version": "6.2.0", 4 | "description": "Fastify Basic auth plugin", 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:types", 12 | "test:unit": "c8 --100 node --test", 13 | "test:types": "tsd" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/fastify/fastify-basic-auth.git" 18 | }, 19 | "keywords": [ 20 | "fastify", 21 | "basic", 22 | "auth", 23 | "authentication", 24 | "plugin" 25 | ], 26 | "author": "Tomas Della Vedova - @delvedor (http://delved.org)", 27 | "contributors": [ 28 | { 29 | "name": "Matteo Collina", 30 | "email": "hello@matteocollina.com" 31 | }, 32 | { 33 | "name": "Manuel Spigolon", 34 | "email": "behemoth89@gmail.com" 35 | }, 36 | { 37 | "name": "Aras Abbasi", 38 | "email": "aras.abbasi@gmail.com" 39 | }, 40 | { 41 | "name": "Frazer Smith", 42 | "email": "frazer.dev@icloud.com", 43 | "url": "https://github.com/fdawgs" 44 | } 45 | ], 46 | "license": "MIT", 47 | "bugs": { 48 | "url": "https://github.com/fastify/fastify-basic-auth/issues" 49 | }, 50 | "homepage": "https://github.com/fastify/fastify-basic-auth#readme", 51 | "funding": [ 52 | { 53 | "type": "github", 54 | "url": "https://github.com/sponsors/fastify" 55 | }, 56 | { 57 | "type": "opencollective", 58 | "url": "https://opencollective.com/fastify" 59 | } 60 | ], 61 | "devDependencies": { 62 | "@fastify/auth": "^5.0.0", 63 | "@fastify/pre-commit": "^2.1.0", 64 | "@types/node": "^22.0.0", 65 | "c8": "^10.1.2", 66 | "eslint": "^9.17.0", 67 | "fastify": "^5.0.0", 68 | "neostandard": "^0.12.0", 69 | "tsd": "^0.32.0" 70 | }, 71 | "dependencies": { 72 | "@fastify/error": "^4.0.0", 73 | "fastify-plugin": "^5.0.0" 74 | }, 75 | "publishConfig": { 76 | "access": "public" 77 | }, 78 | "pre-commit": [ 79 | "lint", 80 | "test" 81 | ] 82 | } 83 | -------------------------------------------------------------------------------- /test/index.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | const Fastify = require('fastify') 5 | const basicAuth = require('..') 6 | const fastifyAuth = require('@fastify/auth') 7 | 8 | test('Basic', async t => { 9 | t.plan(2) 10 | 11 | const fastify = Fastify() 12 | await fastify.register(basicAuth, { validate }) 13 | 14 | async function validate (username, password, _req, _res) { 15 | if (username !== 'user' && password !== 'pwd') { 16 | return new Error('Unauthorized') 17 | } 18 | } 19 | 20 | fastify.after(() => { 21 | fastify.route({ 22 | method: 'GET', 23 | url: '/', 24 | preHandler: fastify.basicAuth, 25 | handler: (_req, reply) => { 26 | reply.send({ hello: 'world' }) 27 | } 28 | }) 29 | }) 30 | 31 | const { statusCode, body } = await fastify.inject({ 32 | url: '/', 33 | method: 'GET', 34 | headers: { 35 | authorization: basicAuthHeader('user', 'pwd') 36 | } 37 | }) 38 | 39 | t.assert.ok(body) 40 | t.assert.strictEqual(statusCode, 200) 41 | }) 42 | 43 | test('Basic utf8: true', async t => { 44 | t.plan(2) 45 | 46 | const fastify = Fastify() 47 | fastify.register(basicAuth, { validate, utf8: true }) 48 | 49 | function validate (username, password, _req, _res, done) { 50 | if (username === 'test' && password === '123\u00A3') { 51 | done() 52 | } else { 53 | done(new Error('Unauthorized')) 54 | } 55 | } 56 | 57 | fastify.after(() => { 58 | fastify.route({ 59 | method: 'GET', 60 | url: '/', 61 | preHandler: fastify.basicAuth, 62 | handler: (_req, reply) => { 63 | reply.send({ hello: 'world' }) 64 | } 65 | }) 66 | }) 67 | 68 | const { statusCode, body } = await fastify.inject({ 69 | url: '/', 70 | method: 'GET', 71 | headers: { 72 | /** 73 | * @see https://datatracker.ietf.org/doc/html/rfc7617#section-2.1 74 | */ 75 | authorization: 'Basic dGVzdDoxMjPCow==' 76 | } 77 | }) 78 | 79 | t.assert.ok(body) 80 | t.assert.strictEqual(statusCode, 200) 81 | }) 82 | 83 | test('Basic - 401, sending utf8 credentials base64 but utf8: false', async t => { 84 | t.plan(3) 85 | 86 | const fastify = Fastify() 87 | fastify.register(basicAuth, { validate, utf8: false }) 88 | 89 | function validate (username, password, _req, _res, done) { 90 | if (username === 'test' && password === '123\u00A3') { 91 | done() 92 | } else { 93 | done(new Error('Unauthorized')) 94 | } 95 | } 96 | 97 | fastify.after(() => { 98 | fastify.route({ 99 | method: 'GET', 100 | url: '/', 101 | preHandler: fastify.basicAuth, 102 | handler: (_req, reply) => { 103 | reply.send({ hello: 'world' }) 104 | } 105 | }) 106 | }) 107 | 108 | const { statusCode, body } = await fastify.inject({ 109 | url: '/', 110 | method: 'GET', 111 | headers: { 112 | /** 113 | * @see https://datatracker.ietf.org/doc/html/rfc7617#section-2.1 114 | */ 115 | authorization: 'Basic dGVzdDoxMjPCow==' 116 | } 117 | }) 118 | 119 | t.assert.ok(body) 120 | t.assert.strictEqual(statusCode, 401) 121 | t.assert.deepStrictEqual(JSON.parse(body), { 122 | error: 'Unauthorized', 123 | message: 'Unauthorized', 124 | statusCode: 401 125 | }) 126 | }) 127 | 128 | test('Basic - 401', async t => { 129 | t.plan(3) 130 | 131 | const fastify = Fastify() 132 | fastify.register(basicAuth, { validate }) 133 | 134 | function validate (username, password, _req, _res, done) { 135 | if (username === 'user' && password === 'pwd') { 136 | done() 137 | } else { 138 | done(new Error('Winter is coming')) 139 | } 140 | } 141 | 142 | fastify.after(() => { 143 | fastify.route({ 144 | method: 'GET', 145 | url: '/', 146 | preHandler: fastify.basicAuth, 147 | handler: (_req, reply) => { 148 | reply.send({ hello: 'world' }) 149 | } 150 | }) 151 | }) 152 | 153 | const { statusCode, body } = await fastify.inject({ 154 | url: '/', 155 | method: 'GET', 156 | headers: { 157 | authorization: basicAuthHeader('user', 'pwdd') 158 | } 159 | }) 160 | t.assert.ok(body) 161 | t.assert.strictEqual(statusCode, 401) 162 | t.assert.deepStrictEqual(JSON.parse(body), { 163 | error: 'Unauthorized', 164 | message: 'Winter is coming', 165 | statusCode: 401 166 | }) 167 | }) 168 | 169 | test('Basic - Invalid Header value /1', async t => { 170 | t.plan(3) 171 | 172 | const fastify = Fastify() 173 | fastify.register(basicAuth, { validate }) 174 | 175 | function validate (username, password, _req, _res, done) { 176 | if (username === 'user' && password === 'pwd') { 177 | done() 178 | } else { 179 | done(new Error('Winter is coming')) 180 | } 181 | } 182 | 183 | fastify.after(() => { 184 | fastify.route({ 185 | method: 'GET', 186 | url: '/', 187 | preHandler: fastify.basicAuth, 188 | handler: (_req, reply) => { 189 | reply.send({ hello: 'world' }) 190 | } 191 | }) 192 | }) 193 | 194 | const { statusCode, body } = await fastify.inject({ 195 | url: '/', 196 | method: 'GET', 197 | headers: { 198 | authorization: 'Bearer ' + Buffer.from('user:pass').toString('base64') 199 | } 200 | }) 201 | t.assert.ok(body) 202 | t.assert.strictEqual(statusCode, 401) 203 | t.assert.deepStrictEqual(JSON.parse(body), { 204 | code: 'FST_BASIC_AUTH_MISSING_OR_BAD_AUTHORIZATION_HEADER', 205 | error: 'Unauthorized', 206 | message: 'Missing or bad formatted authorization header', 207 | statusCode: 401 208 | }) 209 | }) 210 | 211 | test('Basic - Invalid Header value /2', async t => { 212 | t.plan(3) 213 | 214 | const fastify = Fastify() 215 | fastify.register(basicAuth, { validate }) 216 | 217 | function validate (username, password, _req, _res, done) { 218 | if (username === 'user' && password === 'pwd') { 219 | done() 220 | } else { 221 | done(new Error('Winter is coming')) 222 | } 223 | } 224 | 225 | fastify.after(() => { 226 | fastify.route({ 227 | method: 'GET', 228 | url: '/', 229 | preHandler: fastify.basicAuth, 230 | handler: (_req, reply) => { 231 | reply.send({ hello: 'world' }) 232 | } 233 | }) 234 | }) 235 | 236 | const { statusCode, body } = await fastify.inject({ 237 | url: '/', 238 | method: 'GET', 239 | headers: { 240 | authorization: 'Basic ' + Buffer.from('user').toString('base64') 241 | } 242 | }) 243 | t.assert.ok(body) 244 | t.assert.strictEqual(statusCode, 401) 245 | t.assert.deepStrictEqual(JSON.parse(body), { 246 | code: 'FST_BASIC_AUTH_MISSING_OR_BAD_AUTHORIZATION_HEADER', 247 | error: 'Unauthorized', 248 | message: 'Missing or bad formatted authorization header', 249 | statusCode: 401 250 | }) 251 | }) 252 | 253 | test('Basic - Invalid Header value /3', async t => { 254 | t.plan(3) 255 | 256 | const fastify = Fastify() 257 | fastify.register(basicAuth, { validate }) 258 | 259 | function validate (username, password, _req, _res, done) { 260 | if (username === 'user' && password === 'pwd') { 261 | done() 262 | } else { 263 | done(new Error('Winter is coming')) 264 | } 265 | } 266 | 267 | fastify.after(() => { 268 | fastify.route({ 269 | method: 'GET', 270 | url: '/', 271 | preHandler: fastify.basicAuth, 272 | handler: (_req, reply) => { 273 | reply.send({ hello: 'world' }) 274 | } 275 | }) 276 | }) 277 | 278 | const { statusCode, body } = await fastify.inject({ 279 | url: '/', 280 | method: 'GET', 281 | headers: { 282 | authorization: 'Basic ' + Buffer.from('user\x00:pwd').toString('base64') 283 | } 284 | }) 285 | t.assert.ok(body) 286 | t.assert.strictEqual(statusCode, 401) 287 | t.assert.deepStrictEqual(JSON.parse(body), { 288 | code: 'FST_BASIC_AUTH_MISSING_OR_BAD_AUTHORIZATION_HEADER', 289 | error: 'Unauthorized', 290 | message: 'Missing or bad formatted authorization header', 291 | statusCode: 401 292 | }) 293 | }) 294 | 295 | test('Basic - strictCredentials: false', async t => { 296 | t.plan(2) 297 | 298 | const fastify = Fastify() 299 | fastify.register(basicAuth, { validate, strictCredentials: false }) 300 | 301 | function validate (username, password, _req, _res, done) { 302 | if (username === 'user' && password === 'pwd') { 303 | done() 304 | } else { 305 | done(new Error('Winter is coming')) 306 | } 307 | } 308 | 309 | fastify.after(() => { 310 | fastify.route({ 311 | method: 'GET', 312 | url: '/', 313 | preHandler: fastify.basicAuth, 314 | handler: (_req, reply) => { 315 | reply.send({ hello: 'world' }) 316 | } 317 | }) 318 | }) 319 | 320 | const { statusCode, body } = await fastify.inject({ 321 | url: '/', 322 | method: 'GET', 323 | headers: { 324 | authorization: ' Basic ' + Buffer.from('user:pwd').toString('base64') 325 | } 326 | }) 327 | t.assert.ok(body) 328 | t.assert.strictEqual(statusCode, 200) 329 | }) 330 | 331 | test('Basic with promises', async t => { 332 | t.plan(2) 333 | 334 | const fastify = Fastify() 335 | fastify.register(basicAuth, { validate }) 336 | 337 | function validate (username, password, _req, _res) { 338 | if (username === 'user' && password === 'pwd') { 339 | return Promise.resolve() 340 | } else { 341 | return Promise.reject(new Error('Unauthorized')) 342 | } 343 | } 344 | 345 | fastify.after(() => { 346 | fastify.route({ 347 | method: 'GET', 348 | url: '/', 349 | preHandler: fastify.basicAuth, 350 | handler: (_req, reply) => { 351 | reply.send({ hello: 'world' }) 352 | } 353 | }) 354 | }) 355 | 356 | const { statusCode, body } = await fastify.inject({ 357 | url: '/', 358 | method: 'GET', 359 | headers: { 360 | authorization: basicAuthHeader('user', 'pwd') 361 | } 362 | }) 363 | t.assert.ok(body) 364 | t.assert.strictEqual(statusCode, 200) 365 | }) 366 | 367 | test('Basic with promises - 401', async t => { 368 | t.plan(3) 369 | 370 | const fastify = Fastify() 371 | fastify.register(basicAuth, { validate }) 372 | 373 | function validate (username, password, _req, _res) { 374 | if (username === 'user' && password === 'pwd') { 375 | return Promise.resolve() 376 | } else { 377 | return Promise.reject(new Error('Winter is coming')) 378 | } 379 | } 380 | 381 | fastify.after(() => { 382 | fastify.route({ 383 | method: 'GET', 384 | url: '/', 385 | preHandler: fastify.basicAuth, 386 | handler: (_req, reply) => { 387 | reply.send({ hello: 'world' }) 388 | } 389 | }) 390 | }) 391 | 392 | const { statusCode, body } = await fastify.inject({ 393 | url: '/', 394 | method: 'GET', 395 | headers: { 396 | authorization: basicAuthHeader('user', 'pwdd') 397 | } 398 | }) 399 | t.assert.ok(body) 400 | t.assert.strictEqual(statusCode, 401) 401 | t.assert.deepStrictEqual(JSON.parse(body), { 402 | error: 'Unauthorized', 403 | message: 'Winter is coming', 404 | statusCode: 401 405 | }) 406 | }) 407 | 408 | test('WWW-Authenticate (authenticate: true)', async t => { 409 | t.plan(6) 410 | 411 | const fastify = Fastify() 412 | const authenticate = true 413 | fastify.register(basicAuth, { validate, authenticate, utf8: false }) 414 | 415 | function validate (username, password, _req, _res, done) { 416 | if (username === 'user' && password === 'pwd') { 417 | done() 418 | } else { 419 | done(new Error('Unauthorized')) 420 | } 421 | } 422 | 423 | fastify.after(() => { 424 | fastify.route({ 425 | method: 'GET', 426 | url: '/', 427 | preHandler: fastify.basicAuth, 428 | handler: (_req, reply) => { 429 | reply.send({ hello: 'world' }) 430 | } 431 | }) 432 | }) 433 | 434 | const res1 = await fastify.inject({ 435 | url: '/', 436 | method: 'GET' 437 | }) 438 | t.assert.ok(res1.body) 439 | t.assert.strictEqual(res1.headers['www-authenticate'], 'Basic') 440 | t.assert.strictEqual(res1.statusCode, 401) 441 | 442 | const res2 = await fastify.inject({ 443 | url: '/', 444 | method: 'GET', 445 | headers: { 446 | authorization: basicAuthHeader('user', 'pwd') 447 | } 448 | }) 449 | t.assert.ok(res2.body) 450 | t.assert.strictEqual(res2.headers['www-authenticate'], undefined) 451 | t.assert.strictEqual(res2.statusCode, 200) 452 | }) 453 | 454 | test('WWW-Authenticate (authenticate: false)', async t => { 455 | t.plan(6) 456 | 457 | const fastify = Fastify() 458 | const authenticate = false 459 | fastify.register(basicAuth, { validate, authenticate, utf8: false }) 460 | 461 | function validate (username, password, _req, _res, done) { 462 | if (username === 'user' && password === 'pwd') { 463 | done() 464 | } else { 465 | done(new Error('Unauthorized')) 466 | } 467 | } 468 | 469 | fastify.after(() => { 470 | fastify.route({ 471 | method: 'GET', 472 | url: '/', 473 | preHandler: fastify.basicAuth, 474 | handler: (_req, reply) => { 475 | reply.send({ hello: 'world' }) 476 | } 477 | }) 478 | }) 479 | 480 | const res1 = await fastify.inject({ 481 | url: '/', 482 | method: 'GET' 483 | }) 484 | t.assert.ok(res1.body) 485 | t.assert.strictEqual(res1.headers['www-authenticate'], undefined) 486 | t.assert.strictEqual(res1.statusCode, 401) 487 | 488 | const res2 = await fastify.inject({ 489 | url: '/', 490 | method: 'GET', 491 | headers: { 492 | authorization: basicAuthHeader('user', 'pwd') 493 | } 494 | }) 495 | t.assert.ok(res2.body) 496 | t.assert.strictEqual(res2.headers['www-authenticate'], undefined) 497 | t.assert.strictEqual(res2.statusCode, 200) 498 | }) 499 | 500 | test('WWW-Authenticate Realm (authenticate: {realm: "example"}, utf8: false)', async t => { 501 | t.plan(6) 502 | 503 | const fastify = Fastify() 504 | const authenticate = { realm: 'example' } 505 | fastify.register(basicAuth, { validate, authenticate, utf8: false }) 506 | 507 | function validate (username, password, _req, _res, done) { 508 | if (username === 'user' && password === 'pwd') { 509 | done() 510 | } else { 511 | done(new Error('Unauthorized')) 512 | } 513 | } 514 | 515 | fastify.after(() => { 516 | fastify.route({ 517 | method: 'GET', 518 | url: '/', 519 | preHandler: fastify.basicAuth, 520 | handler: (_req, reply) => { 521 | reply.send({ hello: 'world' }) 522 | } 523 | }) 524 | }) 525 | 526 | const res1 = await fastify.inject({ 527 | url: '/', 528 | method: 'GET' 529 | }) 530 | t.assert.ok(res1.body) 531 | t.assert.strictEqual(res1.headers['www-authenticate'], 'Basic realm="example"') 532 | t.assert.strictEqual(res1.statusCode, 401) 533 | 534 | const res2 = await fastify.inject({ 535 | url: '/', 536 | method: 'GET', 537 | headers: { 538 | authorization: basicAuthHeader('user', 'pwd') 539 | } 540 | }) 541 | t.assert.ok(res2.body) 542 | t.assert.strictEqual(res2.headers['www-authenticate'], undefined) 543 | t.assert.strictEqual(res2.statusCode, 200) 544 | }) 545 | 546 | test('WWW-Authenticate Realm (authenticate: {realm: "example"}, utf8: true)', async t => { 547 | t.plan(6) 548 | 549 | const fastify = Fastify() 550 | const authenticate = { realm: 'example' } 551 | fastify.register(basicAuth, { validate, authenticate, utf8: true }) 552 | 553 | function validate (username, password, _req, _res, done) { 554 | if (username === 'user' && password === 'pwd') { 555 | done() 556 | } else { 557 | done(new Error('Unauthorized')) 558 | } 559 | } 560 | 561 | fastify.after(() => { 562 | fastify.route({ 563 | method: 'GET', 564 | url: '/', 565 | preHandler: fastify.basicAuth, 566 | handler: (_req, reply) => { 567 | reply.send({ hello: 'world' }) 568 | } 569 | }) 570 | }) 571 | 572 | const res1 = await fastify.inject({ 573 | url: '/', 574 | method: 'GET' 575 | }) 576 | t.assert.ok(res1.body) 577 | t.assert.strictEqual(res1.headers['www-authenticate'], 'Basic realm="example", charset="UTF-8"') 578 | t.assert.strictEqual(res1.statusCode, 401) 579 | 580 | const res2 = await fastify.inject({ 581 | url: '/', 582 | method: 'GET', 583 | headers: { 584 | authorization: basicAuthHeader('user', 'pwd') 585 | } 586 | }) 587 | t.assert.ok(res2.body) 588 | t.assert.strictEqual(res2.headers['www-authenticate'], undefined) 589 | t.assert.strictEqual(res2.statusCode, 200) 590 | }) 591 | 592 | test('WWW-Authenticate Custom Header (authenticate: {realm: "example", header: "x-custom-authenticate" }, utf8: false)', async t => { 593 | t.plan(8) 594 | 595 | const fastify = Fastify() 596 | const authenticate = { realm: 'example', header: 'x-custom-authenticate' } 597 | fastify.register(basicAuth, { validate, authenticate, utf8: false }) 598 | 599 | function validate (username, password, _req, _res, done) { 600 | if (username === 'user' && password === 'pwd') { 601 | done() 602 | } else { 603 | done(new Error('Unauthorized')) 604 | } 605 | } 606 | 607 | fastify.after(() => { 608 | fastify.route({ 609 | method: 'GET', 610 | url: '/', 611 | preHandler: fastify.basicAuth, 612 | handler: (_req, reply) => { 613 | reply.send({ hello: 'world' }) 614 | } 615 | }) 616 | }) 617 | 618 | const res1 = await fastify.inject({ 619 | url: '/', 620 | method: 'GET' 621 | }) 622 | t.assert.ok(res1.body) 623 | t.assert.strictEqual(res1.headers['x-custom-authenticate'], 'Basic realm="example"') 624 | t.assert.strictEqual(res1.headers['www-authenticate'], undefined) 625 | t.assert.strictEqual(res1.statusCode, 401) 626 | 627 | const res2 = await fastify.inject({ 628 | url: '/', 629 | method: 'GET', 630 | headers: { 631 | authorization: basicAuthHeader('user', 'pwd') 632 | } 633 | }) 634 | t.assert.ok(res2.body) 635 | t.assert.strictEqual(res2.headers['x-custom-authenticate'], undefined) 636 | t.assert.strictEqual(res2.headers['www-authenticate'], undefined) 637 | t.assert.strictEqual(res2.statusCode, 200) 638 | }) 639 | 640 | test('WWW-Authenticate Custom Header (authenticate: {realm: "example", header: "x-custom-authenticate" }, utf8: true)', async t => { 641 | t.plan(8) 642 | 643 | const fastify = Fastify() 644 | const authenticate = { realm: 'example', header: 'x-custom-authenticate' } 645 | fastify.register(basicAuth, { validate, authenticate, utf8: true }) 646 | 647 | function validate (username, password, _req, _res, done) { 648 | if (username === 'user' && password === 'pwd') { 649 | done() 650 | } else { 651 | done(new Error('Unauthorized')) 652 | } 653 | } 654 | 655 | fastify.after(() => { 656 | fastify.route({ 657 | method: 'GET', 658 | url: '/', 659 | preHandler: fastify.basicAuth, 660 | handler: (_req, reply) => { 661 | reply.send({ hello: 'world' }) 662 | } 663 | }) 664 | }) 665 | 666 | const res1 = await fastify.inject({ 667 | url: '/', 668 | method: 'GET' 669 | }) 670 | t.assert.ok(res1.body) 671 | t.assert.strictEqual(res1.headers['x-custom-authenticate'], 'Basic realm="example", charset="UTF-8"') 672 | t.assert.strictEqual(res1.headers['www-authenticate'], undefined) 673 | t.assert.strictEqual(res1.statusCode, 401) 674 | 675 | const res2 = await fastify.inject({ 676 | url: '/', 677 | method: 'GET', 678 | headers: { 679 | authorization: basicAuthHeader('user', 'pwd') 680 | } 681 | }) 682 | t.assert.ok(res2.body) 683 | t.assert.strictEqual(res2.headers['x-custom-authenticate'], undefined) 684 | t.assert.strictEqual(res2.headers['www-authenticate'], undefined) 685 | t.assert.strictEqual(res2.statusCode, 200) 686 | }) 687 | 688 | test("Proxy authentication (proxyMode: true, authenticate: { realm: 'example' }, utf8: true)", async t => { 689 | t.plan(12) 690 | 691 | const fastify = Fastify() 692 | const authenticate = { realm: 'example' } 693 | fastify.register(basicAuth, { validate, authenticate, utf8: true, proxyMode: true }) 694 | 695 | function validate (username, password, _req, _res, done) { 696 | if (username === 'user' && password === 'pwd') { 697 | done() 698 | } else { 699 | done(new Error('Unauthorized')) 700 | } 701 | } 702 | 703 | fastify.after(() => { 704 | fastify.route({ 705 | method: 'GET', 706 | url: '/', 707 | preHandler: fastify.basicAuth, 708 | handler: (_req, reply) => { 709 | reply.send({ hello: 'world' }) 710 | } 711 | }) 712 | }) 713 | 714 | const res1 = await fastify.inject({ 715 | url: '/', 716 | method: 'GET' 717 | }) 718 | t.assert.ok(res1.body) 719 | t.assert.strictEqual(res1.headers['proxy-authenticate'], 'Basic realm="example", charset="UTF-8"') 720 | t.assert.strictEqual(res1.headers['www-authenticate'], undefined) 721 | t.assert.strictEqual(res1.statusCode, 407) 722 | 723 | const res2 = await fastify.inject({ 724 | url: '/', 725 | method: 'GET', 726 | headers: { 727 | authorization: basicAuthHeader('user', 'pwd') 728 | } 729 | }) 730 | 731 | t.assert.ok(res2.body) 732 | t.assert.strictEqual(res2.headers['proxy-authenticate'], 'Basic realm="example", charset="UTF-8"') 733 | t.assert.strictEqual(res2.headers['www-authenticate'], undefined) 734 | t.assert.strictEqual(res2.statusCode, 407) 735 | 736 | const res3 = await fastify.inject({ 737 | url: '/', 738 | method: 'GET', 739 | headers: { 740 | 'proxy-authorization': basicAuthHeader('user', 'pwd') 741 | } 742 | }) 743 | 744 | t.assert.ok(res3.body) 745 | t.assert.strictEqual(res3.headers['proxy-authenticate'], undefined) 746 | t.assert.strictEqual(res3.headers['www-authenticate'], undefined) 747 | t.assert.strictEqual(res3.statusCode, 200) 748 | }) 749 | 750 | test('Header option specified', async t => { 751 | t.plan(2) 752 | 753 | const fastify = Fastify() 754 | fastify.register(basicAuth, { 755 | validate, 756 | header: 'X-Forwarded-Authorization' 757 | }) 758 | 759 | function validate (username, password, _req, _res, done) { 760 | if (username === 'user' && password === 'pwd') { 761 | done() 762 | } else { 763 | done(new Error('Unauthorized')) 764 | } 765 | } 766 | 767 | fastify.after(() => { 768 | fastify.route({ 769 | method: 'GET', 770 | url: '/', 771 | preHandler: fastify.basicAuth, 772 | handler: (_req, reply) => { 773 | reply.send({ hello: 'world' }) 774 | } 775 | }) 776 | }) 777 | 778 | const { statusCode, body } = await fastify.inject({ 779 | url: '/', 780 | method: 'GET', 781 | headers: { 782 | authorization: basicAuthHeader('notuser', 'notpwd'), 783 | 'x-forwarded-authorization': basicAuthHeader('user', 'pwd') 784 | } 785 | }) 786 | t.assert.ok(body) 787 | t.assert.strictEqual(statusCode, 200) 788 | }) 789 | 790 | test('Missing validate function', async t => { 791 | t.plan(2) 792 | 793 | const fastify = Fastify() 794 | fastify.register(basicAuth) 795 | 796 | await t.assert.rejects( 797 | async () => fastify.ready(), 798 | (err) => { 799 | t.assert.strictEqual(err.message, 'Basic Auth: Missing validate function') 800 | return true 801 | } 802 | ) 803 | }) 804 | 805 | test('Hook - 401', async t => { 806 | t.plan(3) 807 | 808 | const fastify = Fastify() 809 | fastify 810 | .register(basicAuth, { validate }) 811 | 812 | function validate (username, password, _req, _res, done) { 813 | if (username === 'user' && password === 'pwd') { 814 | done() 815 | } else { 816 | done(new Error('Winter is coming')) 817 | } 818 | } 819 | 820 | fastify.after(() => { 821 | fastify.addHook('preHandler', fastify.basicAuth) 822 | fastify.route({ 823 | method: 'GET', 824 | url: '/', 825 | handler: (_req, reply) => { 826 | reply.send({ hello: 'world' }) 827 | } 828 | }) 829 | }) 830 | 831 | const { statusCode, body } = await fastify.inject({ 832 | url: '/', 833 | method: 'GET', 834 | headers: { 835 | authorization: basicAuthHeader('user', 'pwdd') 836 | } 837 | }) 838 | t.assert.ok(body) 839 | t.assert.strictEqual(statusCode, 401) 840 | t.assert.deepStrictEqual(JSON.parse(body), { 841 | error: 'Unauthorized', 842 | message: 'Winter is coming', 843 | statusCode: 401 844 | }) 845 | }) 846 | 847 | test('With @fastify/auth - 401', async t => { 848 | t.plan(3) 849 | 850 | const fastify = Fastify() 851 | fastify 852 | .register(fastifyAuth) 853 | .register(basicAuth, { validate }) 854 | 855 | function validate (username, password, _req, _res, done) { 856 | if (username === 'user' && password === 'pwd') { 857 | done() 858 | } else { 859 | done(new Error('Winter is coming')) 860 | } 861 | } 862 | 863 | fastify.after(() => { 864 | fastify.route({ 865 | method: 'GET', 866 | url: '/', 867 | preHandler: fastify.auth([fastify.basicAuth]), 868 | handler: (_req, reply) => { 869 | reply.send({ hello: 'world' }) 870 | } 871 | }) 872 | }) 873 | 874 | const { statusCode, body } = await fastify.inject({ 875 | url: '/', 876 | method: 'GET', 877 | headers: { 878 | authorization: basicAuthHeader('user', 'pwdd') 879 | } 880 | }) 881 | t.assert.ok(body) 882 | t.assert.strictEqual(statusCode, 401) 883 | t.assert.deepStrictEqual(JSON.parse(body), { 884 | error: 'Unauthorized', 885 | message: 'Winter is coming', 886 | statusCode: 401 887 | }) 888 | }) 889 | 890 | test('Hook with @fastify/auth- 401', async t => { 891 | t.plan(3) 892 | 893 | const fastify = Fastify() 894 | fastify 895 | .register(fastifyAuth) 896 | .register(basicAuth, { validate }) 897 | 898 | function validate (username, password, _req, _res, done) { 899 | if (username === 'user' && password === 'pwd') { 900 | done() 901 | } else { 902 | done(new Error('Winter is coming')) 903 | } 904 | } 905 | 906 | fastify.after(() => { 907 | fastify.addHook('preHandler', fastify.auth([fastify.basicAuth])) 908 | fastify.route({ 909 | method: 'GET', 910 | url: '/', 911 | handler: (_req, reply) => { 912 | reply.send({ hello: 'world' }) 913 | } 914 | }) 915 | }) 916 | 917 | const { statusCode, body } = await fastify.inject({ 918 | url: '/', 919 | method: 'GET', 920 | headers: { 921 | authorization: basicAuthHeader('user', 'pwdd') 922 | } 923 | }) 924 | t.assert.ok(body) 925 | t.assert.strictEqual(statusCode, 401) 926 | t.assert.deepStrictEqual(JSON.parse(body), { 927 | error: 'Unauthorized', 928 | message: 'Winter is coming', 929 | statusCode: 401 930 | }) 931 | }) 932 | 933 | test('Missing header', async t => { 934 | t.plan(3) 935 | 936 | const fastify = Fastify() 937 | fastify.register(basicAuth, { validate }) 938 | 939 | function validate (username, password, _req, _res, done) { 940 | if (username === 'user' && password === 'pwd') { 941 | done() 942 | } else { 943 | done(new Error('Unauthorized')) 944 | } 945 | } 946 | 947 | fastify.after(() => { 948 | fastify.route({ 949 | method: 'GET', 950 | url: '/', 951 | preHandler: fastify.basicAuth, 952 | handler: (_req, reply) => { 953 | reply.send({ hello: 'world' }) 954 | } 955 | }) 956 | }) 957 | 958 | const { statusCode, body } = await fastify.inject({ 959 | url: '/', 960 | method: 'GET' 961 | }) 962 | t.assert.ok(body) 963 | t.assert.strictEqual(statusCode, 401) 964 | t.assert.deepStrictEqual(JSON.parse(body), { 965 | statusCode: 401, 966 | code: 'FST_BASIC_AUTH_MISSING_OR_BAD_AUTHORIZATION_HEADER', 967 | error: 'Unauthorized', 968 | message: 'Missing or bad formatted authorization header' 969 | }) 970 | }) 971 | 972 | test('Fastify context', async t => { 973 | t.plan(3) 974 | 975 | const fastify = Fastify() 976 | fastify.decorate('test', true) 977 | fastify.register(basicAuth, { validate }) 978 | 979 | function validate (username, password, _req, _res, done) { 980 | t.assert.ok(this.test) 981 | if (username === 'user' && password === 'pwd') { 982 | done() 983 | } else { 984 | done(new Error('Unauthorized')) 985 | } 986 | } 987 | 988 | fastify.after(() => { 989 | fastify.route({ 990 | method: 'GET', 991 | url: '/', 992 | preHandler: fastify.basicAuth, 993 | handler: (_req, reply) => { 994 | reply.send({ hello: 'world' }) 995 | } 996 | }) 997 | }) 998 | 999 | const { statusCode, body } = await fastify.inject({ 1000 | url: '/', 1001 | method: 'GET', 1002 | headers: { 1003 | authorization: basicAuthHeader('user', 'pwd') 1004 | } 1005 | }) 1006 | t.assert.ok(body) 1007 | t.assert.strictEqual(statusCode, 200) 1008 | }) 1009 | 1010 | test('setErrorHandler custom error and 401', async t => { 1011 | t.plan(4) 1012 | 1013 | const fastify = Fastify() 1014 | fastify 1015 | .register(fastifyAuth) 1016 | .register(basicAuth, { validate }) 1017 | 1018 | function validate (_username, _password, _req, _res, done) { 1019 | done(new Error('Winter is coming')) 1020 | } 1021 | 1022 | fastify.after(() => { 1023 | fastify.addHook('preHandler', fastify.auth([fastify.basicAuth])) 1024 | fastify.route({ 1025 | method: 'GET', 1026 | url: '/', 1027 | handler: (_req, reply) => { 1028 | reply.send({ hello: 'world' }) 1029 | } 1030 | }) 1031 | }) 1032 | 1033 | fastify.setErrorHandler(function (err, _req, reply) { 1034 | t.assert.strictEqual(err.statusCode, 401) 1035 | reply.send(err) 1036 | }) 1037 | 1038 | const { statusCode, body } = await fastify.inject({ 1039 | url: '/', 1040 | method: 'GET', 1041 | headers: { 1042 | authorization: basicAuthHeader('user', 'pwdd') 1043 | } 1044 | }) 1045 | t.assert.ok(body) 1046 | t.assert.strictEqual(statusCode, 401) 1047 | t.assert.deepStrictEqual(JSON.parse(body), { 1048 | error: 'Unauthorized', 1049 | message: 'Winter is coming', 1050 | statusCode: 401 1051 | }) 1052 | }) 1053 | 1054 | test('Missing header and custom error handler', async t => { 1055 | t.plan(6) 1056 | 1057 | const fastify = Fastify() 1058 | fastify.register(basicAuth, { validate }) 1059 | 1060 | function validate (username, password, _req, _res, done) { 1061 | if (username === 'user' && password === 'pwd') { 1062 | done() 1063 | } else { 1064 | done(new Error('Unauthorized')) 1065 | } 1066 | } 1067 | 1068 | fastify.after(() => { 1069 | fastify.route({ 1070 | method: 'GET', 1071 | url: '/', 1072 | preHandler: fastify.basicAuth, 1073 | handler: (_req, reply) => { 1074 | reply.send({ hello: 'world' }) 1075 | } 1076 | }) 1077 | }) 1078 | 1079 | fastify.setErrorHandler(function (err, _req, reply) { 1080 | t.assert.ok(err instanceof Error) 1081 | t.assert.ok(err.statusCode === 401) 1082 | t.assert.ok(err.code === 'FST_BASIC_AUTH_MISSING_OR_BAD_AUTHORIZATION_HEADER') 1083 | reply.send(err) 1084 | }) 1085 | 1086 | const { statusCode, body } = await fastify.inject({ 1087 | url: '/', 1088 | method: 'GET' 1089 | }) 1090 | t.assert.ok(body) 1091 | t.assert.strictEqual(statusCode, 401) 1092 | t.assert.deepStrictEqual(JSON.parse(body), { 1093 | statusCode: 401, 1094 | code: 'FST_BASIC_AUTH_MISSING_OR_BAD_AUTHORIZATION_HEADER', 1095 | error: 'Unauthorized', 1096 | message: 'Missing or bad formatted authorization header' 1097 | }) 1098 | }) 1099 | 1100 | test('Invalid options (authenticate)', async t => { 1101 | t.plan(2) 1102 | 1103 | const fastify = Fastify() 1104 | fastify 1105 | .register(basicAuth, { validate, authenticate: 'i am invalid' }) 1106 | 1107 | async function validate (username, password, _req, _res) { 1108 | if (username !== 'user' && password !== 'pwd') { 1109 | return new Error('Unauthorized') 1110 | } 1111 | } 1112 | 1113 | await t.assert.rejects( 1114 | async () => fastify.ready(), 1115 | (err) => { 1116 | t.assert.strictEqual(err.message, 'Basic Auth: Invalid authenticate option') 1117 | return true 1118 | } 1119 | ) 1120 | }) 1121 | 1122 | test('authenticate: true, utf8: true', async t => { 1123 | t.plan(6) 1124 | 1125 | const fastify = Fastify() 1126 | fastify 1127 | .register(basicAuth, { validate, authenticate: true, utf8: true }) 1128 | 1129 | function validate (username, password, _req, _res, done) { 1130 | if (username === 'user' && password === 'pwd') { 1131 | done() 1132 | } else { 1133 | done(new Error('Unauthorized')) 1134 | } 1135 | } 1136 | 1137 | fastify.after(() => { 1138 | fastify.route({ 1139 | method: 'GET', 1140 | url: '/', 1141 | preHandler: fastify.basicAuth, 1142 | handler: (_req, reply) => { 1143 | reply.send({ hello: 'world' }) 1144 | } 1145 | }) 1146 | }) 1147 | 1148 | let res = await fastify.inject({ 1149 | url: '/', 1150 | method: 'GET' 1151 | }) 1152 | t.assert.ok(res.body) 1153 | t.assert.strictEqual(res.headers['www-authenticate'], 'Basic charset="UTF-8"') 1154 | t.assert.strictEqual(res.statusCode, 401) 1155 | 1156 | res = await fastify.inject({ 1157 | url: '/', 1158 | method: 'GET', 1159 | headers: { 1160 | authorization: basicAuthHeader('user', 'pwd') 1161 | } 1162 | }) 1163 | t.assert.ok(res.body) 1164 | t.assert.strictEqual(res.headers['www-authenticate'], undefined) 1165 | t.assert.strictEqual(res.statusCode, 200) 1166 | }) 1167 | 1168 | test('authenticate realm: false, utf8: true', async t => { 1169 | t.plan(6) 1170 | 1171 | const fastify = Fastify() 1172 | fastify 1173 | .register(basicAuth, { validate, authenticate: { realm: false }, utf8: true }) 1174 | 1175 | function validate (username, password, _req, _res, done) { 1176 | if (username === 'user' && password === 'pwd') { 1177 | done() 1178 | } else { 1179 | done(new Error('Unauthorized')) 1180 | } 1181 | } 1182 | 1183 | fastify.after(() => { 1184 | fastify.route({ 1185 | method: 'GET', 1186 | url: '/', 1187 | preHandler: fastify.basicAuth, 1188 | handler: (_req, reply) => { 1189 | reply.send({ hello: 'world' }) 1190 | } 1191 | }) 1192 | }) 1193 | 1194 | let res = await fastify.inject({ 1195 | url: '/', 1196 | method: 'GET' 1197 | }) 1198 | t.assert.ok(res.body) 1199 | t.assert.strictEqual(res.headers['www-authenticate'], 'Basic charset="UTF-8"') 1200 | t.assert.strictEqual(res.statusCode, 401) 1201 | 1202 | res = await fastify.inject({ 1203 | url: '/', 1204 | method: 'GET', 1205 | headers: { 1206 | authorization: basicAuthHeader('user', 'pwd') 1207 | } 1208 | }) 1209 | t.assert.ok(res.body) 1210 | t.assert.strictEqual(res.headers['www-authenticate'], undefined) 1211 | t.assert.strictEqual(res.statusCode, 200) 1212 | }) 1213 | 1214 | test('Invalid options (authenticate realm, utf8: false)', async t => { 1215 | t.plan(6) 1216 | 1217 | const fastify = Fastify() 1218 | await fastify 1219 | .register(basicAuth, { validate, authenticate: { realm: true }, utf8: false }) 1220 | 1221 | function validate (username, password, _req, _res, done) { 1222 | if (username === 'user' && password === 'pwd') { 1223 | done() 1224 | } else { 1225 | done(new Error('Unauthorized')) 1226 | } 1227 | } 1228 | 1229 | fastify.after(() => { 1230 | fastify.route({ 1231 | method: 'GET', 1232 | url: '/', 1233 | preHandler: fastify.basicAuth, 1234 | handler: (_req, reply) => { 1235 | reply.send({ hello: 'world' }) 1236 | } 1237 | }) 1238 | }) 1239 | 1240 | let res = await fastify.inject({ 1241 | url: '/', 1242 | method: 'GET' 1243 | }) 1244 | t.assert.ok(res.body) 1245 | t.assert.strictEqual(res.headers['www-authenticate'], 'Basic') 1246 | t.assert.strictEqual(res.statusCode, 401) 1247 | 1248 | res = await fastify.inject({ 1249 | url: '/', 1250 | method: 'GET', 1251 | headers: { 1252 | authorization: basicAuthHeader('user', 'pwd') 1253 | } 1254 | }) 1255 | t.assert.ok(res.body) 1256 | t.assert.strictEqual(res.headers['www-authenticate'], undefined) 1257 | t.assert.strictEqual(res.statusCode, 200) 1258 | }) 1259 | 1260 | test('Invalid options (authenticate realm), utf8: true', async t => { 1261 | t.plan(6) 1262 | 1263 | const fastify = Fastify() 1264 | fastify 1265 | .register(basicAuth, { validate, utf8: true, authenticate: { realm: true } }) 1266 | 1267 | function validate (username, password, _req, _res, done) { 1268 | if (username === 'user' && password === 'pwd') { 1269 | done() 1270 | } else { 1271 | done(new Error('Unauthorized')) 1272 | } 1273 | } 1274 | 1275 | fastify.after(() => { 1276 | fastify.route({ 1277 | method: 'GET', 1278 | url: '/', 1279 | preHandler: fastify.basicAuth, 1280 | handler: (_req, reply) => { 1281 | reply.send({ hello: 'world' }) 1282 | } 1283 | }) 1284 | }) 1285 | 1286 | let res = await fastify.inject({ 1287 | url: '/', 1288 | method: 'GET' 1289 | }) 1290 | t.assert.ok(res.body) 1291 | t.assert.strictEqual(res.headers['www-authenticate'], 'Basic charset="UTF-8"') 1292 | t.assert.strictEqual(res.statusCode, 401) 1293 | 1294 | res = await fastify.inject({ 1295 | url: '/', 1296 | method: 'GET', 1297 | headers: { 1298 | authorization: basicAuthHeader('user', 'pwd') 1299 | } 1300 | }) 1301 | t.assert.ok(res.body) 1302 | t.assert.strictEqual(res.headers['www-authenticate'], undefined) 1303 | t.assert.strictEqual(res.statusCode, 200) 1304 | }) 1305 | 1306 | test('Invalid options (authenticate realm = undefined, utf8: false)', async t => { 1307 | t.plan(6) 1308 | 1309 | const fastify = Fastify() 1310 | fastify 1311 | .register(basicAuth, { validate, authenticate: { realm: undefined }, utf8: false }) 1312 | 1313 | function validate (username, password, _req, _res, done) { 1314 | if (username === 'user' && password === 'pwd') { 1315 | done() 1316 | } else { 1317 | done(new Error('Unauthorized')) 1318 | } 1319 | } 1320 | 1321 | fastify.after(() => { 1322 | fastify.route({ 1323 | method: 'GET', 1324 | url: '/', 1325 | preHandler: fastify.basicAuth, 1326 | handler: (_req, reply) => { 1327 | reply.send({ hello: 'world' }) 1328 | } 1329 | }) 1330 | }) 1331 | 1332 | let res = await fastify.inject({ 1333 | url: '/', 1334 | method: 'GET' 1335 | }) 1336 | t.assert.ok(res.body) 1337 | t.assert.strictEqual(res.headers['www-authenticate'], 'Basic') 1338 | t.assert.strictEqual(res.statusCode, 401) 1339 | 1340 | res = await fastify.inject({ 1341 | url: '/', 1342 | method: 'GET', 1343 | headers: { 1344 | authorization: basicAuthHeader('user', 'pwd') 1345 | } 1346 | }) 1347 | t.assert.ok(res.body) 1348 | t.assert.strictEqual(res.headers['www-authenticate'], undefined) 1349 | t.assert.strictEqual(res.statusCode, 200) 1350 | }) 1351 | 1352 | test('WWW-Authenticate Realm (authenticate: {realm (req) { }}, utf8: false)', async t => { 1353 | t.plan(7) 1354 | 1355 | const fastify = Fastify() 1356 | const authenticate = { 1357 | realm (req) { 1358 | t.assert.strictEqual(req.url, '/') 1359 | return 'root' 1360 | } 1361 | } 1362 | fastify.register(basicAuth, { validate, authenticate, utf8: false }) 1363 | 1364 | function validate (username, password, _req, _res, done) { 1365 | if (username === 'user' && password === 'pwd') { 1366 | done() 1367 | } else { 1368 | done(new Error('Unauthorized')) 1369 | } 1370 | } 1371 | 1372 | fastify.after(() => { 1373 | fastify.route({ 1374 | method: 'GET', 1375 | url: '/', 1376 | preHandler: fastify.basicAuth, 1377 | handler: (_req, reply) => { 1378 | reply.send({ hello: 'world' }) 1379 | } 1380 | }) 1381 | }) 1382 | 1383 | let res = await fastify.inject({ 1384 | url: '/', 1385 | method: 'GET' 1386 | }) 1387 | t.assert.ok(res.body) 1388 | t.assert.strictEqual(res.headers['www-authenticate'], 'Basic realm="root"') 1389 | t.assert.strictEqual(res.statusCode, 401) 1390 | 1391 | res = await fastify.inject({ 1392 | url: '/', 1393 | method: 'GET', 1394 | headers: { 1395 | authorization: basicAuthHeader('user', 'pwd') 1396 | } 1397 | }) 1398 | t.assert.ok(res.body) 1399 | t.assert.strictEqual(res.headers['www-authenticate'], undefined) 1400 | t.assert.strictEqual(res.statusCode, 200) 1401 | }) 1402 | 1403 | test('WWW-Authenticate Realm (authenticate: {realm (req) { }}), utf8', async t => { 1404 | t.plan(7) 1405 | 1406 | const fastify = Fastify() 1407 | const authenticate = { 1408 | realm (req) { 1409 | t.assert.strictEqual(req.url, '/') 1410 | return 'root' 1411 | } 1412 | } 1413 | fastify.register(basicAuth, { validate, authenticate, utf8: true }) 1414 | 1415 | function validate (username, password, _req, _res, done) { 1416 | if (username === 'user' && password === 'pwd') { 1417 | done() 1418 | } else { 1419 | done(new Error('Unauthorized')) 1420 | } 1421 | } 1422 | 1423 | fastify.after(() => { 1424 | fastify.route({ 1425 | method: 'GET', 1426 | url: '/', 1427 | preHandler: fastify.basicAuth, 1428 | handler: (_req, reply) => { 1429 | reply.send({ hello: 'world' }) 1430 | } 1431 | }) 1432 | }) 1433 | 1434 | let res = await fastify.inject({ 1435 | url: '/', 1436 | method: 'GET' 1437 | }) 1438 | t.assert.ok(res.body) 1439 | t.assert.strictEqual(res.headers['www-authenticate'], 'Basic realm="root", charset="UTF-8"') 1440 | t.assert.strictEqual(res.statusCode, 401) 1441 | 1442 | res = await fastify.inject({ 1443 | url: '/', 1444 | method: 'GET', 1445 | headers: { 1446 | authorization: basicAuthHeader('user', 'pwd') 1447 | } 1448 | }) 1449 | t.assert.ok(res.body) 1450 | t.assert.strictEqual(res.headers['www-authenticate'], undefined) 1451 | t.assert.strictEqual(res.statusCode, 200) 1452 | }) 1453 | 1454 | test('No 401 no realm', async t => { 1455 | t.plan(4) 1456 | 1457 | const fastify = Fastify() 1458 | fastify.register(basicAuth, { validate, authenticate: true }) 1459 | 1460 | function validate (_username, _password, _req, _res) { 1461 | const err = new Error('Winter is coming') 1462 | err.statusCode = 402 1463 | return Promise.reject(err) 1464 | } 1465 | 1466 | fastify.after(() => { 1467 | fastify.route({ 1468 | method: 'GET', 1469 | url: '/', 1470 | preHandler: fastify.basicAuth, 1471 | handler: (_req, reply) => { 1472 | reply.send({ hello: 'world' }) 1473 | } 1474 | }) 1475 | }) 1476 | 1477 | const { headers, statusCode, body } = await fastify.inject({ 1478 | url: '/', 1479 | method: 'GET', 1480 | headers: { 1481 | authorization: basicAuthHeader('user', 'pwdd') 1482 | } 1483 | }) 1484 | t.assert.ok(body) 1485 | t.assert.strictEqual(statusCode, 402) 1486 | t.assert.strictEqual(headers['www-authenticate'], undefined) 1487 | t.assert.deepStrictEqual(JSON.parse(body), { 1488 | error: 'Payment Required', 1489 | message: 'Winter is coming', 1490 | statusCode: 402 1491 | }) 1492 | }) 1493 | 1494 | function basicAuthHeader (username, password) { 1495 | return 'Basic ' + Buffer.from(`${username}:${password}`).toString('base64') 1496 | } 1497 | -------------------------------------------------------------------------------- /types/index.d.ts: -------------------------------------------------------------------------------- 1 | import { 2 | FastifyRequest, 3 | FastifyPluginAsync, 4 | FastifyReply, 5 | onRequestHookHandler, 6 | preValidationHookHandler, 7 | preHandlerHookHandler, 8 | FastifyInstance 9 | } from 'fastify' 10 | 11 | declare module 'fastify' { 12 | interface FastifyInstance { 13 | basicAuth: onRequestHookHandler | 14 | preValidationHookHandler | 15 | preHandlerHookHandler 16 | } 17 | } 18 | 19 | type FastifyBasicAuth = FastifyPluginAsync 20 | 21 | declare namespace fastifyBasicAuth { 22 | export interface FastifyBasicAuthOptions { 23 | validate( 24 | this: FastifyInstance, 25 | username: string, 26 | password: string, 27 | req: FastifyRequest, 28 | reply: FastifyReply, 29 | done: (err?: Error) => void 30 | ): void | Promise; 31 | authenticate?: boolean | { realm?: string | ((req: FastifyRequest) => string); header?: string }; 32 | proxyMode?: boolean; 33 | header?: string; 34 | strictCredentials?: boolean | undefined; 35 | utf8?: boolean | undefined; 36 | } 37 | 38 | export const fastifyBasicAuth: FastifyBasicAuth 39 | export { fastifyBasicAuth as default } 40 | } 41 | 42 | declare function fastifyBasicAuth (...params: Parameters): ReturnType 43 | export = fastifyBasicAuth 44 | -------------------------------------------------------------------------------- /types/index.test-d.ts: -------------------------------------------------------------------------------- 1 | import { expectType, expectAssignable } from 'tsd' 2 | import fastify, { 3 | FastifyRequest, 4 | FastifyReply, 5 | onRequestHookHandler, 6 | preValidationHookHandler, 7 | preHandlerHookHandler, 8 | FastifyInstance 9 | } from 'fastify' 10 | import fastifyBasicAuth from '..' 11 | 12 | const app = fastify() 13 | 14 | // validation ok 15 | app.register(fastifyBasicAuth, { 16 | validate: async function validatePromise (username, password, req, reply) { 17 | expectType(username) 18 | expectType(password) 19 | expectType(req) 20 | expectType(reply) 21 | expectType(this) 22 | }, 23 | header: 'x-forwarded-authorization' 24 | }) 25 | 26 | // validation failure 27 | app.register(fastifyBasicAuth, { 28 | validate: async function validatePromise (username, password, req, reply) { 29 | expectType(username) 30 | expectType(password) 31 | expectType(req) 32 | expectType(reply) 33 | expectType(this) 34 | return new Error('unauthorized') 35 | }, 36 | header: 'x-forwarded-authorization' 37 | }) 38 | 39 | app.register(fastifyBasicAuth, { 40 | validate: function validateCallback (username, password, req, reply, done) { 41 | expectType(username) 42 | expectType(password) 43 | expectType(req) 44 | expectType(reply) 45 | expectAssignable<(err?: Error) => void>(done) 46 | expectType(this) 47 | } 48 | }) 49 | 50 | // authenticate boolean 51 | app.register(fastifyBasicAuth, { 52 | validate: () => {}, 53 | authenticate: true 54 | }) 55 | 56 | // authenticate with realm 57 | app.register(fastifyBasicAuth, { 58 | validate: () => {}, 59 | authenticate: { realm: 'example' } 60 | }) 61 | 62 | // authenticate with realm (function) 63 | app.register(fastifyBasicAuth, { 64 | validate: () => {}, 65 | authenticate: { 66 | realm: function realm (req) { 67 | return req.url 68 | } 69 | } 70 | }) 71 | 72 | // authenticate with custom header 73 | app.register(fastifyBasicAuth, { 74 | validate: () => {}, 75 | authenticate: { header: 'x-custom-authenticate' } 76 | }) 77 | 78 | // authenticate in proxy mode 79 | app.register(fastifyBasicAuth, { 80 | validate: () => {}, 81 | proxyMode: true, 82 | authenticate: true, 83 | }) 84 | 85 | app.register(fastifyBasicAuth, { 86 | validate: () => {}, 87 | strictCredentials: true 88 | }) 89 | 90 | app.register(fastifyBasicAuth, { 91 | validate: () => {}, 92 | utf8: true 93 | }) 94 | 95 | app.register(fastifyBasicAuth, { 96 | validate: () => {}, 97 | strictCredentials: undefined, 98 | utf8: undefined 99 | }) 100 | 101 | expectAssignable(app.basicAuth) 102 | expectAssignable(app.basicAuth) 103 | expectAssignable(app.basicAuth) 104 | --------------------------------------------------------------------------------