Packages

A complete, production-grade Elixir client for the Monzo API: OAuth2, Accounts, Balance, Pots, Transactions, Feed Items, Attachments, Transaction Receipts, and Webhooks. Zero required dependencies.

Current section

Files

Jump to
monzo_client lib monzo token_store.ex
Raw

lib/monzo/token_store.ex

defmodule Monzo.TokenStore do
@moduledoc """
A supervised `GenServer` holding the current access/refresh token pair
for one Monzo user, and performing the actual refresh call.
You normally don't interact with this module directly - `Monzo.Client`
starts and owns one automatically. It's exposed publicly for advanced
use cases: sharing a single token store across multiple clients, or
wiring it into your own supervision tree with a registered `:name`.
"""
use GenServer
alias Monzo.Auth
defstruct [
:access_token,
:refresh_token,
:client_id,
:client_secret,
:on_refresh,
:adapter,
:base_url
]
@type t :: %__MODULE__{
access_token: String.t() | nil,
refresh_token: String.t() | nil,
client_id: String.t() | nil,
client_secret: String.t() | nil,
on_refresh: (String.t(), String.t() | nil -> any()) | nil,
adapter: module(),
base_url: String.t()
}
@doc "Starts a token store under `Monzo.TokenStore.Supervisor`, unlinked from the caller."
@spec start_supervised(keyword()) :: DynamicSupervisor.on_start_child()
def start_supervised(opts) do
DynamicSupervisor.start_child(Monzo.TokenStore.Supervisor, {__MODULE__, opts})
end
@doc "Starts a token store process directly (e.g. under your own supervision tree)."
@spec start_link(keyword()) :: GenServer.on_start()
def start_link(opts) do
{name, opts} = Keyword.pop(opts, :name)
gen_opts = if name, do: [name: name], else: []
GenServer.start_link(__MODULE__, opts, gen_opts)
end
@doc "Returns the current access token, or nil."
@spec access_token(GenServer.server()) :: String.t() | nil
def access_token(server), do: GenServer.call(server, :access_token)
@doc "Manually sets new tokens (e.g. after completing the OAuth flow)."
@spec set_tokens(GenServer.server(), String.t(), String.t() | nil) :: :ok
def set_tokens(server, access_token, refresh_token \\ nil) do
GenServer.call(server, {:set_tokens, access_token, refresh_token})
end
@doc """
Refreshes the access token using the stored refresh token and client
credentials. Returns `{:error, :no_refresh_credentials}` without making
any request if they aren't configured.
"""
@spec refresh(GenServer.server()) :: {:ok, String.t()} | {:error, term()}
def refresh(server), do: GenServer.call(server, :refresh)
## GenServer callbacks
@impl true
def init(opts) do
state = %__MODULE__{
access_token: Keyword.get(opts, :access_token),
refresh_token: Keyword.get(opts, :refresh_token),
client_id: Keyword.get(opts, :client_id),
client_secret: Keyword.get(opts, :client_secret),
on_refresh: Keyword.get(opts, :on_refresh),
adapter: Keyword.get(opts, :adapter, Monzo.HTTP.HttpcAdapter),
base_url: Keyword.get(opts, :base_url, "https://api.monzo.com")
}
{:ok, state}
end
@impl true
def handle_call(:access_token, _from, state) do
{:reply, state.access_token, state}
end
@impl true
def handle_call({:set_tokens, access_token, refresh_token}, _from, state) do
state =
%{state | access_token: access_token}
|> maybe_put_refresh_token(refresh_token)
{:reply, :ok, state}
end
@impl true
def handle_call(:refresh, _from, state) do
if state.refresh_token && state.client_id && state.client_secret do
# A bare, unauthenticated client (no token store dependency) is used
# for the refresh call itself so it doesn't recursively depend on
# this same token store for its own auth header.
bare_client = %Monzo.Client{
base_url: state.base_url,
adapter: state.adapter,
retry: Monzo.HTTP.default_retry_policy(),
logger: fn _level, _message -> :ok end,
user_agent: "monzo-elixir",
token_store: nil
}
case Auth.refresh_token(bare_client, %{
client_id: state.client_id,
client_secret: state.client_secret,
refresh_token: state.refresh_token
}) do
{:ok, token} ->
new_state =
%{state | access_token: token.access_token}
|> maybe_put_refresh_token(token.refresh_token)
if state.on_refresh, do: state.on_refresh.(token.access_token, new_state.refresh_token)
{:reply, {:ok, token.access_token}, new_state}
{:error, reason} ->
{:reply, {:error, reason}, state}
end
else
{:reply, {:error, :no_refresh_credentials}, state}
end
end
defp maybe_put_refresh_token(state, nil), do: state
defp maybe_put_refresh_token(state, refresh_token), do: %{state | refresh_token: refresh_token}
@doc false
def child_spec(opts) do
%{
id: __MODULE__,
start: {__MODULE__, :start_link, [opts]},
restart: :temporary
}
end
end