A couple of weeks ago I was working with Sumit Verma on securing the Taffy REST API that we have implemented for Slatwall eCommerce. While there are tons of resources online about different methodologies for REST authentication, we were having a hard time finding a deffinitive "best practice". Most of the time when people talk about REST authentication they talk about 3rd party services using the API over HTTP in some form or another. However there is an additional ussage for our Taffy API, and that is to provide a solid backbone for all of our AJAX requests. With that in mind here is the solution that I came up with for our REST authentication.
With the 3rd party service access, we want to hand over an API key along with password to give them access to a selected subset of resources. However with AJAX we want to give access to any resource a developer wants as long as the developer has given explicet permission for the view to use that resource via AJAX.
Originally we talked about generating an new API key on every request and storing it in the session. Then on the Taffy requests, we would check the session to make sure that the API key exisits and the proceed with serving up the JSON. The problem with having a single API key for a user is that it doesn't actually define which resources they have access to. The end user could just view the source copy the API key, and then use it for whatever they like. What I really wanted to do is programatically create a API key on the fly that is only valid for a single users session, and for a single resource / method... so here is how it works.
First we create two methods to generate and validate API keys on the fly:
public string function getAPIKey(required string resource, required string verb) {
var apiKey = hash("#now()##lcase(arguments.resource)##lcase(arguments.verb)#");
session.APIKeys[ apiKey ] = {resource=arguments.resource, verb=arguments.verb};
return apiKey;
}
public boolean function verifyAPIKey(required string resource, required string verb, required string apiKey) {
var sessionAPIKeys = getValue("apiKeys", structNew());
if(structKeyExists(session.APIKeys, arguments.apiKey)) {
try {
if(session.APIKeys[arguments.apiKey].resource == arguments.resource && sessionAPIKeys[arguments.apiKey].verb == arguments.verb) {
return true;
}
} catch(any e){
return false;
}
}
return false;
}
As you can see, all we are doing is generating an api key that is unique to this session with a specific resource and a specific REST verb. Now all we need to do is generate this API key when we are setting up our jQuery or Javascript like this:
jQuery.ajax({
type: 'post',
url: '/plugins/Slatwall/api/index.cfm/product/#variables.productID#/',
data: {apiKey: '#getAPIKey('product', 'post')#'},
dataType: "json",
context: document.body,
success: function(r) {
// do something with results
}
});
Now we have a request being made to the taffy API that has a unique API key for a specific resource and verb. Also, that API key has been stored in the session so that when Taffy gets the request it can check the current session to make sure that the API key exists. In order to actually do the validation we use the taffy's built in onTaffyRequest() method:
public any function onTaffyRequest(string verb, string cfc, struct requestArguments, string mimeExt, struct headers) {
var apiKey = "";
if(structKeyExists(arguments.requestArguments, "apiKey")){
apiKey = arguments[3].apiKey;
}
if(request.context.$.slatwall.getService("sessionService").verifyAPIKey(resource=arguments.cfc, verb=arguments.verb, apiKey=apiKey)){
return true;
}
return createObject("component", "taffy.core.nativeJsonRepresentation").noData().withStatus(403);
}
And that is it, with no more than about 30 lines of code we have a REST API that block all requests with the exception of AJAX code that is generated on the server itself.
Comments
- Glyn Jackson Posted: September 9, 2011, 10:48 AM
-
Hi Greg,
Great post, but for 3rd parties i.e. a desktop app or mobile app how would this work securely. Have you looked at full oAuth? @ oauth.riaforge.org
I am just wondering why you took the direction you did and not a full oAuth implementation? You could still work in your solution for site requests without the user having to approve.
- Glyn Jackson Posted: September 9, 2011, 12:16 PM
-
also in your example you show a function called getValue() I take it this just exists/searches the session?
PS, now I have had time to play on a internal API here at work your solutions knida works better. However I still keep an Basic authentication system for apps not running in a browser. i.e.
public any function authenticateReuqest(required string verb,required string cfc,required struct requestArguments,required struct requestHeaders) {
// Check for Authorisation headers
if(not structkeyexists(arguments.requestHeaders,"Authorization")) {
return createAuthenticationRequiredMessage("Authentication Required");
}
// Check Authorization valid
local.apiAccess=retrieveApiUserFromAuthorizationHeader(arguments.requestHeaders["Authorization"]);
if(not len(local.apiAccess)) {
return createAuthenticationRequiredMessage("Invalid login credentials provided");
}
if(local.apiAccess eq true) {
return true;
}
return createAuthenticationRequiredMessage("Invalid login credentials provided");
}
- Glyn Jackson Posted: September 9, 2011, 12:17 PM
-
public any function createAuthenticationRequiredMessage(string message) {
local.bodyContent=structnew();
local.returnHeaders=structnew();
local.reponseObject=createObject("component","taffy.core.genericRepresentation");
bodycontent.msg=arguments.message;
structinsert(local.returnHeaders,"WWW-Authenticate","Basic realm=""FlipScape API - #arguments.message#""");
return reponseObject.setData(local.bodyContent).withStatus(401).withHeaders(local.returnHeaders);
}
public any function retrieveApiUserFromAuthorizationHeader(required string authorizationHeader) {
local.decodedAuthHeader=tostring(tobinary(listlast(arguments.authorizationHeader," ")));
local.username=ListFirst(local.decodedAuthHeader,":");
local.password=Listlast(local.decodedAuthHeader,":");
return validateLoginCredentials(local.username,local.password);
}
public any function validateLoginCredentials(required string login,required string password) {
local.result=getDAO().readByUserNameandPassword(arguments.login,arguments.password);
// If we have a match return true
if(!isNull(local.result)) {
return true;
}
// Default is always false.
return False;
}
- Greg Moser Posted: September 12, 2011, 5:53 PM
-
@Glyn you are correct that this type of authentication is just for AJAX request from the same site that is using the API. In addition to this methodology we are also implementing two other authentications.
1) Full oAuth for people that would like to use a user account from a separate site as their credentials for this platform, as well as use the services from the other site.
2) Standard API access with a Username / Password type setup where you give 3rd party applications long term access to specific resources in your API. Think of how you would access a service like authorize.net or UPS.com where you get a UN, PW, API Key to access certain API tools they offer.
Hopefully in future weeks I will have time to blog about those other two methods of authentication with Taffy.
-Greg
- Glyn Jackson Posted: September 14, 2011, 11:17 AM
-
Sounds good I have been looking at this too for another project below is a signing method for a 2 leg oAuth process.
https://gist.github.com/1216243
Looking forward to hearing how you guys have done this.
Glyn







