A few weeks ago, we had a hacking session at Opendoor. As we continue growing, we're starting to implement an increasing number of smaller, more focused services within our stack in a range of languages. For my hack week project I decided to work on a way to authenticate these services both inside and outside of our private network. Service authentication becomes increasingly painful as the number of services you support grows.

I wanted something low maintenance, easy to deploy and easy to add new services to – ideally something that would:

Paladin

  • Use standard tooling
  • Use the same mechanism for authenticating internal and external services/clients
  • Be easy to use in development
  • Be easy to run in production
  • Make adding new services (and controlling their permissions/authentication) easy
  • Require minimal setup in various languages and rely heavily on existing libraries
  • Allow existing services to continue running without excessive changes required

Over the past holiday season I worked on a few things, played in the snow and read a few RFC's. One of the RFC's that @scrogson pointed out to me was the OAuth2 assertion token extension. This describes a way to generate access tokens from a central auth server where one client, known to the auth server, generates a JWT with requested credentials signed with their secret. The auth server checks the signature and if appropriate generates a new access token for use with the target service.

Getting started

When you start your service, chances are you're not thinking about service-to-service authentication. It's likely a simple setup that talks from your service to a browser or mobile device.

Starting Out

If you're using JWTs already via a library like Guardian you're in a great place. Your application is likely already minting JWTs and using them for authentication. When you receive an authenticated request, the flow usually resembles:

  1. Receive request
  2. Check for a valid JWT signed with your app's secret
  3. Serve request if valid

A JWT is valid if it's not expired, is signed with the correct secret, and meets any other logic your application decides to implement. It doesn't matter to your application if the token is in a browser session, an authorization header or even on a channel – when it's valid, things just work.

The OAuth2 assertion framework allows this flow to continue as is. You can continue to issue JWTs, but this setup allows other actors to also come onto the scene without any changes to your application as your eco-system grows.

Growing up

When you start to get a lot of services and actors, the choice you make for your authentication mechanism matters.

Adding new authorized services

Now that you want to build a new service (you do right?) the assertion framework allows you to do this by providing a mechanism for you to fit it into this flow. Your original app won't even know (unless you want it to). This flow goes like:

  1. Service B wants to talk to Service A
  2. Service B mints a JWT including all the credentials it needs to talk to Service A and signs it with Service B's secret.
  3. Service B issues an authorization request to the Authorization service (including who it is, and which service it wants to communicate with).
  4. The Authorization service checks to see if Service B is allowed to communicate with Service A, and with what permissions.
  5. The Authorization service (assuming everything is allowed) then issues an access token to Service B that is signed with Service A's secret.
  6. Service B issues its request to Service A with its access token
  7. Service A receives the request, signed with its own secret and serves the request.

Service B was able to obtain an access token from the Authorization service and make the request to Service A, without Service A needing any additional configuration or management.

Paladin in action

The authorization service

In this story, Paladin is the Authorization service.

Paladin's role is to act as the middleman, providing a live ACL implementation and issuing access tokens. Paladin knows everyone's JWT secrets so that it can correctly mint JWT access tokens for each service, and verify incoming authorization requests.

When you register your service with Paladin, you'll be issued a secret. This secret is needed to sign your JWTs. In Guardian this is your secret_key_base.

Getting Paladin up and running

Paladin is set up to run as part of an umbrella application. The main Paladin application is the Phoenix app; the other application for use in the umbrella is your own code that actually authenticates users for the web UI (via Überauth).

We have provided an example umbrella application at Devise Paladin that is designed to allow Paladin to find your users in a Devise database to add and configure services from a web UI. The main parts are:

  1. Guardian serializer - serialize your user in and out of the token
  2. Paladin.UserLogin behaviour - given an ueberauth auth struct, find the user and grant permissions

The remainder of Devise Paladin is concerned with setting up Ecto and supervisors and such.

Devise Paladin can be used as is, but I suggest you fork it and make it your own. Paladin is installed as a git submodule so that the main part of Paladin can be updated independently of all projects that use it, and then brought into your umbrella and customized for your needs.

If possible you should run Paladin inside your firewall. It doesn't need to be, as Paladin is authenticated and run as an edge server, but it's best run in private.

Using Paladin

Once you have Paladin up and running and talking to your database (i.e., you can login), you can then add your services.

