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