API Authentication with Taffy

Recently I have been working with Taffy to create a simply REST API. The API is used by a native mobile application on the iPhone. When it was complete I needed a simply way to authentication the application talking to the API.

This is not something I was familiar with at all. I looked into lots of different methods before I started. I really did not want, nor have the experience to reinvent the wheel, so I look at current methodologies around the web.

My first attempt was BASIC authentication, but this did not feel right. For reasons I won't go into here, the API was not over HTTPS, anyone could sniff out the password. Bad, very bad!

I found a really good post by Greg Moser on AJAX Authentication with Taffy REST API. He talks about using sessions as an API key. This is a good idea, but as my application is mobile, not really applicable for my situation. However his post did get me thinking and was very helpful.

I didn't need the complexity of OAuth. I found that I liked the way Amazon secures their API. So I looked more into this approach.

I needed a simple "half OAuth" approach. Mainly without the user having to approve. This is how I got my head around it all and what I ended up with, code and explanation below.

Taffy's application.cfc

view plain print about
1//this function is called after the request has been parsed
2    function onTaffyRequest(string verb, string cfc, struct requestArguments,
3 string mimeExt,
4 struct requestHeaders){            
5        return authenticationService.authenticateReuqest(verb=verb, cfc=cfc,
6 requestArguments=requestArguments, requestHeaders=requestHeaders);
7    }

authenticationService.cfc

view plain print about
1component persistent="false" accessors="true" output="false" {
2
3    property name="fw" type="any";
4    property name="apiUsersDAO" type="any";
5
6    public any function authenticateReuqest(required string verb,required string cfc,
7    required struct requestArguments,required struct requestHeaders) {
8    
9        if(!checkRequiredArguments(arguments.requestArguments)) {
10            return createAuthenticationRequiredMessage("Authentication Required: Missing request arguments.");
11        }
12    
13        if(!havePrivateKey(arguments.requestArguments.publicKey)) {
14            return createAuthenticationRequiredMessage("Authentication Required: No API user by the key.");
15        }
16    
17        if(!timeInAcceptableBounds(arguments.requestArguments.timestamp)) {
18            return createAuthenticationRequiredMessage("Authentication: Request timeout");
19        }
20        
21        /*
22         Since I already determined this timestamp was within acceptable bounds to be accepted,
23        I still use it within my own hash calculation to make sure it was the same timestamp sent
24        from the client originally in their sign, and not a made-up timestamp
25        from a man-in-the-middle attack.
26        */

27    
28        if(!compareSignature(theirSign=arguments.requestArguments.signature,
29         areSign=EncryptSignature(argValue=createSignString(arguments.requestArguments),
30         publicKey=arguments.requestArguments.publicKey))) {
31            return createAuthenticationRequiredMessage("Signature is bad, get lost.");
32        }
33        
34        return true;
35    }
36    
37    public any function timeInAcceptableBounds(required any timestamp)
38    hint="I check the request was send within acceptable bounds." {
39        local.dif=DateDiff("s",arguments.timestamp,now());
40        if(local.dif gt 20) {
41            return false;
42        } else {
43        
44            return true;
45        }
46    }
47    
48    public any function compareSignature(required any theirSign,required any areSign)
49    hint="I compare the two signatures." {
50        /*     I don't think trim is needed, I just don't know or care it's
51        8pm and I am still in the office! */

52        if(trim(arguments.theirSign) eq trim(arguments.areSign)) {
53            return true;
54        } else {
55            return false;
56        }
57    }
58    
59    public any function EncryptSignature(required string argValue,required string publicKey)
60    hint="I create my own signature that I will match with the clients" {
61        // Why can CF not do this?
62        var jMsg=JavaCast("string",arguments.argValue).getBytes("iso-8859-1");
63        var jKey=JavaCast("string",getapiUsersDAO().getSecretKey(arguments.publicKey)).getBytes("iso-8859-1");
64        var key=createObject("java","javax.crypto.spec.SecretKeySpec");
65        var mac=createObject("java","javax.crypto.Mac");
66        key=key.init(jKey,"HmacSHA1");
67        mac=mac.getInstance(key.getAlgorithm());
68        mac.init(key);
69        mac.update(jMsg);
70        return lCase(binaryEncode(mac.doFinal(),'Hex'));
71    }
72    
73    public string function createSignString(required struct requestArguments)
74    hint="I create the string for my signature." {
75        var local.returnString = arguments.requestArguments.timestamp & arguments.requestArguments.publicKey;        
76        return local.returnString;
77    }
78    
79    public any function havePrivateKey(required string publicKey)
80    hint="I check API User exisits and has a key." {
81        return getapiUsersDAO().hasPrivateKey(arguments.publicKey);
82    }
83    
84    public any function checkRequiredArguments(required requestArguments)
85    hint="I check we have the required arguments to authenticate this request." {
86        if(structkeyexists(arguments.requestArguments,"publicKey")
87        AND structkeyexists(arguments.requestArguments,"signature")
88        AND structkeyexists(arguments.requestArguments,"timestamp")) {
89            return true;
90        }
91        return false;
92    }
93    
94    public any function createAuthenticationRequiredMessage(string message) {
95        local.bodyContent=structnew();
96        local.reponseObject=createObject("component","site.api.representations.myRepresentation");
97        bodycontent.msg=arguments.message;
98        return reponseObject.setData("").withMessagesAndErrors(local.bodyContent);
99    }
100    
101    
102    
103
104    
105}

