Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat documentation authorization #466

Merged
merged 4 commits into from
Sep 15, 2021
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 [`fastify-basic-auth`](https://github.com/fastify/fastify-basic-auth) plugin:
Eomm marked this conversation as resolved.
Show resolved Hide resolved

```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,
climba03003 marked this conversation as resolved.
Show resolved Hide resolved
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'])
})