Appcelerator Blog

The Leading Resource for All Things Mobile

Arrow Custom Authentication Scheme Example

21 Flares 21 Flares ×

Arrow supports various API authentications out of the box. The first three, None, Basic and APIKey simply require setting APIKeyAuthType in the conf/default.js file and Arrow takes care of the rest. This post provides an example of the fourth method called Plugin which is used for custom or 3rd-party API authentication. Appcelerator Arrow Authentication is described in detail here.

I’ll also demonstrate how to implement some API Management features such as:

  1. Suporting multiple API Keys for different users
  2. Auto API Key generation
  3. Tracking API usage per user

ArrowDB will be utilized to store the API user authentication database which will contain the Username, API Key and API usage stats. This database will be used to authenticate and track each API request.

Custom Authentication Basics

The basic approach for implementing your own custom authentication is to:

  1. Code your custom authentication in prescribed methods in a library file, Plugin.js
  2. Specify that you are using the plugin authentication scheme
  3. Point to this Plugin library file in the conf/default.js config file as follows:
conf/default.js

module.exports = {
    ...
    APIKeyAuthType: 'plugin',
    APIKeyAuthPlugin: './lib/plugin.js',
    ...
}

and

lib/plugin.js

function Plugin(server) {
  // Constructor to get a reference to the config object
}

Plugin.prototype.matchURL = function(request) {
  // Specify which APIs to apply custom validation to
};

Plugin.prototype.validateRequest = function(request, response, callback) {
  // Validate the API request
};

Plugin.prototype.applyCredentialsForTest = function(options) {
  // Support internal requests, e.g. from the admin console
};

Plugin.prototype.applyResponseForTest = function(response, body) {
  // Support internal requests, e.g. from the admin console
};

module.exports = Plugin;

Custom Authentication is described in the Appcelerator online docs here. A simple example of a basic custom authentication is provided as well.

ArrowDB API User Database

In this example we will leverage ArrowDB to store each user’s API Key and the custom authentication plugin method, validateRequest, will validate the request header against the ArrowDB.

My ArrowDB model is shown below:

models/apiuser.js

var Arrow = require("arrow");
var keygen = require("keygenerator");

var Model = Arrow.createModel("apiuser",{
    "fields": {
        "username": {
            "type": "String"
        },
        "key": {
            "type": "String",
            "set":function(val,key,model){
        return keygen._();
      }
        },
        "enabled": {
            "type": "Boolean",
            "default": true,
        },
        "count": {
            "type": "Number",
            "default": 0,
        }
    },
    "before": "validateapiuser",
    "connector": "appc.arrowdb",
    "actions": [
        "create",
        "read",
        "update",
        "delete",
        "deleteAll"
    ],
    "singular": "apiuser",
    "plural": "apiusers"
});

module.exports = Model;

The apiuser model above defines fields for the username and key (API key) , an enabled field for whether the user is enabled or not and a count field to contain the number of API calls for the user. The Plugin module will leverage the key field to validate that the request using an API key passed in the x-api-key header. Then it will make sure that the user is enabled and finally will increment the count field as an example of some simple API Management features. The model uses the keygenerator npm to create an API key when the user record is created by using the model set property. It sets some default values using the model default property.

Finally a pre-block, validateuser is used to make sure that the username is unique. This block is shown here:

blocks/validateuser.js

var Arrow = require('arrow');

var PreBlock = Arrow.Block.extend({
    name: 'validateapiuser',
    description: 'validate api user',

	action: function (req, resp, next) {

	    if((req.method==="POST" || req.method==="PUT")) {
	        var model = Arrow.getModel("apiuser");
	    model.query({username: req.params.username}, function(err, data){
	        if(err)
	                resp.response.status(500); //workaround - https://jira.appcelerator.org/browse/API-852
	                resp.send({"error": "cannot access user api database"});
	                next(false);
	        } else {
	                if(data.length > 0) {
	                    resp.response.status(500); //workaround - https://jira.appcelerator.org/browse/API-852
	                    resp.send({"error": "duplicate username"});
	                    next(false);
	                } else {
	                    next();
	                }
	        }
	    });
	    } else {
	        next();
	    }
	}

});

module.exports = PreBlock;

So a POST curl command looks like this:

javascript
curl -X POST "http://127.0.0.1:8080/api/apiuser" -d '{"username":"user1"}' -H "Content-Type: application/json"

After entering a few users, my apiuser authentication database looks like the following:

{
    "success": true,
    "request-id": "d7f19519-96d7-4145-9a92-a596e1effc9a",
    "key": "apiusers",
    "apiusers": [
        {
            "id": "55886d461b40070b891e6b48",
            "username": "user3",
            "key": "bnL1Pr02msoCiQeC1Lh87cl5izAflVOw",
            "enabled": false,
            "count": 0
        },
        {
            "id": "558861291b40070b891e4da9",
            "username": "user2",
            "key": "mXDSM3u9y9RhL8WdRA0vbTVscXyaisCw",
            "enabled": true,
            "count": 0
        },
        {
            "id": "55886125e014044797092cc8",
            "username": "user1",
            "key": "pwPAD5uUHjyppztNGytmVMBoF2eE8VUk",
            "enabled": true,
            "count": 0
        }
    ]
}

Coding the Custom Authentication

Since my apiuser authentication database is an ArrowDB database (with it’s own API) and I don’t want my API users to have access to this, I will create a special admin-secret header authentication that will provide access to all APIs. Only admins should have access to this. The API users would only have access to their API key and will use their API key in a header to access the data APIs.

The value of the admin-secret will be stored in conf/default.js as follows:

conf/default.js

