Files
clinch/docs/webauthn-passkeys-plan.md

788 lines
23 KiB
Markdown

# 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*