Using JWT and Auth0 for an Elixir/Phoenix web app

TL;DR check out our sample project and commits to see it in action.

JWT or JSON Web Token is quickly becoming the standard of choice for secure API authentication and information exchange. In this post, I will explore how to build a sample Web application using JWT for authentication of clients.

Note that there are a variety of authentication stratgies, such as ueberauth, and you’ll have to pick the right model for your project. Since we offloaded user management to Auth0, we decided to take another approach which is not necessarily superior but is simple enough for our use case.

We’re using Elixir 1.3 and Phoenix 1.2.1 for this example. Lets create a new phoenix project.

mix phoenix.new jwt_phoenix --no-brunch --no-html

Lets add joken in the list of application dependencies and fetch our dependencies first.

defp deps do
...
{:joken, "~> 1.1"}
...
end

Then run the mix deps.get command to fetch the dependencies. We’re going to use Auth0 so lets create an appropriate config block in config.exs.

config :jwt_phoenix, :auth0,
app_baseurl: System.get_env("AUTH0_BASEURL"),
app_id: System.get_env("AUTH0_APP_ID"),
app_secret: "AUTH0_APP_SECRET"
|> System.get_env
|> Kernel.||("")
|> Base.url_decode64
|> elem(1)

Once we create an application in Auth0, we have three main configuration items.

  • app_baseurl — The base URL or issuer of the JWT tokens
  • app_id — the application id or the audience of the JWT tokens
  • app_secret — the application secret, used to sign the JWT tokens

We’re using environment variables instead of hard-coding these configuration items in config.exs but you could also use something like prod.secret.exs to inject sensitive data to your deployments. Hard-coding your app_secret is a big security no-no as malicious users can use it to create valid tokens. The other important thing to note is that we have to decode the application secret using Base64.url_decode64/1 as Auth0 base64 encodes its secret.

Now that the configuration is setup, lets build a helper module that consists of functions for verifying and creating tokens on the server-side.

defmodule JwtPhoenix.JWTHelpers do
import Joken, except: [verify: 1]

@doc """
use for future verification, eg. on socket connect
"""
def verify(jwt) do
verify
|> with_json_module(Poison)
|> with_compact_token(jwt)
|> Joken.verify
end

@doc """
use for verification via plug
issuer should be our auth0 domain
app_metadata must be present in id_token
"""
def verify do
%Joken.Token{}
|> with_json_module(Poison)
|> with_signer(hs256(config[:app_secret]))
|> with_validation("aud", &(&1 == config[:app_id]))
|> with_validation("exp", &(&1 > current_time))
|> with_validation("iat", &(&1 <= current_time))
|> with_validation("iss", &(&1 == config[:app_baseurl]))
end

@doc """
Create token from client id and secret
Used for unit tests
"""
def create_bearer_token(auth_scopes, config_items \\ %{:signer => :app_secret, :aud => :app_id}) do
%Joken.Token{claims: auth_scopes}
|> with_json_module(Poison)
|> with_signer(hs256(config[config_items[:signer]]))
|> with_aud(config[config_items[:aud]])
|> with_iat
|> with_iss(config[:app_baseurl])
|> with_exp(current_time + 86_400)
|> sign
|> get_compact
end

@doc """
Return error message for `on_error`
"""
def error(conn, _msg) do
{conn, %{:errors => %{:detail => "unauthorized"}}}
end

defp config, do: Application.get_env(:jwt_phoenix, :auth0)
end

In the above code, we created the verify/0 function which uses the Joken.Plug. The Joken implementation will automatically grab the authorization header and pass it along similar to how our verify/1 function works. You can see that we import all the functions but verify/1 from Joken because we’re writing our own verify/1. The verify/1 we’ve written will be used for authorization connections via phoenix channels as the normal Plug flow does not work there. You can see we’ve passed our token through a few with_validation function so that we test our token against each of the expectations such as correct issued time, issuer, expiration time, audience and client secret.

We have also written a simple create_bearer_token/2 function which we can use for creating bearer token for our unit tests. As you might have noticed, we don’t need Auth0 to create tokens; anyone with the right client id and secret can create valid tokens.

We also have an error/2 so that we can send error message for invalid tokens. We will specify both verify/0 and error/2 as part of the plug when we add Joken.Plug in the :api pipeline. Lets configure our web/router.ex

pipeline :api do
plug :accepts, ["json"]
plug Joken.Plug,
verify: &JwtPhoenix.JWTHelpers.verify/0,
on_error: &JwtPhoenix.JWTHelpers.error/2
end

We’ve added the plug in the pipeline so now all API requests will need a valid JWT in the Authorization header. To see this in action, lets create a controller that will return a simple JSON response {success: true}.

# web/controllers/status_controller.ex
defmodule JwtPhoenix.StatusController do
use JwtPhoenix.Web, :controller

def index(conn, _params) do
status = %{
success: true
}
render(conn, "status.json", status: status)
end
end
# web/views/status_view.ex
defmodule JwtPhoenix.StatusView do
use JwtPhoenix.Web, :view

def render("status.json", %{status: status}) do
status
end
end
# web/router.ex ; lets add status endpoint in router.ex
scope "/api", JwtPhoenix do
pipe_through :api

get "/status", StatusController, :index
end

We are ready to access our /api/status endpoint. For now, we will not dive into how we can do this on the frontend side but with Auth0 lock, its fairly easy to setup. Lets see a sample curl request to see this in action.

$ curl -D - http://localhost:4000/api/status
HTTP/1.1 401 Unauthorized
server: Cowboy
date: Mon, 22 Aug 2016 17:00:00 GMT
content-length: 36
cache-control: max-age=0, private, must-revalidate
x-request-id: fco562v0k8eleuhr0oeks0i1f2boigcm
content-type: application/json; charset=utf-8

{"errors":{"detail":"unauthorized"}}

Since we didn’t specify a valid token (same for unauthorized tokens), our API now responds with status 401. For authenticated requests, it responds with the appropriate response and http status code 200.

$ curl -D - http://localhost:4000/api/status -H "Authorization: Bearer <valid_jwt_token>"
HTTP/1.1 200 OK
server: Cowboy
date: Mon, 22 Aug 2016 17:02:55 GMT
content-length: 16
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
x-request-id: stcjk670bdih5n2phgpm3h5nkgmc2dor

{"success":true}

Now that we’ve got the basics working, we are going to add an authorization feature. While the work we’ve done so far is good enough to check for a valid token, we don’t have a way to limit particular endpoints to particular roles. For simplicity, we are going to add two functions to the router.ex but you could easily extract it to a separate Plug module.

# web/router.ex
@doc """
Function that will serve as Plug for verifying metadata
"""
def check_admin_metadata(conn, opts) do
claims = Map.get(conn.assigns, :joken_claims)
case Map.get(claims, "app_metadata") do
%{"role" => "admin"} ->
assign(conn, :admin, true)
_ ->
conn
|> forbidden
end
end

@doc """
send 403 to client
"""
def forbidden(conn) do
msg = %{
errors: %{
details: "forbidden resource"
}
}
conn
|> put_resp_content_type("application/json")
|> send_resp(403, Poison.encode!(msg))
|> halt
end
# create a new pipeline for admin: web/router.ex
pipeline :api_admin do
plug :check_admin_metadata
end
# add a new scope
scope "/api/admin", JwtPhoenix do
pipe_through [:api, :api_admin]

get "/", StatusController, :admin
end

We just wrote a simple function to check if the user has the admin role. Auth0 provides two fields, app_metadata and user_metadata. The former one is only editable by the application so it can be used for managing roles and access. The later is user editable and can be used for things like user preferences. The above example checks the claims has the right app_metadata otherwise it will send a 403 status code. We also put admin: true in conn.assigns so that we can do certain actions later. Finally, we created a new pipeline :api_admin which we will be used under the /api/admin scope. The beauty with Phoenix is that we can pipe_through a list of pipelines giving us flexibility on transforming the %Plug.Conn{} struct. Lets add a function in our StatusController to handle the /api/admin endpoint.

def admin(conn, _params) do
status = %{
success: true,
role: "admin"
}
render(conn, "status.json", status: status)
end

This is a simplistic example as a real world scenario would be a lot more complex but it serves our purpose of demonstrating the feature. Let’s see how curl responds to two properly signed tokens, one with the admin role and other one without.

$ curl -D - http://localhost:4000/api/admin -H "Authorization: Bearer <valid_token_with_no_role>"
HTTP/1.1 403 Forbidden
server: Cowboy
date: Mon, 22 Aug 2016 17:40:01 GMT
content-length: 43
cache-control: max-age=0, private, must-revalidate
x-request-id: 7imahtg2gnpiodeo20q9ku1udtf3jvl4
content-type: application/json; charset=utf-8

{"errors":{"details":"forbidden resource"}}
$ curl -D - http://localhost:4000/api/admin -H "Authorization: Bearer <valid_token_with_admin_role>"
HTTP/1.1 200 OK
server: Cowboy
date: Mon, 22 Aug 2016 17:40:26 GMT
content-length: 31
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
x-request-id: fcemumhu2kakr8e083f2u1n65ntt173h

{"success":true,"role":"admin"}

The /api/status endpoint still works for tokens without admin role but the /api/admin sends 403 status code.

This concludes our JWT and Auth0 demonstration. Hopefully now you know how to setup your web app for JWT based authentication and authorization. Some parts of this article is Auth0 specific but the flow should generally work with any provider. There’s so much more we could cover such as JWT token invalidation using short lived expiration or refresh tokens or token blacklist, but we’ll leave thatfor another day!

The sample application we built is hosted on GitHub. You can check the Ueberauth organization for authentication systems such as Guardian and Ueberauth and various strategies supported by Ueberauth. Check out joken for all your JWT needs.