module.exports = {
    ...
    APIKeyAuthType: 'plugin',
    APIKeyAuthPlugin: './lib/plugin.js',
    adminsecret: 'adminsecret',
    ...
}

The Plugin constructor now looks like this:

lib/plugin.js

...
function Plugin(server) {
  this.config = server.config;
}

The next thing we need to do is specify which Arrow APIs should be validated using this custom authentication scheme. I would like all APIs to be validated, so my matchURL methdod will look like this:

lib/plugin.js

...
Plugin.prototype.matchURL = function(request) {
    return true;
};
...

(Refer to the this post for a sample of more fine grained control of API endpoints for authentication).

The last thing we need to do is define the custom validation method validateRequest. This method is shown below:

lib/plugin.js

...
Plugin.prototype.validateRequest = function(request, response, callback) {

	//admins have access to all APIs
	if (request.headers['admin-secret'] && request.headers['admin-secret'] === this.config.adminsecret) {
	  callback(null, true);
	  return false;
	}

	if(request.url != '/api/users'){ //don't allow access to the users database
	  var model = Arrow.getModel("users");
	  model.query({key: request.headers['x-api-key'] || "key not found"}, function(err, collection){
	    if(err) {
	      callback(null, false);
	    } else {
	      if(collection.length>0){
	        model.findOne(collection[0].id, function(err, data){
	          if(err) {
	            callback(errorMsg_serverError, false);
	          } else {
	            if(data.enabled) {
	              data.count++;
	              data.update();
	              callback(null, true);
	            } else {
	              callback(null, false);
	            }
	          }
	        });
	      } else {
	        callback(null, false);
	      }
	    }
	  });
	} else {
	  callback(null, false);
	}
};
...

In the validateRequest method above, the first if statement checks for API calls that have the admin-secret header set to the value in conf/default.js. If this header is set properly, then access is granted and the method exits. This gives admins access to the users database for creating, reading, updating and deleting and prevents non admin access to the users authentication database.

if (request.headers['admin-secret'] && request.headers['admin-secret'] === this.config.adminsecret) {
  callback(null, true);
  return false;
}

(Refer to the this post for an alternate example of implementing a fallback authentication scheme that could be used for admins).

If the admin-secret header is not set or not equal to the correct value then the next if statement executes.

It then performs the following:

  1. Query the users authentication data to see if a matching API key exists
  2. If a match exists get the corresponding record from the users database
  3. If the user record enabled field is true, then increment the count field and send a success=true response via the callback() method
  4. If no API key match is found or if found and enabled is false then set the success=false response via the callback() method

The reply to a properly validated request (to Salesforce, for example) is shown below:

curl "http://127.0.0.1:8080/api/account"  -H "x-api-key:bnL1Pr02msoCiQeC1Lh87cl5izAflVOw"

{
    "success": true,
    "request-id": "ac539521-b0cd-4df8-9cb9-64e96dfaf447",
    "key": "accounts",
    "accounts": [
        {
            "id": "001i000000PscewAAB",
            "Name": "GenePoint",
            "Type": "Customer - Channel",
            "Website": "www.genepoint.com"
        },
        ...
        {
            "id": "001i000000Pscf6AAB",
            "Name": "United Oil & Gas Corp.",
            "Type": "Customer - Direct",
            "Website": "http://www.uos.com"
        },
        {
            "id": "001i000000Pscf7AAB",
            "Name": "sForce",
            "Website": "www.sforce.com"
        }
    ]
}

The reply to an invalid request (i.e. header not set or wrong API Key) is shown below:

curl "http://127.0.0.1:8080/api/account"

{
    "id": "com.appcelerator.api.unauthorized",
    "message": "Unauthorized",
    "success": false
}

A non admin request to the apiuser database is shown below:

curl "http://127.0.0.1:8080/api/users"

{
    "id": "com.appcelerator.api.unauthorized",
    "message": "Unauthorized",
    "success": false
}

After several (14) API reqeusts from user1 the apiuser database is shown below. You can see how the count for user3 has increased to 14.

{
    "success": true,
    "request-id": "d7f19519-96d7-4145-9a92-a596e1effc9a",
    "key": "apiusers",
    "apiusers": [
        {
            "id": "55886d461b40070b891e6b48",
            "username": "user3",
            "key": "bnL1Pr02msoCiQeC1Lh87cl5izAflVOw",
            "enabled": false,
            "count": 14
        },
        {
            "id": "558861291b40070b891e4da9",
            "username": "user2",
            "key": "mXDSM3u9y9RhL8WdRA0vbTVscXyaisCw",
            "enabled": true,
            "count": 0
        },
        {
            "id": "55886125e014044797092cc8",
            "username": "user1",
            "key": "pwPAD5uUHjyppztNGytmVMBoF2eE8VUk",
            "enabled": true,
            "count": 0
        }
    ]
}

Note, that normally, one would also code the applyCredentialsForTest and applyResponseForTest methods in your Plugin as these are used to enabled admin console access to your custom authenticated APIs during development. I have left these out for this blog post for brevity.

Summary

In this blog post we saw how easy it is to implement a custom authentication scheme and implement basic API Management features. Using these techniques you could implement other API Management features such as rate limiting and role based authorization/authentication.

Code for this example can be found here.

21 Flares Twitter 0 Facebook 0 Google+ 0 LinkedIn 21 Email -- 21 Flares ×

Sign up for updates!

Become a mobile leader. Take the first step to scale mobile innovation throughout your enterprise.
Get in touch
computer and tablet showing Appcelerator software
Start free, grow from there.
21 Flares Twitter 0 Facebook 0 Google+ 0 LinkedIn 21 Email -- 21 Flares ×