Skip to content

Instantly share code, notes, and snippets.

@lbrenman
Last active December 3, 2017 11:44
Show Gist options
  • Save lbrenman/f884ea1f43d75ed98deb to your computer and use it in GitHub Desktop.
Save lbrenman/f884ea1f43d75ed98deb to your computer and use it in GitHub Desktop.
Arrow Authentication Scheme Example - Multiple API Keys

#Arrow Authentication Scheme

https://gist.github.com/lbrenman/f884ea1f43d75ed98deb

*Using Basic APIKey Authentication provides a single key for all users of the API

  • What if want to provide a different API Key for different users/customers
  • this is an API Management feature
  • Today, we don’t support this but it can easily be built using Arrow Authentication Scheme
  • Further, ArrowDB can be used to store the users and their keys for authentication
  • Then, access to the API can use different keys (passed in a header) and the user API can use a separate private key that only the admin/Arrow project developer knows
  • The user API will be used for creating users/keys as well for authentication during access. It can also be used for analytics, rate limiting, etc…

Blog post notes:

  • reference Q&A posts from Fokke and Jeff on Auth Scheme

API’s:

User API’s are for admins only and require the adminsecret (from default.js) to be passed in header admin-secret

GET /api/users - get all users

POST /api/users - create a user. Provide only the username. Other fields will be auto populated, in particular an API Key. Will return the id of the user record

GET /api/users/:id - get user by id (and get key)

DELETE /api/users/:id - delete user by id

DELETE /api/users - delete all users

examples:

curl -is "http://127.0.0.1:8080/api/users"  -H "admin-secret:adminsecret"
curl -is -X POST "http://127.0.0.1:8080/api/users" -d '{"username":"user1"}' -H "Content-Type: application/json"  -H "admin-secret:adminsecret"
curl -is "http://127.0.0.1:8080/api/users/557b3bde1b40070b8900321f"  -H "admin-secret:adminsecret"

Database API is an example API for users and requires an API Key that is unique to each user.

This key is created by the Arrow app during the user creation.

Pass it in the x-secret header on each API call. Each call to the database API will increment the user count.

This is an example of how you can track calls per user, do rate limiting (with more code, etc…).

examples:

