lib/ueberauth/strategy/workos/oauth.ex

defmodule Ueberauth.Strategy.WorkOS.OAuth do
  @moduledoc """
  Implementation of an OAuth 2.0 strategy for WorkOS Single Sign-On

  > #### Note {:.info}
  > This module is not intended to be called or configured directly. Please see
  > `Ueberauth.Strategy.WorkOS` for all relevant configuration and documentation.

  ## Configuration

  This module uses the configuration defined in `Ueberauth.Strategy.WorkOS`. Unlike other Ueberauth
  strategies, there is no need to define application environment variables for this module.
  """
  use OAuth2.Strategy

  @defaults [
    authorize_url: "https://api.workos.com/sso/authorize",
    headers: [{"user-agent", "ueberauth_workos"}],
    site: "https://api.workos.com",
    strategy: __MODULE__,
    token_url: "https://api.workos.com/sso/token"
  ]

  #
  # Ueberauth Strategy Helpers
  #

  @doc """
  Construct a client for requests to the WorkOS API

  ## Options

  This function accepts the same options as `OAuth2.Client.new/1`, including a `redirect_uri`. All
  options are collected and merged in this way:

    * Options passed directly to this function have the highest precedence, overriding all others.
    * Options configured with `Ueberauth.Strategy.WorkOS` have next-highest priority.
    * There are default values for `authorize_url`, `headers`, `site`, and `token_url`.

  OAuth2 will use the same JSON serialization library as returned by `Ueberauth.json_library/0`.
  """
  @spec client(keyword) :: OAuth2.Client.t()
  def client(opts \\ []) do
    config = Application.get_env(:ueberauth, Ueberauth.Strategy.WorkOS) || []

    client_opts =
      @defaults
      |> Keyword.merge(config)
      |> Keyword.merge(opts)
      |> check_credential(:client_id)
      |> check_credential(:api_key, :client_secret)

    json_library = Ueberauth.json_library()

    client_opts
    |> OAuth2.Client.new()
    |> OAuth2.Client.put_serializer("application/json", json_library)
  end

  @spec check_credential(keyword, atom, atom) :: keyword
  defp check_credential(config, key, rename_to \\ nil) do
    validate_key_exists(config, key)

    case Keyword.get(config, key) do
      value when is_binary(value) ->
        if rename_to do
          config
          |> Keyword.delete(key)
          |> Keyword.put(rename_to, value)
        else
          config
        end

      {:system, env_key} ->
        case System.get_env(env_key) do
          nil ->
            raise """
            Missing environment variable #{inspect(env_key)} in configuration for Ueberauth.Strategy.WorkOS

            This variable is configured for the #{inspect(key)} key.
            """

          value ->
            new_key_name = rename_to || key
            Keyword.put(config, new_key_name, value)
        end

      value ->
        raise """
        Invalid value for required key #{inspect(key)} in configuration for Ueberauth.Strategy.WorkOS

        This value must be a string, got: #{inspect(value)}
        """
    end
  end

  @spec validate_key_exists(keyword, atom) :: keyword | no_return
  defp validate_key_exists(config, key) when is_list(config) do
    unless Keyword.has_key?(config, key) do
      raise """
      Missing required key #{inspect(key)} in configuration for Ueberauth.Strategy.WorkOS

      Example configuration:

          config :ueberauth, Ueberauth,
            providers: [
              workos: {Ueberauth.Strategy.WorkOS, [
                api_key: "...",
                client_id: "..."
              ]}
            ]

      OR

          config :ueberauth, Ueberauth.Strategy.WorkOS,
            api_key: "...",
            client_id: "..."

      """
    end

    config
  end

  @doc "Get the authorization URL used during the request phase of Ueberauth"
  @spec authorize_url!(keyword, keyword) :: String.t()
  def authorize_url!(params \\ [], opts \\ []) do
    opts
    |> client()
    |> OAuth2.Client.authorize_url!(params)
  end

  #
  # OAuth2 Strategy Callbacks
  #

  @doc false
  @impl OAuth2.Strategy
  def authorize_url(client, params) do
    OAuth2.Strategy.AuthCode.authorize_url(client, params)
  end

  @doc false
  @impl OAuth2.Strategy
  def get_token(client, params, headers) do
    client
    |> put_header("Accept", "application/json")
    |> put_param(:client_secret, client.client_secret)
    |> OAuth2.Strategy.AuthCode.get_token(params, headers)
  end
end