6.3 KiB
OIDC Account Linking Implementation
Overview
This feature implements secure account linking between password-based accounts and OIDC authentication. When a user attempts to log in via OIDC with an email that already exists as a password-only account, the system requires password verification before linking the accounts.
Architecture
Key Components
1. Security Fix: lib/accounts/user.ex
Change: The sign_in_with_rauthy action now filters by oidc_id instead of email.
read :sign_in_with_rauthy do
argument :user_info, :map, allow_nil?: false
argument :oauth_tokens, :map, allow_nil?: false
prepare AshAuthentication.Strategy.OAuth2.SignInPreparation
# SECURITY: Filter by oidc_id, NOT by email!
filter expr(oidc_id == get_path(^arg(:user_info), [:sub]))
end
Why: Prevents OIDC users from bypassing password authentication and taking over existing accounts.
2. Custom Error: lib/accounts/user/errors/password_verification_required.ex
Custom error raised when OIDC login conflicts with existing password account.
Fields:
user_id: ID of the existing useroidc_user_info: OIDC user information for account linking
3. Validation: lib/accounts/user/validations/oidc_email_collision.ex
Validates email uniqueness during OIDC registration.
Scenarios:
- User exists with matching
oidc_id: Allow (upsert) - User exists without
oidc_id(password-protected OR passwordless): RaisePasswordVerificationRequired- The
LinkOidcAccountLivewill auto-link passwordless users without password prompt - Password-protected users must verify their password
- The
- User exists with different
oidc_id: Hard error (cannot link multiple OIDC providers) - No user exists: Allow (new user creation)
4. Account Linking Action: lib/accounts/user.ex
update :link_oidc_id do
description "Links an OIDC ID to an existing user after password verification"
accept []
argument :oidc_id, :string, allow_nil?: false
argument :oidc_user_info, :map, allow_nil?: false
# ... implementation
end
Features:
- Links
oidc_idto existing user - Updates email if it differs from OIDC provider
- Syncs email changes to linked member
5. Controller: lib/mv_web/controllers/auth_controller.ex
Refactored for better complexity and maintainability.
Key improvements:
- Reduced cyclomatic complexity from 11 to below 9
- Better separation of concerns with helper functions
- Comprehensive documentation
Flow:
- Detects
PasswordVerificationRequirederror - Stores OIDC info in session
- Redirects to account linking page
6. LiveView: lib/mv_web/live/auth/link_oidc_account_live.ex
Interactive UI for password verification and account linking.
Flow:
- Retrieves OIDC info from session
- Auto-links passwordless users immediately (no password prompt)
- Displays password verification form for password-protected users
- Verifies password using AshAuthentication
- Links OIDC account on success
- Redirects to complete OIDC login
- Logs all security-relevant events (successful/failed linking attempts)
Locale Persistence
Problem: Locale was lost on logout (session cleared).
Solution: Store locale in persistent cookie (1 year TTL) with security flags.
Changes:
lib/mv_web/locale_controller.ex: Sets locale cookie withhttp_onlyandsecureflagslib/mv_web/router.ex: Reads locale from cookie if session empty
Security Features:
http_only: true- Cookie not accessible via JavaScript (XSS protection)secure: true- Cookie only transmitted over HTTPS in productionsame_site: "Lax"- CSRF protection
Security Considerations
1. OIDC ID Matching
- Before: Matched by email (vulnerable to account takeover)
- After: Matched by
oidc_id(secure)
2. Account Linking Flow
- Password verification required before linking (for password-protected users)
- Passwordless users are auto-linked immediately (secure, as they have no password)
- OIDC info stored in session (not in URL/query params)
- CSRF protection on all forms
- All linking attempts logged for audit trail
3. Email Updates
- Email updates from OIDC provider are applied during linking
- Email changes sync to linked member (if exists)
4. Error Handling
- Internal errors are logged but not exposed to users (prevents information disclosure)
- User-friendly error messages shown in UI
- Security-relevant events logged with appropriate levels:
Logger.infofor successful operationsLogger.warningfor failed authentication attemptsLogger.errorfor system errors
Usage Examples
Scenario 1: New OIDC User
# User signs in with OIDC for the first time
# → New user created with oidc_id
Scenario 2: Existing OIDC User
# User with oidc_id signs in via OIDC
# → Matched by oidc_id, email updated if changed
Scenario 3: Password User + OIDC Login
# User with password account tries OIDC login
# → PasswordVerificationRequired raised
# → Redirected to /auth/link-oidc-account
# → User enters password
# → Password verified and logged
# → oidc_id linked to account
# → Successful linking logged
# → Redirected to complete OIDC login
Scenario 4: Passwordless User + OIDC Login
# User without password (invited user) tries OIDC login
# → PasswordVerificationRequired raised
# → Redirected to /auth/link-oidc-account
# → System detects passwordless user
# → oidc_id automatically linked (no password prompt)
# → Auto-linking logged
# → Redirected to complete OIDC login
API
Custom Actions
link_oidc_id
Links an OIDC ID to existing user after password verification.
Arguments:
oidc_id(required): OIDC sub/id from provideroidc_user_info(required): Full OIDC user info map
Returns: Updated user with linked oidc_id
Side Effects:
- Updates email if different from OIDC provider
- Syncs email to linked member (if exists)