Fix OIDC household invitation (issue #900) (#904)

* Fix OIDC household invitation (issue #900)

- Auto-add existing user when inviting by email (no invite email sent)
- Accept page: choose 'Create account' or 'Sign in' (supports OIDC)
- Store invitation token in session on sign-in; accept after login (password,
  OIDC, OIDC link, OIDC JIT, MFA)
- Invitation#accept_for!(user): add user to household and mark accepted
- Defensive guards: nil/blank user, token normalization, accept_for! return check

* Address PR review: rename accept_for! to accept_for, i18n OIDC notice, test fixes, stub Rails.application.config

* Fix flaky system test: assert only configure step, not flash message

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Signed-off-by: Juan José Mata <juanjo.mata@gmail.com>
Co-authored-by: mkdev11 <jaysmth689+github@users.noreply.github.com>
Co-authored-by: Juan José Mata <juanjo.mata@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
MkDev11
2026-02-06 07:14:42 -08:00
committed by GitHub
parent c77971ea0d
commit 87117445fe
12 changed files with 189 additions and 10 deletions

View File

@@ -18,6 +18,28 @@ class ApplicationController < ActionController::Base
helper_method :demo_config, :demo_host_match?, :show_demo_warning?
private
def accept_pending_invitation_for(user)
return false if user.blank?
token = session[:pending_invitation_token]
return false if token.blank?
invitation = Invitation.pending.find_by(token: token.to_s)
return false unless invitation
return false unless invitation.accept_for(user)
session.delete(:pending_invitation_token)
true
end
def store_pending_invitation_if_valid
token = params[:invitation].to_s.presence
return if token.blank?
invitation = Invitation.pending.find_by(token: token)
session[:pending_invitation_token] = token if invitation
end
def detect_os
user_agent = request.user_agent
@os = case user_agent

View File

@@ -15,8 +15,16 @@ class InvitationsController < ApplicationController
@invitation.inviter = Current.user
if @invitation.save
InvitationMailer.invite_email(@invitation).deliver_later unless self_hosted?
flash[:notice] = t(".success")
normalized_email = @invitation.email.to_s.strip.downcase
existing_user = User.find_by(email: normalized_email)
if existing_user && @invitation.accept_for(existing_user)
flash[:notice] = t(".existing_user_added")
elsif existing_user
flash[:alert] = t(".failure")
else
InvitationMailer.invite_email(@invitation).deliver_later unless self_hosted?
flash[:notice] = t(".success")
end
else
flash[:alert] = t(".failure")
end
@@ -28,7 +36,7 @@ class InvitationsController < ApplicationController
@invitation = Invitation.find_by!(token: params[:id])
if @invitation.pending?
redirect_to new_registration_path(invitation: @invitation.token)
render :accept_choice, layout: "auth"
else
raise ActiveRecord::RecordNotFound
end

View File

@@ -32,6 +32,7 @@ class MfaController < ApplicationController
if @user&.verify_otp?(params[:code])
session.delete(:mfa_user_id)
@session = create_session_for(@user)
flash[:notice] = t("invitations.accept_choice.joined_household") if accept_pending_invitation_for(@user)
redirect_to root_path
else
flash.now[:alert] = t(".invalid_code")

View File

@@ -47,13 +47,17 @@ class OidcAccountsController < ApplicationController
# Clear pending auth from session
session.delete(:pending_oidc_auth)
# Check if user has MFA enabled
if user.otp_required?
session[:mfa_user_id] = user.id
redirect_to verify_mfa_path
else
@session = create_session_for(user)
redirect_to root_path, notice: "Account successfully linked to #{@pending_auth['provider']}"
notice = if accept_pending_invitation_for(user)
t("invitations.accept_choice.joined_household")
else
t("sessions.openid_connect.account_linked", provider: @pending_auth["provider"])
end
redirect_to root_path, notice: notice
end
else
@email = params[:email]
@@ -139,9 +143,9 @@ class OidcAccountsController < ApplicationController
# Clear pending auth from session
session.delete(:pending_oidc_auth)
# Create session and log them in
@session = create_session_for(@user)
redirect_to root_path, notice: "Welcome! Your account has been created."
notice = accept_pending_invitation_for(@user) ? t("invitations.accept_choice.joined_household") : "Welcome! Your account has been created."
redirect_to root_path, notice: notice
else
render :new_user, status: :unprocessable_entity
end

View File

@@ -10,6 +10,7 @@ class SessionsController < ApplicationController
end
def new
store_pending_invitation_if_valid
# Clear any stale mobile SSO session flag from an abandoned mobile flow
session.delete(:mobile_sso)
@@ -64,6 +65,7 @@ class SessionsController < ApplicationController
else
log_super_admin_override_login(user)
@session = create_session_for(user)
flash[:notice] = t("invitations.accept_choice.joined_household") if accept_pending_invitation_for(user)
redirect_to root_path
end
else
@@ -180,6 +182,7 @@ class SessionsController < ApplicationController
redirect_to verify_mfa_path
else
@session = create_session_for(user)
flash[:notice] = t("invitations.accept_choice.joined_household") if accept_pending_invitation_for(user)
redirect_to root_path
end
else