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

Idea: setting a return_to URI to return to following a successful authentication #581

Open
matthewsinclair opened this issue Feb 16, 2025 · 2 comments

Comments

@matthewsinclair
Copy link

matthewsinclair commented Feb 16, 2025

I was trying to use ash_authentication, ash_authentication_phoenix, and beacon in such a way that I could put the Beacon Admin functionality behind an Ash authentication. The desired behaviour I was after was for any request to something like /admin/cms to first of all trigger the usual authentication workflow, and then redirect to the originally requested URL.

After a very long chat [0] with @zachdaniel and @leandrocp today, there were a few changes made to both ash_authentication_phoenix and beacon to allow for scoping the BeaconCMS admin pages so that they trigger an ash_authentication workflow.

The results of that mean that you can now do something like this:

  scope "/admin/cms" do
    pipe_through [:browser, :beacon_admin, :set_return_to_based_on_uri]

    beacon_live_admin "/",
                      AshAuthentication.Phoenix.LiveSession.opts(
                        on_mount: [{MyappWeb.LiveUserAuth, :live_user_required}]
                      )
  end

The only problem with this is that the default ash_authentication behaviour is that the app then returns to the page specified in MyappWeb.AuthController.success/4 like this:

defmodule MyappWeb.AuthController do
  use MyappWeb, :controller
  use AshAuthentication.Phoenix.Controller

  def success(conn, activity, user, _token) do
    return_to = get_session(conn, :return_to) || ~p"/app"

    message =
      case activity do
        {:confirm_new_user, :confirm} -> "Your email address has now been confirmed"
        {:password, :reset} -> "Your password has successfully been reset"
        _ -> "You are now signed in"
      end

    conn
    |> delete_session(:return_to)
    |> store_in_session(user)
    |> assign(:current_user, user)
    |> put_flash(:info, message)
    |> redirect(to: return_to)
  end

  def failure(conn, activity, reason) do
    message =
      case {activity, reason} do
        {_,
         %AshAuthentication.Errors.AuthenticationFailed{
           caused_by: %Ash.Error.Forbidden{
             errors: [%AshAuthentication.Errors.CannotConfirmUnconfirmedUser{}]
           }
         }} ->
          """
          You have already signed in another way, but have not confirmed your account.
          You can confirm your account using the link we sent to you, or by resetting your password.
          """

        _ ->
          "Incorrect email or password"
      end

    conn
    |> put_flash(:error, message)
    |> redirect(to: ~p"/sign-in")
  end

  def sign_out(conn, _params) do
    conn
    |> delete_session(:return_to)
    |> clear_session()
    |> put_flash(:info, "You are now signed out")
    |> redirect(to: ~p"/")
  end
end

Unless, that is, you can inject return_to into the session somehow. After a bit of guidance from @zachdaniel we came up with a solution that I know is not perfect, but kind of works. Well, almost kind of.

First I made a new plug:

defmodule MyappWebb.Plugs.SetReturnToBasedOnUriPlug do
  @moduledoc """
  A plug that sets the session's `:return_to` key to the current URI.

  This is useful for redirecting users back to the original page after authentication or other actions.
  """

  import Plug.Conn

  @behaviour Plug

  @impl true
  @spec init(Keyword.t()) :: Keyword.t()
  def init(opts), do: opts

  @impl true
  @spec call(Plug.Conn.t(), Keyword.t()) :: Plug.Conn.t()
  def call(conn, _opts) do
    put_session(conn, :return_to, current_uri(conn))
  end

  @doc false
  @spec current_uri(Plug.Conn.t()) :: String.t()
  defp current_uri(%Plug.Conn{request_path: path, query_string: ""}), do: path
  defp current_uri(%Plug.Conn{request_path: path, query_string: query}), do: "#{path}?#{query}"
end

And then I added a new pipeline to my router:

  pipeline :set_return_to_based_on_uri do
    plug MyappWeb.Plugs.SetReturnToBasedOnUriPlug
  end

And then updated the /admin/cms scope to look like this:

  scope "/admin/cms" do
    pipe_through [:browser, :beacon_admin, :set_return_to_based_on_uri]

    beacon_live_admin "/",
                      AshAuthentication.Phoenix.LiveSession.opts(
                        on_mount: [{IcpzeroWeb.LiveUserAuth, :live_user_required}]
                      )
  end

Now, this almost works, but not quite.

What is happening now is that that put_session(conn, :return_to, return_to) call ends up leaving the return_to value set in the session so that any subsequent sign-out/sign-in attempt sees the old value and ends up returning to that page even if the user just tries to sign in to /app.

I am trying to track that down now, but in the meantime, if there is a better way to manage this return_to functionality, I'd love to hear about it.

UPDATE-MDS20250218: Tidied up SetReturnToBasedOnUriPlug after re-examining the (very rough) first version of the code.
UPDATE-MFS20250217: I noticed a very obvious bug in the first version, which I have now edited. The code above works from the perspective of :return_to functionality. Just waiting on the fix for AshAuth+Beacon integration.

References:
[0]: 20250216 Slack conversation that motivated the idea: https://elixir-lang.slack.com/archives/C02T04D147M/p1739642323323869

@zachdaniel
Copy link
Collaborator

You could try clearing the return_to before redirecting in your auth controller?

@zachdaniel
Copy link
Collaborator

But we should still potentially build this in by default, even if its just in the generated code 😄

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