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)