23 KiB
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
- Primary Authentication: Allow users to register and use passkeys as their primary login method (passwordless)
- MFA Enhancement: Support passkeys as a second factor alongside TOTP
- Cross-Device Support: Enable both platform authenticators (Face ID, Touch ID, Windows Hello) and roaming authenticators (YubiKey, security keys)
- User Experience: Provide seamless registration, authentication, and management of multiple passkeys
- 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
- WebAuthn Credentials Model: Store registered authenticators
- WebAuthn Controller: Handle registration and authentication ceremonies
- Session Flow Updates: Integrate passkey authentication into existing login flow
- User Management UI: Allow users to register, name, and delete passkeys
- Admin Controls: Configure WebAuthn policies per user/group
Database Schema
New Table: webauthn_credentials
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
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
webauthngem to Gemfile (~3.0) - Create WebAuthn initializer with configuration
- Generate migration for
webauthn_credentialstable - Add WebAuthn user handle generation to User model
1.2 Models
File: app/models/webauthn_credential.rb
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
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
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 pagePOST /webauthn/challenge- Generate registration challengePOST /webauthn/create- Verify and store credential
Registration Process:
- User clicks "Add Passkey" in profile settings
- Server generates challenge options (stored in session)
- Browser calls
navigator.credentials.create() - User authenticates with device (Touch ID, Face ID, etc.)
- Browser returns signed credential
- 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 challengePOST /sessions/webauthn/verify- Verify credential and sign in
Authentication Process:
- User clicks "Sign in with Passkey" on login page
- Server generates challenge (stored in session)
- Browser calls
navigator.credentials.get() - User authenticates with device
- Browser returns signed assertion
- 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:
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:
- User enters email address
- Server checks if user has passkeys
- If yes, show "Continue with Passkey" button
- 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):
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/discoverableendpoint
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:
# 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
webauthninacr_valuesparameter - Include
amrclaim in ID token:["webauthn"]or["pwd", "totp"]
Example ID token:
{
"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:
- User enters password (first factor)
- If
webauthn_requiredis true OR user chooses WebAuthn - Trigger WebAuthn challenge (instead of TOTP)
- User authenticates with passkey
- Create session
Configuration:
# 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:
- Keep existing backup codes system (works for TOTP, not WebAuthn-only)
- Require email verification for account recovery
- 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:
-
Model tests (
test/models/webauthn_credential_test.rb)- Credential creation and validation
- Sign count updates
- Credential scopes and queries
-
Controller tests (
test/controllers/webauthn_controller_test.rb)- Registration challenge generation
- Credential verification
- Authentication challenge generation
- Assertion verification
-
Integration tests (
test/integration/webauthn_authentication_test.rb)- Full registration flow
- Full authentication flow
- Error handling (invalid signatures, expired challenges)
-
System tests (
test/system/webauthn_test.rb)- End-to-end browser testing with virtual authenticator
- Chrome DevTools Protocol virtual authenticator
Example virtual authenticator test:
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:
-
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
-
Admin Guide (
docs/webauthn-admin-guide.md)- WebAuthn policies and configuration
- Enforcing passkeys for users/groups
- Security considerations
- Audit logging
-
Developer Guide (
docs/webauthn-developer-guide.md)- Architecture overview
- WebAuthn ceremony flows
- Testing with virtual authenticators
- OIDC integration details
-
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_HOSTenvironment 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.comworks forauth.example.comandapp.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_idcolumn
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_eligibleandbackup_stateflags - 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:
- Create account with email + password (existing flow)
- Offer optional passkey registration
- If accepted, walk through registration
- If declined, remind later in dashboard
Performance Considerations
Database Indexes
# 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
-
Attestation Level: Should we validate authenticator attestation? (Recommendation: No for v1)
-
Resident Key Strategy: Require resident keys (discoverable credentials)? (Recommendation: Preferred, not required)
-
Backup Credential Policy: Allow synced passkeys (iCloud Keychain, Google Password Manager)? (Recommendation: Yes, allow)
-
Account Recovery: How should users recover if they lose all passkeys? (Recommendation: Email verification + temporary password)
-
2FA Replacement: Should WebAuthn replace TOTP for 2FA? (Recommendation: Offer both, user choice)
-
Enforcement Timeline: When should we require passkeys for admins? (Recommendation: 3 months after launch)
-
Cross-Platform Keys: Encourage users to register both platform and roaming authenticators? (Recommendation: Yes, show prompt)
-
Audit Logging: Log all WebAuthn events? (Recommendation: Yes, use Rails ActiveSupport::Notifications)
Dependencies
Ruby Gems
webauthn(~> 3.0) - WebAuthn server librarybase64(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
- Conditional UI: Show passkey option only if user has credentials for that device
- Cross-Device Flow: QR code to authenticate on one device, complete login on another
- Passkey Sync Status: Show which passkeys are synced vs device-only
- Authenticator Icons: Display icons for known authenticators (YubiKey, etc.)
- Security Key Attestation: Verify hardware security keys for high-security apps
- Multi-Device Registration: Easy workflow to register passkey on multiple devices
- Admin Analytics: Dashboard showing WebAuthn adoption and usage stats
- FIDO2 Compliance: Full FIDO2 conformance certification
References
Specifications
Ruby Libraries
Browser APIs
Best Practices
Appendix A: File Changes Summary
New Files
app/models/webauthn_credential.rbapp/controllers/webauthn_controller.rbapp/javascript/controllers/webauthn_controller.jsapp/views/webauthn/new.html.erbapp/views/webauthn/show.html.erbconfig/initializers/webauthn.rbdb/migrate/YYYYMMDD_create_webauthn_credentials.rbdb/migrate/YYYYMMDD_add_webauthn_to_users.rbtest/models/webauthn_credential_test.rbtest/controllers/webauthn_controller_test.rbtest/integration/webauthn_authentication_test.rbtest/system/webauthn_test.rbdocs/webauthn-user-guide.mddocs/webauthn-admin-guide.mddocs/webauthn-developer-guide.md
Modified Files
Gemfile- Add webauthn gemapp/models/user.rb- Add webauthn associations and methodsapp/controllers/sessions_controller.rb- Add webauthn authenticationapp/views/sessions/new.html.erb- Add "Sign in with Passkey" buttonapp/views/profiles/show.html.erb- Add passkey management sectionapp/controllers/oidc_controller.rb- Add AMR claim supportconfig/routes.rb- Add webauthn routesREADME.md- Document WebAuthn feature
Database Migrations
- Create
webauthn_credentialstable - Add
webauthn_idandwebauthn_requiredtouserstable
Appendix B: Example User Flows
Flow 1: Register First Passkey
- User logs in with password
- Sees banner: "Secure your account with a passkey"
- Clicks "Set up passkey"
- Browser prompts: "Save a passkey for auth.example.com?"
- User authenticates with Touch ID
- Success message: "Passkey registered as 'MacBook Touch ID'"
Flow 2: Sign In with Passkey
- User visits login page
- Enters email address
- Clicks "Continue with Passkey"
- Browser prompts: "Sign in to auth.example.com with your passkey?"
- User authenticates with Touch ID
- Immediately signed in, redirected to dashboard
Flow 3: WebAuthn as 2FA
- User enters password (first factor)
- Instead of TOTP, prompted for passkey
- User authenticates with Face ID
- Signed in successfully
Flow 4: Cross-Device Authentication
- User on desktop enters email
- Clicks "Use passkey from phone"
- QR code displayed
- User scans with phone, authenticates
- 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:
- Review and approve this plan
- Create GitHub issues for each phase
- Begin Phase 1 implementation
- Set up development environment for testing
Document Version: 1.0 Last Updated: 2025-10-26 Author: Claude (Anthropic) Status: Awaiting Review