# WebAuthn / Passkeys Implementation Plan for Clinch ## Executive Summary This document outlines a comprehensive plan to add WebAuthn/Passkeys support to Clinch, enabling modern passwordless authentication alongside the existing password + TOTP authentication methods. ## Goals 1. **Primary Authentication**: Allow users to register and use passkeys as their primary login method (passwordless) 2. **MFA Enhancement**: Support passkeys as a second factor alongside TOTP 3. **Cross-Device Support**: Enable both platform authenticators (Face ID, Touch ID, Windows Hello) and roaming authenticators (YubiKey, security keys) 4. **User Experience**: Provide seamless registration, authentication, and management of multiple passkeys 5. **Backward Compatibility**: Maintain existing password + TOTP flows without disruption ## Architecture Overview ### Technology Stack - **webauthn gem** (~3.0): Ruby library for WebAuthn server implementation - **Rails 8.1**: Existing framework - **Browser WebAuthn API**: Native browser support (all modern browsers) ### Core Components 1. **WebAuthn Credentials Model**: Store registered authenticators 2. **WebAuthn Controller**: Handle registration and authentication ceremonies 3. **Session Flow Updates**: Integrate passkey authentication into existing login flow 4. **User Management UI**: Allow users to register, name, and delete passkeys 5. **Admin Controls**: Configure WebAuthn policies per user/group --- ## Database Schema ### New Table: `webauthn_credentials` ```ruby create_table :webauthn_credentials do |t| t.references :user, null: false, foreign_key: true, index: true # WebAuthn specification fields t.string :external_id, null: false, index: { unique: true } # credential ID (base64) t.string :public_key, null: false # public key (base64) t.integer :sign_count, null: false, default: 0 # signature counter (clone detection) # Metadata t.string :nickname # User-friendly name ("MacBook Touch ID") t.string :authenticator_type # "platform" or "cross-platform" t.boolean :backup_eligible, default: false # Can be backed up (passkey sync) t.boolean :backup_state, default: false # Currently backed up # Tracking t.datetime :last_used_at t.string :last_used_ip t.string :user_agent # Browser/OS info timestamps end add_index :webauthn_credentials, [:user_id, :external_id], unique: true ``` ### Update `users` table ```ruby add_column :users, :webauthn_required, :boolean, default: false, null: false add_column :users, :webauthn_id, :string # WebAuthn user handle (random, stable, opaque) add_index :users, :webauthn_id, unique: true ``` --- ## Implementation Phases ### Phase 1: Foundation (Core WebAuthn Support) **Objective**: Enable basic passkey registration and authentication #### 1.1 Setup & Dependencies - [ ] Add `webauthn` gem to Gemfile (~3.0) - [ ] Create WebAuthn initializer with configuration - [ ] Generate migration for `webauthn_credentials` table - [ ] Add WebAuthn user handle generation to User model #### 1.2 Models **File**: `app/models/webauthn_credential.rb` ```ruby class WebauthnCredential < ApplicationRecord belongs_to :user validates :external_id, presence: true, uniqueness: true validates :public_key, presence: true validates :sign_count, presence: true, numericality: { greater_than_or_equal_to: 0 } scope :active, -> { where(revoked_at: nil) } scope :platform_authenticators, -> { where(authenticator_type: "platform") } scope :roaming_authenticators, -> { where(authenticator_type: "cross-platform") } # Update last used timestamp and sign count after successful authentication def update_usage!(sign_count:, ip_address: nil) update!( last_used_at: Time.current, last_used_ip: ip_address, sign_count: sign_count ) end end ``` **Update**: `app/models/user.rb` ```ruby has_many :webauthn_credentials, dependent: :destroy # Generate stable WebAuthn user handle on first use def webauthn_user_handle return webauthn_id if webauthn_id.present? # Generate random 64-byte opaque identifier (base64url encoded) handle = SecureRandom.urlsafe_base64(64) update_column(:webauthn_id, handle) handle end def webauthn_enabled? webauthn_credentials.active.exists? end def can_authenticate_with_webauthn? webauthn_enabled? && active? end ``` #### 1.3 WebAuthn Configuration **File**: `config/initializers/webauthn.rb` ```ruby WebAuthn.configure do |config| # Relying Party name (displayed in authenticator) config.origin = ENV.fetch("CLINCH_HOST", "http://localhost:3000") # Relying Party ID (must match origin domain) config.rp_name = "Clinch Identity Provider" # Credential timeout (60 seconds) config.credential_options_timeout = 60_000 # Supported algorithms (ES256, RS256) config.algorithms = ["ES256", "RS256"] end ``` #### 1.4 Registration Flow (Ceremony) **File**: `app/controllers/webauthn_controller.rb` Key actions: - `GET /webauthn/new` - Display registration page - `POST /webauthn/challenge` - Generate registration challenge - `POST /webauthn/create` - Verify and store credential **Registration Process**: 1. User clicks "Add Passkey" in profile settings 2. Server generates challenge options (stored in session) 3. Browser calls `navigator.credentials.create()` 4. User authenticates with device (Touch ID, Face ID, etc.) 5. Browser returns signed credential 6. Server verifies signature and stores credential #### 1.5 Authentication Flow (Ceremony) **Update**: `app/controllers/sessions_controller.rb` New actions: - `POST /sessions/webauthn/challenge` - Generate authentication challenge - `POST /sessions/webauthn/verify` - Verify credential and sign in **Authentication Process**: 1. User clicks "Sign in with Passkey" on login page 2. Server generates challenge (stored in session) 3. Browser calls `navigator.credentials.get()` 4. User authenticates with device 5. Browser returns signed assertion 6. Server verifies signature, checks sign count, creates session #### 1.6 Frontend JavaScript **File**: `app/javascript/controllers/webauthn_controller.js` (Stimulus) Responsibilities: - Encode/decode base64url data for WebAuthn API - Handle browser WebAuthn API calls - Error handling and user feedback - Progressive enhancement (feature detection) **Example registration**: ```javascript async register() { const options = await this.fetchChallenge() const credential = await navigator.credentials.create(options) await this.submitCredential(credential) } ``` --- ### Phase 2: User Experience & Management **Objective**: Provide intuitive UI for managing passkeys #### 2.1 Profile Management **File**: `app/views/profiles/show.html.erb` (update) Features: - List all registered passkeys with nicknames - Show last used timestamp - Badge for platform vs roaming authenticators - Add new passkey button - Delete passkey button (with confirmation) - Show "synced passkey" badge if backup_state is true #### 2.2 Registration Improvements - Auto-detect device type and suggest nickname ("Chrome on MacBook") - Show preview of what authenticator will display - Require at least one authentication method (password OR passkey) - Warning if removing last authentication method #### 2.3 Login Page Updates **File**: `app/views/sessions/new.html.erb` (update) - Add "Sign in with Passkey" button (conditional rendering) - Show button only if WebAuthn is supported by browser - Progressive enhancement: fallback to password if WebAuthn fails - Email field for identifying which user's passkeys to request **Flow**: 1. User enters email address 2. Server checks if user has passkeys 3. If yes, show "Continue with Passkey" button 4. If no passkeys, show password field #### 2.4 First-Run Wizard Update **File**: `app/views/users/new.html.erb` (first-run wizard) - Option to register passkey immediately after creating account - Skip passkey registration if not supported or user declines - Encourage passkey setup but don't require it --- ### Phase 3: Security & Advanced Features **Objective**: Harden security and add enterprise features #### 3.1 Sign Count Verification **Purpose**: Detect cloned authenticators Implementation: - Store sign_count after each authentication - Verify new sign_count > old sign_count - If count doesn't increase: log warning, optionally disable credential - Add admin alert for suspicious activity #### 3.2 Attestation Validation (Optional) **Purpose**: Verify authenticator is genuine hardware Options: - None (most compatible, recommended for self-hosted) - Indirect (some validation) - Direct (strict validation, enterprise) **Configuration** (per-application): ```ruby class Application < ApplicationRecord enum webauthn_attestation: { none: 0, indirect: 1, direct: 2 }, _default: :none end ``` #### 3.3 User Verification Requirements **Levels**: - `discouraged`: No user verification (not recommended) - `preferred`: Request if available (default) - `required`: Must have PIN/biometric (high security apps) **Configuration**: Per-application setting #### 3.4 Resident Keys (Discoverable Credentials) **Feature**: Passkey contains username, no email entry needed **Implementation**: - Set `residentKey: "preferred"` or `"required"` in credential options - Allow users to sign in without entering email first - Add `POST /sessions/webauthn/discoverable` endpoint **Benefits**: - Faster login (no email typing) - Better UX on mobile devices - Works with password managers (1Password, etc.) #### 3.5 Admin Controls **File**: `app/views/admin/users/edit.html.erb` Admin capabilities: - View all user passkeys - Revoke compromised passkeys - Require WebAuthn for specific users/groups - View WebAuthn authentication audit log - Configure WebAuthn policies **New fields**: ```ruby # On User model webauthn_required: boolean # Must have at least one passkey # On Group model webauthn_enforcement: enum # :none, :encouraged, :required ``` --- ### Phase 4: Integration with Existing Flows **Objective**: Seamlessly integrate with OIDC, ForwardAuth, and 2FA #### 4.1 OIDC Authorization Flow **Update**: `app/controllers/oidc_controller.rb` Integration points: - If user has no password but has passkey, trigger WebAuthn - Application can request `webauthn` in `acr_values` parameter - Include `amr` claim in ID token: `["webauthn"]` or `["pwd", "totp"]` **Example ID token**: ```json { "sub": "user-123", "email": "user@example.com", "amr": ["webauthn"], // Authentication Methods References "acr": "urn:mace:incommon:iap:silver" } ``` #### 4.2 WebAuthn as Second Factor **Scenario**: User signs in with password, then WebAuthn as 2FA **Flow**: 1. User enters password (first factor) 2. If `webauthn_required` is true OR user chooses WebAuthn 3. Trigger WebAuthn challenge (instead of TOTP) 4. User authenticates with passkey 5. Create session **Configuration**: ```ruby # User can choose 2FA method user.preferred_2fa # :totp or :webauthn # Admin can require specific 2FA method user.required_2fa # :any, :totp, :webauthn ``` #### 4.3 ForwardAuth Integration **Update**: `app/controllers/api/forward_auth_controller.rb` No changes needed! WebAuthn creates standard sessions, ForwardAuth works as-is. **Header injection**: ``` Remote-User: user@example.com Remote-Groups: admin,family Remote-Auth-Method: webauthn # NEW optional header ``` #### 4.4 Backup Codes **Consideration**: What if user loses all passkeys? **Options**: 1. Keep existing backup codes system (works for TOTP, not WebAuthn-only) 2. Require email verification for account recovery 3. Require at least one roaming authenticator (YubiKey) + platform authenticator **Recommended**: Require password OR email-verified recovery flow --- ### Phase 5: Testing & Documentation **Objective**: Ensure reliability and provide clear documentation #### 5.1 Automated Tests **Test Coverage**: 1. **Model tests** (`test/models/webauthn_credential_test.rb`) - Credential creation and validation - Sign count updates - Credential scopes and queries 2. **Controller tests** (`test/controllers/webauthn_controller_test.rb`) - Registration challenge generation - Credential verification - Authentication challenge generation - Assertion verification 3. **Integration tests** (`test/integration/webauthn_authentication_test.rb`) - Full registration flow - Full authentication flow - Error handling (invalid signatures, expired challenges) 4. **System tests** (`test/system/webauthn_test.rb`) - End-to-end browser testing with virtual authenticator - Chrome DevTools Protocol virtual authenticator **Example virtual authenticator test**: ```ruby test "user registers passkey" do driver.add_virtual_authenticator(protocol: :ctap2) visit profile_path click_on "Add Passkey" fill_in "Nickname", with: "Test Key" click_on "Register" assert_text "Passkey registered successfully" end ``` #### 5.2 Documentation **Files to create/update**: 1. **User Guide** (`docs/webauthn-user-guide.md`) - What are passkeys? - How to register a passkey - How to sign in with a passkey - Managing multiple passkeys - Troubleshooting 2. **Admin Guide** (`docs/webauthn-admin-guide.md`) - WebAuthn policies and configuration - Enforcing passkeys for users/groups - Security considerations - Audit logging 3. **Developer Guide** (`docs/webauthn-developer-guide.md`) - Architecture overview - WebAuthn ceremony flows - Testing with virtual authenticators - OIDC integration details 4. **README Update** (`README.md`) - Add WebAuthn/Passkeys to Authentication Methods section - Update feature list #### 5.3 Browser Compatibility **Supported Browsers**: - Chrome/Edge 90+ (Chromium) - Firefox 90+ - Safari 14+ (macOS Big Sur, iOS 14) **Graceful Degradation**: - Feature detection: check `window.PublicKeyCredential` - Hide passkey UI if not supported - Always provide password fallback --- ## Security Considerations ### 1. Challenge Storage - Store challenges in server-side session (not cookies) - Challenges expire after 60 seconds - One-time use (mark as used after verification) ### 2. Origin Validation - WebAuthn library automatically validates origin - Ensure `CLINCH_HOST` environment variable is correct - Must use HTTPS in production (required by WebAuthn spec) ### 3. Relying Party ID - Must match the origin domain - Cannot be changed after credentials are registered - Use apex domain for subdomain compatibility (e.g., `example.com` works for `auth.example.com` and `app.example.com`) ### 4. User Handle Privacy - User handle is opaque, random, and stable - Never use email or user ID as user handle - Store in `users.webauthn_id` column ### 5. Sign Count Verification - Always check sign_count increases - Log suspicious activity (counter didn't increase) - Consider disabling credential if counter resets ### 6. Credential Backup Awareness - Track `backup_eligible` and `backup_state` flags - Inform users about synced passkeys - Higher security apps may want to disallow backed-up credentials ### 7. Account Recovery - Don't lock users out if they lose all passkeys - Require email verification for recovery - Send alerts when recovery is used --- ## Migration Strategy ### For Existing Users **Option 1: Opt-in (Recommended)** - Add "Register Passkey" button in profile settings - Show banner encouraging passkey setup - Don't require passkeys initially - Gradually increase adoption through UI prompts **Option 2: Mandatory Migration** - Set deadline for passkey registration - Email users with instructions - Admins can enforce passkey requirement per group - Provide support documentation ### For New Users **During First-Run Wizard**: 1. Create account with email + password (existing flow) 2. Offer optional passkey registration 3. If accepted, walk through registration 4. If declined, remind later in dashboard --- ## Performance Considerations ### Database Indexes ```ruby # Essential indexes for performance add_index :webauthn_credentials, :user_id add_index :webauthn_credentials, :external_id, unique: true add_index :webauthn_credentials, [:user_id, :last_used_at] ``` ### Query Optimization - Eager load credentials with user: `User.includes(:webauthn_credentials)` - Cache credential count: `user.webauthn_credentials.count` ### Cleanup Jobs - Remove expired challenges from session store - Archive old credentials (last_used > 1 year ago) --- ## Rollout Plan ### Phase 1: Development (Week 1-2) - [ ] Setup gem and database schema - [ ] Implement registration ceremony - [ ] Implement authentication ceremony - [ ] Add basic UI components ### Phase 2: Testing (Week 2-3) - [ ] Write unit and integration tests - [ ] Test with virtual authenticators - [ ] Test on real devices (iOS, Android, Windows, macOS) - [ ] Security audit ### Phase 3: Beta (Week 3-4) - [ ] Deploy to staging environment - [ ] Enable for admin users only - [ ] Gather feedback - [ ] Fix bugs and UX issues ### Phase 4: Production (Week 4-5) - [ ] Deploy to production - [ ] Enable for all users (opt-in) - [ ] Monitor error rates and adoption - [ ] Document and share user guides ### Phase 5: Enforcement (Week 6+) - [ ] Analyze adoption metrics - [ ] Consider enforcement for high-security groups - [ ] Continuous improvement based on feedback --- ## Open Questions & Decisions Needed 1. **Attestation Level**: Should we validate authenticator attestation? (Recommendation: No for v1) 2. **Resident Key Strategy**: Require resident keys (discoverable credentials)? (Recommendation: Preferred, not required) 3. **Backup Credential Policy**: Allow synced passkeys (iCloud Keychain, Google Password Manager)? (Recommendation: Yes, allow) 4. **Account Recovery**: How should users recover if they lose all passkeys? (Recommendation: Email verification + temporary password) 5. **2FA Replacement**: Should WebAuthn replace TOTP for 2FA? (Recommendation: Offer both, user choice) 6. **Enforcement Timeline**: When should we require passkeys for admins? (Recommendation: 3 months after launch) 7. **Cross-Platform Keys**: Encourage users to register both platform and roaming authenticators? (Recommendation: Yes, show prompt) 8. **Audit Logging**: Log all WebAuthn events? (Recommendation: Yes, use Rails ActiveSupport::Notifications) --- ## Dependencies ### Ruby Gems - `webauthn` (~> 3.0) - WebAuthn server library - `base64` (stdlib) - Encoding/decoding credentials ### JavaScript Libraries - Native WebAuthn API (no libraries needed) - Stimulus controller for UX ### Browser Requirements - WebAuthn API support - HTTPS (required in production) - Modern browser (Chrome 90+, Firefox 90+, Safari 14+) --- ## Success Metrics ### Adoption Metrics - % of users with at least one passkey registered - % of logins using passkey vs password - Time to register passkey (UX metric) ### Security Metrics - Reduction in password reset requests - Reduction in account takeover attempts - Phishing resistance (passkeys can't be phished) ### Performance Metrics - Average authentication time (should be faster) - Error rate during registration/authentication - Browser compatibility issues --- ## Future Enhancements ### Post-Launch Improvements 1. **Conditional UI**: Show passkey option only if user has credentials for that device 2. **Cross-Device Flow**: QR code to authenticate on one device, complete login on another 3. **Passkey Sync Status**: Show which passkeys are synced vs device-only 4. **Authenticator Icons**: Display icons for known authenticators (YubiKey, etc.) 5. **Security Key Attestation**: Verify hardware security keys for high-security apps 6. **Multi-Device Registration**: Easy workflow to register passkey on multiple devices 7. **Admin Analytics**: Dashboard showing WebAuthn adoption and usage stats 8. **FIDO2 Compliance**: Full FIDO2 conformance certification --- ## References ### Specifications - [W3C WebAuthn Level 2](https://www.w3.org/TR/webauthn-2/) - [FIDO2 Overview](https://fidoalliance.org/fido2/) - [WebAuthn Guide](https://webauthn.guide/) ### Ruby Libraries - [webauthn-ruby gem](https://github.com/cedarcode/webauthn-ruby) - [webauthn-ruby documentation](https://github.com/cedarcode/webauthn-ruby#usage) ### Browser APIs - [MDN: Web Authentication API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Authentication_API) - [Chrome: WebAuthn](https://developer.chrome.com/docs/devtools/webauthn/) ### Best Practices - [FIDO2 Server Best Practices](https://fidoalliance.org/specifications/) - [WebAuthn Awesome List](https://github.com/herrjemand/awesome-webauthn) --- ## Appendix A: File Changes Summary ### New Files - `app/models/webauthn_credential.rb` - `app/controllers/webauthn_controller.rb` - `app/javascript/controllers/webauthn_controller.js` - `app/views/webauthn/new.html.erb` - `app/views/webauthn/show.html.erb` - `config/initializers/webauthn.rb` - `db/migrate/YYYYMMDD_create_webauthn_credentials.rb` - `db/migrate/YYYYMMDD_add_webauthn_to_users.rb` - `test/models/webauthn_credential_test.rb` - `test/controllers/webauthn_controller_test.rb` - `test/integration/webauthn_authentication_test.rb` - `test/system/webauthn_test.rb` - `docs/webauthn-user-guide.md` - `docs/webauthn-admin-guide.md` - `docs/webauthn-developer-guide.md` ### Modified Files - `Gemfile` - Add webauthn gem - `app/models/user.rb` - Add webauthn associations and methods - `app/controllers/sessions_controller.rb` - Add webauthn authentication - `app/views/sessions/new.html.erb` - Add "Sign in with Passkey" button - `app/views/profiles/show.html.erb` - Add passkey management section - `app/controllers/oidc_controller.rb` - Add AMR claim support - `config/routes.rb` - Add webauthn routes - `README.md` - Document WebAuthn feature ### Database Migrations 1. Create `webauthn_credentials` table 2. Add `webauthn_id` and `webauthn_required` to `users` table --- ## Appendix B: Example User Flows ### Flow 1: Register First Passkey 1. User logs in with password 2. Sees banner: "Secure your account with a passkey" 3. Clicks "Set up passkey" 4. Browser prompts: "Save a passkey for auth.example.com?" 5. User authenticates with Touch ID 6. Success message: "Passkey registered as 'MacBook Touch ID'" ### Flow 2: Sign In with Passkey 1. User visits login page 2. Enters email address 3. Clicks "Continue with Passkey" 4. Browser prompts: "Sign in to auth.example.com with your passkey?" 5. User authenticates with Touch ID 6. Immediately signed in, redirected to dashboard ### Flow 3: WebAuthn as 2FA 1. User enters password (first factor) 2. Instead of TOTP, prompted for passkey 3. User authenticates with Face ID 4. Signed in successfully ### Flow 4: Cross-Device Authentication 1. User on desktop enters email 2. Clicks "Use passkey from phone" 3. QR code displayed 4. User scans with phone, authenticates 5. Desktop session created --- ## Conclusion This plan provides a comprehensive roadmap for adding WebAuthn/Passkeys to Clinch. The phased approach allows for iterative development, testing, and rollout while maintaining backward compatibility with existing authentication methods. **Key Benefits**: - Enhanced security (phishing-resistant) - Better UX (faster, no passwords to remember) - Modern authentication standard (FIDO2) - Cross-platform support (iOS, Android, Windows, macOS) - Synced passkeys (iCloud, Google Password Manager) **Estimated Timeline**: 4-6 weeks for full implementation and testing. **Next Steps**: 1. Review and approve this plan 2. Create GitHub issues for each phase 3. Begin Phase 1 implementation 4. Set up development environment for testing --- *Document Version: 1.0* *Last Updated: 2025-10-26* *Author: Claude (Anthropic)* *Status: Awaiting Review*