Add a couple of services and note the UUID of each one and put the secret into the owning application for generating your JWTs.

Get an access token

Let's see how to get an access token:

First - generate a JWT from the requesting service signed with its secret:

  def assertion_jwt(user, app_id_to_talk_to)
    claims = {
      aud: app_id_to_talk_to,
      sub: "User:#{user.id}",
      iss: MY_PALADIN_APP_ID,
      iat: Time.now.utc.to_i,
      exp: (Time.now + 2.minutes).utc.to_i,
    }

    JWT.encode(claims, MY_PALADIN_SECRET, 'HS256')
  end

You can add any other claims as needed, but the above are required.

Now make a request for an assertion token:

 def access_token(user, app_to_talk_to_id)
    params = {
      "grant_type" => "urn:ietf:params:oauth:grant-type:sam12-bearer",
      "assertion" => assertion_jwt(user, app_to_talk_to_id),
      "client_id" => MY_PALADIN_APP_ID
    }

    response = HTTParty.post(
      PALADIN_DOMAIN + "/authorize",
      body: params.to_json,
      headers: { "Content-Type" => "application/json" }
    )

    json = JSON.parse(response.body)
    if json["token"]
      exp = response.headers("x-expiry").to_i
      { token: json["token"], exp: exp }
    else
      raise "OH NO! #{json["error"]} - #{json["error_description"]}"
    end
  end

The grant type is part of the OAuth2 spec. Now that we have the access token we can make a request to the service.

token = access_token(user)[:token]

# Now we have a token, let's party.
profit = HTTParty.get(
  "https://the_service.com/blah/things",
  headers: {
    "Authorization" => "Bearer #{token}"
  }
)

It's just like you're one of any number of clients talking to the service!

Implementing the server side is even easier:

token = read_auth_header_and_strip_bearer(env)
decoded_token = JWT.decode(token, PALADIN_SECRET, true, { :algorithm => 'HS512' }).first

user = case decoded_token["sub"]
when "anon"
  nil
when /^User:.+$/
  User.find_by_token(decoded_token["sub"].split(":").last)
else
  raise "NOPE"
end

Token expiry

When you configure a service to talk to another, you can specify the maximum expiry time allowable in the access token. If your requesting service asks for an expiry greater than this it will be knocked down to the maximum allowable.

When you're generating your token to exchange for the access token include a field rexp with the expiry timestamp you'd like.

Permissions

Paladin uses Guardian's permission feature to encode permissions into the JWT. When setting up Paladin, you'll need to include all known permissions in its Guardian configuration - then for each service you can select the maximum permissions. If a request comes in with permissions greater than this it will be knocked down to the maximum configured.

See the Devise Paladin README for which environment variables are required.

Mega configuration

The use of an Umbrella application means that you can override any and all configurations belonging to either Paladin or your part of the umbrella application. Just include the configurations into your umbrella app and away you go.

Security concerns

There's never a silver bullet and every solution has its drawbacks. Using JWT for your auth mechanism is well understood to have limitations. Once a JWT is issued it's valid until its expiry runs out or you track it in some way, like with GuardianDb for example. For this reason there's a couple of things to bear in mind.

  1. Assertion tokens (the ones requesting access tokens) should have a short expiry time
  2. We need a kill switch so we can remove bad actors

If something goes rogue what do you do? The easiest solution is to remove it from Paladin so it cannot communicate with other services and they cannot communicate with it. This does not address already issued tokens however, if you want to invalidate all existing tokens to a service the easiest way to do this is to regenerate its secret in Paladin, pdate the secret in the services configuration and restart it. This will invalidate all existing tokens for that service (a pretty heavy handed approach).

If you're really concerned about it, you can use Guardian.Hooks to store the assertion tokens in a DB and have each service validate with Paladin on each request that it's still valid. That's pretty heavy handed but sometimes appropriate. I haven't set this up in Paladin by default but given that Paladin exists in an umbrella application it's easy for you to do. Create the Hooks module you want to use and set up the Guardian configuration to use it in your umbrella. Paladin can have whatever 'verification' logic you want it to in this way. You can also implement a custom Guardian.ClaimValidation module for full control over what you consider valid.

In the short time we've had it available to us at Opendoor we've found that it's been easy to use and maintain – hopefully it helps meet your needs too. We'd love to have Paladin become a community asset and welcome any pull requests to help make it awesome for everyone.