fix: adds user friendly flas message

This commit is contained in:
carla 2026-02-17 19:29:49 +01:00
parent 2b1f49d60a
commit a25263b721
7 changed files with 163 additions and 12 deletions

View file

@ -45,4 +45,11 @@ defmodule MvWeb.AuthOverrides do
Gettext.gettext(MvWeb.Gettext, "or") Gettext.gettext(MvWeb.Gettext, "or")
end) end)
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 end

View file

@ -33,6 +33,7 @@
</script> </script>
</head> </head>
<body> <body>
<.flash_group flash={@flash} />
{@inner_content} {@inner_content}
</body> </body>
</html> </html>

View file

@ -57,7 +57,9 @@ defmodule MvWeb.AuthController do
handle_authentication_failed(conn, caused_by) 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
end end
@ -74,14 +76,39 @@ defmodule MvWeb.AuthController do
handle_oidc_email_collision(conn, errors) 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
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 # Catch-all clause for any other error types
defp handle_rauthy_failure(conn, reason) do defp handle_rauthy_failure(conn, reason) do
Logger.warning("Unhandled Rauthy failure reason: #{inspect(reason)}") 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 end
# Handle generic AuthenticationFailed errors # 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. 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 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
end end
defp handle_authentication_failed(conn, _other) do 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 end
# Handle OIDC email collision - user needs to verify password to link accounts # Handle OIDC email collision - user needs to verify password to link accounts
@ -112,7 +145,10 @@ defmodule MvWeb.AuthController do
nil -> nil ->
# Check if it's a "different OIDC account" error or email uniqueness error # Check if it's a "different OIDC account" error or email uniqueness error
error_message = extract_meaningful_error_message(errors) 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
end end
@ -177,13 +213,46 @@ defmodule MvWeb.AuthController do
|> redirect(to: ~p"/auth/link-oidc-account") |> redirect(to: ~p"/auth/link-oidc-account")
end end
# Generic error redirect helper # Extract safe metadata from Assent errors for logging
defp redirect_with_error(conn, message) do # Never logs sensitive data: no tokens, secrets, or full request URLs
conn # Returns keyword list for Logger.warning/2
|> put_flash(:error, message) defp safe_assent_meta(%{request_url: url} = err) when is_binary(url) do
|> redirect(to: ~p"/sign-in") [
request_url: redact_url(url),
http_adapter: Map.get(err, :http_adapter)
]
|> Enum.filter(fn {_key, value} -> not is_nil(value) end)
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 def sign_out(conn, _params) do
return_to = get_session(conn, :return_to) || ~p"/" return_to = get_session(conn, :return_to) || ~p"/"

View file

@ -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." msgid "Unable to authenticate with OIDC. Please try again."
msgstr "OIDC-Authentifizierung fehlgeschlagen. Bitte versuche es erneut." 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 #: lib/mv_web/controllers/auth_controller.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Unable to sign in. Please try again." msgid "Unable to sign in. Please try again."

View file

@ -583,6 +583,16 @@ msgstr ""
msgid "Unable to authenticate with OIDC. Please try again." msgid "Unable to authenticate with OIDC. Please try again."
msgstr "" 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 #: lib/mv_web/controllers/auth_controller.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Unable to sign in. Please try again." msgid "Unable to sign in. Please try again."

View file

@ -583,6 +583,16 @@ msgstr ""
msgid "Unable to authenticate with OIDC. Please try again." msgid "Unable to authenticate with OIDC. Please try again."
msgstr "" 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 #: lib/mv_web/controllers/auth_controller.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Unable to sign in. Please try again." msgid "Unable to sign in. Please try again."

View file

@ -248,4 +248,48 @@ defmodule MvWeb.AuthControllerTest do
assert to =~ "/auth/user/password/sign_in_with_token" assert to =~ "/auth/user/password/sign_in_with_token"
end 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 end