lib/relyra/security/redirect.ex

defmodule Relyra.Security.Redirect do
  @moduledoc """
  Security utility for validating redirect paths to prevent Open Redirect vulnerabilities.
  """

  alias Relyra.Error

  @doc """
  Validates a path is a safe local redirect.

  Rejects:
  - Absolute URLs (http://, https://)
  - Protocol-relative URLs (//)
  - Paths not starting with /
  - Nil or non-binary values
  """
  @spec safe_local_redirect(binary() | nil, keyword()) :: {:ok, binary()} | {:error, Error.t()}
  def safe_local_redirect(path, opts \\ [])

  def safe_local_redirect(path, _opts) when is_binary(path) do
    cond do
      String.starts_with?(path, "//") ->
        rejected(:protocol_relative, %{path: path})

      String.starts_with?(path, "http://") or String.starts_with?(path, "https://") ->
        rejected(:absolute_url, %{path: path})

      String.starts_with?(path, "/") ->
        {:ok, path}

      true ->
        rejected(:not_local, %{path: path})
    end
  end

  def safe_local_redirect(nil, _opts) do
    rejected(:nil_path, %{path: nil})
  end

  def safe_local_redirect(_path, _opts) do
    rejected(:non_binary, %{path: :non_binary})
  end

  defp rejected(reason, details) do
    {:error,
     Error.new(
       :invalid_redirect,
       "Redirect path rejected by safety policy",
       Map.put(details, :reason, reason)
     )}
  end
end