Basically the principle is as follows...

1) My server and the mobile client have a public and private key. Only the server and mobile client know the private key. The private key is never transmitted, very important! The public key is only used as a means to identify who is speaking to the API. I can identify who is making the call so I know which private key to use.

2) The mobile client creates a unique HMAC (hash) representing it's request to the server by combining the values of the parameters being passed. The client also adds a timestamp to the hash.

3)The client then sends the HASH value to the server, along with all the arguments and values it was going to send anyway. In my example code above the hash value is just another parameter, but you could add this to the request header. If you do tho please remember base64, the fun I had!

4) Now the server receives the request and looks at the timestamp (the non-hashed version) to decide if this is an "old" request. The timestamp was also included into the HMAC generation (effectively stamping a created-on time on the hash) in addition to being checked "within acceptable bounds" on the server.

5) The server re-generates its own unique HMAC (hash) based on the submitted values that were not hashed (including the non-hashed version timestamp) using the same methods the mobile client used with their own private key.

6) The server then compares the two HMACs values. If these are equal, then the server trusts the client, and runs the request.

That's it. If the request has been changed in anyway by a 3rd parity for example the the non-hashed version timestamp, then the signature would not match and it would be rejected.

Every time a request is generated by the mobile application it has XX of seconds to use it.

A middle man could change the time in the URL, but it would make no difference. This is because it would be rejected as the signatures would no longer match. I used the timestamp within my own hash calculation. This makes sure it was the same timestamp sent from the client originally in their sign, and not a made-up one from a man-in-the-middle.

The API can still be compromised if someone gets the private key by decompile the application. However, I now have more control over how the keys are used. I could reissue a new private key and limit the old one until the developer has found a new way to secure the application.

Nothing I have done is different, but this concept did take a while for me get my head around. This was something I have never looked into nor had any experience with before. I am sure I will need full oAuth down the line, for now this seems to work well.

I would be very interested in the pros and cons of the method I went with from people who have more experience securing an API. My requirements were not the norm so given my scenario I personally think the approach was acceptable.

Sep22

Comments 2

  1. spills's Gravatar # Posted By spills
    22/09/11 22:41

    Thanks for a very detailed post with some awesome ideas. If this not being done over HTTPS your API key is not secure, just the nature of the beast. I am not sure creating private and public keys adds anything to the overall security. I am relatively new at REST but have experience with web services in health related areas where HIPPA is an issue. We will exchange trusted public keys with vendors and internal servers but never our private keys. This system only works if the data packet is encrypted and signed before ever going over the web via ftp, http or https so typically we are talking application servers communicating with one another. If the request is coming from a browser or mobile app that can't encrypt and sign the data packet before communicating with the REST application then you aren't gaining anything and processing keys can be very server intensive once you have a few of them.

    Spills

  1. Glyn Jackson's Gravatar # Posted By Glyn Jackson
    23/09/11 10:21

    Thanks Spills. You are right if it’s not over HTTPS that it can be seen.

    You really don’t care who see the public key for the request or even the timestamp for that matter with the above. The public key is only a way to identify who is talking to you. The private key is never passed ever! Even if a client intercepts the request, they cannot use it on the next request because it will have already timed out.

    If they try and change the request i.e. the timestamp (which acts as short lived token) then the signatures would not match and the request would be rejected anyway. If I changed or lengthened my request time then this would be a mistake.

    Still, it likely other sensitive information like peoples names and account details are in request and I would recommend using SSL on top of this to protect peoples data.

    Not sure on the performance hit tho. If the user is entering sensitive information, even though it may be a trusted website URL, it is still vulnerable.