defmodule IndieWeb.Auth do @moduledoc """ Provides logic for handling [IndieAuth](https://indieauth.spec.indieweb.org) interactions. """ @doc "Provides endpoint information for well known endpoints in IndieAuth." @spec endpoint_for(atom(), binary()) :: list(binary()) | nil def endpoint_for(type, uri) def endpoint_for(component, url) when component in ~w(authorization token)a do IndieWeb.LinkRel.find(url, "#{component}_endpoint") end def endpoint_for(:redirect_uri, url) do IndieWeb.LinkRel.find(url, "redirect_uri") end def endpoint_for(_, _) do [] end @doc """ Generates an authentication URL. Depending on the particular `response_type`, this will generate a URL that can be used to sign in a user. """ @spec authenticate(map(), keyword()) :: {:ok, any()} | {:error, any()} def authenticate(params, options \\ []) def authenticate(%{"response_type" => type} = params, options) when type in ~w(code id) do case do_validate_request(params, ~w(client_id redirect_uri state), options) do {:error, _} = error -> error {:ok, %{ "redirect_uri" => redirect_uri, "state" => state }} -> {_code_verifier, code_challenge, code_challenge_method} = IndieWeb.Auth.Code.generate_challenge(options) # TODO: Store generated challenege for later confirmation. do_generate_redirect_uri(redirect_uri, {code_challenge, code_challenge_method}, state) end end def authenticate(_, _), do: {:error, :unrecognized_authorization_request} def supported?(url) do endpoints = ~w(authorization token)a |> Enum.map(&endpoint_for(&1, url)) |> Enum.concat() Enum.count(endpoints) >= 2 && Enum.all?(endpoints, &is_binary/1) end defp do_generate_redirect_uri( redirect_uri, {code_challenge, code_challenge_method}, state, me \\ "" ) do params = %{ "code_challenge" => code_challenge, "code_challenge_method" => code_challenge_method, "state" => state } |> (fn query_params -> if me != "" do Map.put(query_params, "me", me) else query_params end end).() query = redirect_uri |> URI.parse() |> Map.get(:query) |> (&(URI.decode_query(&1 || "", params) |> URI.encode_query())).() redirect_uri |> URI.parse() |> Map.put(:query, query) |> URI.to_string() end defp do_validate_request(params, expected_keys, options) do proper_args = Map.take(params, expected_keys) missing_keys = expected_keys -- Map.keys(params) cond do !Enum.empty?(missing_keys) -> {:error, :missing_required_keys, keys: missing_keys} !user_adapter(options).valid_user?(params["me"]) -> {:error, :invalid_user} true -> {:ok, proper_args} end end defp user_adapter(options) do user_adapter_module = options |> Keyword.get(:adapters, []) |> Keyword.get(:user) case user_adapter_module do nil -> raise "No user adapter was provided for the IndieWeb library." _ -> user_adapter_module end end end