99 lines
3.0 KiB
Elixir
99 lines
3.0 KiB
Elixir
defmodule Parrhesia.Auth.Nip98 do
|
|
@moduledoc """
|
|
Minimal NIP-98 HTTP auth validation.
|
|
"""
|
|
|
|
alias Parrhesia.Protocol.EventValidator
|
|
|
|
@max_age_seconds 60
|
|
|
|
@spec validate_authorization_header(String.t() | nil, String.t(), String.t()) ::
|
|
{:ok, map()} | {:error, atom()}
|
|
def validate_authorization_header(authorization, method, url) do
|
|
validate_authorization_header(authorization, method, url, [])
|
|
end
|
|
|
|
@spec validate_authorization_header(String.t() | nil, String.t(), String.t(), keyword()) ::
|
|
{:ok, map()} | {:error, atom()}
|
|
def validate_authorization_header(nil, _method, _url, _opts),
|
|
do: {:error, :missing_authorization}
|
|
|
|
def validate_authorization_header("Nostr " <> encoded_event, method, url, opts)
|
|
when is_binary(method) and is_binary(url) and is_list(opts) do
|
|
with {:ok, event_json} <- decode_base64(encoded_event),
|
|
{:ok, event} <- JSON.decode(event_json),
|
|
:ok <- validate_event_shape(event, opts),
|
|
:ok <- validate_http_binding(event, method, url) do
|
|
{:ok, event}
|
|
else
|
|
{:error, reason} -> {:error, reason}
|
|
_other -> {:error, :invalid_authorization}
|
|
end
|
|
end
|
|
|
|
def validate_authorization_header(_header, _method, _url, _opts),
|
|
do: {:error, :invalid_authorization}
|
|
|
|
defp decode_base64(encoded_event) do
|
|
case Base.decode64(encoded_event) do
|
|
{:ok, event_json} -> {:ok, event_json}
|
|
:error -> Base.url_decode64(encoded_event, padding: false)
|
|
end
|
|
end
|
|
|
|
defp validate_event_shape(event, opts) when is_map(event) do
|
|
with :ok <- EventValidator.validate(event),
|
|
:ok <- validate_kind(event),
|
|
:ok <- validate_fresh_created_at(event, opts) do
|
|
:ok
|
|
else
|
|
{:error, :stale_event} -> {:error, :stale_event}
|
|
{:error, _reason} -> {:error, :invalid_event}
|
|
end
|
|
end
|
|
|
|
defp validate_event_shape(_event, _opts), do: {:error, :invalid_event}
|
|
|
|
defp validate_kind(%{"kind" => 27_235}), do: :ok
|
|
defp validate_kind(_event), do: {:error, :invalid_event}
|
|
|
|
defp validate_fresh_created_at(%{"created_at" => created_at}, opts)
|
|
when is_integer(created_at) do
|
|
now = System.system_time(:second)
|
|
max_age_seconds = Keyword.get(opts, :max_age_seconds, @max_age_seconds)
|
|
|
|
if abs(now - created_at) <= max_age_seconds do
|
|
:ok
|
|
else
|
|
{:error, :stale_event}
|
|
end
|
|
end
|
|
|
|
defp validate_fresh_created_at(_event, _opts), do: {:error, :invalid_event}
|
|
|
|
defp validate_http_binding(event, method, url) do
|
|
tags = Map.get(event, "tags", [])
|
|
|
|
method_matches? =
|
|
Enum.any?(tags, fn
|
|
["method", tagged_method | _rest] when is_binary(tagged_method) ->
|
|
String.upcase(tagged_method) == String.upcase(method)
|
|
|
|
_tag ->
|
|
false
|
|
end)
|
|
|
|
url_matches? =
|
|
Enum.any?(tags, fn
|
|
["u", tagged_url | _rest] when is_binary(tagged_url) -> tagged_url == url
|
|
_tag -> false
|
|
end)
|
|
|
|
cond do
|
|
not method_matches? -> {:error, :invalid_method_tag}
|
|
not url_matches? -> {:error, :invalid_url_tag}
|
|
true -> :ok
|
|
end
|
|
end
|
|
end
|