127 lines
3.7 KiB
Elixir
127 lines
3.7 KiB
Elixir
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
|