Skip to content

Commit

Permalink
RFC6717 conformancy (#112)
Browse files Browse the repository at this point in the history
* integrate basic-auth

* make credentials more strict

* add strict option for fallback opportunity

* modify strictness

* handle charset auth-token in challenge properly

* add options to readme

* Update README.md

Co-authored-by: James Sumners <[email protected]>

* rename strict to strictCredentials

* Update README.md

Co-authored-by: Manuel Spigolon <[email protected]>

Co-authored-by: James Sumners <[email protected]>
Co-authored-by: Manuel Spigolon <[email protected]>
  • Loading branch information
3 people authored Dec 6, 2022
1 parent 3214bdb commit 330e6fb
Show file tree
Hide file tree
Showing 6 changed files with 649 additions and 38 deletions.
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,23 @@ fastify.setErrorHandler(function (err, req, reply) {

## Options

### `utf8` <Boolean> (optional, default: true)

User-ids or passwords containing characters outside the US-ASCII
character set will cause interoperability issues, unless both
communication partners agree on what character encoding scheme is to
be used. If utf8 is set to true the server will send the 'charset' parameter
to indicate a preference of "UTF-8", increasing the probability that
clients will switch to that encoding.

### `strictCredentials` <Boolean> (optional, default: true)

If strictCredentials is set to false the authorization header can contain
additional whitespaces at the beginning, in the midde and at the end of the
authorization header.
This is a fallback option to ensure the same behaviour as `@fastify/basic-auth`
version <=5.x.

### `validate` <Function> (required)

The `validate` function is called on each request made,
Expand Down
141 changes: 115 additions & 26 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
'use strict'

const fp = require('fastify-plugin')
const auth = require('basic-auth')
const createError = require('@fastify/error')

const MissingOrBadAuthorizationHeader = createError(
Expand All @@ -10,25 +9,117 @@ const MissingOrBadAuthorizationHeader = createError(
401
)

/**
* HTTP provides a simple challenge-response authentication framework
* that can be used by a server to challenge a client request and by a
* client to provide authentication information. It uses a case-
* insensitive token as a means to identify the authentication scheme,
* followed by additional information necessary for achieving
* authentication via that scheme.
*
* @see https://datatracker.ietf.org/doc/html/rfc7235#section-2.1
*
* The scheme name is "Basic".
* @see https://datatracker.ietf.org/doc/html/rfc7617#section-2
*/
const authScheme = '(?:[Bb][Aa][Ss][Ii][Cc])'
/**
* The BWS rule is used where the grammar allows optional whitespace
* only for historical reasons. A sender MUST NOT generate BWS in
* messages. A recipient MUST parse for such bad whitespace and remove
* it before interpreting the protocol element.
*
* @see https://datatracker.ietf.org/doc/html/rfc7230#section-3.2.3
*/
const BWS = '[ \t]'
/**
* The token68 syntax allows the 66 unreserved URI characters
* ([RFC3986]), plus a few others, so that it can hold a base64,
* base64url (URL and filename safe alphabet), base32, or base16 (hex)
* encoding, with or without padding, but excluding whitespace
* ([RFC4648]).
* @see https://datatracker.ietf.org/doc/html/rfc7235#section-2.1
*/
const token68 = '([A-Za-z0-9._~+/-]+=*)'

/**
* @see https://datatracker.ietf.org/doc/html/rfc7235#appendix-C
*/
const credentialsStrictRE = new RegExp(`^${authScheme} ${token68}$`)

const credentialsLaxRE = new RegExp(`^${BWS}*${authScheme}${BWS}+${token68}${BWS}*$`)

/**
* @see https://datatracker.ietf.org/doc/html/rfc5234#appendix-B.1
*/
const CTL = '[\x00-\x1F\x7F]'
const controlRE = new RegExp(CTL)

/**
* RegExp for basic auth user/pass
*
* user-pass = userid ":" password
* userid = *<TEXT excluding ":">
* password = *TEXT
*/

const userPassRE = /^([^:]*):(.*)$/

async function fastifyBasicAuth (fastify, opts) {
if (typeof opts.validate !== 'function') {
throw new Error('Basic Auth: Missing validate function')
}
const authenticateHeader = getAuthenticateHeader(opts.authenticate)

const strictCredentials = opts.strictCredentials ?? true
const useUtf8 = opts.utf8 ?? true
const charset = useUtf8 ? 'utf-8' : 'ascii'
const authenticateHeader = getAuthenticateHeader(opts.authenticate, useUtf8)
const header = (opts.header && opts.header.toLowerCase()) || 'authorization'

const credentialsRE = strictCredentials
? credentialsStrictRE
: credentialsLaxRE

const validate = opts.validate.bind(fastify)
fastify.decorate('basicAuth', basicAuth)

function basicAuth (req, reply, next) {
const credentials = auth.parse(req.headers[header])
if (credentials == null) {
const credentials = req.headers[header]

if (typeof credentials !== 'string') {
done(new MissingOrBadAuthorizationHeader())
} else {
const result = validate(credentials.name, credentials.pass, req, reply, done)
if (result && typeof result.then === 'function') {
result.then(done, done)
}
return
}

// parse header
const match = credentialsRE.exec(credentials)
if (match === null) {
done(new MissingOrBadAuthorizationHeader())
return
}

// decode user pass
const credentialsDecoded = Buffer.from(match[1], 'base64').toString(charset)

/**
* The user-id and password MUST NOT contain any control characters (see
* "CTL" in Appendix B.1 of [RFC5234]).
* @see https://datatracker.ietf.org/doc/html/rfc7617#section-2
*/
if (controlRE.test(credentialsDecoded)) {
done(new MissingOrBadAuthorizationHeader())
return
}

const userPass = userPassRE.exec(credentialsDecoded)
if (userPass === null) {
done(new MissingOrBadAuthorizationHeader())
return
}

const result = validate(userPass[1], userPass[2], req, reply, done)
if (result && typeof result.then === 'function') {
result.then(done, done)
}

function done (err) {
Expand All @@ -39,14 +130,7 @@ async function fastifyBasicAuth (fastify, opts) {
}

if (err.statusCode === 401) {
switch (typeof authenticateHeader) {
case 'string':
reply.header('WWW-Authenticate', authenticateHeader)
break
case 'function':
reply.header('WWW-Authenticate', authenticateHeader(req))
break
}
reply.header('WWW-Authenticate', authenticateHeader(req))
}
next(err)
} else {
Expand All @@ -56,24 +140,29 @@ async function fastifyBasicAuth (fastify, opts) {
}
}

function getAuthenticateHeader (authenticate) {
if (!authenticate) return false
function getAuthenticateHeader (authenticate, useUtf8) {
if (!authenticate) return () => false
if (authenticate === true) {
return 'Basic'
return useUtf8
? () => 'Basic charset="UTF-8"'
: () => 'Basic'
}
if (typeof authenticate === 'object') {
const realm = authenticate.realm
switch (typeof realm) {
case 'undefined':
return 'Basic'
case 'boolean':
return 'Basic'
return useUtf8
? () => 'Basic charset="UTF-8"'
: () => 'Basic'
case 'string':
return `Basic realm="${realm}"`
return useUtf8
? () => `Basic realm="${realm}", charset="UTF-8"`
: () => `Basic realm="${realm}"`
case 'function':
return function (req) {
return `Basic realm="${realm(req)}"`
}
return useUtf8
? (req) => `Basic realm="${realm(req)}", charset="UTF-8"`
: (req) => `Basic realm="${realm(req)}"`
}
}

Expand Down
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@
},
"dependencies": {
"@fastify/error": "^3.0.0",
"basic-auth": "^2.0.1",
"fastify-plugin": "^4.0.0"
},
"publishConfig": {
Expand Down
Loading

0 comments on commit 330e6fb

Please sign in to comment.