mirror of
https://github.com/outbackdingo/firezone.git
synced 2026-01-27 18:18:55 +00:00
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:
@@ -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
|
||||
##############################################
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
58
elixir/apps/web/lib/web/live/sign_in/success.ex
Normal file
58
elixir/apps/web/lib/web/live/sign_in/success.ex
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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}"
|
||||
|
||||
@@ -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"])
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
44
elixir/apps/web/test/web/live/sign_in/success_test.exs
Normal file
44
elixir/apps/web/test/web/live/sign_in/success_test.exs
Normal 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
|
||||
@@ -129,6 +129,8 @@ config :web, Web.Plugs.SecureHeaders,
|
||||
|
||||
config :web, api_url_override: "ws://localhost:13001/"
|
||||
|
||||
config :web, client_redirect_delay: 1
|
||||
|
||||
###############################
|
||||
##### API #####################
|
||||
###############################
|
||||
|
||||
@@ -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 #####################
|
||||
###############################
|
||||
|
||||
@@ -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 #####################
|
||||
###############################
|
||||
|
||||
Reference in New Issue
Block a user