defmodule AuthToken do @moduledoc """ Simplified encrypted authentication tokens using JWE. Configuration needed: config :authtoken, token_key: PUT_KEY_HERE Generate a token for your user after successful authentication like this: ## Examples token_content = %{userid: user.id} token = AuthToken.generate_token(token_content) """ @doc """ Generate a random key for AES128 ## Examples iex> AuthToken.generate_key() {:ok, <<153, 67, 252, 211, 199, 186, 212, 114, 109, 99, 222, 205, 31, 26, 100, 253>>} """ @spec generate_key() :: {:ok, binary} def generate_key do {:ok, :crypto.strong_rand_bytes(16)} end @doc """ Generates an encrypted auth token. Contains an encoded version of the provided map, plus a timestamp for timeout and refresh. """ @spec generate_token(map) :: {:ok, String.t} def generate_token(user_data) do base_data = %{ "ct" => DateTime.to_unix(DateTime.utc_now()), "rt" => DateTime.to_unix(DateTime.utc_now())} token_content = user_data |> Enum.into(base_data) jwt = JOSE.JWT.encrypt(get_jwk(), get_jwe(), token_content) |> JOSE.JWE.compact |> elem(1) # Remove JWT header {:ok, Regex.run(~r/.+?\.(.+)/, jwt) |> List.last} end @doc """ Checks a token and refreshes if necessary. ## Examples case AuthToken.refresh_token(token) do {:error, :timedout} -> # Redirect to login {:error, :stillfresh} -> # Do nothing {:ok, token} -> # Check credentials and send back new token end """ @spec refresh_token(map) :: {:ok, String.t} | {:error, :stillfresh} | {:error, :timedout} def refresh_token(token) when is_map(token) do cond do is_timedout?(token) -> {:error, :timedout} !needs_refresh?(token) -> {:error, :stillfresh} needs_refresh?(token) -> token = %{"rt" => DateTime.to_unix(DateTime.utc_now())} |> Enum.into(token) generate_token(token) end end @spec refresh_token(String.t) :: {:ok, String.t} | {:error, :stillfresh} | {:error, :timedout} def refresh_token(token) when is_binary(token) do {:ok, token} = decrypt_token(token) token |> refresh_token end @doc """ Check if token is timedout and not valid anymore """ @spec is_timedout?(map) :: boolean def is_timedout?(token) do {:ok, ct} = DateTime.from_unix(token["ct"]) DateTime.diff(DateTime.utc_now(), ct) > get_config(:timeout) end @doc """ Check if token is stale and needs to be refreshed """ @spec needs_refresh?(map) :: boolean def needs_refresh?(token) do {:ok, rt} = DateTime.from_unix(token["rt"]) DateTime.diff(DateTime.utc_now(), rt) > get_config(:refresh) end @doc """ Decrypt an authentication token Format "bearer: tokengoeshere" and "bearer tokengoeshere" will be accepted and parsed out. """ @spec decrypt_token(Plug.Conn.t) :: {:ok, String.t} | {:error} def decrypt_token(%Plug.Conn{} = conn) do token_header = Plug.Conn.get_req_header(conn, "authorization") |> List.first crypto_token = if token_header, do: Regex.run(~r/(bearer\:? )?(.+)/, token_header) |> List.last decrypt_token(crypto_token) end @spec decrypt_token(String.t) :: {:ok, String.t} | {:error} def decrypt_token(headless_token) when is_binary(headless_token) do header = get_jwe() |> OJSON.encode! |> Base.url_encode64(padding: false) auth_token = header <> "." <> headless_token try do %{fields: token} = JOSE.JWT.decrypt(get_jwk(), auth_token) |> elem(1) {:ok, token} rescue _ -> {:error} end end @spec decrypt_token(nil) :: {:error} def decrypt_token(_) do {:error} end @spec get_jwe() :: %{alg: String.t, enc: String.t, typ: String.t} defp get_jwe do %{ "alg" => "dir", "enc" => "A128GCM", "typ" => "JWT" } end @spec get_jwk() :: %{} defp get_jwk do get_config(:token_key) |> JOSE.JWK.from_oct() end @spec get_config(atom) :: %{} def get_config(atom) do content = Application.get_env(:authtoken, atom) unless content, do: raise "No AuthToken configuration set for " <> atom content end end