curl "http://127.0.0.1:8080/api/database"  -H "x-secret:nBT6G7XSJpE2guLEaDu3QKcasj5ejFnK"
curl "http://127.0.0.1:8080/api/database/5579d44d730b8233ab0f97b5" -H "x-secret:nBT6G7XSJpE2guLEaDu3QKcasj5ejFnK"
curl -is -X POST "http://127.0.0.1:8080/api/database" -d '{"fname":"John","lname":"Doe","title":"VP"}' -H "Content-Type: application/json"  -H "x-secret:nBT6G7XSJpE2guLEaDu3QKcasj5ejFnK"
var Arrow = require("arrow");
var Model = Arrow.createModel("database",{
"fields": {
"fname": {
"type": "String"
},
"lname": {
"type": "String"
},
"title": {
"type": "String"
}
},
"connector": "appc.arrowdb",
"actions": [
"create",
"read",
"update",
"delete",
"deleteAll"
],
// "before": "checkheaders",
"singular": "database",
"plural": "databases"
});
module.exports = Model;
/**
* this is your configuration file defaults.
*
* You can create additional configuration files to that the server will load based on your
* environment. For example, if you want to have specific settings for production which are different
* than your local development environment, you can create a production.js and a local.js. Any changes
* in those files will overwrite changes to this file (a object merge is performed). By default, your
* local.js file will not be commited to git or the registry.
*
* This is a JavaScript file (instead of JSON) so you can also perform logic in this file if needed.
*/
module.exports = {
// these are your generated API keys. They were generated uniquely when you created this project.
// DO NOT SHARE these keys with other projects and be careful with these keys since they control
// access to your API using the default configuration. if you don't want two different keys for
// production and test (not recommended), use the key 'apikey'. To simulate running in production,
// set the environment variable NODE_ENV to production before running such as:
//
// NODE_ENV=production appc run
//
// production key, this is the key that will be required when you are running in production
apikey_production: 'OEp0jzsqpke8NUCT0uoKSk8HQRn27DbP',
// development key, this is the key that will be required when you are testing non-production (such as locally)
apikey_development: '8u6FTQCBWoCifXRXm9PTMkxznosVqvZd',
// by default the authentication strategy is 'basic' which will use HTTP Basic Authorization where the
// usename is the key and the password is blank. the other option is 'apikey' where the value of the
// APIKey header is the value of the key. you can also set this to 'plugin' and define the key 'APIKeyAuthPlugin'
// which points to a file or a module that implements the authentication strategy
// APIKeyAuthType: 'basic',
APIKeyAuthType: 'plugin',
APIKeyAuthPlugin: './lib/plugin.js',
adminsecret: 'adminsecret',
// logging configuration
logging: {
// location of the logs if enabled
logdir: './logs',
// turn on transaction logs
transactionLogEnabled: true
},
// prefix to use for apis
apiPrefix: '/api',
// control the settings for the admin website
admin: {
// control whether the admin website is available
enabled: true,
// the prefix to the admin website
prefix: '/arrow',
// the prefix for the public apidocs website
apiDocPrefix: '/apidoc',
// if you set disableAuth, in production only your API docs will show up
disableAuth: false,
// if you set disableAPIDoc, you APIs docs will not show up (regardless of disableAuth)
disableAPIDoc: false,
// set to true to allow the admin website to be accessed in production. however, you will still need a
// login unless disableAuth is false. if you set this to false, the admin website will not be enabled
// when in production (still respects enabled above)
enableAdminInProduction: true,
// set the email addresses you want to enable while in production (assuming enableAdminInProduction=true)
validEmails: ["[email protected]"],
// set the organization ids you want to enable while in production (assuming enableAdminInProduction=true)
validOrgs: [100000142]
},
// you can generally leave this as-is since it is generated for each new project you created.
session: {
encryptionAlgorithm: 'aes256',
encryptionKey: '8sYsej4sLe9hFmeo24rfwOTgXkdOjp5wbzT60ivof1k=',
signatureAlgorithm: 'sha512-drop256',
signatureKey: 'NXbG67rJOv6v2LmA8BmCmWICNPrkrT/mk8K+zO6TgCdeKmB1Cm2o4C7HTF5lbyPPwN1c1tTdS2vek4/6m81QXA==',
secret: 'D9BBBhcJ62UvRt4c+ijAwFwlcx6Ykilj', // should be a large unguessable string
duration: 86400000, // how long the session will stay valid in ms
activeDuration: 300000 // if expiresIn < activeDuration, the session will be extended by activeDuration milliseconds
},
// your connector configuration goes here
connectors: {
}
};
var Arrow = require('arrow');
// var errorMsg_serverError = {success: false, message: "server error"};
// var errorMsg_serverError = JSON.stringify({success: false, message: "server error"});
var errorMsg_serverError = "server error";
// var errorMsg_keyError = {success: false, message: "invalid key"};
// var errorMsg_keyError = JSON.stringify({success: false, message: "invalid key"});
var errorMsg_keyError = "invalid key";
// var errorMsg_accountDisbledError = {success: false, message: "account disabled"};
// var errorMsg_accountDisbledError = JSON.stringify({success: false, message: "account disabled"});
var errorMsg_accountDisbledError = "account disabled";
function Plugin(server) {
console.log("Plugin()");
this.config = server.config;
}
Plugin.prototype.matchURL = function(request) {
console.log("matchURL(), request.url = "+request.url);
// Validate all apis
return true;
};
Plugin.prototype.validateRequest = function(request, response, callback) {
console.log("validateRequest(), request.url = "+request.url);
if (request.headers['admin-secret'] && request.headers['admin-secret'] === this.config.adminsecret) {
console.log('admin user');
callback(null, true);
return false;
}
var model = Arrow.getModel("users");
model.query({key: request.headers['x-secret'] || "key not found"}, function(err, collection){
if(err) {
console.log('error getting users database, err = '+err);
callback(errorMsg_serverError, false);
} else {
if(collection.length>0){
model.findOne(collection[0].id, function(err, data){
if(err) {
console.log('error getting database, err = '+err);
callback(errorMsg_serverError, false);
} else {
if(data.enabled) {
console.log("update user record for user = "+data.username);
data.count++;
data.update();
callback(null, true);
} else {
callback(errorMsg_accountDisbledError, false);
}
}
});
} else {
callback(errorMsg_keyError, false);
}
}
});
};
// Add the X-Secret header for internal requests
Plugin.prototype.applyCredentialsForTest = function(options) {
console.log("applyCredentialsForTest()");
options.headers['admin-secret'] = this.config.adminsecret;
};
// Do not process the response
Plugin.prototype.applyResponseForTest = function(response, body) {
console.log("applyResponseForTest()");
return body;
};
module.exports = Plugin;
var Arrow = require("arrow");
var keygen = require("keygenerator");
var Model = Arrow.createModel("users",{
"fields": {
"username": {
"type": "String"
},
"key": {
"type": "String",
// "readonly": true,
"set":function(val,key,model){
return keygen._();
}
},
"enabled": {
"type": "Boolean",
// "readonly": true,
"default": true,
// "set":function(val,key,model){
// return true;
// }
},
"count": {
"type": "Number",
// "readonly": true,
"default": 0,
// "set":function(val,key,model){
// return 0;
// }
}
},
"connector": "appc.arrowdb",
"actions": [
"create",
"read",
// "update",
"delete",
"deleteAll"
],
"singular": "users",
"plural": "userss"
});
module.exports = Model;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment