Skip to content

Commit

Permalink
feat: extend composite auth (#217)
Browse files Browse the repository at this point in the history
  • Loading branch information
yakovenkodenis authored Feb 2, 2024
1 parent af8e73f commit 3c895a2
Show file tree
Hide file tree
Showing 4 changed files with 365 additions and 60 deletions.
14 changes: 12 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,8 @@ fastify
})
```

If you need composited authentication, such as verifying user account passwords and levels or meeting VIP eligibility criteria. e.g. [(verifyUserPassword `and` verifyLevel) `or` (verifyVIP)]
If you need composite authentication, such as verifying user account passwords and levels or meeting VIP eligibility criteria, you can use nested arrays.
For example if you need the following logic: [(verifyUserPassword `and` verifyLevel) `or` (verifyVIP)], it can be achieved with the code below:
```js
fastify
.decorate('verifyUserPassword', function (request, reply, done) {
Expand All @@ -94,7 +95,7 @@ fastify
method: 'POST',
url: '/auth-multiple',
preHandler: fastify.auth([
[fastify.verifyUserPassword, fastify.verifyLevel], // The arrays within an array always have an AND relationship.
[fastify.verifyUserPassword, fastify.verifyLevel], // The arrays within an array have the opposite relation to the main (default) relation.
fastify.verifyVIP
], {
relation: 'or' // default relation
Expand All @@ -107,6 +108,15 @@ fastify
})
```

If the `relation` (`defaultRelation`) parameter is `and`, then the relation inside sub-arrays will be `or`.
If the `relation` (`defaultRelation`) parameter is `or`, then the relation inside sub-arrays will be `and`.

| auth code | resulting logical expression |
| ------------- |:-------------:|
| `fastify.auth([f1, f2, [f3, f4]], { relation: 'or' })` | `f1 OR f2 OR (f3 AND f4)` |
| `fastify.auth([f1, f2, [f3, f4]], { relation: 'and' })` | `f1 AND f2 AND (f3 OR f4)` |


You can use the `defaultRelation` option while registering the plugin, to change the default `relation`:
```js
fastify.register(require('@fastify/auth'), { defaultRelation: 'and'} )
Expand Down
142 changes: 90 additions & 52 deletions auth.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,13 @@ function auth (pluginOptions) {
/* eslint-disable-next-line no-var */
for (var i = 0; i < functions.length; i++) {
if (Array.isArray(functions[i]) === false) {
functions[i] = [functions[i].bind(this)]
functions[i] = functions[i].bind(this)
} else {
/* eslint-disable-next-line no-var */
for (var j = 0; j < functions[i].length; j++) {
if (Array.isArray(functions[i][j])) {
throw new Error('Nesting sub-arrays is not supported')
}
functions[i][j] = functions[i][j].bind(this)
}
}
Expand All @@ -61,8 +64,9 @@ function auth (pluginOptions) {
obj.options = this.options
obj.i = 0
obj.j = 0
obj.firstResult = null
obj.sufficient = false
obj.currentError = null
obj.skipFurtherErrors = false
obj.skipFurtherArrayErrors = false

obj.nextAuth()
}
Expand All @@ -78,80 +82,114 @@ function auth (pluginOptions) {
this.request = null
this.reply = null
this.done = null
this.firstResult = null
this.sufficient = false
this.currentError = null
this.skipFurtherErrors = false
this.skipFurtherArrayErrors = false

const that = this

this.nextAuth = function nextAuth (err) {
const func = that.functions[that.i][that.j++]
if (!that.skipFurtherErrors) that.currentError = err

const func = that.functions[that.i++]
if (!func) {
that.completeAuthArray(err)
return
return that.completeAuth()
}

try {
const maybePromise = func(that.request, that.reply, that.onAuth)

if (maybePromise && typeof maybePromise.then === 'function') {
maybePromise.then(results => that.onAuth(null, results), that.onAuth)
}
} catch (err) {
this.onAuth(err)
if (!Array.isArray(func)) {
that.processAuth(func, (err) => {
if (that.options.run !== 'all') that.currentError = err

if (that.options.relation === 'and') {
if (err && that.options.run !== 'all') {
that.completeAuth()
} else {
if (err && that.options.run === 'all' && !that.skipFurtherErrors) {
that.skipFurtherErrors = true
that.currentError = err
}
that.nextAuth(err)
}
} else {
if (!err && that.options.run !== 'all') {
that.completeAuth()
} else {
if (!err && that.options.run === 'all') {
that.skipFurtherErrors = true
that.currentError = null
}
that.nextAuth(err)
}
}
})
} else {
that.j = 0
that.skipFurtherArrayErrors = false
that.processAuthArray(func, (err) => {
if (that.options.relation === 'and') { // sub-array relation is OR
if (!err && that.options.run !== 'all') {
that.nextAuth(err)
} else {
that.currentError = err
that.nextAuth(err)
}
} else { // sub-array relation is AND
if (err && that.options.run !== 'all') {
that.currentError = err
that.nextAuth(err)
} else {
if (!err && that.options.run !== 'all') {
that.currentError = null
that.completeAuth()
}
that.nextAuth(err)
}
}
})
}
}

this.onAuth = function onAuth (err, results) {
if (err) {
return that.completeAuthArray(err)
}
this.processAuthArray = function processAuthArray (funcs, callback, err) {
const func = funcs[that.j++]
if (!func) return callback(err)

return that.nextAuth(err)
}

this.completeAuthArray = function (err) {
if (err) {
if (that.options.relation === 'and') {
if (that.options.run === 'all') {
that.firstResult = that.firstResult ?? err
that.processAuth(func, (err) => {
if (that.options.relation === 'and') { // sub-array relation is OR
if (!err && that.options.run !== 'all') {
callback(err)
} else {
that.firstResult = err
this.completeAuth()
return
if (!err && that.options.run === 'all') {
that.skipFurtherArrayErrors = true
}
that.processAuthArray(funcs, callback, that.skipFurtherArrayErrors ? null : err)
}
} else {
that.firstResult = that.sufficient ? null : err
} else { // sub-array relation is AND
if (err && that.options.run !== 'all') callback(err)
else that.processAuthArray(funcs, callback, err)
}
} else {
if (that.options.relation === 'or') {
that.sufficient = true
that.firstResult = null
})
}

if (that.options.run !== 'all') {
this.completeAuth()
return
}
}
}
this.processAuth = function processAuth (func, callback) {
try {
const maybePromise = func(that.request, that.reply, callback)

if (that.i < that.functions.length - 1) {
that.i += 1
that.j = 0
return that.nextAuth(err)
if (maybePromise && typeof maybePromise.then === 'function') {
maybePromise.then(() => callback(null), callback)
}
} catch (err) {
callback(err)
}

this.completeAuth()
}

this.completeAuth = function () {
if (that.firstResult && (!that.reply.raw.statusCode || that.reply.raw.statusCode < 400)) {
if (that.currentError && (!that.reply.raw.statusCode || that.reply.raw.statusCode < 400)) {
that.reply.code(401)
} else if (!that.firstResult && that.reply.raw.statusCode && that.reply.raw.statusCode >= 400) {
} else if (!that.currentError && that.reply.raw.statusCode && that.reply.raw.statusCode >= 400) {
that.reply.code(200)
}

that.done(that.firstResult)
that.done(that.currentError)
instance.release(that)
}
}
Expand Down
98 changes: 98 additions & 0 deletions test/example-composited.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ function build (opts) {
fastify.decorate('verifyNumber', verifyNumber)
fastify.decorate('verifyOdd', verifyOdd)
fastify.decorate('verifyBig', verifyBig)
fastify.decorate('verifyOddAsync', verifyOddAsync)
fastify.decorate('verifyBigAsync', verifyBigAsync)

function verifyNumber (request, reply, done) {
const n = request.body.n
Expand Down Expand Up @@ -49,6 +51,24 @@ function build (opts) {
return done()
}

function verifyOddAsync (request, reply) {
return new Promise((resolve, reject) => {
verifyOdd(request, reply, (err) => {
if (err) reject(err)
resolve()
})
})
}

function verifyBigAsync (request, reply) {
return new Promise((resolve, reject) => {
verifyBig(request, reply, (err) => {
if (err) reject(err)
resolve()
})
})
}

function routes () {
fastify.route({
method: 'GET',
Expand Down Expand Up @@ -78,6 +98,84 @@ function build (opts) {
}
})

fastify.route({
method: 'POST',
url: '/check-composite-and',
preHandler: fastify.auth([fastify.verifyNumber, [fastify.verifyOdd, fastify.verifyBig]], { relation: 'and' }),
handler: (req, reply) => {
req.log.info('Auth route')
reply.send({ hello: 'world' })
}
})

fastify.route({
method: 'POST',
url: '/check-composite-and-async',
preHandler: fastify.auth([fastify.verifyNumber, [fastify.verifyOddAsync, fastify.verifyBigAsync]], { relation: 'and' }),
handler: (req, reply) => {
req.log.info('Auth route')
reply.send({ hello: 'world' })
}
})

fastify.route({
method: 'POST',
url: '/check-composite-and-run-all',
preHandler: fastify.auth([fastify.verifyNumber, [fastify.verifyOdd, fastify.verifyBig]], { relation: 'and', run: 'all' }),
handler: (req, reply) => {
req.log.info('Auth route')
reply.send({
odd: req.odd,
big: req.big,
number: req.number
})
}
})

fastify.route({
method: 'POST',
url: '/check-composite-or',
preHandler: fastify.auth([fastify.verifyNumber, [fastify.verifyOdd, fastify.verifyBig]], { relation: 'or' }),
handler: (req, reply) => {
req.log.info('Auth route')
reply.send({ hello: 'world' })
}
})

fastify.route({
method: 'POST',
url: '/check-two-sub-arrays-or',
preHandler: fastify.auth([[fastify.verifyBigAsync], [fastify.verifyOddAsync]], { relation: 'or' }),
handler: (req, reply) => {
req.log.info('Auth route')
reply.send({ hello: 'world' })
}
})

fastify.route({
method: 'POST',
url: '/check-composite-or-async',
preHandler: fastify.auth([fastify.verifyNumber, [fastify.verifyOddAsync, fastify.verifyBigAsync]], { relation: 'or' }),
handler: (req, reply) => {
req.log.info('Auth route')
reply.send({ hello: 'world' })
}
})

fastify.route({
method: 'POST',
url: '/check-composite-or-run-all',
preHandler: fastify.auth([fastify.verifyNumber, [fastify.verifyOdd, fastify.verifyBig]], { relation: 'or', run: 'all' }),
handler: (req, reply) => {
req.log.info('Auth route')
reply.send({
odd: req.odd,
big: req.big,
number: req.number
})
}
})

fastify.route({
method: 'POST',
url: '/checkor',
Expand Down
Loading

0 comments on commit 3c895a2

Please sign in to comment.