lib/wax/client_data.ex

defmodule Wax.ClientData do
  defmodule TokenBinding do
    @enforce_keys [:status]

    defstruct [
      :status,
      :id
    ]

    @type t :: %__MODULE__{
            status: String.t(),
            id: String.t()
          }
  end

  @enforce_keys [:type, :challenge, :origin]

  defstruct [
    :type,
    :challenge,
    :origin,
    :token_binding
  ]

  @type t :: %__MODULE__{
          type: :create | :get,
          challenge: binary(),
          origin: String.t(),
          token_binding: TokenBinding.t()
        }

  @type hash :: binary()

  @typedoc """
  The raw string as returned by the javascript WebAuthn API

  Example: `{"challenge":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaY","clientExtensions":{},"hashAlgorithm":"SHA-256","origin":"http://localhost:4000","type":"webauthn.create"}`
  """

  @type raw_string :: String.t()

  @doc false

  @spec parse_raw_json(raw_string()) :: {:ok, t()} | {:error, Exception.t()}
  def parse_raw_json(client_data_json_raw) do
    with {:ok, client_data_map} <- Jason.decode(client_data_json_raw),
         {:ok, maybe_token_binding} <- parse_token_binding(client_data_map["tokenBinding"]) do
      type =
        case client_data_map["type"] do
          "webauthn.create" ->
            :create

          "webauthn.get" ->
            :get
        end

      {:ok,
       %__MODULE__{
         type: type,
         challenge: Base.url_decode64!(client_data_map["challenge"], padding: false),
         origin: client_data_map["origin"],
         token_binding: maybe_token_binding
       }}
    else
      {:error, %Jason.DecodeError{}} ->
        {:error, %Wax.InvalidClientDataError{reason: :malformed_json}}

      error ->
        error
    end
  end

  defp parse_token_binding(nil) do
    {:ok, nil}
  end

  defp parse_token_binding(%{"status" => status} = token_binding)
       when status in ["supported", "not-supported"] do
    {:ok, %TokenBinding{status: status, id: token_binding["id"]}}
  end

  defp parse_token_binding(%{"status" => "present", "id" => id}) do
    {:ok, %TokenBinding{status: "present", id: id}}
  end

  defp parse_token_binding(_) do
    {:error, %Wax.InvalidClientDataError{reason: :invalid_token_binding_data}}
  end
end