diff --git a/lib/mv_web/auth_overrides.ex b/lib/mv_web/auth_overrides.ex index 1367150..b121c4e 100644 --- a/lib/mv_web/auth_overrides.ex +++ b/lib/mv_web/auth_overrides.ex @@ -45,4 +45,11 @@ defmodule MvWeb.AuthOverrides do Gettext.gettext(MvWeb.Gettext, "or") end) end + + # Hide AshAuthentication's Flash component since we use flash_group in root layout + # This prevents duplicate flash messages + override AshAuthentication.Phoenix.Components.Flash do + set :message_class_info, "hidden" + set :message_class_error, "hidden" + end end diff --git a/lib/mv_web/components/layouts/root.html.heex b/lib/mv_web/components/layouts/root.html.heex index 35e73ab..f6c3014 100644 --- a/lib/mv_web/components/layouts/root.html.heex +++ b/lib/mv_web/components/layouts/root.html.heex @@ -33,6 +33,7 @@ + <.flash_group flash={@flash} /> {@inner_content} diff --git a/lib/mv_web/controllers/auth_controller.ex b/lib/mv_web/controllers/auth_controller.ex index 20a8b20..8249a10 100644 --- a/lib/mv_web/controllers/auth_controller.ex +++ b/lib/mv_web/controllers/auth_controller.ex @@ -57,7 +57,9 @@ defmodule MvWeb.AuthController do handle_authentication_failed(conn, caused_by) _ -> - redirect_with_error(conn, gettext("Incorrect email or password")) + conn + |> put_flash(:error, gettext("Incorrect email or password")) + |> redirect(to: ~p"/sign-in") end end @@ -74,14 +76,39 @@ defmodule MvWeb.AuthController do handle_oidc_email_collision(conn, errors) _ -> - redirect_with_error(conn, gettext("Unable to authenticate with OIDC. Please try again.")) + conn + |> put_flash(:error, gettext("Unable to authenticate with OIDC. Please try again.")) + |> redirect(to: ~p"/sign-in") end end + # Handle Assent server unreachable errors (network/connectivity issues) + defp handle_rauthy_failure(conn, %Assent.ServerUnreachableError{} = err) do + # Use warning level: server unreachable is often transient, not a critical system error + Logger.warning("OIDC authentication server unreachable", safe_assent_meta(err)) + + conn + |> put_flash(:error, gettext("The authentication server is currently unavailable. Please try again later.")) + |> redirect(to: ~p"/sign-in") + end + + # Handle Assent invalid response errors (configuration or malformed responses) + defp handle_rauthy_failure(conn, %Assent.InvalidResponseError{} = err) do + # Use warning level: configuration errors are operational issues, not critical failures + Logger.warning("OIDC authentication invalid response", safe_assent_meta(err)) + + conn + |> put_flash(:error, gettext("Authentication configuration error. Please contact the administrator.")) + |> redirect(to: ~p"/sign-in") + end + # Catch-all clause for any other error types defp handle_rauthy_failure(conn, reason) do Logger.warning("Unhandled Rauthy failure reason: #{inspect(reason)}") - redirect_with_error(conn, gettext("Unable to authenticate with OIDC. Please try again.")) + + conn + |> put_flash(:error, gettext("Unable to authenticate with OIDC. Please try again.")) + |> redirect(to: ~p"/sign-in") end # Handle generic AuthenticationFailed errors @@ -93,14 +120,20 @@ defmodule MvWeb.AuthController do You can confirm your account using the link we sent to you, or by resetting your password. """) - redirect_with_error(conn, message) + conn + |> put_flash(:error, message) + |> redirect(to: ~p"/sign-in") else - redirect_with_error(conn, gettext("Authentication failed. Please try again.")) + conn + |> put_flash(:error, gettext("Authentication failed. Please try again.")) + |> redirect(to: ~p"/sign-in") end end defp handle_authentication_failed(conn, _other) do - redirect_with_error(conn, gettext("Authentication failed. Please try again.")) + conn + |> put_flash(:error, gettext("Authentication failed. Please try again.")) + |> redirect(to: ~p"/sign-in") end # Handle OIDC email collision - user needs to verify password to link accounts @@ -112,7 +145,10 @@ defmodule MvWeb.AuthController do nil -> # Check if it's a "different OIDC account" error or email uniqueness error error_message = extract_meaningful_error_message(errors) - redirect_with_error(conn, error_message) + + conn + |> put_flash(:error, error_message) + |> redirect(to: ~p"/sign-in") end end @@ -177,13 +213,46 @@ defmodule MvWeb.AuthController do |> redirect(to: ~p"/auth/link-oidc-account") end - # Generic error redirect helper - defp redirect_with_error(conn, message) do - conn - |> put_flash(:error, message) - |> redirect(to: ~p"/sign-in") + # Extract safe metadata from Assent errors for logging + # Never logs sensitive data: no tokens, secrets, or full request URLs + # Returns keyword list for Logger.warning/2 + defp safe_assent_meta(%{request_url: url} = err) when is_binary(url) do + [ + request_url: redact_url(url), + http_adapter: Map.get(err, :http_adapter) + ] + |> Enum.filter(fn {_key, value} -> not is_nil(value) end) end + defp safe_assent_meta(%{response: %{status_code: status_code}} = err) do + [ + status_code: status_code, + http_adapter: Map.get(err, :http_adapter) + ] + |> Enum.filter(fn {_key, value} -> not is_nil(value) end) + end + + defp safe_assent_meta(err) do + # Only extract safe, simple fields + [ + http_adapter: Map.get(err, :http_adapter) + ] + |> Enum.filter(fn {_key, value} -> not is_nil(value) end) + end + + # Redact URL to only show scheme and host, hiding path, query, and fragments + defp redact_url(url) when is_binary(url) do + case URI.parse(url) do + %URI{scheme: scheme, host: host} when not is_nil(scheme) and not is_nil(host) -> + "#{scheme}://#{host}" + + _ -> + "[redacted]" + end + end + + defp redact_url(_), do: "[redacted]" + def sign_out(conn, _params) do return_to = get_session(conn, :return_to) || ~p"/" diff --git a/priv/gettext/de/LC_MESSAGES/default.po b/priv/gettext/de/LC_MESSAGES/default.po index 0187da6..527a849 100644 --- a/priv/gettext/de/LC_MESSAGES/default.po +++ b/priv/gettext/de/LC_MESSAGES/default.po @@ -582,6 +582,16 @@ msgstr "Ein Konto mit dieser E-Mail existiert bereits. Bitte verifiziere dein Pa msgid "Unable to authenticate with OIDC. Please try again." msgstr "OIDC-Authentifizierung fehlgeschlagen. Bitte versuche es erneut." +#: lib/mv_web/controllers/auth_controller.ex +#, elixir-autogen, elixir-format +msgid "The authentication server is currently unavailable. Please try again later." +msgstr "Der Authentifizierungsserver ist derzeit nicht erreichbar. Bitte versuche es später erneut." + +#: lib/mv_web/controllers/auth_controller.ex +#, elixir-autogen, elixir-format +msgid "Authentication configuration error. Please contact the administrator." +msgstr "Authentifizierungskonfigurationsfehler. Bitte kontaktiere den Administrator." + #: lib/mv_web/controllers/auth_controller.ex #, elixir-autogen, elixir-format msgid "Unable to sign in. Please try again." diff --git a/priv/gettext/default.pot b/priv/gettext/default.pot index a05597c..8247f31 100644 --- a/priv/gettext/default.pot +++ b/priv/gettext/default.pot @@ -583,6 +583,16 @@ msgstr "" msgid "Unable to authenticate with OIDC. Please try again." msgstr "" +#: lib/mv_web/controllers/auth_controller.ex +#, elixir-autogen, elixir-format +msgid "The authentication server is currently unavailable. Please try again later." +msgstr "" + +#: lib/mv_web/controllers/auth_controller.ex +#, elixir-autogen, elixir-format +msgid "Authentication configuration error. Please contact the administrator." +msgstr "" + #: lib/mv_web/controllers/auth_controller.ex #, elixir-autogen, elixir-format msgid "Unable to sign in. Please try again." diff --git a/priv/gettext/en/LC_MESSAGES/default.po b/priv/gettext/en/LC_MESSAGES/default.po index 261cbe4..bff719d 100644 --- a/priv/gettext/en/LC_MESSAGES/default.po +++ b/priv/gettext/en/LC_MESSAGES/default.po @@ -583,6 +583,16 @@ msgstr "" msgid "Unable to authenticate with OIDC. Please try again." msgstr "" +#: lib/mv_web/controllers/auth_controller.ex +#, elixir-autogen, elixir-format +msgid "The authentication server is currently unavailable. Please try again later." +msgstr "" + +#: lib/mv_web/controllers/auth_controller.ex +#, elixir-autogen, elixir-format +msgid "Authentication configuration error. Please contact the administrator." +msgstr "" + #: lib/mv_web/controllers/auth_controller.ex #, elixir-autogen, elixir-format msgid "Unable to sign in. Please try again." diff --git a/test/mv_web/controllers/auth_controller_test.exs b/test/mv_web/controllers/auth_controller_test.exs index 444571b..c177c96 100644 --- a/test/mv_web/controllers/auth_controller_test.exs +++ b/test/mv_web/controllers/auth_controller_test.exs @@ -248,4 +248,48 @@ defmodule MvWeb.AuthControllerTest do assert to =~ "/auth/user/password/sign_in_with_token" end + + # OIDC/Rauthy error handling tests + describe "handle_rauthy_failure/2" do + test "Assent.ServerUnreachableError redirects to sign-in with error flash", %{ + conn: authenticated_conn + } do + conn = build_unauthenticated_conn(authenticated_conn) + # Create a mock Assent.ServerUnreachableError struct + error = %Assent.ServerUnreachableError{request_url: "https://auth.example.com/callback?token=secret123"} + + conn = MvWeb.AuthController.failure(conn, {:rauthy, :callback}, error) + + assert redirected_to(conn) == ~p"/sign-in" + assert get_flash(conn, :error) == "The authentication server is currently unavailable. Please try again later." + end + + test "Assent.InvalidResponseError redirects to sign-in with error flash", %{ + conn: authenticated_conn + } do + conn = build_unauthenticated_conn(authenticated_conn) + # Create a mock Assent.InvalidResponseError struct + error = %Assent.InvalidResponseError{ + response: %{status_code: 400, body: "invalid_request"}, + request_url: "https://auth.example.com/callback" + } + + conn = MvWeb.AuthController.failure(conn, {:rauthy, :callback}, error) + + assert redirected_to(conn) == ~p"/sign-in" + assert get_flash(conn, :error) == "Authentication configuration error. Please contact the administrator." + end + + test "unknown reason triggers catch-all and redirects to sign-in with error flash", %{ + conn: authenticated_conn + } do + conn = build_unauthenticated_conn(authenticated_conn) + unknown_reason = :oops + + conn = MvWeb.AuthController.failure(conn, {:rauthy, :callback}, unknown_reason) + + assert redirected_to(conn) == ~p"/sign-in" + assert get_flash(conn, :error) == "Unable to authenticate with OIDC. Please try again." + end + end end