defmodule AccessToken do @moduledoc """ Access token generation and processing. First off, the secret signing key must be configured in the application environment, usually defined in `config/config.exs`: config :access_token, key: "6m/pr714TP8ijQeVdJ2gBOxuYwrD7nR/p5BhhcQ2ejURZpNYz9T//ze9mfx+TNpo" `AccessToken` provides a simple interface to generate and process access tokens: iex> access_token = AccessToken.encode(%{user_id: 1}) iex> AccessToken.decode(access_token) {:ok, %{user_id: user_id}} See `encode/2` and `decode/1` for more information. """ @jti_length 32 @doc """ Returns an access token, a string representing the information provided that is encoded in a JSON Web Token (JWT). iex> AccessToken.encode(%{user_id: 1}) "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJqdGkiOiJfV" Every access token is unique, no matter what information it carries. All access tokens are permanent by default. Temporary access tokens can be created by passing the expiration time (a `DateTime` or a UNIX timestamp in seconds) on or after which the access token MUST NOT be accepted for processing. iex> expiration_time = DateTime.to_unix(DateTime.utc_now()) + 3600 iex> AccessToken.encode(%{user_id: 1}, expiration_time) "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJqdGkiOiJObG83bnJObU44cjdfSEk2Q2x6NFRNcmFq" """ @spec encode(any, DateTime.t | integer) :: String.t def encode(data, exp \\ nil) def encode(data, nil), do: JsonWebToken.sign(%{jti: generate_jti(), data: data}, key: get_key()) def encode(data, %DateTime{} = exp), do: encode(data, DateTime.to_unix(exp)) def encode(data, exp) when is_integer(exp) do JsonWebToken.sign(%{jti: generate_jti(), data: data, exp: exp}, key: get_key()) end @doc """ Returns a tuple `{:ok, info}` with the information encoded in the access token, or `{:error, :invalid}` otherwise. iex> access_token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJqdGkiOiJfV" iex> AccessToken.decode(access_token) {:ok, %{user_id: 1}} Temporary access tokens might return the tuple `{:error, :expired}` if the expiration time passed. ### Caveats In the process of decoding, all the `Map` keys are converted to atoms. iex> AccessToken.encode(%{"user_id" => 1}) |> AccessToken.decode() {:ok, %{user_id: 1}} """ @spec decode(String.t) :: {:ok, any} | {:error, :invalid} | {:error, :expired} def decode(access_token) when not is_binary(access_token), do: {:error, :invalid} def decode(access_token) do {:ok, payload} = JsonWebToken.verify(access_token, key: get_key()) if expired?(payload), do: {:error, :expired}, else: {:ok, payload.data} rescue _ -> {:error, :invalid} end defp get_key do Application.get_env(:access_token, :key) || raise ":key not set in :access_token configuration" end defp generate_jti do @jti_length |> :crypto.strong_rand_bytes() |> Base.url_encode64() |> binary_part(0, @jti_length) end defp expired?(%{exp: exp}), do: exp < DateTime.to_unix(DateTime.utc_now()) defp expired?(_), do: false end