defmodule PlugIndie.Token do require Logger def verify( access_token, token_endpoint, required_scope, supported_scopes, own_hostname, user_agent ) do case do_verify_token(access_token, token_endpoint, user_agent) do {:ok, %{status: 200, body: body}} -> body |> map_keys_to_string() |> verify_token_response(required_scope, supported_scopes, own_hostname) {:ok, %{status: status}} -> {:error, :request_error, status} {:error, %{code: code}} -> Logger.error("Token endpoint responded with unexpected code: #{inspect(code)}") {:error, :request_error, code} {:error, %{reason: reason}} -> Logger.error("Could not reach token endpoint: #{inspect(reason)}") {:error, :request_error, reason} error -> Logger.error("Unexpected error: #{inspect(error)}") {:error, :request_error, "Internal Server Error"} end end defp do_verify_token(access_token, token_endpoint, user_agent) do client = Tesla.client([ Tesla.Middleware.JSON, {Tesla.Middleware.Headers, [ {"User-Agent", user_agent}, {"Authorization", "Bearer #{access_token}"}, {"Accept", "application/json"} ]} ]) Tesla.get(client, token_endpoint) end def verify_token_response( %{ "me" => host_uri, "scope" => scope, "client_id" => client_id, "issued_at" => _issued_at, "issued_by" => _issued_by, "nonce" => _nonce }, required_scope, supported_scopes, own_hostname ) do # {%{ # "client_id" => "https://indiepass.app/", # "issued_at" => 1_733_382_601, # "issued_by" => "https://tokens.indieauth.com/token", # "me" => "https://blog.inhji.de/", # "nonce" => 358_618_865, # "scope" => "create update delete media read follow channels mute block" # }, "create", ["create", "media"], "inhji.de"} Logger.info("Host-URI: '#{host_uri}'") Logger.info("ClientId: '#{client_id}'") Logger.info("Scopes: '#{scope}'") with :ok <- verify_hostname_match(host_uri, own_hostname), :ok <- verify_scope_support(scope, required_scope, supported_scopes) do :ok else {:error, name, reason} -> Logger.error("Could not verify token response: #{reason}") {:error, name, reason} end end def verify_token_response(_, _, _, _), do: {:error, "verify_token_response", "bad request"} defp verify_hostname_match(host_uri, own_hostname) do hostnames_match? = get_hostname(host_uri) == own_hostname case hostnames_match? do true -> :ok _ -> Logger.warning("Hostnames do not match: Given #{host_uri}, Actual: #{own_hostname}") {:error, "verify_hostname_match", "hostname does not match"} end end defp get_hostname(host_uri) do host_uri |> URI.parse() |> Map.get(:host) end defp verify_scope_support(_scopes, nil, _supported_scopes), do: :ok defp verify_scope_support(scopes, required_scope, supported_scopes) when not is_nil(required_scope) do required = Enum.member?(supported_scopes, required_scope) requested = Enum.member?(String.split(scopes), required_scope) cond do required && requested -> :ok !required -> {:error, "verify_scope_support", "scope '#{required_scope}' is not supported"} !requested -> {:error, "verify_scope_support", "scope '#{required_scope}' was not requested"} end end defp map_keys_to_string(map) do for {key, val} <- map, into: %{}, do: {to_string(key), val} end end