Skip to content

Commit

Permalink
Introduce new middleware for oauth scopes verification (#15)
Browse files Browse the repository at this point in the history
  • Loading branch information
annalesniak authored and kiebzak committed Feb 27, 2019
1 parent 81a45e0 commit 34f1984
Show file tree
Hide file tree
Showing 6 changed files with 258 additions and 2 deletions.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,8 @@ Currently following middlewares are available:
- setting default query parameters values
- removing unspecified query parameters,
- content type validation,
- routing to appropriate controller.
- routing to appropriate controller,
- oauth scopes authorization.

### Validation middlewares

Expand Down
3 changes: 2 additions & 1 deletion docs/docs.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,8 @@ Currently following middlewares are available:
- setting default query parameters values
- removing unspecified query parameters,
- content type validation,
- routing to appropriate controller.
- routing to appropriate controller,
- oauth scopes authorization.

### Validation middlewares

Expand Down
7 changes: 7 additions & 0 deletions errors/MissingRequiredScopes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
module.exports = class MissingRequiredScopes extends Error {
constructor(requiredScopes) {
super()
this.name = this.constructor.name
this.requiredScopes = requiredScopes
}
}
12 changes: 12 additions & 0 deletions lib/middlewares/oauth/oauth-scopes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
const {get} = require('lodash')
const MissingRequiredScopes = require('../../../errors/MissingRequiredScopes')

module.exports = (requiredScopes, grantedScopesLocation) => (req, res, next) => {
const grantedScopes = get(req, grantedScopesLocation)
if (grantedScopes) {
return grantedScopes.some(scope => requiredScopes.includes(scope))
? next()
: next(new MissingRequiredScopes(requiredScopes))
}
return next()
}
36 changes: 36 additions & 0 deletions middlewares/oauth/oauth-scopes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
const {pickBy, keys, values, flatten} = require('lodash')

const oauthScopes = require('../../lib/middlewares/oauth/oauth-scopes')

/**
* Verifies scope access to operation by comparing granted OAuth scopes and required scopes to perform that operation.
*
* Uses required OAuth scopes assigned to given operation or global security definition
* if the first one is not specified.
* Granted scopes are retrieved from `options.grantedScopesLocation`.
*
* In situation when there is more than one OAuth strategy assigned to operation the first one is taken for the check.
*
* To successfully pass through the middleware the request must have at least one of the required scopes.
* It is also possible to successfully pass through when request has undefined `options.grantedScopesLocation` property.
* On error `MissingRequiredScopes` error is created.
*
* @param {Object} options options
* @param {object} options.grantedScopesLocation request property to retrieve granted scopes
* @returns {Function} middleware
*/
module.exports = ({grantedScopesLocation = 'headers.x-oauth-scopes'} = {}) => (operation, {spec}) => {
const security = operation.security ? operation.security : spec.security

if (!security) {
return
}

const oauthSchemes = pickBy(spec.components.securitySchemes, scheme => scheme.type === 'oauth2')
const oauthNames = keys(oauthSchemes)
const oauthStrategy = security.find(strategy => oauthNames.includes(keys(strategy).toString()))

if (oauthStrategy) {
return oauthScopes(flatten(values(oauthStrategy)), grantedScopesLocation)
}
}
199 changes: 199 additions & 0 deletions test/middlewares/oauth/oauth-scopes.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
const oauthScopes = require('../../../middlewares/oauth/oauth-scopes')
const MissingRequiredScopes = require('../../../errors/MissingRequiredScopes')

describe('oauth scopes middleware', () => {

const spec = {
paths: {
'/hello': {
get: {
security: [
{
oauth: [
'write',
'execute'
]
}
]
}
}
},
components: {
securitySchemes: {
oauth: {
type: 'oauth2'
},
key: {
type: 'apiKey'
}
}
}
}

const operation = spec.paths['/hello'].get

it('should pass request containing required scopes in user', done => {
// given
const middleware = oauthScopes({grantedScopesLocation: 'user.grantedScopes'})(operation, {spec})
const req = {user: {grantedScopes: ['write']}}

// when
middleware(req, undefined, done)
})

it('should pass request containing required scopes in header', done => {
// given
const middleware = oauthScopes()(operation, {spec})
const req = {headers: {'x-oauth-scopes': ['read', 'write']}}

// when
middleware(req, undefined, done)
})

it('should indicate missing required scopes', () => {
//given
const middleware = oauthScopes({grantedScopesLocation: 'user.grantedScopes'})(operation, {spec})
const req = {user: {grantedScopes: ['read']}}
const next = sinon.spy()

// when
middleware(req, undefined, next)

// then
sinon.assert.calledWithMatch(next, sinon.match.instanceOf(MissingRequiredScopes)
.and(sinon.match.has('requiredScopes', operation.security.find(strategy => strategy.oauth).oauth)))
})

it('should pass request when granted scopes not set', done => {
// given
const middleware = oauthScopes({grantedScopesLocation: 'user.grantedScopes'})(operation, {spec})
const req = {user: {}}

// when
middleware(req, undefined, done)
})

it('should pass request when security property not set anywhere', () => {
// given
const spec = {
paths: {
'/hello': {
get: {}
}
},
components: {
securitySchemes: {
oauth: {
type: 'oauth2'
}
}
}
}
const operation = spec.paths['/hello'].get
const middleware = oauthScopes()(operation, {spec})

// when
expect(middleware).to.be.undefined
})

it('should pass request when security property does not contain oauth', () => {
// given
const operation = {
security: [
{
key: []
}
]
}
const middleware = oauthScopes()(operation, {spec})

// when
expect(middleware).to.be.undefined
})

it('should use global security property', () => {
// given
const spec = {
security: [
{
oauth: [
'write',
'execute'
]
}
],
paths: {
'/hello': {
get: {}
}
},
components: {
securitySchemes: {
oauth: {
type: 'oauth2'
}
}
}
}
const operation = spec.paths['/hello'].get
const middleware = oauthScopes({grantedScopesLocation: 'user.grantedScopes'})(operation, {spec})
const req = {user: {grantedScopes: ['read']}}
const next = sinon.spy()

// when
middleware(req, undefined, next)

// then
sinon.assert.calledWithMatch(next, sinon.match.instanceOf(MissingRequiredScopes)
.and(sinon.match.has('requiredScopes', spec.security.find(strategy => strategy.oauth).oauth)))
})

it('should use first oauth security definition when more than one', () => {
// given
const spec = {
paths: {
'/hello': {
get: {
security: [
{
oauth1: [
'write',
'execute'
]
},
{
oauth2: [
'read'
]
}
]
}
}
},
components: {
securitySchemes: {
oauth1: {
type: 'oauth2'
},
oauth2: {
type: 'oauth2'
},
key: {
type: 'apiKey'
}
}
}
}
const operation = spec.paths['/hello'].get
const middleware = oauthScopes({grantedScopesLocation: 'user.grantedScopes'})(operation, {spec})
const req = {user: {grantedScopes: ['read']}}
const next = sinon.spy()

// when
middleware(req, undefined, next)

// then
sinon.assert.calledWithMatch(next, sinon.match.instanceOf(MissingRequiredScopes)
.and(sinon.match.has('requiredScopes', operation.security.find(strategy => strategy.oauth1).oauth1)))
})
})

0 comments on commit 34f1984

Please sign in to comment.