Simple API Authentication With Authorization Headers in Sinatra

Rails has a really helpful (but not well documented) token authentication mechanism you can use for authenticating your API users. But Rails is overkill for many APIs and so I usually turn to Sinatra. Sinatra doesn’t have much selection when it comes to API authentication. At least not the secure methods I’m interested in. So for my company’s new insurance quoting and licensing API I rolled my own. Here’s how to implement secure token authentication in a Sinatra app.

Here I’ll go over just the API authentication mechanism. In our application we have a simple UI that allows for users to sign up for our service as well as the programmable API. Because of this we have to structure our application so that API requests don’t load up view helpers or CSRF tokens. We basically had to sort of mount our Sinatra app as two Rack applications each being loaded up differently because of their use cases.

In any case, this is just something to be aware of as we go forward. If you have questions or need help implementing any of this leave me a comment. If you’re a Ruby developer looking for a job making the insurance industry less boring, more modern, and more open, you should also leave a comment. Anyway, here we go.

How Token Authentication Works

For Q, our Quoting API we decided to rip off Amazon and do API authentication by signing requests and sending those signatures in a header. In our case we take the URI and request body, concatenate them, and then hash them (using HMAC) with the API secret token as they key.

For example, this request: curl -i https://q.aploquote.com/plans/il/60654/ would be handled like this (pseudo code follows):

1
2
3
4
5
6
7
8
9
10
11
URI='/plans/il/60654/'
BODY=''
CLIENT_ID='my-public-api-key'
CLIENT_SECRET='my-secret-token'

SIGNATURE = HMAC('sha1', URI + BODY, CLIENT_SECRET)

HEADER = 'Authorization: ' + CLIENT_ID:SIGNATURE

# Then you can take the result of the signature and use it in a cUrl request like so:
curl -i -H $HEADER https://q.aploquote.com/plans/il/60654/

See what we did there? Let’s take it line by line before we get into how we can accept this in a Sinatra application.

  1. The request URI is the endpoint we’re sending the request to minus the domain. Trailing slashes or case sensitivity shouldn’t matter so long as the data you use to create the signature must exactly match the request you end up making.
  2. In this case our example is a GET request so the body is an empty string.
  3. We then take our API client ID (which is public) and our secret token (which obviously needs to be kept secret) and hold them in a few variables.
  4. We then generate a signature by running all this data through our language of choice’s HMAC function. Here we use sha1 as the algorithm, our request URI and request body concatenated as the data to hash, and our API secret token as the key.
  5. Now we generate the header. This is a simple authorization header in this format: Authorization: CLIENT_ID:SIGNATURE. Notice that the content of the Authorization header is the API public key/client ID and the request signature concatenated with a : (colon) separating them. This is important because its how we’ll separate the signature from the client who signed it later.
  6. Now we just send out a cURL request with the header we generated.

How tokens are verified

On the server side, the application will get the content of the Authorization header and then split it into two strings by the : character. The first string is the API client who sent the request and the second is the signature. The API server will then look up the key belonging to the API client and use it to run the same exact HMAC function that was done on the client. If the signature that was sent by the client and the one generated on the server match then the server can be sure that whoever sent the request definitely has the client ID and secret on them.

How secure is this?

Very. Well, provided you take a few measures to ensure security it is quite secure. You can make some dumb mistakes that will bite you however so here’s how to mitigate them.

Intercepting the client ID and secret is possible. Its a fact of life that someone can be sniffing packets and pick up the credentials you send to the client when they sign up. The way to mitigate this is by ensuring you use SSL everywhere in your API with no exceptions. If you only send the token once then there’s only one chance for an attacker to sniff out the token because from then on you never send the token itself over the wire. Instead you’re always sending hashes that were generated by the token. Because its a hash there’s no way to retrieve the token from any signatures it generates.

If the client forgets their token do not allow them to retrieve it. Instead force them to reset it.

Don’t store the token in plain text in your database. Communicating between the API and client using these signatures is very secure but if an attacker gains access to your database then they have the keys to the castle. In our API we don’t store the client’s secret tokens in plain text. Instead we have an encrpytion library which encrypts the token using AES-256 before it hits the database. When we generate a new client ID and token we send the token and ID to the client as a forced download of a text file and then encrypt the token just before its saved to the database. When we look up our client’s token the next time they make a request we decrypt it before using it to compare the signature.

Don’t store your encryption keys on disk! That’s how you’ll get them stolen. If someone can get access to your database then they could also get access to your file system. So from there they can decrypt all your stored tokens and all you’ve done is slowed them down. Storing your encrpytion key in an environment variable that your app reads when it starts up is a step up as it’ll be wiped when the system shuts down. There are lots of other solutions to this problem that you can Google for if you’d like. There’s actually a lot of cool services that’ll handle passing off sensitive credentials to your web applications in a secure way.

Remember this – the client ID is public. There should never be any fear of anyone getting their hands on it. Don’t worry about securing that as much as the token.

Finally, authenticating API clients using an Authorization header in Sinatra

So here we go, the reason you’re reading. Now that you know how this all works I’ll show you the code. We implemented this as a before filter but I’m actually in the process of breaking this out into a Rack middleware that we’ll be open sourcing soon.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
before do
  # Pass on selected routes
  pass if ['some-route'].include? request.path_info.split('/')[1]

  # Pull out the authorization header
  if env['HTTP_AUTHORIZATION'] && env['HTTP_AUTHORIZATION'].split(':').length == 2
    auth_header = env['HTTP_AUTHORIZATION'].split(':')
  else
    halt 401
  end

  public_key  = auth_header[0]
  signature   = auth_header[1]

  client = User.first(public_key: public_key)

  halt 403 if client.nil?

  # Our 'ENCRYPTION_KEY' variable is available in the app via secure means
  security      = Aplo::Security.new(ENCRYPTION_KEY)
  client_secret = security.decrypt(client[:token])

  data = request.path
  data = "#{data}?#{request.query_string}" if request.query_string.present?

  if ['POST', 'PUT', 'PATCH'].include? request.request_method
    request.body.rewind
    data += request.body.read
  end

  computed_signature = OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('sha1'), client_secret, data)
  if computed_signature == signature
    pass
  else
    halt 403
  end
end

This before filter can be used with just a few modifications. You’ll just need to set your own encrpytion key and set up the User model.

The code above demonstrates everything I’ve explained. The first line skips authentication for any routes that you may not need authentication for like a ping route or something. Then we get into pulling the auth header from the… well, headers. If it doesn’t exist then we deny access. We pull out the client ID and signature into a couple of variables after splitting up the auth header using #split(':'). We take the client ID and use it to look up in our database to find the token we need to compute the signature.

Here’s where things get a little less portable. We have our own encryption library that we use to handle the encryption and decryption. It’s pretty standard and you can pop in any gem to handle this if you need it.

After that we we decrypt the secret token and generate our own request signature using the request URI, query string params, and any reqest body that was sent over if applicable. From there we use Ruby’s OpenSSL and SecureRandom libraries to generate an HMAC signature from these request params.

If everything went well then the signature we generate on the server should match the signature the client sent to us. If that’s the case we know the request is genuine and we let it pass. Otherwise we throw up a 403 in the client’s face.

Troubleshooting

There’s a couple of issues you may run into when you start creating client libraries to communicate with your API. They’re simple mistakes that’ll trip you up for a long time and when you finally figure out what’s wrong you’ll feel stupid. So here are the common problems we encountered. Hopefully they save you some debugging time.

  1. Always send a content type and content length header. If you don’t send Rack or Sinatra a content length header you may see your app start adding in random integers in your request body which then makes your signatures mismatch even if they shouldn’t. Content length is likely the most important header here.

  2. Make sure your signature is generated as a hex string. I had to write some Go code that talked to this API and it wasn’t working because by default it generates hashes as bytes instead of a string. In fact a lot of languages will default to outputting the result of an HMAC function as a byte type instead of a hex string.

  3. If you use Node and your API endpoints are using SSL then you need to use Node’s https library, not the regular http library. Even if you use port 443 with the http library it’ll still be sending plain HTTP requests. That one bit me. Turns out https works the same way the http module works except you know, it does it over SSL instead.

Within the next week or two the links I posted to our API will actually start working and there will be a UI along with a way to sign up to use it and a way to peruse its docs. When that happens you can take a look at those docs to get a better sense of how this sort of authorization works. It won’t include anything about Sinatra, just the authentication concepts.

API, Security, Web development

« Initializing a class in Node We don't use CoffeeScript ever »

Comments