-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Introduce new middleware for oauth scopes verification (#15)
- Loading branch information
1 parent
81a45e0
commit 34f1984
Showing
6 changed files
with
258 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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))) | ||
}) | ||
}) |