Skip to content

Commit

Permalink
feat: documentation authorization (#466)
Browse files Browse the repository at this point in the history
* feat: ui hooks extension

* add docs

* Apply suggestions from code review

Co-authored-by: Vincent LE GOFF <vince.legoff@gmail.com>

* Update README.md

Co-authored-by: Frazer Smith <frazer.dev@outlook.com>

Co-authored-by: Vincent LE GOFF <vince.legoff@gmail.com>
Co-authored-by: Frazer Smith <frazer.dev@outlook.com>
  • Loading branch information
3 people committed Sep 15, 2021
1 parent 2202cbc commit 1a68013
Show file tree
Hide file tree
Showing 6 changed files with 167 additions and 2 deletions.
31 changes: 31 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,10 @@ fastify.register(require('fastify-swagger'), {
docExpansion: 'full',
deepLinking: false
},
uiHooks: {
onRequest: function (request, reply, next) { next() },
preHandler: function (request, reply, next) { next() }
},
staticCSP: true,
transformStaticCSP: (header) => header,
exposeRoute: true
Expand Down Expand Up @@ -225,6 +229,7 @@ An example of using `fastify-swagger` with `static` mode enabled can be found [h
| transform | null | Transform method for schema. |
| transformStaticCSP | undefined | Synchronous function to transform CSP header for static resources if the header has been previously set. |
| uiConfig | {} | Configuration options for [Swagger UI](https://github.com/swagger-api/swagger-ui/blob/master/docs/usage/configuration.md). Must be literal values, see [#5710](https://github.com/swagger-api/swagger-ui/issues/5710).|
| uiHooks | {} | Additional hooks for the documentation's routes. You can provide the `onRequest` and `preHandler` hooks with the same [route's options](https://www.fastify.io/docs/latest/Routes/#options) interface.|
| refResolver | {} | Option to manage the `$ref`s of your application's schemas. Read the [`$ref` documentation](#register.options.refResolver) |

If you set `exposeRoute` to `true` the plugin will expose the documentation with the following APIs:
Expand Down Expand Up @@ -642,6 +647,32 @@ There are two ways to hide a route from the Swagger UI:
- Pass `{ hide: true }` to the schema object inside the route declaration.
- Use the tag declared in `hiddenTag` options property inside the route declaration. Default is `X-HIDDEN`.
<a name="route.uiHooks"></a>
#### Protect your documentation routes
You can protect your documentation by configuring an authentication hook.
Here is an example using the [`fastify-basic-auth`](https://github.com/fastify/fastify-basic-auth) plugin:
```js
await fastify.register(require('fastify-basic-auth'), {
validate (username, password, req, reply, done) {
if (username === 'admin' && password === 'admin') {
done()
} else {
done(new Error('You can not access'))
}
},
authenticate: true
})

fastify.register(fastifySwagger, {
exposeRoute: true,
uiHooks: {
onRequest: fastify.basicAuth
}
})
```
<a name="function.options"></a>
### Swagger function options
Expand Down
9 changes: 8 additions & 1 deletion lib/mode/dynamic.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,14 @@ module.exports = function (fastify, opts, done) {
const initOAuth = opts.initOAuth || {}
const staticCSP = opts.staticCSP
const transformStaticCSP = opts.transformStaticCSP
fastify.register(require('../routes'), { prefix, uiConfig, initOAuth, staticCSP, transformStaticCSP })
fastify.register(require('../routes'), {
prefix,
uiConfig,
initOAuth,
staticCSP,
transformStaticCSP,
hooks: opts.uiHooks
})
}

const cache = {
Expand Down
3 changes: 2 additions & 1 deletion lib/mode/static.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,8 @@ module.exports = function (fastify, opts, done) {
initOAuth: opts.initOAuth || {},
baseDir: opts.specification.baseDir,
staticCSP: opts.staticCSP,
transformStaticCSP: opts.transformStaticCSP
transformStaticCSP: opts.transformStaticCSP,
hooks: opts.uiHooks
}

fastify.register(require('../routes'), options)
Expand Down
17 changes: 17 additions & 0 deletions lib/routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,22 @@ function fastifySwagger (fastify, opts, done) {
})
}

const hooks = Object.create(null)
if (opts.hooks) {
const additionalHooks = [
'onRequest',
'preHandler'
]
for (const hook of additionalHooks) {
hooks[hook] = opts.hooks[hook]
}
}

fastify.route({
url: '/',
method: 'GET',
schema: { hide: true },
...hooks,
handler: (req, reply) => {
reply.redirect(getRedirectPathForTheRootRoute(req.raw.url))
}
Expand All @@ -64,6 +76,7 @@ function fastifySwagger (fastify, opts, done) {
url: '/uiConfig',
method: 'GET',
schema: { hide: true },
...hooks,
handler: (req, reply) => {
reply.send(opts.uiConfig)
}
Expand All @@ -73,6 +86,7 @@ function fastifySwagger (fastify, opts, done) {
url: '/initOAuth',
method: 'GET',
schema: { hide: true },
...hooks,
handler: (req, reply) => {
reply.send(opts.initOAuth)
}
Expand All @@ -82,6 +96,7 @@ function fastifySwagger (fastify, opts, done) {
url: '/json',
method: 'GET',
schema: { hide: true },
...hooks,
handler: function (req, reply) {
reply.send(fastify.swagger())
}
Expand All @@ -91,6 +106,7 @@ function fastifySwagger (fastify, opts, done) {
url: '/yaml',
method: 'GET',
schema: { hide: true },
...hooks,
handler: function (req, reply) {
reply
.type('application/x-yaml')
Expand All @@ -115,6 +131,7 @@ function fastifySwagger (fastify, opts, done) {
url: '/*',
method: 'GET',
schema: { hide: true },
...hooks,
handler: function (req, reply) {
const file = req.params['*']
reply.sendFile(file)
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
"devDependencies": {
"@types/node": "^16.0.0",
"fastify": "^3.7.0",
"fastify-basic-auth": "^2.1.0",
"fastify-helmet": "^5.0.3",
"fs-extra": "^10.0.0",
"joi": "^14.3.1",
Expand Down
108 changes: 108 additions & 0 deletions test/hooks.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
'use strict'

const { test } = require('tap')
const Fastify = require('fastify')
const yaml = require('js-yaml')

const fastifySwagger = require('../index')
const { swaggerOption, schemaBody } = require('../examples/options')

const authOptions = {
validate (username, password, req, reply, done) {
if (username === 'admin' && password === 'admin') {
done()
} else {
done(new Error('Winter is coming'))
}
},
authenticate: true
}

function basicAuthEncode (username, password) {
return 'Basic ' + Buffer.from(username + ':' + password).toString('base64')
}

test('hooks on static swagger', async t => {
const fastify = Fastify()
await fastify.register(require('fastify-basic-auth'), authOptions)
fastify.register(fastifySwagger, {
mode: 'static',
specification: {
path: './examples/example-static-specification.yaml'
},
exposeRoute: true,
uiHooks: {
onRequest: fastify.basicAuth
}
})

let res = await fastify.inject('/documentation')
t.equal(res.statusCode, 401, 'root auth required')

res = await fastify.inject('/documentation/yaml')
t.equal(res.statusCode, 401, 'auth required yaml')
res = await fastify.inject({
method: 'GET',
url: '/documentation/yaml',
headers: { authorization: basicAuthEncode('admin', 'admin') }
})
t.equal(res.statusCode, 200)
t.equal(res.headers['content-type'], 'application/x-yaml')
try {
yaml.load(res.payload)
t.pass('valid swagger yaml')
} catch (err) {
t.fail(err)
}

res = await fastify.inject('/documentation/json')
t.equal(res.statusCode, 401, 'auth required json')
res = await fastify.inject({
method: 'GET',
url: '/documentation/json',
headers: { authorization: basicAuthEncode('admin', 'admin') }
})
t.equal(typeof res.payload, 'string')
t.equal(res.headers['content-type'], 'application/json; charset=utf-8')
try {
yaml.load(res.payload)
t.pass('valid swagger json')
} catch (err) {
t.fail(err)
}
})

test('hooks on dynamic swagger', async t => {
const fastify = Fastify()
await fastify.register(require('fastify-basic-auth'), authOptions)

fastify.register(fastifySwagger, {
...swaggerOption,
exposeRoute: true,
uiHooks: {
onRequest: fastify.basicAuth
}
})

fastify.get('/fooBar123', schemaBody, () => {})

let res = await fastify.inject('/documentation')
t.equal(res.statusCode, 401, 'root auth required')

res = await fastify.inject('/documentation/yaml')
t.equal(res.statusCode, 401, 'auth required yaml')

res = await fastify.inject('/documentation/json')
t.equal(res.statusCode, 401, 'auth required json')
res = await fastify.inject({
method: 'GET',
url: '/documentation/json',
headers: { authorization: basicAuthEncode('admin', 'admin') }
})
t.equal(typeof res.payload, 'string')
t.equal(res.headers['content-type'], 'application/json; charset=utf-8')

const swaggerObject = res.json()
t.ok(swaggerObject.paths)
t.ok(swaggerObject.paths['/fooBar123'])
})

0 comments on commit 1a68013

Please sign in to comment.