Current section

Files

Jump to
indieweb lib indieweb auth.ex
Raw

lib/indieweb/auth.ex

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