lib/boruta/oauth/authorization/code.ex

defmodule Boruta.Oauth.Authorization.Code do
  @moduledoc """
  Check against given params and return the corresponding code
  """

  alias Boruta.CodesAdapter
  alias Boruta.Oauth.Client
  alias Boruta.Oauth.Error
  alias Boruta.Oauth.Token

  @doc """
  Authorize the code corresponding to the given params.

  ## Examples
      iex> authorize(value: "value", redirect_uri: "redirect_uri")
      {:ok, %Boruta.Oauth.Token{...}}
  """
  @spec authorize(%{
          value: String.t(),
          redirect_uri: String.t(),
          client: Client.t(),
          code_verifier: String.t()
        }) ::
          {:error,
           %Error{
             :error => :invalid_code,
             :error_description => String.t(),
             :format => nil,
             :redirect_uri => nil,
             :status => :bad_request
           }}
          | {:ok, Token.t()}
  def authorize(%{
        value: value,
        redirect_uri: redirect_uri,
        client: %Client{id: client_id, pkce: false}
      }) do
    with %Token{client: %Client{id: ^client_id}} = token <-
           CodesAdapter.get_by(value: value, redirect_uri: redirect_uri),
         :ok <- Token.ensure_valid(token) do
      {:ok, token}
    else
      _ ->
        {:error,
         %Error{
           status: :bad_request,
           error: :invalid_grant,
           error_description: "Given authorization code is invalid, revoked, or expired."
         }}
    end
  end

  def authorize(%{
        value: value,
        redirect_uri: redirect_uri,
        client: %Client{id: client_id, pkce: true},
        code_verifier: code_verifier
      }) do
    with %Token{client: %Client{id: ^client_id}} = token <-
           CodesAdapter.get_by(value: value, redirect_uri: redirect_uri),
         :ok <- check_code_challenge(token, code_verifier),
         :ok <- Token.ensure_valid(token) do
      {:ok, token}
    else
      {:error, :invalid_code_verifier} ->
        {:error,
         %Error{
           status: :bad_request,
           error: :invalid_request,
           error_description: "Code verifier is invalid."
         }}

      _ ->
        {:error,
         %Error{
           status: :bad_request,
           error: :invalid_grant,
           error_description: "Given authorization code is invalid, revoked, or expired."
         }}
    end
  end

  defp check_code_challenge(
         %Token{
           code_challenge_hash: code_challenge_hash,
           code_challenge_method: "plain"
         },
         code_verifier
       ) do
    case Token.hash(code_verifier) == code_challenge_hash do
      true -> :ok
      false -> {:error, :invalid_code_verifier}
    end
  end

  defp check_code_challenge(
         %Token{
           code_challenge_hash: code_challenge_hash,
           code_challenge_method: "S256"
         },
         code_verifier
       ) do
    case :crypto.hash(:sha256, code_verifier) |> Base.url_encode64(padding: false) |> Token.hash() ==
           code_challenge_hash do
      true -> :ok
      false -> {:error, :invalid_code_verifier}
    end
  end
end