lib/boruta/oauth/responses/authorize.ex

defmodule Boruta.Oauth.AuthorizeResponse do
  @moduledoc """
  Response returned in case of authorization request success. Provides utilities and mandatory data needed to respond to the authorize part of implicit, code and hybrid flows.
  """

  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(has_token_type?(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 has_token_type?(params) do
    is_hybrid?(params) && has_access_token?(params)
  end

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

  defp has_access_token?(params) do
    Map.has_key?(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