Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add ReturnTo Plug #584

Open
ketupia opened this issue Feb 26, 2025 · 2 comments
Open

add ReturnTo Plug #584

ketupia opened this issue Feb 26, 2025 · 2 comments

Comments

@ketupia
Copy link

ketupia commented Feb 26, 2025

The installer creates an AuthController that will redirect the user based on :return_to in the session but doesn't supply a means to capture and set the :return_to value.

Create a plug something like the following and include it on install or create an igniter task to create it. The example code will look for certain paths like /sign-in with the specified query parameter and store the value in the session.

defmodule YourAppWeb.Plugs.ReturnToPlug do
  @moduledoc """
  Plug to capture the return_to query parameter and store it in the session.
  This allows for proper redirection after successful authentication while
  preventing redirect loops to authentication-related pages.

  ## Options

    * `:paths` - A list of paths where the plug should capture the return_to parameter
      default: ["/sign-in"]
    * `:param_name` - The name of the query parameter to capture
      default: "return_to"
    * `:session_key` - The session key where the return path will be stored
      default: :return_to
    * `:blocked_redirect_paths` - A list of paths that should be blocked as return destinations.  This is a starts_with? comparison.
      default: ["/auth", "/password-reset", "/reset", "/register", "/sign-in", "/sign-out"]

  ## Examples

  Using default options:

      # In your router.ex
      pipeline :browser do
        # ...other plugs
        plug YourAppWeb.Plugs.ReturnToPlug
      end

  With custom paths:

      # Capture return_to on multiple paths
      plug YourAppWeb.Plugs.ReturnToPlug, paths: ["/sign-in", "/login", "/register"]

  Fully customized configuration:

      # Custom parameter name, session key, and blocked paths
      plug YourAppWeb.Plugs.ReturnToPlug,
        paths: ["/sign-in", "/login"],
        param_name: "redirect_to",
        session_key: :redirect_after_login,
        blocked_redirect_paths: ["/auth", "/password-reset", "/reset", "/register", "/sign-in", "/sign-out"]
  """
  import Plug.Conn

  @default_options [
    paths: ["/sign-in"],
    param_name: "return_to",
    session_key: :return_to,
    blocked_redirect_paths: [
      "/auth",
      "/password-reset",
      "/reset",
      "/register",
      "/sign-in",
      "/sign-out"
    ]
  ]

  def init(opts) do
    Keyword.merge(@default_options, opts)
  end

  def call(conn, opts) do
    conn = fetch_query_params(conn)

    # Check if current path is in the configured paths
    if matching_path?(conn, opts[:paths]) && has_return_to_param?(conn, opts[:param_name]) do
      # Extract the return_to parameter
      return_to = get_return_to_param(conn, opts[:param_name])

      # Only store it if it's not pointing to a blocked path
      if blocked_return_path?(return_to, opts[:blocked_redirect_paths]) do
        # If blocked, we could either keep the conn unchanged or clear any existing return_to
        # Here we choose to clear it to be extra safe
        delete_session(conn, opts[:session_key])
      else
        put_session(conn, opts[:session_key], return_to)
      end
    else
      conn
    end
  end

  # Checks if the current path matches any of the configured paths
  defp matching_path?(conn, paths) do
    Enum.member?(paths, conn.request_path)
  end

  # Checks if the request has the configured query parameter
  defp has_return_to_param?(conn, param_name) do
    conn.query_params[param_name] != nil
  end

  # Gets the configured parameter value from the query parameters
  defp get_return_to_param(conn, param_name) do
    conn.query_params[param_name]
  end

  # Checks if the return path starts with any of the blocked prefixes
  defp blocked_return_path?(return_path, blocked_redirect_paths) do
    path_to_check = URI.parse(return_path).path

    Enum.any?(blocked_redirect_paths, fn prefix ->
      String.starts_with?(path_to_check, prefix)
    end)
  end
end
@ketupia
Copy link
Author

ketupia commented Feb 26, 2025

Elixir Form Post Setting the return_to for ash authentication

@zachdaniel
Copy link
Collaborator

I think it's a great idea. Let's hear from @jimsynz if he is into it, and if so PRs welcome! This should be a relatively straight forward change to the ash_authentication_phoenix installer to create this module into user's applications.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants