lib/boruta/oauth/responses/authorize.ex

defmodule Boruta.Oauth.AuthorizeResponse do
  @moduledoc """
  Authorize response
  """

  alias Boruta.Oauth.Error

  @enforce_keys [:type, :redirect_uri]
  defstruct type: nil,
            redirect_uri: nil,
            code: nil,
            id_token: nil,
            access_token: nil,
            expires_in: nil,
            state: nil,
            code_challenge: nil,
            code_challenge_method: nil,
            token_type: nil

  @type t :: %__MODULE__{
          type: :token | :code | :hybrid,
          redirect_uri: String.t(),
          expires_in: integer(),
          code: String.t() | nil,
          id_token: String.t() | nil,
          access_token: String.t() | nil,
          state: String.t() | nil,
          code_challenge: String.t() | nil,
          code_challenge_method: String.t() | nil,
          token_type: String.t() | nil
        }

  alias Boruta.Oauth.AuthorizeResponse
  alias Boruta.Oauth.Token

  @spec from_tokens(%{
          (type :: :code | :token | :id_token) => token :: Boruta.Oauth.Token.t() | String.t()
        }) :: t()
  def from_tokens(
        %{
          code: %Token{
            expires_at: expires_at,
            value: value,
            redirect_uri: redirect_uri,
            state: state,
            code_challenge: code_challenge,
            code_challenge_method: code_challenge_method
          }
        } = params
      ) do
    {:ok, expires_at} = DateTime.from_unix(expires_at)
    expires_in = DateTime.diff(expires_at, DateTime.utc_now())

    type =
      case is_hybrid?(params) do
        true -> :hybrid
        false -> :code
      end

    %AuthorizeResponse{
      type: type,
      redirect_uri: redirect_uri,
      code: value,
      id_token: params[:id_token] && params[:id_token].value,
      access_token: params[:token] && params[:token].value,
      expires_in: expires_in,
      state: state,
      code_challenge: code_challenge,
      code_challenge_method: code_challenge_method,
      token_type: if(is_hybrid?(params), do: "bearer")
    }
  end

  def from_tokens(
        %{
          token: %Token{
            expires_at: expires_at,
            value: value,
            redirect_uri: redirect_uri,
            state: state
          }
        } = params
      ) do
    {:ok, expires_at} = DateTime.from_unix(expires_at)
    expires_in = DateTime.diff(expires_at, DateTime.utc_now())

    %AuthorizeResponse{
      type: :token,
      redirect_uri: redirect_uri,
      access_token: value,
      id_token: params[:id_token] && params[:id_token].value,
      expires_in: expires_in,
      state: state,
      token_type: "bearer"
    }
  end

  def from_tokens(%{
        id_token: %Token{
          value: id_token,
          redirect_uri: redirect_uri,
          state: state
        }
      }) do
    %AuthorizeResponse{
      type: :token,
      redirect_uri: redirect_uri,
      id_token: id_token,
      state: state
    }
  end

  def from_tokens(_) do
    {:error,
     %Error{
       status: :bad_request,
       error: :invalid_request,
       error_description:
         "Neither code, nor access_token, nor id_token could be created with given parameters."
     }}
  end

  defp is_hybrid?(params) do
    !is_nil(params[:id_token] || params[:token])
  end

  @spec redirect_to_url(__MODULE__.t()) :: url :: String.t()
  def redirect_to_url(%__MODULE__{} = response) do
    query_params = query_params(response)
    url(response, query_params)
  end

  defp query_params(%__MODULE__{
         access_token: access_token,
         code: code,
         id_token: id_token,
         expires_in: expires_in,
         state: state,
         token_type: token_type
       }) do
    %{
      code: code,
      id_token: id_token,
      access_token: access_token,
      expires_in: expires_in,
      state: state,
      token_type: token_type
    }
    |> Enum.map(fn {param_type, value} ->
      value && {param_type, value}
    end)
    |> Enum.reject(&is_nil/1)
    |> URI.encode_query()
  end

  defp url(%__MODULE__{type: :token, redirect_uri: redirect_uri}, query_params),
    do: "#{redirect_uri}##{query_params}"

  defp url(%__MODULE__{type: :code, redirect_uri: redirect_uri}, query_params),
    do: "#{redirect_uri}?#{query_params}"

  defp url(%__MODULE__{type: :hybrid, redirect_uri: redirect_uri}, query_params),
    do: "#{redirect_uri}##{query_params}"
end