DEV Community

Masatoshi Nishiguchi
Masatoshi Nishiguchi

Posted on

Simple token authentication for Phoenix API

Recently I am really into IoT development using Elixir programming language, Nerves IoT platform and Phoenix web framework. After quickly learning the basics of electronics, I built a real-time temperature and humidity monitoring system for my living room. It has been successful and really fun.

Now that the system is working well, I want to make it more secure. Today I will implement simple token authentication for my API server.

日本語版

hello-nerves-2

Plans

  • Implement custom plugs for token authentication
  • Manually generates a token for each user in IEx
  • Reject access if a token in the request headers is missing or invalid

Phoenix.Token

Thankfully, Phoenix has all the useful utilities for generating and verifying a token in Phoenix.Token module. Nice!

Custom plug module example

Using Phoenix.Token module, I wrote two custom plugs:

  • ExampleWeb.API.Auth - a module plug that verifies the bearer token in the request headers and assigns :current_user
  • ExampleWeb.API.Auth.authenticate_api_user/2 - a function plug that ensures that :current_user value is present.

I learned about the plugs from the Programming Phoenix book.

defmodule ExampleWeb.API.Auth do @moduledoc """ A module plug that verifies the bearer token in the request headers and assigns `:current_user`. The authorization header value may look like `Bearer xxxxxxx`. """ import Plug.Conn import Phoenix.Controller def init(opts), do: opts def call(conn, _opts) do conn |> get_token() |> verify_token() |> case do {:ok, user_id} -> assign(conn, :current_user, user_id) _unauthorized -> assign(conn, :current_user, nil) end end @doc """ A function plug that ensures that `:current_user` value is present. ## Examples # in a router pipeline pipe_through [:api, :authenticate_api_user] # in a controller plug :authenticate_api_user when action in [:index, :create] """ def authenticate_api_user(conn, _opts) do if Map.get(conn.assigns, :current_user) do conn else conn |> put_status(:unauthorized) |> put_view(ExampleWeb.ErrorView) |> render(:"401") # Stop any downstream transformations. |> halt() end end @doc """ Generate a new token for a user id. ## Examples iex> ExampleWeb.API.Auth.generate_token(123) "xxxxxxx" """ def generate_token(user_id) do Phoenix.Token.sign( ExampleWeb.Endpoint, inspect(__MODULE__), user_id ) end @doc """ Verify a user token. ## Examples iex> ExampleWeb.API.Auth.verify_token("good-token") {:ok, 1} iex> ExampleWeb.API.Auth.verify_token("bad-token") {:error, :invalid} iex> ExampleWeb.API.Auth.verify_token("old-token") {:error, :expired} iex> ExampleWeb.API.Auth.verify_token(nil) {:error, :missing} """ @spec verify_token(nil | binary) :: {:error, :expired | :invalid | :missing} | {:ok, any} def verify_token(token) do one_month = 30 * 24 * 60 * 60 Phoenix.Token.verify( ExampleWeb.Endpoint, inspect(__MODULE__), token, max_age: one_month ) end @spec get_token(Plug.Conn.t()) :: nil | binary def get_token(conn) do case get_req_header(conn, "authorization") do ["Bearer " <> token] -> token _ -> nil end end end 
Enter fullscreen mode Exit fullscreen mode

How to use custom plugs

The function plug authenticate_api_user/2 needs to be imported before use. There are two possible scenarios.

A: when used in a router pipeline

This pattern is useful to affect all the controllers in the pipeline. We need to import the function plug within the quote block of ExampleWeb.router function.

 defmodule ExampleWeb do   ...   def router do quote do use Phoenix.Router   import Plug.Conn import Phoenix.Controller import Phoenix.LiveView.Router + import ExampleWeb.API.Auth, only: [authenticate_api_user: 2]  end end 
Enter fullscreen mode Exit fullscreen mode

Here is an example usage.

 defmodule ExampleWeb.Router do use ExampleWeb, :router   pipeline :api do plug :accepts, ["json"] + plug ExampleWeb.API.Auth  end   scope "/api", ExampleWeb do - pipe_through [:api] + pipe_through [:api, :authenticate_api_user]   resources "/measurements", API.Environment.MeasurementController, only: [:index, :show, :create] end 
Enter fullscreen mode Exit fullscreen mode

B: when used in a specific controller

This pattern is useful when we want to affect only specific controller actions. We need to import the function plug within the quote block of ExampleWeb.controller function.

 defmodule ExampleWeb do   ...   def controller do quote do use Phoenix.Controller, namespace: ExampleWeb   import Plug.Conn import ExampleWeb.Gettext + import ExampleWeb.API.Auth, only: [authenticate_api_user: 2]  alias ExampleWeb.Router.Helpers, as: Routes end end 
Enter fullscreen mode Exit fullscreen mode

Here is an example usage.

 defmodule ExampleWeb.Router do use ExampleWeb, :router   pipeline :api do plug :accepts, ["json"] + plug ExampleWeb.API.Auth  end 
Enter fullscreen mode Exit fullscreen mode
 defmodule ExampleWeb.API.MeasurementController do use ExampleWeb, :controller   alias Example.Measurement   action_fallback ExampleWeb.API.FallbackController + + plug :authenticate_api_user when action in [:create] + 
Enter fullscreen mode Exit fullscreen mode

Quick test

In an IEx console, generate a token.

iex> ExampleWeb.API.Auth.generate_token(1) "xxxxxxx" 
Enter fullscreen mode Exit fullscreen mode

Then hit an API endpoint with or without a token.

❯ curl -X POST \ -H "Content-Type: application/json" \ -H "Authorization: Bearer SFMyNTY.g2gDYQFuBgCtL76NeAFiAAFRgB" \ -d '{"measurement": {"temperature_c": "23.5"}}' \ http://localhost:4000/api/measurements {"data":{"id":37,"temperature_c":23.5}} ❯ curl -X POST \ -H "Content-Type: application/json" \ -d '{"measurement": {"temperature_c": "23.5" }}' \ http://localhost:4000/api/measurements "Unauthorized" 
Enter fullscreen mode Exit fullscreen mode

Production

According to the documentation SECRET_KEY_BASE is used for the token generation, so the token generated in development environment won't work in production. I generate a token in the production IEx.

That's it!

Top comments (0)