It’s quite a common scenario that we are building an API that’ll be consumed by external applications and clients (machine to machine) who want to integrate with our backend. In many cases authentication and authorisation is an important element of this integration. When the OAuth flow is just too complex and inconvenient / not a viable solution the old username / password authentication method can still shine (especially when another API is consuming ours).
So what do we want?
- Authenticate calls to make sure our APIs are only accessed through the appropriate credentials
- Make sure with every request that the resource can be accessed by the caller
- Control the level of access to certain operations (read / write / manage, etc.)
- Capability to revoke access to a compromised token
- A secure and reliable solution end to end
If you don’t need the above functionality then rolling your own implementation might be fine, however in my experience with production grade public APIs the requirements above are quite common.
So what are we going to build?
- We’ll implement an API with a sign-in endpoint that’ll use Auth0 to authenticate users and issue JWT tokens with appropriate claims (Part 1)
- We’ll use this token to issue requests to our APIs, and use Guardian to verify it (Part 2)
- Use Plugs and Guardian to limit resource access based on permissions (Part 3)
Side note: I’ll assume you are somewhat familiar with Elixir, Phoenix and Plugs, so I’ll focus on explaining on how to integrate Guardian and Auth0 into your Phoenix app
If you want to jump directly to the solutions, you can find the repository here.
Auth0 is a fully managed identity solution. It manages storing users and credentials, metadata in a secure way. It deals with authentication, managing and issueing tokens while providing scale and high availability.
After signing up, setup our API application in Auth0:
- Create a new API called
OrdersSample
- Use HS256 signing algorithm (os RS256, however using the PK will be different in this case)
- Add a new permission:
read:orders
- Make a note of your Signing Secret (API page), ClientID and Client Secret (Application page)
- On the Test Application Settings page, at the bottom, select Advanced Settings and choose Password inside Grant Types. (see picture below)
- Create a new user on the Users & Roles page, assign the permission read:orders from OrdersSample.
Side note: In the background Auth0 has setup a connection (to a database) to store users and credentials, a connected test application for your API that defines the ClientID and the Client Secret. You can configure this to your needs, use an external database, define password policies, roles, etc.
Ultimately you want to see something like this:
Side note: Auth0 recommends that machine to machine authentication should be using the Auth0 tenant directly to obtain the access token. This is an OK approach when this is happening between APIs within the same system / company. When the partner who uses the API is external then having a proxy API which owns the ClientID and ClientSecret is a much safer approach. Additionally Guardian can use its own database to blacklist access tokens. Having this control “in-house” will pay off. However if this is not something you want to do you can skip the section about Sign-In with Auth0 and jump to Part 2.
After you generate your Phoenix application, include the Guardian library.
Now you need to create a configuration for accessing Auth0 to be able to log in. The values for production should be in environment variables, for development and testing purposes it’s fine to hard-code them.
In this section we’ll create the following components:
Credentials
module to define a schema for username / password and validate itTokenResult
module to encapsulate the access_token and the expires_in fieldsAuth
module to call Auth0 APIs and perform login. Accepts aCredentials
struct and returns aTokenResult
struct.
Let’s start with the Credentials
module.
This needs some explanations :)
- We are using
embedded_schema
that is great for input validations with Phoenix. Basically we get a struct on steroids. We need to turn off setting primary keys because by default anid
field is added. - Unfortunately embedded schemas don’t define typespecs so we need to do that manually.
- Inside the validate function,
apply_action(:insert}
applies the changeset to the map and outputs the{:ok, %Credentials{}}
tuple. Without invoking this function we get back the changeset instead. In case of errors we get the changeset in the{:error, %Ecto.Changeset{}}
tuple. - Passwords sent to our APIs are Base64 encoded so we need to decode and validate the password.
Let’s see the TokenResult
:
This module is fairly straightforward. It has some type specifications and two fields that we need at the moment: access_token
and expires_in
. Later to make our solution more robust we could also support refresh tokens. However in practice in case of API integrations periodic logins and caching the token is more then enough by the client-side.
Now let’s see the Auth
module, that talks to Auth0:
So what’s happening here?
- In
sign_in/1
we call Auth0 using HTTP. Insidebuild_payload
we need to prepare the payload for the Resource Owner Password authentication flow. - We expect errors 401, 403 and everything else. 403 can be returned when we don’t have access to a given scope, that we consider a
:forbidden
scenario. Everything else we can translate as:unauthorized
while providing a bit more context on the error in our logs - In case of a successful response we just take the
access_token
andexpires_in
fields and return theTokenResult
All we need now is to define the usual stuff: routes, controllers, view. I’ll leave this bit of code up to you, I’ll show you the controller code, alternatively you can pull the entire project (with tests!) from GitHub. See link at the top.
At this point your app should be working correctly. In case Auth0 returns you the following error: 403, “Authorization server not configured with default connection.”:
- Go to your Account / Settings / General, then for the Default Directory add the connection name that has been used for you application. In my case it’s called: Username-Password-Authentication. Hit save, and you should be good to go.
In Part 2 we’ll explore how to use Guardian to validate the token.