defmodule IndieWeb.Plug.InjectCredentials do import Plug.Conn require Logger def init(opts) do opts end def call(conn, _opts) do with( auth_token when is_binary(auth_token) and auth_token != "" <- do_extract_from_request(conn), me_url when is_binary(me_url) <- find_me_from_token(auth_token), endpoint when is_binary(endpoint) <- IndieWeb.Auth.endpoint_for(:token, me_url), {:ok, %{"scope" => scope}} <- IndieWeb.Clients.IndieAuth.verify_token(endpoint, auth_token) ) do Logger.debug("Found a user and their accompanying token.", indieauth_user: me_url ) conn |> put_private(:indieauth_me, me_url) |> put_private(:indieauth_access_token, auth_token) |> put_private(:indieauth_scope, scope) else nil -> Logger.warn("The request was not authorized with IndieAuth information.") # TODO: Make into a more OAuth2-y error. conn |> put_resp_content_type("application/json") |> Explode.unauthorized() {:error, error} -> Logger.warn("Token provided but no user found.", error: error ) # TODO: Make into a more OAuth2-y error. conn |> put_resp_content_type("application/json") |> Explode.bad_request() {:error, name, data} -> Logger.warn("Unexpected error.", error: name, data: inspect(data)) # TODO: Make into a more OAuth2-y error. conn |> put_resp_content_type("application/json") |> Explode.bad_request() end end defp do_extract_from_request(conn) do [ conn |> fetch_query_params() |> Map.get(:params) |> Map.get("access_token", nil), conn |> get_req_header("authorization") |> List.first() |> List.wrap() |> List.first() |> String.split(" ", parts: 2) |> List.last() ] |> Enum.reject(&is_nil/1) |> Enum.reject(&(&1 == "")) |> List.first() end @spec add(binary(), binary()) :: {:ok, me: binary(), token: binary()} | {:error, :failed_to_set_token} def add(me, token) do hashed_token = do_generate_hash(token) Logger.info("Generated hashed token for user.", me: me, hashed_token: hashed_token ) with( :ok <- IndieWeb.Cache.set("indieauth:proxied_token:#{hashed_token}", me), :ok <- IndieWeb.Cache.set("indieauth:proxied_me:#{me}", token) ) do Logger.info("Added user with their provided token to the cache.", me: me) {:ok, me: me, token: token} else error -> Logger.error("Failed to insert user with their token.", me: me, error: inspect(error) ) {:error, :failed_to_set_token} end end @spec remove(binary()) :: :ok def remove(token) do hashed_token = do_generate_hash(token) me = find_me_from_token(hashed_token) IndieWeb.Cache.delete("indieauth:proxied_token:#{hashed_token}") IndieWeb.Cache.delete("indieauth:proxied_me:#{me}") Logger.info("Removed token and user from cache.", hashed_token: hashed_token) :ok end @doc "Finds the user's URL from the provided IndieAuth token." @spec find_me_from_token(binary()) :: binary() | nil def find_me_from_token(token) do hashed_token = do_generate_hash(token) case IndieWeb.Cache.get("indieauth:proxied_token:#{hashed_token}") do {:ok, me} when is_binary(me) -> me _ -> nil end end @doc "Finds the user's IndieAuth token from the provided me." @spec find_token_from_me(binary()) :: binary() | nil def find_token_from_me(me) do case IndieWeb.Cache.get("indieauth:proxied_me:#{me}") do {:ok, token} when is_binary(token) -> token _ -> nil end end defp do_generate_hash(token) do :crypto.hash(:sha512, token) |> Base.decode64(padding: false) end end