diff --git a/elixir/apps/web/lib/web/auth.ex b/elixir/apps/web/lib/web/auth.ex index 83e7f8d8f..6e5853049 100644 --- a/elixir/apps/web/lib/web/auth.ex +++ b/elixir/apps/web/lib/web/auth.ex @@ -6,7 +6,7 @@ defmodule Web.Auth do do: ~p"/#{subject.account}/dashboard" def put_subject_in_session(conn, %Auth.Subject{} = subject) do - {:ok, session_token} = Domain.Auth.create_session_token_from_subject(subject) + {:ok, session_token} = Auth.create_session_token_from_subject(subject) conn |> Plug.Conn.put_session(:signed_in_at, DateTime.utc_now()) @@ -15,38 +15,59 @@ defmodule Web.Auth do end @doc """ - This is a wrapper around `Domain.Auth.sign_in/5` that fails authentication and redirects - to app install instructions for the users that should not have access to the control plane UI. + Redirects the signed in user depending on the actor type. + + The account admin users are sent to dashboard or a return path if it's stored in session. + + The account users are only expected to authenticate using client apps. + If the platform is known, we direct them to the application through a deep link or an app link; + if not, we guide them to the install instructions accompanied by an error message. """ - def sign_in(conn, provider, provider_identifier, secret) do - case Domain.Auth.sign_in( - provider, - provider_identifier, - secret, - conn.assigns.user_agent, - conn.remote_ip - ) do - {:ok, %Auth.Subject{actor: %{type: :account_admin_user}} = subject} -> - {:ok, subject} + def signed_in_redirect( + conn, + %Auth.Subject{actor: %{type: :account_admin_user}} = subject, + _client_platform, + _client_csrf_token + ) do + redirect_to = Plug.Conn.get_session(conn, :user_return_to) || signed_in_path(subject) - {:ok, %Auth.Subject{}} -> - {:error, :invalid_actor_type} - - {:error, reason} -> - {:error, reason} - end + conn + |> Web.Auth.renew_session() + |> Web.Auth.put_subject_in_session(subject) + |> Plug.Conn.delete_session(:user_return_to) + |> Phoenix.Controller.redirect(to: redirect_to) end - def sign_in(conn, provider, payload) do - case Domain.Auth.sign_in(provider, payload, conn.assigns.user_agent, conn.remote_ip) do - {:ok, %Auth.Subject{actor: %{type: :account_admin_user}} = subject} -> - {:ok, subject} + def signed_in_redirect( + conn, + %Auth.Subject{actor: %{type: :account_user}} = subject, + client_platform, + client_csrf_token + ) do + platform_redirect_urls = + Domain.Config.fetch_env!(:web, __MODULE__) + |> Keyword.fetch!(:platform_redirect_urls) - {:ok, %Auth.Subject{}} -> - {:error, :invalid_actor_type} + if redirect_to = Map.get(platform_redirect_urls, client_platform) do + {:ok, client_token} = Auth.create_session_token_from_subject(subject) - {:error, reason} -> - {:error, reason} + query = + %{ + client_auth_token: client_token, + client_csrf_token: client_csrf_token + } + |> Enum.reject(&is_nil(elem(&1, 1))) + |> URI.encode_query() + + conn + |> Phoenix.Controller.redirect(external: "#{redirect_to}?#{query}") + else + conn + |> Phoenix.Controller.put_flash( + :info, + "Please use a client application to access Firezone." + ) + |> Phoenix.Controller.redirect(to: ~p"/#{conn.path_params["account_id_or_slug"]}/") end end diff --git a/elixir/apps/web/lib/web/controllers/auth_controller.ex b/elixir/apps/web/lib/web/controllers/auth_controller.ex index 74b1697e6..931427227 100644 --- a/elixir/apps/web/lib/web/controllers/auth_controller.ex +++ b/elixir/apps/web/lib/web/controllers/auth_controller.ex @@ -22,20 +22,24 @@ defmodule Web.AuthController do def verify_credentials(conn, %{ "account_id_or_slug" => account_id_or_slug, "provider_id" => provider_id, - "userpass" => %{ - "provider_identifier" => provider_identifier, - "secret" => secret - } + "userpass" => + %{ + "provider_identifier" => provider_identifier, + "secret" => secret + } = form }) do with {:ok, provider} <- Domain.Auth.fetch_active_provider_by_id(provider_id), - {:ok, subject} <- Web.Auth.sign_in(conn, provider, provider_identifier, secret) do - redirect_to = get_session(conn, :user_return_to) || Auth.signed_in_path(subject) - - conn - |> Web.Auth.renew_session() - |> Web.Auth.put_subject_in_session(subject) - |> delete_session(:user_return_to) - |> redirect(to: redirect_to) + {:ok, subject} <- + Domain.Auth.sign_in( + provider, + provider_identifier, + secret, + conn.assigns.user_agent, + conn.remote_ip + ) do + client_platform = form["client_platform"] + client_csrf_token = form["client_csrf_token"] + Web.Auth.signed_in_redirect(conn, subject, client_platform, client_csrf_token) else {:error, :not_found} -> conn @@ -43,11 +47,6 @@ defmodule Web.AuthController do |> put_flash(:error, "You can not use this method to sign in.") |> redirect(to: "/#{account_id_or_slug}/sign_in") - {:error, :invalid_actor_type} -> - conn - |> put_flash(:info, "Please use client application to access Firezone.") - |> redirect(to: ~p"/#{account_id_or_slug}") - {:error, _reason} -> conn |> put_flash(:userpass_provider_identifier, String.slice(provider_identifier, 0, 160)) @@ -62,9 +61,10 @@ defmodule Web.AuthController do def request_magic_link(conn, %{ "account_id_or_slug" => account_id_or_slug, "provider_id" => provider_id, - "email" => %{ - "provider_identifier" => provider_identifier - } + "email" => + %{ + "provider_identifier" => provider_identifier + } = form }) do _ = with {:ok, provider} <- Domain.Auth.fetch_active_provider_by_id(provider_id), @@ -75,7 +75,10 @@ defmodule Web.AuthController do |> Web.Mailer.deliver() end - redirect(conn, to: "/#{account_id_or_slug}/sign_in/providers/email/#{provider_id}") + conn + |> put_session(:client_platform, form["client_platform"]) + |> put_session(:client_csrf_token, form["client_csrf_token"]) + |> redirect(to: "/#{account_id_or_slug}/sign_in/providers/email/#{provider_id}") end @doc """ @@ -89,25 +92,27 @@ defmodule Web.AuthController do "secret" => secret }) do with {:ok, provider} <- Domain.Auth.fetch_active_provider_by_id(provider_id), - {:ok, subject} <- Web.Auth.sign_in(conn, provider, identity_id, secret) do - redirect_to = get_session(conn, :user_return_to) || Auth.signed_in_path(subject) + {:ok, subject} <- + Domain.Auth.sign_in( + provider, + identity_id, + secret, + conn.assigns.user_agent, + conn.remote_ip + ) do + client_platform = get_session(conn, :client_platform) + client_csrf_token = get_session(conn, :client_csrf_token) conn - |> Web.Auth.renew_session() - |> Web.Auth.put_subject_in_session(subject) - |> delete_session(:user_return_to) - |> redirect(to: redirect_to) + |> delete_session(:client_platform) + |> delete_session(:client_csrf_token) + |> Web.Auth.signed_in_redirect(subject, client_platform, client_csrf_token) else {:error, :not_found} -> conn |> put_flash(:error, "You can not use this method to sign in.") |> redirect(to: "/#{account_id_or_slug}/sign_in") - {:error, :invalid_actor_type} -> - conn - |> put_flash(:info, "Please use client application to access Firezone.") - |> redirect(to: ~p"/#{account_id_or_slug}") - {:error, _reason} -> conn |> put_flash(:error, "The sign in link is invalid or expired.") @@ -119,11 +124,17 @@ defmodule Web.AuthController do This controller redirects user to IdP during sign in for authentication while persisting verification state to prevent various attacks on OpenID Connect. """ - def redirect_to_idp(conn, %{ - "account_id_or_slug" => account_id_or_slug, - "provider_id" => provider_id - }) do + def redirect_to_idp( + conn, + %{ + "account_id_or_slug" => account_id_or_slug, + "provider_id" => provider_id + } = params + ) do with {:ok, provider} <- Domain.Auth.fetch_active_provider_by_id(provider_id) do + conn = put_session(conn, :client_platform, params["client_platform"]) + conn = put_session(conn, :client_csrf_token, params["client_csrf_token"]) + redirect_url = url(~p"/#{provider.account_id}/sign_in/providers/#{provider.id}/handle_callback") @@ -165,25 +176,21 @@ defmodule Web.AuthController do } with {:ok, provider} <- Domain.Auth.fetch_active_provider_by_id(provider_id), - {:ok, subject} <- Web.Auth.sign_in(conn, provider, payload) do - redirect_to = get_session(conn, :user_return_to) || Auth.signed_in_path(subject) + {:ok, subject} <- + Domain.Auth.sign_in(provider, payload, conn.assigns.user_agent, conn.remote_ip) do + client_platform = get_session(conn, :client_platform) + client_csrf_token = get_session(conn, :client_csrf_token) conn - |> Web.Auth.renew_session() - |> Web.Auth.put_subject_in_session(subject) - |> delete_session(:user_return_to) - |> redirect(to: redirect_to) + |> delete_session(:client_platform) + |> delete_session(:client_csrf_token) + |> Web.Auth.signed_in_redirect(subject, client_platform, client_csrf_token) else {:error, :not_found} -> conn |> put_flash(:error, "You can not use this method to sign in.") |> redirect(to: "/#{account_id_or_slug}/sign_in") - {:error, :invalid_actor_type} -> - conn - |> put_flash(:info, "Please use client application to access Firezone.") - |> redirect(to: ~p"/#{account_id_or_slug}") - {:error, _reason} -> conn |> put_flash(:error, "You can not authenticate to this account.") diff --git a/elixir/apps/web/lib/web/live/auth/sign_in.ex b/elixir/apps/web/lib/web/live/auth/sign_in.ex index 8079f5272..e753bc46a 100644 --- a/elixir/apps/web/lib/web/live/auth/sign_in.ex +++ b/elixir/apps/web/lib/web/live/auth/sign_in.ex @@ -2,7 +2,7 @@ defmodule Web.Auth.SignIn do use Web, {:live_view, layout: {Web.Layouts, :public}} alias Domain.{Auth, Accounts} - def mount(%{"account_id_or_slug" => account_id_or_slug}, _session, socket) do + def mount(%{"account_id_or_slug" => account_id_or_slug} = params, _session, socket) do with {:ok, account} <- Accounts.fetch_account_by_id_or_slug(account_id_or_slug), {:ok, [_ | _] = providers} <- Auth.list_active_providers_for_account(account) do providers_by_adapter = @@ -19,6 +19,7 @@ defmodule Web.Auth.SignIn do {:ok, socket, temporary_assigns: [ + params: Map.take(params, ["client_platform", "client_csrf_token"]), account: account, providers_by_adapter: providers_by_adapter, page_title: "Sign in" @@ -53,6 +54,7 @@ defmodule Web.Auth.SignIn do <.providers_group_form adapter="openid_connect" providers={@providers_by_adapter[:openid_connect]} + params={@params} /> @@ -65,6 +67,7 @@ defmodule Web.Auth.SignIn do adapter="userpass" provider={List.first(@providers_by_adapter[:userpass])} flash={@flash} + params={@params} /> @@ -77,6 +80,7 @@ defmodule Web.Auth.SignIn do adapter="email" provider={List.first(@providers_by_adapter[:email])} flash={@flash} + params={@params} /> @@ -100,7 +104,7 @@ defmodule Web.Auth.SignIn do def providers_group_form(%{adapter: "openid_connect"} = assigns) do ~H"""