mitgliederverwaltung/docs/oidc-account-linking.md
2025-11-13 16:33:29 +01:00

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 user
  • oidc_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:

  1. User exists with matching oidc_id: Allow (upsert)
  2. User exists without oidc_id (password-protected OR passwordless): Raise PasswordVerificationRequired
    • The LinkOidcAccountLive will auto-link passwordless users without password prompt
    • Password-protected users must verify their password
  3. User exists with different oidc_id: Hard error (cannot link multiple OIDC providers)
  4. 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_id to 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:

  1. Detects PasswordVerificationRequired error
  2. Stores OIDC info in session
  3. Redirects to account linking page

Interactive UI for password verification and account linking.

Flow:

  1. Retrieves OIDC info from session
  2. Auto-links passwordless users immediately (no password prompt)
  3. Displays password verification form for password-protected users
  4. Verifies password using AshAuthentication
  5. Links OIDC account on success
  6. Redirects to complete OIDC login
  7. 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 with http_only and secure flags
  • lib/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 production
  • same_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.info for successful operations
    • Logger.warning for failed authentication attempts
    • Logger.error for 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

Links an OIDC ID to existing user after password verification.

Arguments:

  • oidc_id (required): OIDC sub/id from provider
  • oidc_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)

References