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 access_token: nil,
            code: nil,
            code_challenge: nil,
            code_challenge_method: nil,
            expires_in: nil,
            id_token: nil,
            redirect_uri: nil,
            response_mode: nil,
            state: nil,
            token_type: nil,
            type: nil

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

  alias Boruta.Oauth.AuthorizeResponse
  alias Boruta.Oauth.CodeRequest
  alias Boruta.Oauth.HybridRequest
  alias Boruta.Oauth.Token
  alias Boruta.Oauth.TokenRequest

  @spec from_tokens(
          %{
            (type :: :code | :token | :id_token) => token :: Boruta.Oauth.Token.t() | String.t()
          },
          request :: CodeRequest.t() | TokenRequest.t() | HybridRequest.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,
        request
      ) 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

    response_mode =
      case request.__struct__ do
        HybridRequest -> request.response_mode
        _ -> nil
      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"),
      response_mode: response_mode
    }
  end

  def from_tokens(
        %{
          token: %Token{
            expires_at: expires_at,
            value: value,
            redirect_uri: redirect_uri,
            state: state
          }
        } = params,
        _request
      ) 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
          }
        },
        _request
      ) do
    %AuthorizeResponse{
      type: :token,
      redirect_uri: redirect_uri,
      id_token: id_token,
      state: state
    }
  end

  def from_tokens(_params, _request) 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
    params = params(response)
    url(response, params)
  end

  defp 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}, params),
    do: "#{redirect_uri}##{params}"

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

  defp url(
         %__MODULE__{type: :hybrid, response_mode: "query", redirect_uri: redirect_uri},
         params
       ),
       do: "#{redirect_uri}?#{params}"

  defp url(
         %__MODULE__{type: :hybrid, response_mode: "fragment", redirect_uri: redirect_uri},
         params
       ),
       do: "#{redirect_uri}##{params}"

  # fallback to fragment since it is the hybrid default response mode
  defp url(
         %__MODULE__{type: :hybrid, response_mode: nil, redirect_uri: redirect_uri},
         params
       ),
       do: "#{redirect_uri}##{params}"
end