feat(portal): Add sign-in success page for clients (#3659)

Why:

* On some clients, the web view that is opened to sign-in to Firezone is
left open and ends up getting stuck on the Sign In page with the
liveview loader on the top of the page also stuck and appearing as
though it is waiting for another response. This commit adds a sign-in
success page that is displayed upon successful sign-in and shows a
message to the user that lets them know they can close the window if
needed. If the client device is able to close the web view that was
opened, then the page will either very briefly be shown or will not be
visible at all due to how quickly the redirect happens.

Closes #3608 

<img width="625" alt="Screenshot 2024-02-15 at 4 30 57 PM"
src="https://github.com/firezone/firezone/assets/2646332/eb6a5df6-4a4c-4e54-b57c-5da239069ea9">

---------

Signed-off-by: Jamil <jamilbk@users.noreply.github.com>
Co-authored-by: Jamil <jamilbk@users.noreply.github.com>
This commit is contained in:
Brian Manifold
2024-02-19 16:00:49 -05:00
committed by GitHub
parent eebd7fc7f1
commit db399651f2
16 changed files with 153 additions and 19 deletions

View File

@@ -89,7 +89,8 @@ defmodule Domain.Config.Definitions do
]},
{"Clients",
[
:clients_upstream_dns
:clients_upstream_dns,
:client_redirect_delay
]},
{"Authorization",
"""
@@ -432,6 +433,13 @@ defmodule Domain.Config.Definitions do
changeset: {Domain.Config.Configuration.ClientsUpstreamDNS, :changeset, []}
)
@doc """
Delay time in milliseconds to wait before redirecting client on the sign in success page.
This is needed for acceptance tests. In dev/staging/prod the default should work fine.
"""
defconfig(:client_redirect_delay, :integer, default: 1)
##############################################
## Userpass / SAML / OIDC / Email authentication
##############################################

View File

@@ -123,13 +123,9 @@ defmodule Web.Auth do
identity_provider_identifier: identity.provider_identifier
}
|> Enum.reject(&is_nil(elem(&1, 1)))
|> URI.encode_query()
client_handler =
Domain.Config.fetch_env!(:web, :client_handler)
Phoenix.Controller.redirect(conn,
external: "#{client_handler}handle_client_sign_in_callback?#{query}"
to: ~p"/#{conn.assigns.account.slug}/signin_success?#{query}"
)
end

View File

@@ -0,0 +1,58 @@
defmodule Web.SignIn.Success do
use Web, {:live_view, layout: {Web.Layouts, :public}}
def mount(params, _session, socket) do
if connected?(socket) do
delay = Domain.Config.fetch_env!(:web, :client_redirect_delay)
Process.send_after(self(), :redirect_client, delay)
end
query_params =
params
|> Map.take(
~w[fragment state actor_name account_slug account_name identity_provider_identifier]
)
socket = assign(socket, :params, query_params)
{:ok, socket}
end
def render(assigns) do
~H"""
<section>
<div class="flex flex-col items-center justify-center px-6 py-8 mx-auto lg:py-0">
<.logo />
<div class="w-full col-span-6 mx-auto bg-white rounded shadow md:mt-0 sm:max-w-lg xl:p-0">
<div class="p-6 space-y-4 lg:space-y-6 sm:p-8">
<h1 class="text-xl text-center leading-tight tracking-tight text-neutral-900 sm:text-2xl">
<span>
Sign in successful.
</span>
</h1>
<p class="text-center">You may close this window.</p>
</div>
</div>
</div>
</section>
"""
end
def handle_info(:redirect_client, socket) do
{scheme, url} =
Domain.Config.fetch_env!(:web, :client_handler)
|> format_redirect_url()
query = URI.encode_query(socket.assigns.params)
{:noreply, redirect(socket, external: {scheme, "#{url}?#{query}"})}
end
defp format_redirect_url(raw_client_handler) do
uri = URI.parse(raw_client_handler)
maybe_host = if uri.host == "", do: "", else: "#{uri.host}:#{uri.port}/"
{uri.scheme, "//#{maybe_host}handle_client_sign_in_callback"}
end
end

View File

@@ -89,6 +89,10 @@ defmodule Web.Router do
scope "/:account_id_or_slug", Web do
pipe_through [:browser, :account]
live_session :client_redirect, on_mount: [Web.Sandbox] do
live "/signin_success", SignIn.Success
end
scope "/sign_in/providers/:provider_id" do
# UserPass
post "/verify_credentials", AuthController, :verify_credentials

View File

@@ -31,11 +31,16 @@ defmodule Web.Sandbox do
end
def allow_live_ecto_sandbox(socket) do
user_agent = Phoenix.LiveView.get_connect_info(socket, :user_agent)
if Phoenix.LiveView.connected?(socket) do
user_agent = Phoenix.LiveView.get_connect_info(socket, :user_agent)
Sandbox.allow(Phoenix.Ecto.SQL.Sandbox, user_agent)
end
with %{owner: test_pid} <- Phoenix.Ecto.SQL.Sandbox.decode_metadata(user_agent) do
Process.put(:last_caller_pid, test_pid)
end
socket
end
end

View File

@@ -112,6 +112,10 @@ defmodule Web.AcceptanceCase.Auth do
Plug.Conn.send_resp(conn, 200, "Client redirected")
end)
Bypass.stub(bypass, "GET", "/favicon.ico", fn conn ->
Plug.Conn.send_resp(conn, 404, "")
end)
bypass
end

View File

@@ -57,6 +57,8 @@ defmodule Web.Acceptance.SignIn.EmailTest do
session
|> email_login_flow(account, identity.provider_identifier, redirect_params)
|> assert_el(Query.text("Sign in successful"))
|> assert_path(~p"/#{account}/signin_success")
|> assert_el(Query.text("Client redirected"))
|> assert_path(~p"/handle_client_sign_in_callback")
@@ -110,6 +112,8 @@ defmodule Web.Acceptance.SignIn.EmailTest do
# And then to a client
session
|> email_login_flow(account, identity.provider_identifier, redirect_params)
|> assert_el(Query.text("Sign in successful"))
|> assert_path(~p"/#{account}/signin_success")
|> assert_el(Query.text("Client redirected"))
|> assert_path(~p"/handle_client_sign_in_callback")

View File

@@ -172,7 +172,7 @@ defmodule Web.Acceptance.Auth.OpenIDConnectTest do
provider_identifier: entity_id
)
# Sign In as an portal user
# Sign In as a portal user
session
|> visit(~p"/#{account}")
|> assert_el(Query.text("Sign in to #{account.name}"))
@@ -187,6 +187,8 @@ defmodule Web.Acceptance.Auth.OpenIDConnectTest do
|> visit(~p"/#{account}?#{redirect_params}")
|> assert_el(Query.text("Sign in to #{account.name}"))
|> click(Query.link("Sign in with Vault"))
|> assert_el(Query.text("Sign in successful"))
|> assert_path(~p"/#{account}/signin_success")
|> assert_el(Query.text("Client redirected"))
|> assert_path(~p"/handle_client_sign_in_callback")
@@ -240,6 +242,8 @@ defmodule Web.Acceptance.Auth.OpenIDConnectTest do
|> assert_el(Query.text("Sign in to #{account.name}"))
|> click(Query.link("Sign in with Vault"))
|> Vault.userpass_flow(oidc_login, oidc_password)
|> assert_el(Query.text("Sign in successful"))
|> assert_path(~p"/#{account}/signin_success")
|> assert_el(Query.text("Client redirected"))
|> assert_path(~p"/handle_client_sign_in_callback")

View File

@@ -167,6 +167,8 @@ defmodule Web.Acceptance.Auth.UserPassTest do
session
|> password_login_flow(account, identity.provider_identifier, password, redirect_params)
|> assert_el(Query.text("Sign in successful"))
|> assert_path(~p"/#{account}/signin_success")
|> assert_el(Query.text("Client redirected"))
|> assert_path(~p"/handle_client_sign_in_callback")
@@ -230,6 +232,8 @@ defmodule Web.Acceptance.Auth.UserPassTest do
# And then to a client
session
|> password_login_flow(account, identity.provider_identifier, password, redirect_params)
|> assert_el(Query.text("Sign in successful"))
|> assert_path(~p"/#{account}/signin_success")
|> assert_el(Query.text("Client redirected"))
|> assert_path(~p"/handle_client_sign_in_callback")

View File

@@ -251,7 +251,7 @@ defmodule Web.AuthTest do
assert conn.assigns.flash["error"] == "Please use a client application to access Firezone."
end
test "redirects regular users to the deep link for client contexts", %{
test "redirects regular users to the sign in success page for client contexts", %{
conn: conn,
context: context,
account: account,
@@ -270,7 +270,7 @@ defmodule Web.AuthTest do
|> signed_in(provider, identity, context, encoded_fragment, redirect_params)
|> redirected_to()
assert redirected_to =~ "firezone-fd0020211111://handle_client_sign_in_callback"
assert redirected_to =~ "#{account.slug}/signin_success"
assert redirected_to =~ "fragment=#{URI.encode_www_form(encoded_fragment)}"
assert redirected_to =~ "state=STATE"
assert redirected_to =~ "account_slug=#{account.slug}"
@@ -299,7 +299,7 @@ defmodule Web.AuthTest do
|> signed_in(provider, identity, context, encoded_fragment, redirect_params)
|> redirected_to()
assert redirected_to =~ "firezone-fd0020211111://handle_client_sign_in_callback"
assert redirected_to =~ "#{account.slug}/signin_success"
assert redirected_to =~ "fragment=#{URI.encode_www_form(encoded_fragment)}"
assert redirected_to =~ "state=STATE"
assert redirected_to =~ "account_slug=#{account.slug}"
@@ -780,7 +780,7 @@ defmodule Web.AuthTest do
assert conn.halted
assert redirected_to = redirected_to(conn)
assert redirected_to =~ "firezone-fd0020211111://handle_client_sign_in_callback"
assert redirected_to =~ "#{account.slug}/signin_success"
assert redirected_to =~ "fragment=#{URI.encode_www_form(encoded_fragment)}"
assert redirected_to =~ "state=STATE"
assert redirected_to =~ "account_slug=#{account.slug}"

View File

@@ -272,8 +272,7 @@ defmodule Web.AuthControllerTest do
assert redirected_to = redirected_to(conn)
assert redirected_to_uri = URI.parse(redirected_to)
assert redirected_to_uri.scheme == "firezone-fd0020211111"
assert redirected_to_uri.host == "handle_client_sign_in_callback"
assert redirected_to_uri.path == "/#{account.slug}/signin_success"
assert %{
"identity_provider_identifier" => identity_provider_identifier,
@@ -626,8 +625,7 @@ defmodule Web.AuthControllerTest do
refute Map.has_key?(conn.cookies, "fz_auth_state_#{provider.id}")
assert redirected_to = conn |> redirected_to() |> URI.parse()
assert redirected_to.scheme == "firezone-fd0020211111"
assert redirected_to.host == "handle_client_sign_in_callback"
assert redirected_to.path == "/#{account.slug}/signin_success"
assert query_params = URI.decode_query(redirected_to.query)
assert not is_nil(query_params["fragment"])
@@ -982,8 +980,7 @@ defmodule Web.AuthControllerTest do
})
assert redirected_to = conn |> redirected_to() |> URI.parse()
assert redirected_to.scheme == "firezone-fd0020211111"
assert redirected_to.host == "handle_client_sign_in_callback"
assert redirected_to.path == "/#{account.slug}/signin_success"
assert query_params = URI.decode_query(redirected_to.query)
assert not is_nil(query_params["fragment"])

View File

@@ -104,7 +104,7 @@ defmodule Web.SignIn.EmailTest do
})
|> submit_form(conn)
assert redirected_to(conn, 302) =~ "firezone-fd0020211111://handle_client_sign_in_callback"
assert redirected_to(conn, 302) =~ "/#{account.slug}/signin_success"
refute conn.assigns.flash["error"]
end

View File

@@ -0,0 +1,44 @@
defmodule Web.SignIn.SuccessTest do
use Web.ConnCase, async: true
setup do
account = Fixtures.Accounts.create_account()
%{account: account}
end
test "redirects to deep link URL", %{
account: account,
conn: conn
} do
query_params = %{
account_name: "account_name",
account_slug: "account_slug",
actor_name: "actor_name",
fragment: "fragment",
identity_provider_identifier: "identifier",
state: "state"
}
{:ok, lv, html} =
conn
|> live(~p"/#{account}/signin_success?#{query_params}")
assert html =~ "success"
assert html =~ "close this window"
client_redirect_delay = Domain.Config.fetch_env!(:web, :client_redirect_delay)
sorted_query_params =
query_params
|> Map.to_list()
|> Enum.sort()
|> URI.encode_query()
assert_redirect(
lv,
"firezone-fd0020211111://handle_client_sign_in_callback?#{sorted_query_params}",
client_redirect_delay + 500
)
end
end

View File

@@ -129,6 +129,8 @@ config :web, Web.Plugs.SecureHeaders,
config :web, api_url_override: "ws://localhost:13001/"
config :web, client_redirect_delay: 1
###############################
##### API #####################
###############################

View File

@@ -107,6 +107,8 @@ if config_env() == :prod do
config :web, api_url_override: compile_config!(:api_url_override)
config :web, client_redirect_delay: compile_config!(:client_redirect_delay)
###############################
##### API #####################
###############################

View File

@@ -45,6 +45,8 @@ config :web, Web.Plugs.SecureHeaders,
"script-src 'self' 'unsafe-inline' https://cdn.tailwindcss.com/"
]
config :web, client_redirect_delay: 500
###############################
##### API #####################
###############################