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.
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
How to use custom plugs
The function plug authenticate_api_user/2
needs to be import
ed 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
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
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
Here is an example usage.
defmodule ExampleWeb.Router do use ExampleWeb, :router pipeline :api do plug :accepts, ["json"] + plug ExampleWeb.API.Auth end
defmodule ExampleWeb.API.MeasurementController do use ExampleWeb, :controller alias Example.Measurement action_fallback ExampleWeb.API.FallbackController + + plug :authenticate_api_user when action in [:create] +
Quick test
In an IEx console, generate a token.
iex> ExampleWeb.API.Auth.generate_token(1) "xxxxxxx"
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"
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)