UI for oidc account linking
This commit is contained in:
parent
87e54cb13f
commit
527657d37b
3 changed files with 256 additions and 15 deletions
162
lib/mv_web/live/auth/link_oidc_account_live.ex
Normal file
162
lib/mv_web/live/auth/link_oidc_account_live.ex
Normal file
|
|
@ -0,0 +1,162 @@
|
|||
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, gettext("Session expired. Please try again."))
|
||||
|> redirect(to: ~p"/sign-in")}
|
||||
end
|
||||
else
|
||||
{:ok,
|
||||
socket
|
||||
|> put_flash(:error, gettext("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, gettext("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,
|
||||
gettext(
|
||||
"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, gettext("Failed to link account: %{error}", error: inspect(error)))
|
||||
|> assign(:form, to_form(%{"password" => ""}))}
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<div class="mx-auto max-w-sm mt-16">
|
||||
<.header class="text-center">
|
||||
{gettext("Link OIDC Account")}
|
||||
<:subtitle>
|
||||
{gettext(
|
||||
"An account with email %{email} already exists. Please enter your password to link your OIDC account.",
|
||||
email: @user.email
|
||||
)}
|
||||
</:subtitle>
|
||||
</.header>
|
||||
|
||||
<.form for={@form} id="link-oidc-form" phx-submit="submit" phx-change="validate" class="mt-8">
|
||||
<div class="space-y-6">
|
||||
<div>
|
||||
<.input field={@form[:password]} type="password" label={gettext("Password")} required />
|
||||
</div>
|
||||
|
||||
<%= if @error do %>
|
||||
<div class="rounded-md bg-red-50 p-4">
|
||||
<p class="text-sm text-red-800">{@error}</p>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<div>
|
||||
<.button phx-disable-with={gettext("Linking...")} class="w-full">
|
||||
{gettext("Link Account")}
|
||||
</.button>
|
||||
</div>
|
||||
</div>
|
||||
</.form>
|
||||
|
||||
<div class="mt-4 text-center text-sm">
|
||||
<.link navigate={~p"/sign-in"} class="text-brand hover:underline">
|
||||
{gettext("Cancel")}
|
||||
</.link>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
end
|
||||
Loading…
Add table
Add a link
Reference in a new issue