Last active
August 12, 2016 22:56
-
-
Save krlicmuhamed/3b1f92d5e916ee11fb795bc883a1a5aa to your computer and use it in GitHub Desktop.
Simple API KEY security using ActionHero and Redis
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
'use strict'; | |
var uuid = require('node-uuid'); | |
exports.action = { | |
name: 'securityApiKey', // Internal security action. Do not change the name. | |
description: 'I hand out api keys.', | |
version: 1.0, | |
inputs: { | |
uniqueId: { | |
required: true, | |
validator: function(param, connection, actionTemplate){ | |
if(typeof param !== 'string'){ return 'uniqueId must be a string'; }else{ return true; } | |
} | |
} | |
}, | |
run: function(api, data, next) { | |
var apikey = 'test-'+uuid(); | |
var hash = 'api:key:'+apikey; | |
api.redis.clients.client.hmset(hash, 'uniq', data.params.uniqueId, 'activated', 0, function(err, result){ | |
if(!err){ | |
data.response.apikey = apikey; | |
return next(); | |
}else{ | |
api.log(err,'error'); | |
data.connection.rawConnection.responseHttpCode = 500; | |
return next({ | |
code: data.connection.rawConnection.responseHttpCode, | |
message: 'Couldn\'t generate a key.' | |
}); | |
} | |
}); | |
} | |
}; |
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
'use strict'; | |
if (process.env.NODE_ENV == "test") { | |
exports.action = { | |
name: 'securityTest', | |
description: 'Only used in test/development envirovments.', | |
blockedConnectionTypes: [], | |
protected: true, | |
inputs: {}, | |
run: function(api, data, next) { | |
data.response.message = 'This action is protected.'; | |
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
exports['default'] = { | |
security: function(api) { | |
return { | |
enabled: { | |
web: true, | |
websocket: false, | |
socket: false, | |
mqtt: false | |
} | |
}; | |
} | |
}; |
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
'use strict'; | |
module.exports = { | |
loadPriority: 1001, | |
startPriority: 1001, | |
stopPriority: 1001, | |
initialize: function(api, next) { | |
api.actions.addMiddleware({ | |
name: 'API Security middleware', | |
global: true, | |
priority: 100, | |
preProcessor: function(data, next) { | |
data.connection.security = { | |
authorized: false | |
}; | |
// Check If enabled | |
if (!api.config.security.enabled[data.connection.type]) { | |
return next(); | |
} else if (data.actionTemplate.protected) { | |
if (data.actionTemplate.protected && data.actionTemplate.name === 'securityApiKey') { | |
data.connection.rawConnection.responseHttpCode = 501; | |
return next({ | |
code: data.connection.rawConnection.responseHttpCode, | |
message: 'Don\'t define protected template in securityApiKey action!' | |
}); | |
} | |
// What apikey and role this connection uses? | |
var request = { | |
// Other servers like socket can use data.connection.mockHeaders | |
headers: data.params.httpHeaders || (data.connection.rawConnection.req ? data.connection.rawConnection.req.headers : undefined) || data.connection.mockHeaders || {}, | |
uri: data.connection.rawConnection.req ? data.connection.rawConnection.req.uri : {} | |
}; | |
var authHeader = request.headers.authorization || | |
request.headers.Authorization || | |
false; | |
if (authHeader) { | |
// Authenticate the requester | |
api.redis.clients.client.hgetall('api:key:'+authHeader, function(err, result){ | |
if(!err){ | |
data.connection.security.authorized = true; | |
if(result.activated === '0'){ | |
data.connection.rawConnection.responseHttpCode = 401; | |
return next({ | |
code: data.connection.rawConnection.responseHttpCode, | |
message: 'Specified api key is not activated yet.' | |
}); | |
} | |
return next(); | |
}else{ | |
data.connection.rawConnection.responseHttpCode = 500; | |
return next({ | |
code: data.connection.rawConnection.responseHttpCode, | |
message: 'Failed fetching the api key.' | |
}); | |
} | |
}); | |
} else { | |
data.connection.rawConnection.responseHttpCode = 401; | |
return next({ | |
code: data.connection.rawConnection.responseHttpCode, | |
message: 'Authorization header is missing.' | |
}); | |
} | |
} else { | |
// skip this action. | |
return next(); | |
} | |
}, | |
postProcessor: function(data, next) { | |
if (api.config.security.enabled[data.connection.type] && | |
data.connection.security.authorized) { | |
//TODO: api key statistics | |
} | |
return next(); | |
} | |
}); | |
api.log('Security initialized.'); | |
return next(); | |
}, | |
start: function(api, next) { | |
return next(); | |
}, | |
stop: function(api, next) { | |
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
process.env.NODE_ENV = 'test'; | |
var request = require('request'); | |
var should = require('should'); | |
var actionheroPrototype = require('actionhero').actionheroPrototype; | |
var actionhero = new actionheroPrototype(); | |
var api, url, apikey; | |
describe('security', function() { | |
before(function(done) { | |
actionhero.start(function(error, a) { | |
api = a; | |
url = 'http://' + api.config.servers.web.bindIP + ':' + api.config.servers.web.port; | |
done(); | |
}) | |
}); | |
after(function(done) { | |
actionhero.stop(function(error) { | |
done(); | |
}); | |
}) | |
it('securityApiKey', function(done) { | |
api.specHelper.runAction('securityApiKey', { | |
uniqueId: 'test123kasjdjhud' | |
}, function(response) { | |
//console.log(response); | |
should.exist(response.apikey); | |
apikey = response.apikey; | |
should.not.exist(response.error); | |
done(); | |
}); | |
}); | |
it('securityTest should return error if header is missing', function(done) { | |
var options = { | |
method: 'get', | |
url: url + '/api/securityTest', | |
timeout: 1500 | |
}; | |
request(options, function(error, response, body) { | |
if (!error) { | |
body = JSON.parse(body); | |
should.exist(body.error); | |
response.statusCode.should.equal(401); | |
body.error.message.should.equal('Authorization header is missing.'); | |
done(); | |
} else { | |
throw error; | |
} | |
}); | |
}); | |
it('securityTest should error unactive', function(done) { | |
var options = { | |
method: 'get', | |
url: url + '/api/securityTest', | |
timeout: 1500, | |
headers: { | |
Authorization: apikey | |
} | |
}; | |
request(options, function(error, response, body) { | |
if (!error) { | |
body = JSON.parse(body); | |
should.exist(body.error); | |
response.statusCode.should.equal(401); | |
body.error.message.should.equal('Specified api key is not activated yet.'); | |
done(); | |
} else { | |
throw error; | |
} | |
}); | |
}); | |
it('securityTest should authorize', function(done) { | |
// This HSET command should be called from a secure network or in some other | |
// secure process, because this is how you activate an api key. | |
api.redis.clients.client.hset('api:key:'+apikey, 'activated', '1', function(err, result){ | |
should.not.exist(err); | |
var options = { | |
method: 'get', | |
url: url + '/api/securityTest', | |
timeout: 1500, | |
headers: { | |
Authorization: apikey | |
} | |
}; | |
request(options, function(error, response, body) { | |
if (!error) { | |
body = JSON.parse(body); | |
should.not.exist(body.error); | |
response.statusCode.should.equal(200); | |
body.message.should.equal('This action is protected.'); | |
done(); | |
} else { | |
throw error; | |
} | |
}); | |
}); | |
}); | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment