Simple Guardian - API authentication

In the last post I went over the simple setup for Guardian to do browser authentication. This post is going to go over API authentication. It's pretty much the same. I'm going to assume that you have Guardian configured and have your browser login setup as per the last post. Now lets add some API magic.

Before we proceed

As in the last post, Guardian doesn't do the initial challenge part of the authentication flow. It assumes that you know who your user is and you're wanting to do per-request authentication. Again there's 3 major parts to it.

  • Generate a token for a known resource (user)
  • On each request, check the token and bail if it's not valid.
  • Logout

The main difference between API and browser logins are that with an API we provide the token to the client directly rather than just putting it in the session and being magical. Rather than use the session we're going to use the Authorization header to send the token to the application.

Login

Lets get the token to the client. We can't just put it into the session, we're going to need to give it to them so they can provide it back to us.

def login(conn, params) do
  case User.find_and_confirm_password(params) do
    {:ok, user} ->
       new_conn = Guardian.Plug.api_sign_in(conn, user)
       jwt = Guardian.Plug.current_token(new_conn)
       claims = Guardian.Plug.claims(new_conn)
       exp = Map.get(claims, "exp")

       new_conn
       |> put_resp_header("authorization", "Bearer #{jwt}")
       |> put_resp_header("x-expires", exp)
       |> render "login.json", user: user, jwt: jwt, exp: exp
    {:error, changeset} ->
      conn
      |> put_status(401)
      |> render "error.json", message: "Could not login"
  end
end

There looks like there's a bit going on there but it's really not much. We use api_sign_in to generate a token and put it on the connection. Then we read off the jwt and fetch the expiry so we can let the client know.

On Request

On each request, as before, we want to find the token, load the resource and ensure they're logged in. There's really only one difference from the browser example. Where to look for the token. We're going to look for it in the header.

pipeline :api_auth do  
  plug Guardian.Plug.VerifyHeader, realm: "Bearer"
  plug Guardian.Plug.LoadResource
end  

The only difference from the browser example is that we're going to look for the token in the authorization header. The realm part just specifies the prefix on the header value we're going to use. In this case "Bearer". That is, when the client supplies the header to the application it will look for a header of the form:

Authorization: Bearer <jwt>

The realm can be almost anything although you might get weird things happening if you use "Digest" or "Basic".

On the controller side it's the same as the browser version. We use EnsureAuthenticated to make sure that we have a valid token. Just to outline it again.

defmodule MyApp.Api.LoggedInController do
  # snip
  plug Guardian.Plug.EnsureAuthenticated, handler: __MODULE__

  def logged_in_action(conn, params) do
    user = Guardian.Plug.current_resource(conn)
    # do your stuff
  end

  def unauthenticated(conn, _params) do
    conn
    |> put_status(401)
    |> render "error.json", message: "Authentication required"
  end
end

The same as the browser example, the unauthenticated/2 function is called when there is no valid token found.

Logout

Logout on API tokens is slightly different than the browser.

def logout(conn, _params) do
  jwt = Guardian.Plug.current_token(conn)
  claims = Guardian.Plug.claims(conn)
  Guardian.revoke!(jwt, claims)
  render "logout.json"
end

In the browser we can just flush the session and we're logged out. When using API tokens though the client has hold of the token. This presents a bit of a problem because when they logout there's not much we can do to invalidate the token. If the client chooses to re-use the token it will still be acceptable. The client should 'forget' the token and then you're logged out. If that doesn't sound like something you like, you should use GuardianDB. GuardianDB stores each token in the DB. GuardianDB tracks each token in the DB and when we revoke! the token it is removed - rendering it useless.

Testing

Testing API endpoints is much easier for API endpoints than browser. We just generate the JWT and include it in the request headers.

setup do
  user = create(:user)
  {:ok, jwt, full_claims} = Guardian.encode_and_sign(user)
  {:ok, %{user: user, jwt: jst, claims: full_claims}}
end

test "GET /api", %{jwt: jwt} do
  conn = conn()
    |> put_req_header("authorization", "Bearer #{jwt}")
    |> get "/api"
 
  # test things
end