An access token state store in Elixir
I’ve recently been working on a small project that uses OAuth for authentication, which is proving to be a great approach for what I need it for. After I got the OAuth authentication layer working through, I realised that I needed a place to store the hash of user data I receive back from the OAuth provider, along with some kind of access token I can use to authenticate requests to my GraphQL API. This post describes the state store I created to manage access tokens.
Elixir has a built in module for background processes called GenServer. I actually managed to implement most of what I needed simply based on the excellent documentation. The API for my module wasn’t very complicated - I just needed to insert access tokens, destroy access tokens (for logout and invalidation), and check whether a given token exists.
The module ended up quite simple:
defmodule ElixirAuthTokenStoreExample.Repo do
use GenServer
@name __MODULE__
# Client
@doc """
Start the token repo server
## Example
iex> {:ok, pid} = ElixirAuthTokenStoreExample.Repo.start_link(name: :repo_start_link_doctest)
iex> is_pid(pid)
true
"""
def start_link(opts \\ []) do
opts = Keyword.put_new(opts, :name, @name)
store = []
GenServer.start_link(__MODULE__, store, opts)
end
@doc """
Insert a new token into the repo
## Example
iex> {:ok, pid} = ElixirAuthTokenStoreExample.Repo.start_link(name: :repo_insert_doctest_1)
iex> ElixirAuthTokenStoreExample.Repo.insert(pid, "abc123")
iex> :sys.get_state(pid)
["abc123"]
"""
def insert(pid \\ @name, token), do: GenServer.cast(pid, {:insert, token})
@doc """
Remove a token from the repo
## Example
iex> {:ok, pid} = ElixirAuthTokenStoreExample.Repo.start_link(name: :repo_destroy_doctest_1)
iex> ElixirAuthTokenStoreExample.Repo.insert(pid, "abc123")
iex> ElixirAuthTokenStoreExample.Repo.destroy(pid, "abc123")
iex> :sys.get_state(pid)
[]
"""
def destroy(pid \\ @name, token), do: GenServer.cast(pid, {:destroy, token})
@doc """
Check whether a given token exists in the store
## Example
iex> {:ok, pid} = ElixirAuthTokenStoreExample.Repo.start_link(name: :repo_exists_doctest_1)
iex> ElixirAuthTokenStoreExample.Repo.insert(pid, "abc123")
iex> ElixirAuthTokenStoreExample.Repo.exists?(pid, "abc123")
true
iex> {:ok, pid} = ElixirAuthTokenStoreExample.Repo.start_link(name: :repo_exists_doctest_2)
iex> ElixirAuthTokenStoreExample.Repo.insert(pid, "abc123")
iex> ElixirAuthTokenStoreExample.Repo.exists?(pid, "abc1234")
false
"""
def exists?(pid \\ @name, token), do: GenServer.call(pid, {:exists, token})
# Server
@impl true
def init(tokens) do
# This is the 'state' of the server
{:ok, tokens}
end
@impl true
def handle_cast({:insert, token}, state) do
{:noreply, [token | state]}
end
def handle_cast({:destroy, token}, state) do
{:noreply, state |> List.delete(token)}
end
@impl true
def handle_call({:exists, token}, _from, state) do
{:reply, state |> Enum.member?(token), state}
end
end
This genserver can be added to your Mix application supervisor, which will ensure that a a process will always be running holding the state that is available application-wide. Because of the simple API we have defined, interacting with the token store is quite simple:
alias ElixirAuthTokenStoreExample.Repo
Repo.start_link()
# Insert a token
Repo.insert("abc123")
# Check it exists
Repo.exists?("abc123")
# Destroy a token
Repo.destroy("abc123")
I have placed this example code into a reference mix application, available at https://github.com/joshmcarthur/elixir-auth-token-store-example.