refactor and docs
This commit is contained in:
parent
4ba03821a2
commit
5ce220862f
13 changed files with 1321 additions and 174 deletions
|
|
@ -1,9 +1,21 @@
|
|||
require Logger
|
||||
|
||||
defmodule MvWeb.AuthController do
|
||||
@moduledoc """
|
||||
Handles authentication callbacks for password and OIDC authentication.
|
||||
|
||||
This controller manages:
|
||||
- Successful authentication (password, OIDC, password reset, email confirmation)
|
||||
- Authentication failures with appropriate error handling
|
||||
- OIDC account linking flow when email collision occurs
|
||||
- Sign out functionality
|
||||
"""
|
||||
|
||||
use MvWeb, :controller
|
||||
use AshAuthentication.Phoenix.Controller
|
||||
|
||||
alias Mv.Accounts.User.Errors.PasswordVerificationRequired
|
||||
|
||||
def success(conn, activity, user, _token) do
|
||||
return_to = get_session(conn, :return_to) || ~p"/"
|
||||
|
||||
|
|
@ -23,107 +35,149 @@ defmodule MvWeb.AuthController do
|
|||
|> redirect(to: return_to)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Handles authentication failures and routes to appropriate error handling.
|
||||
|
||||
Manages:
|
||||
- OIDC email collisions (triggers password verification flow)
|
||||
- Generic OIDC authentication failures
|
||||
- Unconfirmed account errors
|
||||
- Generic authentication failures
|
||||
"""
|
||||
def failure(conn, activity, reason) do
|
||||
# Log the error for debugging
|
||||
Logger.warning(
|
||||
"Authentication failure - Activity: #{inspect(activity)}, Reason: #{inspect(reason)}"
|
||||
)
|
||||
|
||||
case {activity, reason} do
|
||||
# OIDC registration with existing email requires password verification (direct error)
|
||||
{{:rauthy, :register}, %Ash.Error.Invalid{errors: errors}} ->
|
||||
handle_oidc_email_collision(conn, errors)
|
||||
{{:rauthy, _action}, reason} ->
|
||||
handle_rauthy_failure(conn, reason)
|
||||
|
||||
# OIDC registration with existing email (wrapped in AuthenticationFailed)
|
||||
{{:rauthy, :register},
|
||||
%AshAuthentication.Errors.AuthenticationFailed{
|
||||
caused_by: %Ash.Error.Invalid{errors: errors}
|
||||
}} ->
|
||||
handle_oidc_email_collision(conn, errors)
|
||||
|
||||
# OIDC sign-in failure (wrapped)
|
||||
{{:rauthy, :sign_in}, %AshAuthentication.Errors.AuthenticationFailed{caused_by: caused_by}} ->
|
||||
# Check if it's actually a registration issue
|
||||
case caused_by do
|
||||
%Ash.Error.Invalid{errors: errors} ->
|
||||
handle_oidc_email_collision(conn, errors)
|
||||
|
||||
_ ->
|
||||
# Real sign-in failure
|
||||
conn
|
||||
|> put_flash(:error, gettext("Unable to sign in with OIDC. Please try again."))
|
||||
|> redirect(to: ~p"/sign-in")
|
||||
end
|
||||
|
||||
# OIDC callback failure (can be either sign-in or registration)
|
||||
{{:rauthy, :callback}, %AshAuthentication.Errors.AuthenticationFailed{caused_by: caused_by}} ->
|
||||
case caused_by do
|
||||
%Ash.Error.Invalid{errors: errors} ->
|
||||
handle_oidc_email_collision(conn, errors)
|
||||
|
||||
_ ->
|
||||
conn
|
||||
|> put_flash(:error, gettext("Unable to authenticate with OIDC. Please try again."))
|
||||
|> redirect(to: ~p"/sign-in")
|
||||
end
|
||||
|
||||
{_,
|
||||
%AshAuthentication.Errors.AuthenticationFailed{
|
||||
caused_by: %Ash.Error.Forbidden{
|
||||
errors: [%AshAuthentication.Errors.CannotConfirmUnconfirmedUser{}]
|
||||
}
|
||||
}} ->
|
||||
message =
|
||||
gettext("""
|
||||
You have already signed in another way, but have not confirmed your account.
|
||||
You can confirm your account using the link we sent to you, or by resetting your password.
|
||||
""")
|
||||
|
||||
conn
|
||||
|> put_flash(:error, message)
|
||||
|> redirect(to: ~p"/sign-in")
|
||||
{_, %AshAuthentication.Errors.AuthenticationFailed{caused_by: caused_by}} ->
|
||||
handle_authentication_failed(conn, caused_by)
|
||||
|
||||
_ ->
|
||||
message = gettext("Incorrect email or password")
|
||||
|
||||
conn
|
||||
|> put_flash(:error, message)
|
||||
|> redirect(to: ~p"/sign-in")
|
||||
redirect_with_error(conn, gettext("Incorrect email or password"))
|
||||
end
|
||||
end
|
||||
|
||||
# Handle OIDC email collision - user needs to verify password
|
||||
defp handle_oidc_email_collision(conn, errors) do
|
||||
password_verification_error =
|
||||
Enum.find(errors, fn err ->
|
||||
match?(%Mv.Accounts.User.Errors.PasswordVerificationRequired{}, err)
|
||||
end)
|
||||
# Handle all Rauthy (OIDC) authentication failures
|
||||
defp handle_rauthy_failure(conn, %Ash.Error.Invalid{errors: errors}) do
|
||||
handle_oidc_email_collision(conn, errors)
|
||||
end
|
||||
|
||||
case password_verification_error do
|
||||
%Mv.Accounts.User.Errors.PasswordVerificationRequired{
|
||||
user_id: user_id,
|
||||
oidc_user_info: oidc_user_info
|
||||
} ->
|
||||
# Store the OIDC info in session for the linking flow
|
||||
conn
|
||||
|> put_session(:oidc_linking_user_id, user_id)
|
||||
|> put_session(:oidc_linking_user_info, oidc_user_info)
|
||||
|> put_flash(
|
||||
:info,
|
||||
gettext(
|
||||
"An account with this email already exists. Please verify your password to link your OIDC account."
|
||||
)
|
||||
)
|
||||
|> redirect(to: ~p"/auth/link-oidc-account")
|
||||
defp handle_rauthy_failure(conn, %AshAuthentication.Errors.AuthenticationFailed{
|
||||
caused_by: caused_by
|
||||
}) do
|
||||
case caused_by do
|
||||
%Ash.Error.Invalid{errors: errors} ->
|
||||
handle_oidc_email_collision(conn, errors)
|
||||
|
||||
_ ->
|
||||
# Other validation errors - show generic error
|
||||
conn
|
||||
|> put_flash(:error, gettext("Unable to sign in. Please try again."))
|
||||
|> redirect(to: ~p"/sign-in")
|
||||
redirect_with_error(conn, gettext("Unable to authenticate with OIDC. Please try again."))
|
||||
end
|
||||
end
|
||||
|
||||
# Handle generic AuthenticationFailed errors
|
||||
defp handle_authentication_failed(conn, %Ash.Error.Forbidden{errors: errors}) do
|
||||
if Enum.any?(errors, &match?(%AshAuthentication.Errors.CannotConfirmUnconfirmedUser{}, &1)) do
|
||||
message =
|
||||
gettext("""
|
||||
You have already signed in another way, but have not confirmed your account.
|
||||
You can confirm your account using the link we sent to you, or by resetting your password.
|
||||
""")
|
||||
|
||||
redirect_with_error(conn, message)
|
||||
else
|
||||
redirect_with_error(conn, gettext("Authentication failed. Please try again."))
|
||||
end
|
||||
end
|
||||
|
||||
defp handle_authentication_failed(conn, _other) do
|
||||
redirect_with_error(conn, gettext("Authentication failed. Please try again."))
|
||||
end
|
||||
|
||||
# Handle OIDC email collision - user needs to verify password to link accounts
|
||||
defp handle_oidc_email_collision(conn, errors) do
|
||||
case find_password_verification_error(errors) do
|
||||
%PasswordVerificationRequired{user_id: user_id, oidc_user_info: oidc_user_info} ->
|
||||
redirect_to_account_linking(conn, user_id, oidc_user_info)
|
||||
|
||||
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)
|
||||
end
|
||||
end
|
||||
|
||||
# Extract meaningful error message from Ash errors
|
||||
defp extract_meaningful_error_message(errors) do
|
||||
# Look for specific error messages in InvalidAttribute errors
|
||||
meaningful_error =
|
||||
Enum.find_value(errors, fn
|
||||
%Ash.Error.Changes.InvalidAttribute{message: message, field: :email}
|
||||
when is_binary(message) ->
|
||||
cond do
|
||||
# Email update conflict during OIDC login
|
||||
String.contains?(message, "Cannot update email to") and
|
||||
String.contains?(message, "already registered to another account") ->
|
||||
gettext(
|
||||
"Cannot update email: This email is already registered to another account. Please change your email in the identity provider."
|
||||
)
|
||||
|
||||
# Different OIDC account error
|
||||
String.contains?(message, "already linked to a different OIDC account") ->
|
||||
gettext(
|
||||
"This email is already linked to a different OIDC account. Cannot link multiple OIDC providers to the same account."
|
||||
)
|
||||
|
||||
true ->
|
||||
nil
|
||||
end
|
||||
|
||||
%Ash.Error.Changes.InvalidAttribute{message: message}
|
||||
when is_binary(message) ->
|
||||
# Return any other meaningful message
|
||||
if String.length(message) > 20 and
|
||||
not String.contains?(message, "has already been taken") do
|
||||
message
|
||||
else
|
||||
nil
|
||||
end
|
||||
|
||||
_ ->
|
||||
nil
|
||||
end)
|
||||
|
||||
meaningful_error || gettext("Unable to sign in. Please try again.")
|
||||
end
|
||||
|
||||
# Find PasswordVerificationRequired error in error list
|
||||
defp find_password_verification_error(errors) do
|
||||
Enum.find(errors, &match?(%PasswordVerificationRequired{}, &1))
|
||||
end
|
||||
|
||||
# Redirect to account linking page with OIDC info stored in session
|
||||
defp redirect_to_account_linking(conn, user_id, oidc_user_info) do
|
||||
conn
|
||||
|> put_session(:oidc_linking_user_id, user_id)
|
||||
|> put_session(:oidc_linking_user_info, oidc_user_info)
|
||||
|> put_flash(
|
||||
:info,
|
||||
gettext(
|
||||
"An account with this email already exists. Please verify your password to link your OIDC account."
|
||||
)
|
||||
)
|
||||
|> 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")
|
||||
end
|
||||
|
||||
def sign_out(conn, _params) do
|
||||
return_to = get_session(conn, :return_to) || ~p"/"
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue