defmodule MvWeb.LinkOidcAccountLive do @moduledoc """ LiveView for linking an OIDC account to an existing password account. This page is shown when a user tries to log in via OIDC using an email that already exists with a password-only account. The user must verify their password before the OIDC account can be linked. """ use MvWeb, :live_view require Ash.Query @impl true def mount(_params, session, socket) do user_id = Map.get(session, "oidc_linking_user_id") oidc_user_info = Map.get(session, "oidc_linking_user_info") if user_id && oidc_user_info do # Load the user case Ash.get(Mv.Accounts.User, user_id) do {:ok, user} -> {:ok, socket |> assign(:user, user) |> assign(:oidc_user_info, oidc_user_info) |> assign(:password, "") |> assign(:error, nil) |> assign(:form, to_form(%{"password" => ""}))} {:error, _} -> {:ok, socket |> put_flash(:error, dgettext("auth", "Session expired. Please try again.")) |> redirect(to: ~p"/sign-in")} end else {:ok, socket |> put_flash(:error, dgettext("auth", "Invalid session. Please try again.")) |> redirect(to: ~p"/sign-in")} end end @impl true def handle_event("validate", %{"password" => password}, socket) do {:noreply, assign(socket, :password, password)} end @impl true def handle_event("submit", %{"password" => password}, socket) do user = socket.assigns.user oidc_user_info = socket.assigns.oidc_user_info # Verify the password using AshAuthentication case verify_password(user.email, password) do {:ok, verified_user} -> # Password correct - link the OIDC account link_oidc_account(socket, verified_user, oidc_user_info) {:error, _reason} -> # Password incorrect {:noreply, socket |> assign(:error, dgettext("auth", "Incorrect password. Please try again.")) |> assign(:form, to_form(%{"password" => ""}))} end end defp verify_password(email, password) do # Use AshAuthentication password strategy to verify strategies = AshAuthentication.Info.authentication_strategies(Mv.Accounts.User) password_strategy = Enum.find(strategies, fn s -> s.name == :password end) if password_strategy do AshAuthentication.Strategy.Password.Actions.sign_in( password_strategy, %{ "email" => email, "password" => password }, [] ) else {:error, "Password authentication not configured"} end end defp link_oidc_account(socket, user, oidc_user_info) do oidc_id = Map.get(oidc_user_info, "sub") || Map.get(oidc_user_info, "id") # Update the user with the OIDC ID case Mv.Accounts.User |> Ash.Query.filter(id == ^user.id) |> Ash.read_one!() |> Ash.Changeset.for_update(:link_oidc_id, %{ oidc_id: oidc_id, oidc_user_info: oidc_user_info }) |> Ash.update() do {:ok, _updated_user} -> # After successful linking, redirect to OIDC login # Since the user now has an oidc_id, the next OIDC login will succeed {:noreply, socket |> put_flash( :info, dgettext( "auth", "Your OIDC account has been successfully linked! Redirecting to complete sign-in..." ) ) |> Phoenix.LiveView.redirect(to: ~p"/auth/user/rauthy")} {:error, error} -> {:noreply, socket |> assign( :error, dgettext("auth", "Failed to link account: %{error}", error: inspect(error)) ) |> assign(:form, to_form(%{"password" => ""}))} end end @impl true def render(assigns) do ~H"""
<%!-- Language Selector --%>
<.header class="text-center"> {dgettext("auth", "Link OIDC Account")} <:subtitle> {dgettext( "auth", "An account with email %{email} already exists. Please enter your password to link your OIDC account.", email: @user.email )} <.form for={@form} id="link-oidc-form" phx-submit="submit" phx-change="validate" class="mt-8">
<.input field={@form[:password]} type="password" label={dgettext("auth", "Password")} required />
<%= if @error do %>

{@error}

<% end %>
<.button phx-disable-with={dgettext("auth", "Linking...")} class="w-full"> {dgettext("auth", "Link Account")}
<.link navigate={~p"/sign-in"} class="text-brand hover:underline"> {dgettext("auth", "Cancel")}
""" end end