Secure Your API Authentication

Hypermedia, REST and HTTP are still bit misunderstood and many developers out there apply their own conclusions about how the authentication and versioning should look like, e.g. /api/v1/posts.json?api_key=KEY.

In most cases when building API, you want to build out some sort of authentication to control who have access to API and who doesn’t. Since authentication is usually driven by need to control amount of users and managing traffic, or managing paid consumers for the API, you want it to be as secure as possible. Therefore, you eventually don’t want to end up with this – /api/posts.json?api_key=KEY, but you should rather embrace HTTP headers instead. Here’s why.

API key as a parameter

Passing API key along with request parameters mixes the key with parameters for the resource actions and altogether just brings confusion, since it derives its function from authentication. You may argue that it doesn’t seem very odd, because many APIs use this approach, but that doesn’t justify it. Secondly, passing an API key as a parameter usually involves server-side lookup for the key in the database and based on it’s existence, server allows/denies access for the request. Much like this.

# app/controllers/api/application_controller.rb

def authenticate
  unless ApiKey.exists?(value: params[:api_key])
    head 401
  end
end

Rather than rendering unauthorized response, you should consider raising an exception. Excuse my brewity.

At the first sight, this approach doesn’t seem that bad. But once you consider timing attacks, you can quickly realize, that you’re approach is faulty from beginning. Comparison in database might be a subject to timing attack as well and can yield some unwanted information, unless you hash the key in database. But that’s not a great solution either, since you’re just covering the approach itself.

Instead of passing the key as a parameter, you should rather use HTTP headers. HTTP header Authorization comes in hand. So you refactor the authenticate method accordingly.

# app/controllers/api/application_controller.rb

# Request Headers:
#   Authorization: Token token="API_KEY"

def authenticate
  key, _ = ActionController::HttpAuthentication::Token.token_and_options(request)

  unless ApiKey.exists?(value: key)
    head 401
  end
end

That looks a lot better. Requesting the API with curl -H 'Authorization: Token token="API_KEY"' http://api.site.com/api/posts works just like before. But it only looks better, since the authentication logic is still flawed. We’re still exposing our database to timing attacks. So how can we fix it?

Token authentication with other metadata

The trick is to pass an additional attribute to validate key without exposing its comparison. In order to do that, we pass in other metadata such as email. We lookup account by the email, fetch stored API key and afterwards we compare the keys, securely.

# app/services/api/authentication_service.rb

# Request Headers:
#   Authorization: Token token="API_KEY", email="user@example.com"

module Api
  class AuthenticationService
    class MissingEmailError < ArgumentError; end

    def self.authenticate(request)
      token, options = ActionController::HttpAuthentication::Token.token_and_options(request)

      raise MissingEmailError unless options[:email].present?

      user = User.find_by(email: options[:email])

      if user && ActiveSupport::SecurityUtils.secure_compare(user.authentication_token, token)
        user
      end
    end
  end
end

The key step is the secure_compare, otherwise you would expose the comparison to another timing attack. So if all goes well – user is found and the key matches user’s key, you receive user object from Api::AuthenticationService and continue with the request or in case of nil or exception, you halt and render unauthrorized response.

But as you can see, this adds new authrorization level for a consumer of the API, since she has to make sure to pass in the email on every request and it seems like bit of a drag. But that’s the price for secure staleless authentication.

This is actually the easier solution to securing this particular API. Other option is to introduce more secure approach to generating API keys, e.g. JWT.

JWT – JSON Web Tokens

JWT is way more robust and standardized RFC7519 solution for authentication, since server is responsible for generating and validating tokens. Client only consumes generated token and passes it along in HTTP Authorization header. Just like before, but without additional metadata. JWT is very exhaustive for the purpose of this blog, so if you want to have thorough knowledge of how it works, look into RFC or check out its very consise introduction.

For purpose of minimal solution, let’s look at rolling you own authentication with JWT with Ruby. First, let me explain the basics.

"aaa.bbb.ccc"
# or
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ"

JWT contains of three parts. The first part – aaa is a header, base64 encoded JSON consisting of token type and encryption/hashing algorithm.

{
  "alg": "HS256",
  "typ": "JWT"
}

The bbb part is a payload. Payload contains information about issuer of token, expiration and so on. But payload is the place to store our identification of user, let’s say user’s id.

{
  "iss": "Identity Issuer",
  ...
  "user_id": 12
}

And the last part is signature. Signuture guaranties that data were not in any way altered on the way to the server, e.g. user_id is maliciously changed. To ensure that, server encrypts it with secret like this.

HMACSHA256(
  base64UrlEncode(header) + "." + base64UrlEncode(payload),
  secret
)

Security is satisfied by signing the payload and header with server’s secret and that way you cannot issue you own token. JWT implementation provides cryptographic hashing via HMAC or PKI with RSA. Take your pick.

Let’s authenticate user.

Authentication with JWT

Install JWT gem for Ruby and create Api::SessionsController for obtaining the token.

# app/controllers/api/sessions_controller.rb

def create
  # Authenticate user by login and password, e.g. has_secure_password, or render authentication error

  payload = { iss: 'Our Secure API', user_id: user.id }
  token = JWT.encode(payload, 'our little secret', 'HS256') # Use HMACSH256

  render json: token
end

That’s it. Let’s authenticate user with her token.

# app/controllers/api/application_controller.rb

# Request Headers:
#   Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJPdXIgU2VjdXJlIEFQSSIsInVzZXJfaWQiOjF9.n_I9tHYNAyEibhDLs0D_mSNV41owFt4GkACg5vg-DI0

def authenticate
  token = request.headers['Authorization'].match(/Bearer (.*)\z/)[1]
  payload, _ = JWT.decode(token, 'our little secret', 'HS256') # This raises an error if signature validation fails
  @current_user = User.find(payload[:user_id])
end

And that’s it. No API key is stored, only credentials for user.

You should extract encode and decode into separate class or service object, e.g. AuthenticationToken, which wraps JWT and know how to extract request headers and lookup user. Good idea is to setup expiration by exp payload attribute as well. Other than that, your simple secure authentication is pretty much done.

As you can see, JWT yields information to user in payload attribute, but you can remidy this by using you own approach for user lookup. You can generate your own cryptic token for each user, for instance. Timing attack would be pointless since you would validate the key first before trying to find a user by your token and the only information about encryption that would be a relevant to timing attack might be encryption algorithm mechanics, depending on proficiency of encryption implementation.

There’re also other robust solutions to authentication, for instance OAuth2. So before jumping into JWT, make sure to look into other alternatives as well.

Resources:

The Clockwords

Ramblings about software by Samuel Molnár.
Mostly about Ruby, JavaScript, databases and occasional peek into other things.

Subscribe via RSS.