Setting the return_to for ash authentication

Bet wishes to everyone, especially to the ash core team members for creating such an amazing framework.

I have a question regarding ash_authentication. I added ash_authentication to my application and so far it is working splendid. The issue I have that is after I sign in the user does not get redirected to the page they were trying to go to. I did some debugging and I noticed that the return_to value in the session of the Authentication Controller is always nil thus the application defaults to the “/” route.

So the question is how can I set this variable so the user gets returned to the page they were trying to log in to.

Kind regards,

1 Like

I believe this is something that you’ll want to set yourself when you redirect the user to a route to log in. @jimsynz may be able to add some more context. Essentially somewhere in your app, either in a plug or in an on_mount hook (or both) is a thing that will redirect users to the log in page if they aren’t logged in. In that redirect, you’d want to set the return_to parameter to the url they were trying to access.

1 Like

Thanks for the pointer, I will need to see a bit how I will approach this.

I added the following plug to my application.

For anyone willing to use this, a few pointers. The log statements are very useful for debugging and understanding whats going on in your app but they will slow down your app because they happen on every request. So remove them for serious use.

The @invalid_return_to is added so the return_to variable does not get overwritten during your authentication process. The exact values depend on the authentication system you are using. So you might need to change them to work with your app.

defmodule MyAppWeb.ReturnToPlug do
  import Plug.Conn

  @invalid_return_to ["auth", "sign-in", "sign-out"]

  def init(default), do: default

  def call(conn, _default) do
    IO.puts("""
    Verb: #{inspect(conn.method)}
    Host: #{inspect(conn.query_string)}
    Headers: #{inspect(conn.request_path)}
    session: #{inspect(get_session(conn))}
    """)

    conn.request_path
    |> is_invalid_return_to()
    |> if do
      conn
    else
      put_session(conn, :return_to, add_query_parameter(conn.request_path, conn.query_string))
    end
  end

  defp is_invalid_return_to(path) do
    @invalid_return_to
    |> Enum.map(fn invalid -> String.contains?(path, invalid) end)
    |> Enum.any?()
  end

  defp add_query_parameter(path, query) do
    if query == "" do
      path
    else
      "#{path}?#{query}"
    end
  end
end

And in the router add:

    plug MyAppWeb.ReturnToPlug
4 Likes

This is neat :slight_smile: Maybe we should add something like this to ash_authentication at some point.

You are welcome to use it, ;). I did not do a lot of testing yet so there might be a few bugs hiding.

I did a bit more testing and I noticed that when i signed out it keeps returning me to the login screen. This is because the auth_controller of AshAuthenticationPhoenix also makes use of the return_to value. In my case I just want it to return me to my homepage so with a small edit to the controller this was easily solved.

    return_to = g̵e̵t̵_̵s̵e̵s̵s̵i̵o̵n̵(̵c̵o̵n̵n̵,̵ ̵:̵r̵e̵t̵u̵r̵n̵_̵t̵o̵)̵ ̵|̵|̵ ~p"/"

Another solution might be to prevent setting the return_to variable when we have a logged in user.

1 Like

That is very nice indeed, and helped me. thank you @kjwvanijk :pray:

It doesn’t look like a RetunToPlug has been added to AshAuthentication.Phoenix. Here’s my version. Thank you @kjwvanijk for your version and pointing me in the right direction.

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

@zachdaniel Would you like me to create an issue against AshAuthenticationPhoenix for either of these versions?

Seems like a great candidate for both a standalone igniter task for devs to use to augment their installs.

1 Like

Yes please. We would likely just include something like this in the default installer.

Done - thank you