Authex

Build StatusAuthex Version

Authex is a simple JWT authentication and authorization library for Elixir.

Installation

The package can be installed by adding authex to your list of dependencies in mix.exs:

def deps do
  [
    {:authex, "~> 0.1.0"}
  ]
end

Documentation

See HexDocs for additional documentation.

Getting Started

Before starting, we should configure Authex. At a minimum, we need to add a secret from which our tokens will be signed with. There is a convenient mix task available for this.

mix authex.gen.secret

We should now add this secret to our config. In production this should be set via an env var. By default, authex will pick up the env var AUTH_SECRET if we have not set one via config.

config :authex, [
  # REQUIRED
  # The secret used to sign tokens with.
  secret: "mysecret",

  # OPTIONAL
  # A blacklist module, or false if disabled.
  blacklist: false,
  # The default serializer module.
  serializer: Authex.Serializer.Basic,
  # The default algorithm used to sign tokens.
  default_alg: :hs256,
  # The default iss claim used in tokens.
  default_iss: nil,
  # The default aud claim used in tokens.
  default_aud: nil,
  # The default time to live for tokens in seconds.
  default_ttl: 3600,
  # The default module, function, and arg used to generate the jti claim.
  jti_mfa: {UUID, :uuid4, [:hex]}
]

The above config is all the defaults "out of the box".

Usage by Example

Here are some basic examples on how to use Authex.

Create a token using the default serializer. This assumes users have an id field.

MyApp.User
|> MyApp.Repo.get(1)
|> Authex.for_token()

Create a token with the sub and iss claim set. The token will also have a time to live of 60 seconds. Authex.token/1 returns an Authex.Token struct. Authex.sign/1 creates a compact token from an Authex.Token struct.

user = MyApp.Repo.get(MyApp.User, 1)
token = Authex.token([sub: user.id, iss: "myapp"], [ttl: 60])
Authex.sign(token)

Verify a compact token and return an Authex.Token struct.

MyApp.User
|> MyApp.Repo.get(1)
|> Authex.for_token()
|> Authex.verify()

Verify a compact token and return a resource created from a serializer.

MyApp.User
|> MyApp.Repo.get(1)
|> Authex.for_token()
|> Authex.from_token()

Create a custom Serializer.

defmodule MyApp.TokenSerializer do
  use Authex.Serializer

  def from_token(%Authex.Token{sub: sub, scopes: scopes}) do
    %MyApp.User{id: sub, scopes: scopes}
  end

  def for_token(%MyApp.User{id: id, scopes: scopes}) do
    Authex.Token.new([sub: id, scopes: scopes])
  end
end

Authenticate a Phoenix controller using a custom serializer. Authex.Plug.Authenticate looks for the Authenicate: Bearer mytoken header. It will then verify, and deserialize the token using the provided serializer.

If any of these steps fails, it will put a 401 status and halt the conn.

Otherwise, the plug will place the value returned from the serializer into the conn. You can access this value again using Authex.current_user/1.

defmodule MyApp.Web.UserController do
  use MyApp.Web, :controller

  plug :authenticate

  def show(conn, _params) do
    with {:ok, %{id: id}} <- Authex.current_user(conn),
         {:ok, user} <- MyApp.Users.get(id)
    do
      render(conn, "show.json", user: user)
    end
  end

  defp authenticate(conn, _opts) do
    opts = Authex.Plug.Authentication.init([serializer: MyApp.TokenSerializer])
    Authex.Plug.Authentication.call(conn, opts)
  end
end

Authorize a user to access a particular endpoint using their token scopes. Authorization works by combining the "permits" with the "type" of request that is being made.

For example, with our controller below, we are permitting "user" and "admin" access. The show action would be a GET request, and would therefore be a "read" type.

So, in order to access the show action, our token would require one of the two following scopes: ["user/read", "admin/read"].

If a user fails to meet the scope requirements with their token, it will put a 403 status and halt the conn.

Requests are bucketed under the following types:

defmodule MyApp.Web.UserController do
  use MyApp.Web, :controller

  plug :authenticate
  plug :authorize, permits: ["user", "admin"]

  def show(conn, _params) do
    with {:ok, %{id: id}} <- Authex.current_user(conn),
         {:ok, user} <- MyApp.Users.get(id)
    do
      render(conn, "show.json", user: user)
    end
  end

  defp authenticate(conn, _opts) do
    opts = Authex.Plug.Authentication.init([serializer: MyApp.TokenSerializer])
    Authex.Plug.Authentication.call(conn, opts)
  end

  defp authorize(conn, opts) do
    opts = Authex.Plug.Authorization.init(opts)
    Authex.Plug.Authorization.call(conn, opts)
  end
end