Skip to content

Commit

Permalink
PLAT-1491 - Support su@provider (#33)
Browse files Browse the repository at this point in the history
* support su@provider

* remove logs

* linting

* linting

* fix tests and checking scopes

* polyfill startsWith

* linting

* bump version and limit to >=4

* remove pollyfil
  • Loading branch information
markwallsgrove authored Jan 29, 2018
1 parent 51192a5 commit 82f006f
Show file tree
Hide file tree
Showing 6 changed files with 114 additions and 21 deletions.
4 changes: 0 additions & 4 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,6 @@ node_js:
- "4.2"
- "4.1"
- "4.0"
- "0.12"
- "0.11"
- "0.10"
- "iojs"
sudo: false
before_install:
- npm cache clean
Expand Down
85 changes: 76 additions & 9 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,81 @@ var hashKey = function hashKey(key) {
.digest('base64');
};

/**
* Split the scope into it's role and namespace components
*
* @param {string} scope scope to split up
* @return {object} scope descriptor (namespace & role)
*/
function chunkScope(scope) {
var chunks = scope.split('@', 2);
var result = { };

if (chunks.length === 2) {
result.namespace = chunks[0];
result.role = chunks[1];
} else {
result.role = chunks[0];
}

return result;
}

/**
* Check if a scope validates a required scope. The majority of the checks can
* be considered to be litral string checks. There is a single edgecase where
* the user has a su@provider scope. This scope will override any other scope if
* the role either matches or is a subset with a delimiter.
*
* i.e su@provider validates su@provider:foo and su@provider
*
* @param {string} hscope scope to validate the required scope.
* @param {string} rscope required scope to validate scope.
* @return {bool} true if the scope validates the required scope.
*/
function validateScope(hscope, rscope) {
var requiredScope = chunkScope(rscope);
var scope = chunkScope(hscope);

var namespaceMatch = requiredScope.namespace === scope.namespace;
var roleMatch = requiredScope.role === scope.role;
var emptyNamespace = requiredScope.namespace == null && scope.namespace == null;

var roleSubset = requiredScope.role.startsWith(scope.role + ':');
var suOverride = scope.namespace === 'su' && (roleMatch || roleSubset);

return suOverride || (emptyNamespace || namespaceMatch) && roleMatch;
}

/**
* Check if any of the required scopes can be fulfilled by given scopes.
*
* @param {array} scopes to check against.
* @param {array|null} required scopes
* @return {bool} true if at least one required scope can be validated
*/
function validateScopes(scopes, requiredScopes) {
if (requiredScopes == null || scopes.indexOf('su') > -1) {
return true;
}


for (var rs in requiredScopes) {
var requiredScope = requiredScopes[rs];
for (var sc in scopes) {
var scope = scopes[sc];
if (validateScope(scope, requiredScope)) {
console.log(scope + ' validates ' + requiredScope);
return true;
}
}
}

return false;
}

exports.validateScopes = validateScopes;

/**
* Constructor you must pass in an appId string identifying your app, plus an optional config object with the
* following properties set:
Expand Down Expand Up @@ -293,10 +368,6 @@ PersonaClient.prototype.validateToken = function (opts, next) {
var xRequestId = opts.xRequestId || uuid.v4();
var scopes = opts.scope && _.isString(opts.scope) ? opts.scope.split(',') : opts.scope;

if (Array.isArray(scopes) && scopes.indexOf('su') === -1) {
scopes.unshift('su');
}

if (!next) {
throw "No callback (next attribute) provided";
} else if (typeof next !== "function") {
Expand Down Expand Up @@ -367,11 +438,7 @@ PersonaClient.prototype.validateToken = function (opts, next) {
return headScopeThenVerify(next, decodedToken);
}

var onlyValidateToken = scopes == null;
var tokensIntersection = _.intersection(decodedToken.scopes, scopes);
var tokenHasAtLeastOneScope = tokensIntersection.length > 0;

if (onlyValidateToken || tokenHasAtLeastOneScope) {
if (validateScopes(decodedToken.scopes, scopes)) {
debug("Verifying token locally passed");
return next(null, "ok", decodedToken);
} else {
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "persona_client",
"version": "4.0.1",
"version": "4.1.0",
"private": true,
"main": "./index.js",
"description": "Node Client for Persona, repsonsible for retrieving, generating, caching and validating OAuth Tokens.",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[]
1 change: 1 addition & 0 deletions test/responses/token_validation/should_validate_roles.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[]
42 changes: 35 additions & 7 deletions test/token_validation_tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,7 @@ describe("Persona Client Test Suite - Token Validation Tests", function() {
var res = _getStubResponse();

personaClient.validateHTTPBearerToken(req, res, function validatedToken(err, result, decodedToken) {
(err != null).should.equal(true);
err.should.be.equal(persona.errorTypes.INSUFFICIENT_SCOPE);
res._statusWasCalled.should.equal(true);
res._jsonWasCalled.should.equal(true);
Expand Down Expand Up @@ -411,7 +412,7 @@ describe("Persona Client Test Suite - Token Validation Tests", function() {

jwt.sign(payload, privateKey, jwtSigningOptions, function(token) {
// We can't replay the recorded response as the token in that request will expire
nock("http://persona").head(/\/oauth\/tokens\/.*\?scope=su,fatuser/).reply(204);
nock("http://persona").head(/\/3\/oauth\/tokens\/.*\?scope=fatuser/).reply(204);

var req = _getStubRequest(token, "fatuser");
var res = _getStubResponse();
Expand Down Expand Up @@ -451,7 +452,7 @@ describe("Persona Client Test Suite - Token Validation Tests", function() {

jwt.sign(payload, privateKey, jwtSigningOptions, function(token) {
// We can't replay the recorded response as the token in that request will expire
nock("http://persona").head(/\/oauth\/tokens\/.*\?scope=su,fatuser,thinuser/).reply(204);
nock("http://persona").head(/\/3\/oauth\/tokens\/.*\?scope=fatuser,thinuser/).reply(204);

var req = _getStubRequest(token, "fatuser,thinuser");
var res = _getStubResponse();
Expand Down Expand Up @@ -490,7 +491,7 @@ describe("Persona Client Test Suite - Token Validation Tests", function() {
};

jwt.sign(payload, privateKey, jwtSigningOptions, function(token) {
nock("http://persona").head(/\/oauth\/tokens\/.*\?scope=su,other_scope/).reply(204);
nock("http://persona").head(/\/3\/oauth\/tokens\/.*\?scope=other_scope/).reply(204);

var req = _getStubRequest(token, "other_scope");
var res = _getStubResponse();
Expand Down Expand Up @@ -530,7 +531,7 @@ describe("Persona Client Test Suite - Token Validation Tests", function() {

jwt.sign(payload, privateKey, jwtSigningOptions, function(token) {
// We can't replay the recorded response as the token in that request will expire
nock("http://persona").head(/\/oauth\/tokens\/.*\?scope=su,invalid/).reply(403);
nock("http://persona").head(/\/3\/oauth\/tokens\/.*\?scope=invalid/).reply(403);

var req = _getStubRequest(token, "invalid");
var res = _getStubResponse();
Expand Down Expand Up @@ -559,7 +560,6 @@ describe("Persona Client Test Suite - Token Validation Tests", function() {
});

it("should not validate a token when the server-side check returns 401", function(done) {

var payload = {
scopeCount: 26
};
Expand All @@ -573,7 +573,7 @@ describe("Persona Client Test Suite - Token Validation Tests", function() {

jwt.sign(payload, privateKey, jwtSigningOptions, function(token) {
// We can't replay the recorded response as the token in that request will expire
nock("http://persona").head(/\/oauth\/tokens\/.*\?scope=su,fatuser/).reply(401);
nock("http://persona").head(/\/3\/oauth\/tokens\/.*\?scope=fatuser/).reply(401);

var req = _getStubRequest(token, "fatuser");
var res = _getStubResponse();
Expand Down Expand Up @@ -615,7 +615,7 @@ describe("Persona Client Test Suite - Token Validation Tests", function() {

jwt.sign(payload, privateKey, jwtSigningOptions, function(token) {
// We can't replay the recorded response as the token in that request will expire
nock("http://persona").head(/\/oauth\/tokens\/.*\?scope=su,fatuser/).reply(500);
nock("http://persona").head(/\/3\/oauth\/tokens\/.*\?scope=fatuser/).reply(500);

var req = _getStubRequest(token, "fatuser");
var res = _getStubResponse();
Expand Down Expand Up @@ -762,5 +762,33 @@ describe("Persona Client Test Suite - Token Validation Tests", function() {
});
});
});

it("should validate roles", function() {
// scopes, required scopes
assert.equal(persona.validateScopes(['su'], ['su']), true);
assert.equal(persona.validateScopes(['su'], ['test']), true);
assert.equal(persona.validateScopes(['su'], ['test test2']), true);
assert.equal(persona.validateScopes(['test2'], ['test', 'test2']), true);
assert.equal(persona.validateScopes(['test2'], ['test2:foo']), false);
assert.equal(persona.validateScopes(['test'], ['test2', 'test1']), false);
assert.equal(persona.validateScopes(['test:foo'], ['test2', 'test1']), false);
});

it("should validate namespaces", function() {
assert.equal(persona.validateScopes(['su@provider'], ['test@provider']), true);
assert.equal(persona.validateScopes(['su@provider'], ['test1@provider', 'test2@provider']), true);
assert.equal(persona.validateScopes(['su@provider'], ['test1@provider:foo', 'test2@provider']), true);
assert.equal(persona.validateScopes(['su@provider'], ['test1@provider:foo', 'test2@Newprovider']), true);
assert.equal(persona.validateScopes(['namespace@role'], ['namespace@role']), true);
assert.equal(persona.validateScopes(['namespace:namespace@role'], ['namespace:namespace@role']), true);
assert.equal(persona.validateScopes(['namespace:namespace@role', 'test@provider'], ['test@provider']), true);
assert.equal(persona.validateScopes(['su'], ['test@provider']), true);
assert.equal(persona.validateScopes(['su'], ['namespace:namespace@role', 'test@provider']), true);
assert.equal(persona.validateScopes(['namespace1@role'], ['namespace@role']), false);
assert.equal(persona.validateScopes(['namespace1:namespace@role'], ['namespace@role']), false);
assert.equal(persona.validateScopes(['namespacey@role'], ['namespace@role']), false);
assert.equal(persona.validateScopes(['namespace@role'], ['namespace@role:blah']), false);
assert.equal(persona.validateScopes(['namespace@role:blah'], ['namespace@role']), false);
});
});
});

0 comments on commit 82f006f

Please sign in to comment.