# 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`. ```elixir 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` ```elixir 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 #### 6. LiveView: `lib/mv_web/live/auth/link_oidc_account_live.ex` 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 ```elixir # User signs in with OIDC for the first time # → New user created with oidc_id ``` ### Scenario 2: Existing OIDC User ```elixir # User with oidc_id signs in via OIDC # → Matched by oidc_id, email updated if changed ``` ### Scenario 3: Password User + OIDC Login ```elixir # 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 ```elixir # 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 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 - [AshAuthentication Documentation](https://hexdocs.pm/ash_authentication) - [OIDC Specification](https://openid.net/specs/openid-connect-core-1_0.html) - [Security Best Practices for Account Linking](https://cheatsheetseries.owasp.org/cheatsheets/Credential_Stuffing_Prevention_Cheat_Sheet.html)