passport-openam-agco
Version:
Passport strategy to query OpenAM for token_info with Redis cache
576 lines (485 loc) • 17.6 kB
JavaScript
var expect = require('chai').expect,
$http = require('http-as-promised'),
MD5 = require('MD5'),
nock = require('nock'),
promise = require('bluebird'),
Redis = require('redis'),
express = require('express'),
passport = require('passport'),
Promise = require('bluebird'),
app = express(),
redisOpenAM = require('../index.js'),
basicKey = redisOpenAM.basicKey,
oauth2Key = redisOpenAM.oauth2Key,
port = '5050',
url = 'http://0.0.0.0:'+port,
openAMBaseURL = 'https://example.com',
openAMTokenPath = '/access_token',
openAMInfoPath = '/tokeninfo',
openAMUserPath = '/userinfo',
openAMURL = openAMBaseURL + openAMTokenPath,
mockToken = "f6dcf133-f00b-4943-a8d4-ee939fc1bf29",
openAMMock = nock(openAMBaseURL),
requestUser;
promise.promisifyAll(Redis.RedisClient.prototype);
promise.promisifyAll(Redis.Multi.prototype);
var redis = Redis.createClient();
nock.enableNetConnect();
before(function() {
var basicOptions = {
openAMBaseURL: openAMURL,
openAMInfoURL: openAMBaseURL + openAMInfoPath,
client_id: 'client_id',
client_secret: 'client_secret',
redis: {},
scope: ["sub", "username", "email"]
},
oauth2Options = {
openAMInfoURL: openAMBaseURL + openAMInfoPath,
openAMUserURL: openAMBaseURL + openAMUserPath,
redis: {}
};
passport.use(redisOpenAM.basic(basicOptions));
passport.use(redisOpenAM.oauth2(oauth2Options));
app.use(passport.initialize());
app.get('/foo', passport.authenticate('basic', {session: false}), sendResponse);
app.get('/bar', passport.authenticate('bearer', {session: false}), sendResponse);
app.get('/foo_err', passport.authenticate('basic', {session: false}), function(req, res) {
res.status(500).send('dummy error');
});
app.get('/bar_err', passport.authenticate('bearer', {session: false}), function(req, res) {
res.status(500).send('dummy error');
});
function sendResponse(req, res, next) {
requestUser = req.user;
res.sendStatus(200, JSON.stringify(req.user));
}
var server = app.listen(port, function() {
var host = server.address().address;
var port = server.address().port;
console.log('Test server listening at http://%s:%s', host, port);
});
return redis.flushallAsync();
});
describe('Basic Authorization', function() {
it('returns 401 for requests without auth headers', function() {
return $http.get(url + '/foo', {error: false})
.spread(function(res) {
expect(res.statusCode).to.equal(401);
});
});
describe('Redis caches authentication', function() {
var user = 'foo',
pass = 'bar',
token = {sub: '234234'};
before(function() {
//create
var header = user + ':' + pass;
var hashedHeader = MD5(header);
return redis.multi()
.set(basicKey(hashedHeader), JSON.stringify(token))
.execAsync();
});
it('checks for an existing base64 token to auth', function() {
return $http.get(url + '/foo', {
error: false,
auth: {
user: user,
pass: pass
}
})
.spread(function(res, body) {
expect(res.statusCode).to.equal(200);
});
});
it('returns downstream errors as is', function() {
return $http.get(url + '/foo_err', {
error: false,
auth: {
user: user,
pass: pass
}
})
.spread(function(res, body) {
expect(res.statusCode).to.equal(500);
});
});
});
describe('Redis expires tokens with basic strategy', function() {
var
user = 'missing',
pass = 'missing';
before(function() {
openAMMock
.post(openAMTokenPath)
.reply(200, {
"expires_in": 2,
"token_type": "Bearer",
"refresh_token": "f9063e26-3a29-41ec-86de-1d0d68aa85e9",
"access_token": mockToken
})
.get(openAMInfoPath+'?access_token='+mockToken)
.reply(200, {
"agcoUUID": "h234ljb234jkn23",
"expires_in": 2,
"scope": [
"agcoUUID",
"username",
"email"
],
"username": "demo",
"email": "foo@bar.com"
});
return $http.get(url + '/foo', {
error: false,
auth: {
user: user,
pass: pass
},
json: {
deviceId: true
}
})
});
var hashedToken = MD5(user + ':' + pass);
after(function() {
return redis.flushdbAsync();
});
it('removes the token', function() {
this.timeout(5000);
return checkWaitAndVerifyExpired(basicKey(hashedToken), 3000);
});
});
describe('openAM check', function() {
before(function() {
openAMMock
.post(openAMTokenPath)
.reply(200, {
"expires_in": 599,
"token_type": "Bearer",
"refresh_token": "f9063e26-3a29-41ec-86de-1d0d68aa85e9",
"access_token": mockToken
})
.get(openAMInfoPath+'?access_token='+mockToken)
.reply(200, {
"agcoUUID": "h234ljb234jkn23",
"expires_in": 599,
"scope": [
"agcoUUID",
"username",
"email"
],
"username": "demo",
"email": "foo@bar.com"
})
.post(openAMTokenPath)
.reply(400);
});
after(function() {
return redis.flushdbAsync();
});
it('validates a user if their credentials exist in openAM and returns '+
'their token info', function() {
var user = 'missing',
pass = 'missing';
return $http.get(url + '/foo', {
error: false,
auth: {
user: user,
pass: pass
},
json: {
deviceId: true
}
})
.spread(function checkResponse(res, body) {
expect(res.statusCode).to.equal(200);
var header = user + ':' + pass,
hashedHeader = MD5(header);
expect(requestUser.sub).to.equal("h234ljb234jkn23");
expect(requestUser.token.agcoUUID).to.equal("h234ljb234jkn23");
expect(requestUser.token.username).to.exist;
expect(requestUser.token.email).to.exist;
return getHeader().then(checkToken);
function getHeader() {
return redis.getAsync(basicKey(hashedHeader));
}
function checkToken(token) {
var parsedToken = JSON.parse(token);
expect(parsedToken.token.agcoUUID).to.equal("h234ljb234jkn23");
expect(parsedToken.sub).to.equal("h234ljb234jkn23");
}
});
});
it('validates with tokenInfo and cached results', function() {
var user = 'missing',
pass = 'missing';
return getValidTokenInfo()
.spread(checkResponse);
function getValidTokenInfo() {
return $http.get(url + '/foo', {
error: false,
auth: {
user: user,
pass: pass
},
json: {
deviceId: true
}
});
}
function checkResponse(res) {
expect(res.statusCode).to.equal(200);
var header = user + ':' + pass,
hashedHeader = MD5(header);
expect(requestUser.sub).to.equal("h234ljb234jkn23");
expect(requestUser.token.agcoUUID).to.equal("h234ljb234jkn23");
expect(requestUser.token.username).to.exist;
expect(requestUser.token.email).to.exist;
return getHeader().then(checkToken);
function getHeader() {
return redis.getAsync(basicKey(hashedHeader));
}
function checkToken(token) {
expect(JSON.parse(token).sub).to.equal("h234ljb234jkn23");
}
}
});
it('invalidates a user if openAM token POST returns a 401 code', function() {
var user = 'invalid',
pass = 'invalid';
return $http.get(url + '/foo', {
error: false,
auth: {
user: user,
pass: pass
},
json: {
deviceId: true
}
})
.spread(function(res, body) {
expect(res.statusCode).to.equal(401);
});
});
});
describe('Error handling check', function() {
before(function() {
openAMMock
.post(openAMTokenPath)
.reply(200, {
"expires_in": 599,
"token_type": "Bearer",
"refresh_token": "f9063e26-3a29-41ec-86de-1d0d68aa85e9",
"access_token": mockToken
})
.get(openAMInfoPath+'?access_token='+mockToken)
.reply(404, {
"error": "Not found",
"error_description": "Could not read token in CTS"
});
});
after(function() {
return redis.flushdbAsync();
});
it('invalidates a user if openAM token POST returns a 404 code', function() {
var user = 'invalid',
pass = 'invalid';
return $http.get(url + '/foo', {
error: false,
auth: {
user: user,
pass: pass
},
json: {
deviceId: true
}
})
.spread(function(res, body) {
expect(res.statusCode).to.equal(401);
});
});
});
});
describe('OAUTH2', function() {
it('returns 401 for requests without auth headers', function() {
return $http.get(url + '/bar', {error: false})
.spread(function(res) {
expect(res.statusCode).to.equal(401);
});
});
describe('Redis caches authentication', function() {
var token = 'qux',
tokenHeader = 'Bearer ' + token,
expectedName = 'Robot';
before(function() {
//create token with info
var hashedHeader = MD5(token),
userInfo = {
name: expectedName
};
return redis.multi()
.set(oauth2Key(hashedHeader), JSON.stringify(userInfo))
.execAsync();
});
it('checks for an existing oauth2 token to auth', function() {
return $http.get(url + '/bar', {
error: false,
headers: {
Authorization: tokenHeader
}
})
.spread(function(res, body) {
expect(res.statusCode).to.equal(200);
expect(requestUser.name).to.equal(expectedName);
});
});
});
describe('Redis expires tokens with oauth2 strategy ', function() {
var mockTokenInfo = {
"profile": "",
"mail": "agc2.dealer.1@agcocorp.com",
"scope": [
"mail",
"cn",
"agcoUUID"
],
"grant_type": "password",
"cn": "agc2 dealer 1",
"realm": "/dealers",
"token_type": "Bearer",
"expires_in": 3,
"access_token": mockToken,
"agcoUUID": "3f946638-ea3f-11e4-b02c-1681e6b88ec1"
};
before(function() {
openAMMock
.get(openAMInfoPath+'?access_token='+mockToken)
.reply(200, mockTokenInfo);
return $http.get(url + '/bar', {
error: false,
headers: {
Authorization: 'Bearer ' + mockToken
}
});
});
it('removes the token', function() {
var hashedToken = MD5(mockToken);
this.timeout(5000);
return checkWaitAndVerifyExpired(oauth2Key(hashedToken), 4000);
});
});
describe('Tokens not in redis are checked against the provided tokeninfo '+
'endpoint', function() {
var mockTokenInfo = {
"profile": "",
"mail": "agc2.dealer.1@agcocorp.com",
"scope": [
"mail",
"cn",
"profile"
],
"grant_type": "password",
"cn": "agc2 dealer 1",
"realm": "/dealers",
"token_type": "Bearer",
"expires_in": 7136,
"access_token": mockToken,
"agcoUUID": "3f946638-ea3f-11e4-b02c-1681e6b88ec1"
},
error = {
"error": "Not found",
"error_description": "Could not read token in CTS"
},
badToken = 'foo';
before(function() {
openAMMock
.get(openAMInfoPath+'?access_token='+mockToken)
.reply(200, mockTokenInfo)
.get(openAMInfoPath+'?access_token='+badToken)
.reply(404, error)
.get(openAMInfoPath+'?access_token='+badToken)
.reply(200, mockTokenInfo);
});
beforeEach(function() {
return redis.flushallAsync();
});
it('Has the user info on the req body if it is found and stores it in '+
'redis', function() {
return $http.get(url + '/bar', {
error: false,
headers: {
Authorization: 'Bearer ' + mockToken
}
})
.spread(function(res, body) {
var hashedToken = MD5(mockToken);
expect(res.statusCode).to.equal(200);
//expect(requestUser.token.mail).to.equal(mockTokenInfo.mail);
expect(requestUser.sub).to.equal(mockTokenInfo.agcoUUID);
return redis.multi()
.get(oauth2Key(hashedToken))
.execAsync().spread(function(body) {
return expect(JSON.parse(body).sub).to.equal(mockTokenInfo.agcoUUID);
});
});
});
it('invalidates a user if oauth returns 401', function() {
return $http.get(url + '/bar', {
error: false,
headers: {
Authorization: 'Bearer ' + 'foo'
}
})
.spread(function(res, body) {
expect(res.statusCode).to.equal(401);
var hashedToken = MD5(badToken);
return redis.multi()
.get(oauth2Key(hashedToken))
.execAsync().spread(function(body) {
return expect(body).to.equal(null);
});
});
});
it('returns downstream errors as is', function() {
return $http.get(url + '/bar_err', {
error: false,
headers: {
Authorization: 'Bearer ' + 'foo'
}
})
.spread(function(res, body) {
expect(res.statusCode).to.equal(500);
});
});
});
});
function checkWaitAndVerifyExpired(hash, wait) {
return checkInitial(hash)
.then(function () {
return Promise.delay(wait);
})
.then(function() {
return checkExpired(hash);
});
}
function checkInitial(hash) {
return checkVal(hash).spread(function (body) {
return expect(body).to.not.be.null;
});
}
function checkExpired(hash) {
return checkVal(hash).spread(function (body) {
return expect(!body).to.be.true;
});
}
function checkVal(hash) {
return redis.multi()
.get(hash)
.execAsync();
}
function setVal(key, obj) {
return redis.multi()
.set(key, JSON.stringify(obj))
.execAsync();
}