From c03034c49f9f7254955307c91ebe40939c76a670 Mon Sep 17 00:00:00 2001 From: Dan Milne Date: Thu, 1 Jan 2026 13:18:30 +1100 Subject: [PATCH] Add files to support brakeman and standardrb. Fix some SRB warnings --- .github/workflows/ci.yml | 14 +- .standard.yml | 7 + Gemfile | 9 +- Gemfile.lock | 24 +- app/channels/application_cable/connection.rb | 9 +- app/controllers/passwords_controller.rb | 17 +- bin/brakeman | 2 - docs/README_RODAUTH_ANALYSIS.md | 275 ++++++ docs/RODAUTH_DECISION_GUIDE.md | 426 +++++++++ docs/caddy-example.md | 176 ++++ docs/forward-auth-testing.md | 227 +++++ docs/oidc-refresh-tokens-client-guide.md | 611 +++++++++++++ docs/rodauth-oauth-analysis.md | 913 +++++++++++++++++++ docs/rodauth-oauth-quick-reference.md | 418 +++++++++ docs/traefik-example.md | 330 +++++++ docs/webauthn-implementation-summary.md | 238 +++++ docs/webauthn-passkeys-plan.md | 787 ++++++++++++++++ 17 files changed, 4440 insertions(+), 43 deletions(-) create mode 100644 .standard.yml create mode 100644 docs/README_RODAUTH_ANALYSIS.md create mode 100644 docs/RODAUTH_DECISION_GUIDE.md create mode 100644 docs/caddy-example.md create mode 100644 docs/forward-auth-testing.md create mode 100644 docs/oidc-refresh-tokens-client-guide.md create mode 100644 docs/rodauth-oauth-analysis.md create mode 100644 docs/rodauth-oauth-quick-reference.md create mode 100644 docs/traefik-example.md create mode 100644 docs/webauthn-implementation-summary.md create mode 100644 docs/webauthn-passkeys-plan.md diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9d674aa..65bbe77 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -41,8 +41,6 @@ jobs: lint: runs-on: ubuntu-latest - env: - RUBOCOP_CACHE_ROOT: tmp/rubocop steps: - name: Checkout code uses: actions/checkout@v5 @@ -52,18 +50,8 @@ jobs: with: bundler-cache: true - - name: Prepare RuboCop cache - uses: actions/cache@v4 - env: - DEPENDENCIES_HASH: ${{ hashFiles('.ruby-version', '**/.rubocop.yml', '**/.rubocop_todo.yml', 'Gemfile.lock') }} - with: - path: ${{ env.RUBOCOP_CACHE_ROOT }} - key: rubocop-${{ runner.os }}-${{ env.DEPENDENCIES_HASH }}-${{ github.ref_name == github.event.repository.default_branch && github.run_id || 'default' }} - restore-keys: | - rubocop-${{ runner.os }}-${{ env.DEPENDENCIES_HASH }}- - - name: Lint code for consistent style - run: bin/rubocop -f github + run: bin/standardrb test: runs-on: ubuntu-latest diff --git a/.standard.yml b/.standard.yml new file mode 100644 index 0000000..90ec6a2 --- /dev/null +++ b/.standard.yml @@ -0,0 +1,7 @@ +ignore: + - 'test_*.rb' # Ignore test files in root directory + - 'tmp/**/*' + - 'vendor/**/*' + - 'node_modules/**/*' + - 'config/initializers/csp_local_logger.rb' # Complex CSP logger with intentional block structure + - 'config/initializers/sentry_subscriber.rb' # Sentry subscriber with module structure diff --git a/Gemfile b/Gemfile index 7754276..9154e09 100644 --- a/Gemfile +++ b/Gemfile @@ -42,7 +42,7 @@ gem "sentry-ruby", "~> 6.2" gem "sentry-rails", "~> 6.2" # Windows does not include zoneinfo files, so bundle the tzinfo-data gem -gem "tzinfo-data", platforms: %i[ windows jruby ] +gem "tzinfo-data", platforms: %i[windows jruby] # Use the database-backed adapters for Rails.cache and Action Cable gem "solid_cache" @@ -63,7 +63,7 @@ gem "image_processing", "~> 1.2" group :development, :test do # See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem - gem "debug", platforms: %i[ mri windows ], require: "debug/prelude" + gem "debug", platforms: %i[mri windows], require: "debug/prelude" # Audits gems for known security defects (use config/bundler-audit.yml to ignore issues) gem "bundler-audit", require: false @@ -71,8 +71,8 @@ group :development, :test do # Static analysis for security vulnerabilities [https://brakemanscanner.org/] gem "brakeman", require: false - # Omakase Ruby styling [https://github.com/rails/rubocop-rails-omakase/] - gem "rubocop-rails-omakase", require: false + # Standard Ruby style guide, linter, and formatter [https://github.com/standardrb/standard] + gem "standard", require: false end group :development do @@ -91,4 +91,3 @@ group :test do # Code coverage analysis gem "simplecov", require: false end - diff --git a/Gemfile.lock b/Gemfile.lock index 2710ae6..809523d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -316,16 +316,6 @@ GEM lint_roller (~> 1.1) rubocop (>= 1.75.0, < 2.0) rubocop-ast (>= 1.47.1, < 2.0) - rubocop-rails (2.34.2) - activesupport (>= 4.2.0) - lint_roller (~> 1.1) - rack (>= 1.1) - rubocop (>= 1.75.0, < 2.0) - rubocop-ast (>= 1.44.0, < 2.0) - rubocop-rails-omakase (1.1.0) - rubocop (>= 1.72) - rubocop-performance (>= 1.24) - rubocop-rails (>= 2.30) ruby-progressbar (1.13.0) ruby-vips (2.2.5) ffi (~> 1.12) @@ -382,6 +372,18 @@ GEM net-sftp (>= 2.1.2) net-ssh (>= 2.8.0) ostruct + standard (1.52.0) + language_server-protocol (~> 3.17.0.2) + lint_roller (~> 1.0) + rubocop (~> 1.81.7) + standard-custom (~> 1.0.0) + standard-performance (~> 1.8) + standard-custom (1.0.2) + lint_roller (~> 1.0) + rubocop (~> 1.50) + standard-performance (1.9.0) + lint_roller (~> 1.1) + rubocop-performance (~> 1.26.0) stimulus-rails (1.3.4) railties (>= 6.0.0) stringio (3.1.8) @@ -467,7 +469,6 @@ DEPENDENCIES rails (~> 8.1.1) rotp (~> 6.3) rqrcode (~> 3.1) - rubocop-rails-omakase selenium-webdriver sentry-rails (~> 6.2) sentry-ruby (~> 6.2) @@ -476,6 +477,7 @@ DEPENDENCIES solid_cache solid_queue (~> 1.2) sqlite3 (>= 2.1) + standard stimulus-rails tailwindcss-rails thruster diff --git a/app/channels/application_cable/connection.rb b/app/channels/application_cable/connection.rb index 4264c74..1ba5d8a 100644 --- a/app/channels/application_cable/connection.rb +++ b/app/channels/application_cable/connection.rb @@ -7,10 +7,11 @@ module ApplicationCable end private - def set_current_user - if session = Session.find_by(id: cookies.signed[:session_id]) - self.current_user = session.user - end + + def set_current_user + if (session = Session.find_by(id: cookies.signed[:session_id])) + self.current_user = session.user end + end end end diff --git a/app/controllers/passwords_controller.rb b/app/controllers/passwords_controller.rb index b0f2df5..d253208 100644 --- a/app/controllers/passwords_controller.rb +++ b/app/controllers/passwords_controller.rb @@ -1,13 +1,13 @@ class PasswordsController < ApplicationController allow_unauthenticated_access - before_action :set_user_by_token, only: %i[ edit update ] + before_action :set_user_by_token, only: %i[edit update] rate_limit to: 10, within: 3.minutes, only: :create, with: -> { redirect_to new_password_path, alert: "Try again later." } def new end def create - if user = User.find_by(email_address: params[:email_address]) + if (user = User.find_by(email_address: params[:email_address])) PasswordsMailer.reset(user).deliver_later end @@ -27,10 +27,11 @@ class PasswordsController < ApplicationController end private - def set_user_by_token - @user = User.find_by_token_for(:password_reset, params[:token]) - redirect_to new_password_path, alert: "Password reset link is invalid or has expired." if @user.nil? - rescue ActiveSupport::MessageVerifier::InvalidSignature - redirect_to new_password_path, alert: "Password reset link is invalid or has expired." - end + + def set_user_by_token + @user = User.find_by_token_for(:password_reset, params[:token]) + redirect_to new_password_path, alert: "Password reset link is invalid or has expired." if @user.nil? + rescue ActiveSupport::MessageVerifier::InvalidSignature + redirect_to new_password_path, alert: "Password reset link is invalid or has expired." + end end diff --git a/bin/brakeman b/bin/brakeman index ace1c9b..171ac12 100755 --- a/bin/brakeman +++ b/bin/brakeman @@ -2,6 +2,4 @@ require "rubygems" require "bundler/setup" -ARGV.unshift("--ensure-latest") - load Gem.bin_path("brakeman", "brakeman") diff --git a/docs/README_RODAUTH_ANALYSIS.md b/docs/README_RODAUTH_ANALYSIS.md new file mode 100644 index 0000000..e041667 --- /dev/null +++ b/docs/README_RODAUTH_ANALYSIS.md @@ -0,0 +1,275 @@ +# Rodauth-OAuth Analysis Documents + +This directory contains a comprehensive analysis of rodauth-oauth and how it compares to your custom OIDC implementation in Clinch. + +## Start Here + +### 1. **RODAUTH_DECISION_GUIDE.md** (15-minute read) +**Purpose:** Help you make a decision about your OAuth/OIDC implementation + +**Contains:** +- TL;DR of three options +- Decision flowchart +- Feature roadmap scenarios +- Effort estimates for each path +- Security comparison +- Real-world questions to ask your team +- Next actions for each option + +**Best for:** Deciding whether to keep your implementation, migrate, or use a hybrid approach + +--- + +### 2. **rodauth-oauth-quick-reference.md** (20-minute read) +**Purpose:** Quick lookup guide and architecture overview + +**Contains:** +- What Rodauth-OAuth is (concise) +- Key statistics and certifications +- Feature advantages & disadvantages +- Architecture diagrams (text-based) +- Database schema comparison +- Feature matrix with implementation effort +- Performance considerations +- Getting started guide +- Code examples (minimal setup) + +**Best for:** Understanding what you're looking at, quick decision support + +--- + +### 3. **rodauth-oauth-analysis.md** (45-minute deep-dive) +**Purpose:** Comprehensive technical analysis for decision-making + +**Contains:** +- Complete architecture breakdown (12 sections) +- All 34 features detailed and explained +- Full database schema documentation +- Request flow diagrams +- Feature dependency graphs +- Integration paths with Rails +- Security analysis +- Migration procedures +- Code comparisons +- Performance metrics + +**Best for:** Deep understanding before making technical decisions, planning migrations + +--- + +## How to Use These Documents + +### Scenario 1: "I have 15 minutes" +1. Read: RODAUTH_DECISION_GUIDE.md (sections: TL;DR + Decision Matrix) +2. Go to: Next Actions for your chosen option +3. Done: You have a direction + +### Scenario 2: "I have 45 minutes" +1. Read: RODAUTH_DECISION_GUIDE.md (complete) +2. Skim: rodauth-oauth-quick-reference.md (focus on code examples) +3. Decide: Which path interests you most +4. Plan: Team discussion using decision matrix + +### Scenario 3: "I'm doing technical deep-dive" +1. Read: RODAUTH_DECISION_GUIDE.md (complete) +2. Read: rodauth-oauth-quick-reference.md (complete) +3. Read: rodauth-oauth-analysis.md (sections 1-6) +4. Reference: rodauth-oauth-analysis.md (sections 7-12 as needed) + +### Scenario 4: "I'm planning a migration" +1. Read: RODAUTH_DECISION_GUIDE.md (effort estimates section) +2. Read: rodauth-oauth-analysis.md (migration path section) +3. Reference: rodauth-oauth-analysis.md (database schema section) +4. Plan: Detailed migration steps + +--- + +## Three Options Explained (Very Brief) + +### Option A: Keep Your Implementation +- **Time:** Ongoing (add features incrementally) +- **Effort:** 4-6 months to reach feature parity +- **Maintenance:** 8-10 hours/month +- **Best if:** Auth Code + PKCE is sufficient forever + +### Option B: Switch to Rodauth-OAuth +- **Time:** 5-9 weeks (one-time migration) +- **Learning:** 1-2 weeks (Roda framework) +- **Maintenance:** 1-2 hours/month +- **Best if:** Need enterprise features, want low maintenance + +### Option C: Hybrid Approach (Microservices) +- **Time:** 3-5 weeks (independent setup) +- **Learning:** Low (Roda is isolated) +- **Maintenance:** 2-3 hours/month +- **Best if:** Want Option B benefits without full Rails→Roda migration + +--- + +## Key Findings + +**What Rodauth-OAuth Provides That You Don't Have:** +- Refresh tokens +- Token revocation (RFC 7009) +- Token introspection (RFC 7662) +- Client Credentials grant (machine-to-machine) +- Device Code flow (IoT/smart TV) +- JWT Access Tokens (stateless) +- Session Management +- Front & Back-Channel Logout +- Token hashing (bcrypt security) +- DPoP support (token binding) +- TLS mutual authentication +- Dynamic Client Registration +- 20+ more optional features + +**Security Differences:** +- Your impl: Tokens stored in plaintext (DB breach = token theft) +- Rodauth: Tokens hashed with bcrypt (secure even if DB breached) + +**Maintenance Burden:** +- Your impl: YOU maintain everything +- Rodauth: Community maintains, you maintain config only + +--- + +## Document Structure + +### RODAUTH_DECISION_GUIDE.md Sections: +``` +1. TL;DR - Three options +2. Decision Matrix - Flowchart +3. Feature Roadmap Comparison +4. Architecture Diagrams (visual) +5. Effort Estimates +6. Real-World Questions +7. Security Comparison +8. Cost-Benefit Summary +9. Decision Scorecard +10. Next Actions +``` + +### rodauth-oauth-quick-reference.md Sections: +``` +1. What Is It? (overview) +2. Key Stats +3. Why Consider It? (advantages) +4. Architecture Overview (your impl vs rodauth) +5. Database Schema Comparison +6. Feature Comparison Matrix +7. Code Examples +8. Integration Paths +9. Getting Started +10. Next Steps +``` + +### rodauth-oauth-analysis.md Sections: +``` +1. Executive Summary +2. What Rodauth-OAuth Is +3. File Structure & Organization +4. OIDC/OAuth Features +5. Architecture: How It Works +6. Database Schema Requirements +7. Integration with Rails +8. Architectural Comparison +9. Feature Matrix +10. Integration Complexity +11. Key Findings & Recommendations +12. Migration Path & Code Examples +``` + +--- + +## For Your Team + +### Sharing with Stakeholders +- **Non-technical:** Use RODAUTH_DECISION_GUIDE.md (TL;DR section) +- **Technical leads:** Use rodauth-oauth-quick-reference.md +- **Engineers:** Use rodauth-oauth-analysis.md (sections 1-6) +- **Security team:** Use rodauth-oauth-analysis.md (security sections) + +### Team Discussion +Print out the decision matrix from RODAUTH_DECISION_GUIDE.md and: +1. Walk through each option +2. Discuss team comfort with framework learning +3. Check against feature roadmap +4. Decide on maintenance philosophy +5. Vote on preferred option + +--- + +## Next Steps After Reading + +### If Choosing Option A (Keep Custom): +- [ ] Plan feature roadmap (refresh tokens first) +- [ ] Allocate team capacity +- [ ] Add token hashing security +- [ ] Set up security monitoring + +### If Choosing Option B (Full Migration): +- [ ] Assign team member to learn Roda/Rodauth +- [ ] Run examples from `/tmp/rodauth-oauth/examples` +- [ ] Plan database migration +- [ ] Prepare rollback plan +- [ ] Schedule migration window + +### If Choosing Option C (Hybrid): +- [ ] Evaluate microservices capability +- [ ] Review service communication plan +- [ ] Set up service infrastructure +- [ ] Plan gradual deployment + +--- + +## Bonus: Running the Example + +Rodauth-OAuth includes a working OIDC server example you can run: + +```bash +cd /Users/dkam/Development/clinch/tmp/rodauth-oauth/examples/oidc +ruby authentication_server.rb + +# Then visit: http://localhost:9292 +# Login with: foo@bar.com / password +# See: Full OIDC provider in action +``` + +--- + +## Questions? + +These documents should answer: +- What is rodauth-oauth? +- How does it compare to my implementation? +- What features would we gain? +- What would we lose? +- How much effort is a migration? +- Should we switch? + +If questions remain, reference the specific section in the analysis documents. + +--- + +## Document Generation Info + +**Generated:** November 12, 2025 +**Analysis Duration:** Complete codebase exploration of rodauth-oauth gem +**Sources Analyzed:** +- 34 feature files (10,000+ lines of code) +- 7 database migrations +- 6 complete example applications +- Comprehensive test suite +- README and migration guides + +**Analysis Includes:** +- Line-by-line code structure review +- Database schema comparison +- Feature cross-reference analysis +- Integration complexity assessment +- Security analysis +- Effort estimation models + +--- + +**Start with RODAUTH_DECISION_GUIDE.md and go from there!** diff --git a/docs/RODAUTH_DECISION_GUIDE.md b/docs/RODAUTH_DECISION_GUIDE.md new file mode 100644 index 0000000..f3717d9 --- /dev/null +++ b/docs/RODAUTH_DECISION_GUIDE.md @@ -0,0 +1,426 @@ +# Rodauth-OAuth Decision Guide + +## TL;DR - Make Your Choice Here + +### Option A: Keep Your Rails Implementation +**Best if:** Authorization Code + PKCE is all you need, forever +- Keep your current 450 lines of OIDC controller code +- Maintain incrementally as needs change +- Stay 100% in Rails ecosystem +- Time investment: Ongoing (2-3 months to feature parity) +- Learning curve: None (already know Rails) + +### Option B: Switch to Rodauth-OAuth +**Best if:** You need enterprise features, standards compliance, low maintenance +- Replace 450 lines with plugin config +- Get 34 optional features on demand +- OpenID Certified, production-hardened +- Time investment: 4-8 weeks (one-time) +- Learning curve: Medium (learn Roda/Rodauth) + +### Option C: Hybrid (Recommended if Option B appeals you) +**Best if:** You want rodauth-oauth benefits without framework change +- Run Rodauth-OAuth as separate microservice +- Keep your Rails app unchanged +- Services talk via HTTP APIs +- Time investment: 2-3 weeks (independent services) +- Learning curve: Low (Roda is isolated) + +--- + +## Decision Matrix + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ Do you need features beyond Authorization Code + PKCE? │ +├─────────────────────────────────────────────────────────────────┤ +│ YES ─→ Go to Question 2 │ +│ NO ─→ KEEP YOUR IMPLEMENTATION │ +└─────────────────────────────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────────────────────┐ +│ Can your team learn Roda (different from Rails)? │ +├─────────────────────────────────────────────────────────────────┤ +│ YES ─→ SWITCH TO RODAUTH-OAUTH │ +│ NO ─→ Go to Question 3 │ +└─────────────────────────────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────────────────────┐ +│ Can you run separate services (microservices)? │ +├─────────────────────────────────────────────────────────────────┤ +│ YES ─→ USE HYBRID APPROACH │ +│ NO ─→ KEEP YOUR IMPLEMENTATION │ +└─────────────────────────────────────────────────────────────────┘ +``` + +--- + +## Feature Roadmap Comparison + +### Scenario 1: You Need Refresh Tokens (Common) + +**Option A (Keep Custom):** +- Implement refresh token endpoints +- Add refresh_token columns to DB +- Token rotation logic +- Estimate: 1-2 weeks of work +- Ongoing: Maintain refresh token security + +**Option B (Rodauth-OAuth):** +- Already built and tested +- Just enable: `:oauth_authorization_code_grant` (includes refresh) +- Token rotation: Configurable options +- Estimate: Already included +- Ongoing: Community maintains + +**Option C (Hybrid):** +- Rodauth-OAuth handles it +- Your app unchanged +- Same as Option B for this feature + +### Scenario 2: You Need Token Revocation + +**Option A (Keep Custom):** +- Build `/oauth/revoke` endpoint +- Implement token blacklist or DB update +- Handle race conditions +- Estimate: 1-2 weeks +- Ongoing: Monitor revocation leaks + +**Option B (Rodauth-OAuth):** +- Enable `:oauth_token_revocation` feature +- RFC 7009 compliant out of the box +- Estimate: Already included +- Ongoing: Community handles RFC updates + +**Option C (Hybrid):** +- Same as Option B + +### Scenario 3: You Need Client Credentials Grant + +**Option A (Keep Custom):** +- New endpoint logic +- Client authentication (different from user auth) +- Token generation for apps without users +- Estimate: 2-3 weeks +- Ongoing: Test with external clients + +**Option B (Rodauth-OAuth):** +- Enable `:oauth_client_credentials_grant` feature +- All edge cases handled +- Estimate: Already included +- Ongoing: Community maintains + +**Option C (Hybrid):** +- Same as Option B + +--- + +## Architecture Diagrams + +### Current Setup (Your Implementation) +``` +┌─────────────────────────────┐ +│ Your Rails Application │ +├─────────────────────────────┤ +│ app/controllers/ │ +│ oidc_controller.rb │ ← 450 lines of OAuth logic +│ │ +│ app/models/ │ +│ OidcAuthorizationCode │ +│ OidcAccessToken │ +│ OidcUserConsent │ +│ │ +│ app/services/ │ +│ OidcJwtService │ +├─────────────────────────────┤ +│ Rails ActiveRecord │ +├─────────────────────────────┤ +│ PostgreSQL Database │ +│ - oidc_authorization_codes +│ - oidc_access_tokens +│ - oidc_user_consents +│ - applications +└─────────────────────────────┘ +``` + +### Option B: Full Migration +``` +┌──────────────────────────────┐ +│ Roda + Rodauth-OAuth App │ +├──────────────────────────────┤ +│ lib/rodauth_app.rb │ ← Config (not code!) +│ enable :oidc, │ +│ enable :oauth_pkce, │ +│ enable :oauth_token_... │ +│ │ +│ [Routes auto-mounted] │ +│ /.well-known/config │ +│ /oauth/authorize │ +│ /oauth/token │ +│ /oauth/userinfo │ +│ /oauth/revoke │ +│ /oauth/introspect │ +├──────────────────────────────┤ +│ Sequel ORM │ +├──────────────────────────────┤ +│ PostgreSQL Database │ +│ - accounts (rodauth) +│ - oauth_applications +│ - oauth_grants (unified!) +│ - optional feature tables +└──────────────────────────────┘ +``` + +### Option C: Microservices Architecture (Hybrid) +``` +┌──────────────────────────┐ ┌──────────────────────────┐ +│ Your Rails App │ │ Rodauth-OAuth Service │ +├──────────────────────────┤ ├──────────────────────────┤ +│ Normal Rails Controllers │ │ lib/rodauth_app.rb │ +│ & Business Logic │ │ [OAuth Features] │ +│ │ │ │ +│ HTTP Calls to →──────────┼─────→ /.well-known/config │ +│ OAuth Service OAuth │ │ /oauth/authorize │ +│ HTTP API │ │ /oauth/token │ +│ │ │ /oauth/userinfo │ +│ Verify Tokens via →──────┼─────→ /oauth/introspect │ +│ /oauth/introspect │ │ │ +├──────────────────────────┤ ├──────────────────────────┤ +│ Rails ActiveRecord │ │ Sequel ORM │ +├──────────────────────────┤ ├──────────────────────────┤ +│ PostgreSQL │ │ PostgreSQL │ +│ [business tables] │ │ [oauth tables] │ +└──────────────────────────┘ └──────────────────────────┘ +``` + +--- + +## Effort Estimates + +### Option A: Keep & Enhance Custom Implementation +``` +Refresh Tokens: 1-2 weeks +Token Revocation: 1-2 weeks +Token Introspection: 1-2 weeks +Client Credentials: 2-3 weeks +Device Code: 3-4 weeks +JWT Access Tokens: 1-2 weeks +Session Management: 2-3 weeks +Front-Channel Logout: 1-2 weeks +Back-Channel Logout: 2-3 weeks +───────────────────────────────── +TOTAL FOR PARITY: 15-25 weeks +(4-6 months of work) + +ONGOING MAINTENANCE: ~8-10 hours/month +(security updates, RFC changes, bug fixes) +``` + +### Option B: Migrate to Rodauth-OAuth +``` +Learn Roda/Rodauth: 1-2 weeks +Migrate Database Schema: 1-2 weeks +Replace OIDC Code: 1-2 weeks +Test & Validation: 2-3 weeks +───────────────────────────────── +ONE-TIME EFFORT: 5-9 weeks +(1-2 months) + +ONGOING MAINTENANCE: ~1-2 hours/month +(dependency updates, config tweaks) +``` + +### Option C: Hybrid Approach +``` +Set up Rodauth service: 1-2 weeks +Configure integration: 1-2 weeks +Test both services: 1 week +───────────────────────────────── +ONE-TIME EFFORT: 3-5 weeks +(less than Option B) + +ONGOING MAINTENANCE: ~2-3 hours/month +(maintain two services, but Roda handles OAuth) +``` + +--- + +## Real-World Questions to Ask Your Team + +### Question 1: Feature Needs +- "Do we need refresh tokens?" +- "Will clients ask for token revocation?" +- "Do we support service-to-service auth (client credentials)?" +- "Will we ever need device code flow (IoT)?" + +If YES to any: **Option B or C makes sense** + +### Question 2: Maintenance Philosophy +- "Do we want to own the OAuth code?" +- "Can we afford to maintain OAuth compliance?" +- "Do we have experts in OAuth/OIDC?" + +If NO to all: **Option B or C is better** + +### Question 3: Framework Flexibility +- "Is Rails non-negotiable for this company?" +- "Can our team learn a new framework?" +- "Can we run microservices?" + +If Rails is required: **Option C (hybrid)** + +### Question 4: Time Constraints +- "Do we have 4-8 weeks for a migration?" +- "Can we maintain OAuth for years?" +- "What if specs change?" + +If time-constrained: **Option B is fastest path to full features** + +--- + +## Security Comparison + +### Your Implementation +- ✓ PKCE support +- ✓ JWT signing +- ✓ HTTPS recommended +- ✗ Token hashing (stores tokens in plaintext) +- ✗ Token rotation +- ✗ DPoP (token binding) +- ✗ Automatic spec compliance +- Risk: Token theft if DB compromised + +### Rodauth-OAuth +- ✓ PKCE support +- ✓ JWT signing +- ✓ Token hashing (bcrypt by default) +- ✓ Token rotation policies +- ✓ DPoP support (RFC 9449) +- ✓ TLS mutual authentication +- ✓ Automatic spec updates +- ✓ Certified compliance +- Risk: Minimal (industry-standard) + +--- + +## Cost-Benefit Summary + +### Keep Your Implementation +``` +Costs: + - 15-25 weeks to feature parity + - Ongoing security monitoring + - Spec compliance tracking + - Bug fixes & edge cases + +Benefits: + - No framework learning + - Full code understanding + - Rails-native patterns + - Minimal dependencies +``` + +### Switch to Rodauth-OAuth +``` +Costs: + - 5-9 weeks migration effort + - Learn Roda/Rodauth + - Database schema changes + - Test all flows + +Benefits: + - Get 34 features immediately + - Certified compliance + - Community-maintained + - Security best practices + - Ongoing support +``` + +### Hybrid Approach +``` +Costs: + - 3-5 weeks setup + - Learn Roda basics + - Operate two services + - Service communication + +Benefits: + - All Rodauth-OAuth features + - Rails app unchanged + - Independent scaling + - Clear separation of concerns +``` + +--- + +## Decision Scorecard + +| Factor | Option A | Option B | Option C | +|--------|----------|----------|----------| +| Initial Time | Low | Medium | Medium-Low | +| Ongoing Effort | High | Low | Medium | +| Feature Completeness | Low | High | High | +| Framework Learning | None | Medium | Low | +| Standards Compliance | Manual | Auto | Auto | +| Deployment Complexity | Simple | Simple | Complex | +| Team Preference | ??? | ??? | ??? | + +--- + +## Next Actions + +### For Option A (Keep Custom): +1. Plan feature roadmap (refresh tokens first) +2. Allocate team capacity for implementation +3. Document OAuth decisions +4. Set up security monitoring + +### For Option B (Full Migration): +1. Assign someone to learn Roda/Rodauth +2. Run rodauth-oauth examples +3. Plan database migration +4. Schedule migration window +5. Prepare rollback plan + +### For Option C (Hybrid): +1. Evaluate microservices capability +2. Run Rodauth-OAuth example +3. Plan service boundaries +4. Set up service communication +5. Plan infrastructure for two services + +--- + +## Still Can't Decide? + +Ask these questions: +1. **Will you add features beyond Auth Code + PKCE in next 12 months?** + - YES → Option B or C + - NO → Option A + +2. **Do you have maintenance bandwidth?** + - YES → Option A + - NO → Option B or C + +3. **Can you run multiple services?** + - YES → Option C (best of both) + - NO → Option B (if framework is OK) or Option A (stay Rails) + +--- + +## Document Files + +You now have three documents: +1. **rodauth-oauth-analysis.md** - Deep technical analysis (12 sections) +2. **rodauth-oauth-quick-reference.md** - Quick lookup guide +3. **RODAUTH_DECISION_GUIDE.md** - This decision framework + +Read in this order: +1. This guide (make a decision) +2. Quick reference (understand architecture) +3. Analysis (deep dive on your choice) + +--- + +**Made Your Decision?** Create an issue/commit to document your choice and next steps! diff --git a/docs/caddy-example.md b/docs/caddy-example.md new file mode 100644 index 0000000..f2f1a77 --- /dev/null +++ b/docs/caddy-example.md @@ -0,0 +1,176 @@ +# Caddy ForwardAuth Configuration Examples + +## Basic Configuration (Protecting MEtube) + +Assuming Caddy and Clinch are running in a docker compose, and we can use the sevice name `clinch`. Exterally, assume you're connecting to https://clinch.example.com/ + +```caddyfile +# Clinch SSO (main authentication server) +clinch.yourdomain.com { + reverse_proxy clinch:3000 +} + +# MEtube (protected by Clinch) +metube.yourdomain.com { + # Forward authentication to Clinch + forward_auth clinch:3000 { + uri /api/verify + # uri /api/verify?rd=https://clinch.yourdomain.com # Shouldn't need this, the rd value should be sent via headers + copy_headers Remote-User Remote-Email Remote-Groups Remote-Admin + } + + # If authentication succeeds, proxy to MEtube + handle { + reverse_proxy * { + to http://:8081 + header_up X-Real-IP {remote_host} + } + } +} +``` + +## How It Works + +1. User visits `https://metube.yourdomain.com` +2. Caddy makes request to `http://clinch:3000/api/verify passing in the url destination for metueb +3. Clinch checks if user is authenticated and authorized: + - If **200**: Caddy forwards request to MEtube with user headers + - If **302**: User is redirected to clinch.yourdomain.com to login + - If **403**: Access denied +4. User signs into Clinch (with TOTP if enabled or Passkey) +5. Clinch redirects back to MEtube +6. User can now access MEtube! + +## Protecting Multiple Applications + +```caddyfile +# Clinch SSO +clinch.yourdomain.com { + reverse_proxy clinch:3000 +} + +# MEtube - Anyone can access (no groups required) +metube.yourdomain.com { + forward_auth clinch:3000 { + uri /api/verify + copy_headers Remote-User Remote-Email Remote-Groups Remote-Admin + } + + handle { + reverse_proxy * { + to http://metube:8081 + header_up X-Real-IP {remote_host} + } + } +} + +# Sonarr - Only "media-managers" group +sonarr.yourdomain.com { + forward_auth clinch:3000 { + uri /api/verify + copy_headers Remote-User Remote-Email Remote-Groups Remote-Admin + } + + handle { + reverse_proxy * { + to http://sonarr:8989 + header_up X-Real-IP {remote_host} + } + } +} + +# Grafana - Only "admins" group +grafana.yourdomain.com { + forward_auth clinch:3000 { + uri /api/verify + copy_headers Remote-User Remote-Email Remote-Groups Remote-Admin + } + + handle { + reverse_proxy * { + to http://grafana:3001 + header_up X-Real-IP {remote_host} + } + } +} +``` + +## Setup Steps + +### 1. Create Applications in Clinch + +Create the Application within Clinch, making sure to set Forward Auth application type + +### 2. Update Caddyfile + +Add the forward_auth directives shown above. + +### 3. Reload Caddy + +```bash +caddy reload +``` + +### 4. Test + +Visit https://metube.yourdomain.com - you should be redirected to Clinch login! + +## Advanced: Passing Headers to Application + +Some applications can use the forwarded headers for user identification: + +```caddyfile +metube.yourdomain.com { + forward_auth clinch:3000 { + uri /api/verify + copy_headers Remote-User Remote-Email Remote-Groups Remote-Admin + } + + # The headers are automatically passed to the backend + handle { + reverse_proxy * { + to http://metube:8081 + header_up X-Real-IP {remote_host} + } + } +} +``` + +Now MEtube receives these headers with every request: +- `Remote-User`: user@example.com +- `Remote-Email`: user@example.com +- `Remote-Groups`: media-managers,users +- `Remote-Admin`: false + +## Troubleshooting + +### Users not staying logged in + +Ensure your Caddy configuration preserves cookies: + +```caddyfile +clinch.yourdomain.com { + reverse_proxy localhost:3000 { + header_up X-Forwarded-Host {host} + header_up X-Forwarded-Proto {scheme} + } +} +``` + +### Authentication loop + +Check that the `/api/verify` endpoint is not itself protected: +- `/api/verify` must be accessible without authentication +- It returns 401/403 for unauthenticated users (this is expected) + +### Check Clinch logs + +```bash +tail -f log/production.log +``` + +You'll see ForwardAuth log messages like: +``` +ForwardAuth: User user@example.com granted access to metube +ForwardAuth: Unauthorized - No session cookie +``` diff --git a/docs/forward-auth-testing.md b/docs/forward-auth-testing.md new file mode 100644 index 0000000..5fd7036 --- /dev/null +++ b/docs/forward-auth-testing.md @@ -0,0 +1,227 @@ +# Forward Auth Testing Guide + +## Overview +Testing forward authentication requires testing multiple layers: HTTP requests, session management, and header forwarding. This guide provides practical testing approaches. + +## Quick Start + +### 1. Start Rails Server +```bash +rails server +``` + +### 2. Basic curl Tests + +#### Test 1: Unauthenticated Request +```bash +curl -v http://localhost:3000/api/verify \ + -H "X-Forwarded-Host: test.example.com" +``` + +**Expected Result:** 302 redirect to login +``` +< HTTP/1.1 302 Found +< Location: http://localhost:3000/signin?rd=https://test.example.com/ +< X-Auth-Reason: No session cookie +``` + +#### Test 2: Authenticated Request +1. Sign in at http://localhost:3000/signin +2. Copy session cookie from browser +3. Run: +```bash +curl -v http://localhost:3000/api/verify \ + -H "X-Forwarded-Host: test.example.com" \ + -H "Cookie: _clinch_session_id=YOUR_SESSION_COOKIE" +``` + +**Expected Result:** 200 OK with headers +``` +< HTTP/1.1 200 OK +< X-Remote-User: your-email@example.com +< X-Remote-Email: your-email@example.com +< X-Remote-Name: your-email@example.com +< X-Remote-Groups: group-name +< X-Remote-Admin: true/false +``` + +## Testing Header Configurations + +### Create Test Rules in Admin Interface + +1. **Default Headers Rule** (`test.example.com`) + - Leave header fields empty (uses defaults) + - Expected: X-Remote-* headers + +2. **No Headers Rule** (`metube.example.com`) + - Set all header fields to empty strings + - Expected: No authentication headers (access only) + +3. **Custom Headers Rule** (`grafana.example.com`) + - Set custom header names: + - User Header: `X-WEBAUTH-USER` + - Groups Header: `X-WEBAUTH-ROLES` + - Email Header: `X-WEBAUTH-EMAIL` + - Expected: Custom header names + +### Test Different Configurations + +```bash +# Test default headers +curl -v http://localhost:3000/api/verify \ + -H "X-Forwarded-Host: test.example.com" \ + -H "Cookie: _clinch_session_id=YOUR_SESSION_COOKIE" + +# Test no headers (access only) +curl -v http://localhost:3000/api/verify \ + -H "X-Forwarded-Host: metube.example.com" \ + -H "Cookie: _clinch_session_id=YOUR_SESSION_COOKIE" + +# Test custom headers +curl -v http://localhost:3000/api/verify \ + -H "X-Forwarded-Host: grafana.example.com" \ + -H "Cookie: _clinch_session_id=YOUR_SESSION_COOKIE" +``` + +## Domain Pattern Testing + +Test various domain patterns: + +```bash +# Wildcard subdomains +curl -v http://localhost:3000/api/verify \ + -H "X-Forwarded-Host: app.test.example.com" + +# Exact domains +curl -v http://localhost:3000/api/verify \ + -H "X-Forwarded-Host: api.example.com" + +# No matching rule (should use defaults) +curl -v http://localhost:3000/api/verify \ + -H "X-Forwarded-Host: unknown.example.com" +``` + +## Integration Testing + +### Test with Real Reverse Proxy (Caddy Example) + +1. Set up Caddy with forward auth: +```caddyfile +example.com { + forward_auth localhost:3000 { + uri /api/verify + copy_headers X-Remote-User X-Remote-Email X-Remote-Groups X-Remote-Admin + } + + reverse_proxy localhost:8080 +} +``` + +2. Test by visiting `https://example.com` in browser +3. Should redirect to Clinch login, then back to application + +## Unit Testing (Rails Console) + +Test the header logic directly: + +```ruby +# Rails console: rails console + +# Get a user +user = User.first + +# Test default headers +rule = ForwardAuthRule.create!(domain_pattern: 'test.example.com', active: true) +headers = rule.headers_for_user(user) +puts headers +# => {"X-Remote-User" => "user@example.com", "X-Remote-Email" => "user@example.com", ...} + +# Test custom headers +rule.update!(headers_config: { user: 'X-Custom-User', groups: 'X-Custom-Groups' }) +headers = rule.headers_for_user(user) +puts headers +# => {"X-Custom-User" => "user@example.com", "X-Remote-Email" => "user@example.com", ...} + +# Test no headers +rule.update!(headers_config: { user: '', email: '', name: '', groups: '', admin: '' }) +headers = rule.headers_for_user(user) +puts headers +# => {} +``` + +## Testing Checklist + +### Basic Functionality +- [ ] Unauthenticated requests redirect to login +- [ ] Authenticated requests return 200 OK +- [ ] Headers are correctly forwarded to applications +- [ ] Session cookies work correctly + +### Header Configurations +- [ ] Default headers (X-Remote-*) work +- [ ] Custom headers work with specific applications +- [ ] No headers option works for access-only apps +- [ ] Empty header fields are handled correctly + +### Domain Matching +- [ ] Wildcard domains (*.example.com) work +- [ ] Exact domains work +- [ ] Case insensitivity works +- [ ] No matching rule falls back to defaults + +### Access Control +- [ ] Group restrictions work correctly +- [ ] Inactive users are denied access +- [ ] Inactive rules are ignored +- [ ] Bypass mode (no groups) works + +## Troubleshooting + +### Common Issues + +1. **Headers not being sent** + - Check rule is active + - Verify headers configuration + - Check user is in allowed groups + +2. **Authentication loops** + - Check session cookie domain + - Verify redirect URLs + - Check browser cookie settings + +3. **Headers not reaching application** + - Check reverse proxy configuration + - Verify proxy is forwarding headers + - Check application expects correct header names + +### Debug Logging + +Enable debug logging in `forward_auth_controller.rb`: +```ruby +Rails.logger.level = Logger::DEBUG +``` + +This will show detailed information about: +- Session extraction +- Rule matching +- Header generation +- Redirect URLs + +## Production Testing + +Before deploying to production: + +1. **SSL/TLS Testing**: Test with HTTPS +2. **Cookie Domains**: Test cross-subdomain cookies +3. **Performance**: Test response times under load +4. **Security**: Test with invalid sessions and malformed headers +5. **Monitoring**: Set up logging and alerting + +## Automation + +For automated testing, consider: + +1. **Integration Tests**: Use Rails integration tests for controller testing +2. **API Tests**: Use tools like Postman or Insomnia for API testing +3. **Browser Tests**: Use Selenium or Cypress for end-to-end testing +4. **Load Testing**: Use tools like k6 or JMeter for performance testing \ No newline at end of file diff --git a/docs/oidc-refresh-tokens-client-guide.md b/docs/oidc-refresh-tokens-client-guide.md new file mode 100644 index 0000000..dca54d9 --- /dev/null +++ b/docs/oidc-refresh-tokens-client-guide.md @@ -0,0 +1,611 @@ +# OIDC Refresh Tokens - Client Implementation Guide + +## Overview + +Clinch now supports **OAuth 2.0 Refresh Tokens**, allowing your applications to maintain long-lived sessions without requiring users to re-authenticate every hour. + +**Key Benefits:** +- ✅ No user re-authentication for 30 days (configurable) +- ✅ Silent token refresh - no redirects, no user interaction +- ✅ Secure token rotation - prevents reuse attacks +- ✅ Token revocation support - users can invalidate sessions + +--- + +## Quick Start + +### Before (Without Refresh Tokens) +``` +User logs in → Access token (1 hour) +After 1 hour → Redirect to /oauth/authorize +User auto-approves → New access token +Repeat every hour... 😞 +``` + +### Now (With Refresh Tokens) +``` +User logs in → Access token (1 hour) + Refresh token (30 days) +After 1 hour → POST to /oauth/token with refresh_token +Get new tokens → No redirect! No user interaction! 🎉 +``` + +--- + +## Initial Authorization + +### 1. Authorization Code Flow (Unchanged) + +**Step 1: Redirect user to authorization endpoint** +``` +GET https://auth.example.com/oauth/authorize? + client_id=YOUR_CLIENT_ID& + redirect_uri=https://yourapp.com/callback& + response_type=code& + scope=openid%20profile%20email& + state=RANDOM_STATE& + code_challenge=BASE64URL(SHA256(code_verifier))& + code_challenge_method=S256 +``` + +**Step 2: Exchange authorization code for tokens** +```http +POST https://auth.example.com/oauth/token +Content-Type: application/x-www-form-urlencoded + +grant_type=authorization_code +&code=AUTHORIZATION_CODE +&redirect_uri=https://yourapp.com/callback +&client_id=YOUR_CLIENT_ID +&client_secret=YOUR_CLIENT_SECRET +&code_verifier=CODE_VERIFIER +``` + +**Response (NEW - now includes refresh_token):** +```json +{ + "access_token": "eyJhbGc...", + "token_type": "Bearer", + "expires_in": 3600, + "id_token": "eyJhbGc...", + "refresh_token": "abc123xyz...", + "scope": "openid profile email" +} +``` + +**IMPORTANT:** Store the `refresh_token` securely! You'll need it to get new access tokens. + +--- + +## Token Refresh Flow + +When your `access_token` expires (after 1 hour), use the `refresh_token` to get new tokens **without user interaction**. + +### How to Refresh Tokens + +**Request:** +```http +POST https://auth.example.com/oauth/token +Content-Type: application/x-www-form-urlencoded + +grant_type=refresh_token +&refresh_token=YOUR_REFRESH_TOKEN +&client_id=YOUR_CLIENT_ID +&client_secret=YOUR_CLIENT_SECRET +``` + +**Response:** +```json +{ + "access_token": "eyJhbGc...NEW", + "token_type": "Bearer", + "expires_in": 3600, + "id_token": "eyJhbGc...NEW", + "refresh_token": "def456uvw...NEW", + "scope": "openid profile email" +} +``` + +**CRITICAL:** +- The old `refresh_token` is **immediately revoked** (single-use) +- You receive a **new `refresh_token`** to use next time +- **Replace** the old refresh token with the new one in your storage + +--- + +## Token Lifecycle + +``` +┌─────────────────────────────────────────────────────────┐ +│ Initial Authorization │ +├─────────────────────────────────────────────────────────┤ +│ GET /oauth/authorize → User logs in │ +│ POST /oauth/token (authorization_code grant) │ +│ ↓ │ +│ Receive: access_token (1h) + refresh_token (30d) │ +└─────────────────────────────────────────────────────────┘ + │ + ↓ +┌─────────────────────────────────────────────────────────┐ +│ Token Refresh (Silent, No User Interaction) │ +├─────────────────────────────────────────────────────────┤ +│ After 1 hour (access_token expires): │ +│ POST /oauth/token (refresh_token grant) │ +│ ↓ │ +│ Receive: NEW access_token + NEW refresh_token │ +│ Old refresh_token is revoked │ +└─────────────────────────────────────────────────────────┘ + │ + ↓ (Repeat for 30 days) +┌─────────────────────────────────────────────────────────┐ +│ Session Expiry │ +├─────────────────────────────────────────────────────────┤ +│ After 30 days (refresh_token expires): │ +│ Redirect user to /oauth/authorize for re-authentication │ +└─────────────────────────────────────────────────────────┘ +``` + +--- + +## Token Storage Best Practices + +### ✅ Secure Storage Recommendations + +**Web Applications (Server-Side):** +- Store refresh tokens in **server-side session** (encrypted) +- Use **HttpOnly, Secure cookies** for access tokens +- **Never** send refresh tokens to browser JavaScript + +**Single Page Applications (SPAs):** +- Store access tokens in **memory only** (JavaScript variable) +- Store refresh tokens in **HttpOnly, Secure cookie** (via backend) +- Use Backend-for-Frontend (BFF) pattern for refresh + +**Mobile Apps:** +- Use platform-specific **secure storage**: + - iOS: Keychain + - Android: EncryptedSharedPreferences or Keystore +- **Never** store in UserDefaults/SharedPreferences + +**Desktop Apps:** +- Use OS-specific credential storage +- Encrypt tokens at rest + +### ❌ DO NOT Store Refresh Tokens In: +- LocalStorage (XSS vulnerable) +- SessionStorage (XSS vulnerable) +- Unencrypted cookies +- Plain text files +- Source code or config files + +--- + +## Token Revocation + +Allow users to invalidate their sessions (e.g., "Sign out of all devices"). + +### Revoke a Token + +**Request:** +```http +POST https://auth.example.com/oauth/revoke +Content-Type: application/x-www-form-urlencoded + +token=YOUR_TOKEN +&token_type_hint=refresh_token +&client_id=YOUR_CLIENT_ID +&client_secret=YOUR_CLIENT_SECRET +``` + +**Parameters:** +- `token` (required) - The token to revoke (access or refresh token) +- `token_type_hint` (optional) - "access_token" or "refresh_token" +- `client_id` + `client_secret` (required) - Client authentication + +**Response:** +``` +HTTP/1.1 200 OK +``` + +**Note:** Per RFC 7009, the response is always `200 OK`, even if the token was invalid or already revoked (prevents token scanning attacks). + +--- + +## Error Handling + +### Refresh Token Errors + +#### 1. Invalid or Expired Refresh Token +```json +{ + "error": "invalid_grant", + "error_description": "Invalid refresh token" +} +``` +**Action:** Redirect user to /oauth/authorize for re-authentication + +#### 2. Refresh Token Revoked (Reuse Detected!) +```json +{ + "error": "invalid_grant", + "error_description": "Refresh token has been revoked" +} +``` +**Action:** +- This indicates a **security issue** (possible token theft) +- All tokens in the same family are revoked +- Redirect user to /oauth/authorize +- Consider alerting the user about suspicious activity + +#### 3. Invalid Client Credentials +```json +{ + "error": "invalid_client" +} +``` +**Action:** Check your `client_id` and `client_secret` + +--- + +## Implementation Examples + +### Example 1: Node.js Express + +```javascript +const axios = require('axios'); + +class OAuthClient { + constructor(config) { + this.clientId = config.clientId; + this.clientSecret = config.clientSecret; + this.tokenEndpoint = config.tokenEndpoint; + this.accessToken = null; + this.refreshToken = null; + this.expiresAt = null; + } + + // Exchange authorization code for tokens + async exchangeCode(code, redirectUri, codeVerifier) { + const response = await axios.post(this.tokenEndpoint, new URLSearchParams({ + grant_type: 'authorization_code', + code: code, + redirect_uri: redirectUri, + client_id: this.clientId, + client_secret: this.clientSecret, + code_verifier: codeVerifier + })); + + this.storeTokens(response.data); + return response.data; + } + + // Refresh access token + async refreshAccessToken() { + if (!this.refreshToken) { + throw new Error('No refresh token available'); + } + + const response = await axios.post(this.tokenEndpoint, new URLSearchParams({ + grant_type: 'refresh_token', + refresh_token: this.refreshToken, + client_id: this.clientId, + client_secret: this.clientSecret + })); + + this.storeTokens(response.data); + return response.data; + } + + // Get valid access token (auto-refresh if needed) + async getAccessToken() { + // Check if token is expired or about to expire (5 min buffer) + if (this.expiresAt && Date.now() >= this.expiresAt - 300000) { + await this.refreshAccessToken(); + } + + return this.accessToken; + } + + storeTokens(tokenResponse) { + this.accessToken = tokenResponse.access_token; + this.refreshToken = tokenResponse.refresh_token; + this.expiresAt = Date.now() + (tokenResponse.expires_in * 1000); + } + + // Revoke tokens + async revokeToken(token, tokenTypeHint) { + await axios.post('https://auth.example.com/oauth/revoke', new URLSearchParams({ + token: token, + token_type_hint: tokenTypeHint, + client_id: this.clientId, + client_secret: this.clientSecret + })); + } +} + +// Usage +const client = new OAuthClient({ + clientId: 'your-client-id', + clientSecret: 'your-client-secret', + tokenEndpoint: 'https://auth.example.com/oauth/token' +}); + +// After initial login +await client.exchangeCode(authCode, redirectUri, codeVerifier); + +// Make API calls (auto-refreshes if needed) +const token = await client.getAccessToken(); +const apiResponse = await axios.get('https://api.example.com/data', { + headers: { Authorization: `Bearer ${token}` } +}); + +// Logout - revoke refresh token +await client.revokeToken(client.refreshToken, 'refresh_token'); +``` + +### Example 2: Python + +```python +import requests +import time +from urllib.parse import urlencode + +class OAuthClient: + def __init__(self, client_id, client_secret, token_endpoint): + self.client_id = client_id + self.client_secret = client_secret + self.token_endpoint = token_endpoint + self.access_token = None + self.refresh_token = None + self.expires_at = None + + def exchange_code(self, code, redirect_uri, code_verifier): + """Exchange authorization code for tokens""" + response = requests.post(self.token_endpoint, data={ + 'grant_type': 'authorization_code', + 'code': code, + 'redirect_uri': redirect_uri, + 'client_id': self.client_id, + 'client_secret': self.client_secret, + 'code_verifier': code_verifier + }) + response.raise_for_status() + self._store_tokens(response.json()) + return response.json() + + def refresh_access_token(self): + """Refresh the access token using refresh token""" + if not self.refresh_token: + raise ValueError('No refresh token available') + + response = requests.post(self.token_endpoint, data={ + 'grant_type': 'refresh_token', + 'refresh_token': self.refresh_token, + 'client_id': self.client_id, + 'client_secret': self.client_secret + }) + response.raise_for_status() + self._store_tokens(response.json()) + return response.json() + + def get_access_token(self): + """Get valid access token, refresh if needed""" + # Check if token is expired (with 5 min buffer) + if self.expires_at and time.time() >= self.expires_at - 300: + self.refresh_access_token() + + return self.access_token + + def _store_tokens(self, token_response): + """Store tokens and expiration time""" + self.access_token = token_response['access_token'] + self.refresh_token = token_response['refresh_token'] + self.expires_at = time.time() + token_response['expires_in'] + + def revoke_token(self, token, token_type_hint='refresh_token'): + """Revoke a token""" + requests.post('https://auth.example.com/oauth/revoke', data={ + 'token': token, + 'token_type_hint': token_type_hint, + 'client_id': self.client_id, + 'client_secret': self.client_secret + }) + +# Usage +client = OAuthClient( + client_id='your-client-id', + client_secret='your-client-secret', + token_endpoint='https://auth.example.com/oauth/token' +) + +# After initial login +client.exchange_code(auth_code, redirect_uri, code_verifier) + +# Make API calls (auto-refreshes if needed) +token = client.get_access_token() +response = requests.get('https://api.example.com/data', + headers={'Authorization': f'Bearer {token}'}) + +# Logout +client.revoke_token(client.refresh_token, 'refresh_token') +``` + +--- + +## Security Considerations + +### 1. Token Rotation (Implemented ✅) +- Each refresh token is **single-use only** +- After use, old refresh token is immediately revoked +- New refresh token is issued +- Prevents replay attacks + +### 2. Token Family Tracking (Implemented ✅) +- All refresh tokens in a rotation chain share a `token_family_id` +- If a **revoked** refresh token is reused → **entire family is revoked** +- Detects stolen token attacks + +### 3. Refresh Token Binding +- Refresh tokens are bound to: + - Specific client (client_id) + - Specific user + - Specific scopes +- Cannot be used by different clients + +### 4. Expiration Times (Configurable per application) +- **Access tokens:** 5 minutes - 24 hours (default: 1 hour) +- **Refresh tokens:** 1 day - 90 days (default: 30 days) +- **ID tokens:** 5 minutes - 24 hours (default: 1 hour) + +--- + +## Discovery Endpoint Updates + +The OIDC discovery endpoint now advertises refresh token support: + +**GET `https://auth.example.com/.well-known/openid-configuration`** + +```json +{ + "issuer": "https://auth.example.com", + "authorization_endpoint": "https://auth.example.com/oauth/authorize", + "token_endpoint": "https://auth.example.com/oauth/token", + "revocation_endpoint": "https://auth.example.com/oauth/revoke", + "userinfo_endpoint": "https://auth.example.com/oauth/userinfo", + "jwks_uri": "https://auth.example.com/.well-known/jwks.json", + "grant_types_supported": ["authorization_code", "refresh_token"], + "response_types_supported": ["code"], + "scopes_supported": ["openid", "profile", "email", "groups"], + "token_endpoint_auth_methods_supported": ["client_secret_post", "client_secret_basic"], + ... +} +``` + +--- + +## Testing Your Implementation + +### Test 1: Initial Token Exchange +```bash +# Get authorization code (manual - visit in browser) +# Then exchange for tokens: + +curl -X POST https://auth.example.com/oauth/token \ + -d "grant_type=authorization_code" \ + -d "code=YOUR_AUTH_CODE" \ + -d "redirect_uri=https://yourapp.com/callback" \ + -d "client_id=YOUR_CLIENT_ID" \ + -d "client_secret=YOUR_CLIENT_SECRET" \ + -d "code_verifier=YOUR_CODE_VERIFIER" + +# Response should include refresh_token +``` + +### Test 2: Token Refresh +```bash +curl -X POST https://auth.example.com/oauth/token \ + -d "grant_type=refresh_token" \ + -d "refresh_token=YOUR_REFRESH_TOKEN" \ + -d "client_id=YOUR_CLIENT_ID" \ + -d "client_secret=YOUR_CLIENT_SECRET" + +# Response should include NEW access_token and NEW refresh_token +``` + +### Test 3: Token Revocation +```bash +curl -X POST https://auth.example.com/oauth/revoke \ + -d "token=YOUR_REFRESH_TOKEN" \ + -d "token_type_hint=refresh_token" \ + -d "client_id=YOUR_CLIENT_ID" \ + -d "client_secret=YOUR_CLIENT_SECRET" + +# Should return 200 OK +``` + +### Test 4: Reuse Detection (Security Test) +```bash +# 1. Use refresh token to get new tokens +curl -X POST ... (as in Test 2) + +# 2. Try to use the OLD refresh token again +curl -X POST ... (with OLD refresh_token) + +# Should return error: "invalid_grant" - token has been revoked +``` + +--- + +## FAQ + +### Q: How long do refresh tokens last? +**A:** By default, 30 days. This is configurable per application (1-90 days). + +### Q: Can I use the same refresh token multiple times? +**A:** No. Refresh tokens are **single-use**. After using a refresh token, you get a new one. + +### Q: What happens if my refresh token is stolen? +**A:** If someone tries to use a revoked refresh token, all tokens in that family are immediately revoked (token rotation security). + +### Q: Do I need to store the ID token? +**A:** Usually no. The ID token is for authentication (verify user identity). You typically decode it, verify it, extract claims, then discard it. + +### Q: Can I refresh an access token before it expires? +**A:** Yes! It's recommended to refresh tokens 5-10 minutes before expiration to avoid race conditions. + +### Q: What if my refresh token expires? +**A:** User must re-authenticate via the normal OAuth flow (redirect to /oauth/authorize). + +### Q: Can I revoke all of a user's sessions at once? +**A:** Yes, but you need to track all refresh tokens per user on your backend, then revoke them all. + +### Q: Are access tokens revocable? +**A:** Yes! You can revoke access tokens using the same `/oauth/revoke` endpoint. + +--- + +## Migration Guide (From Access Token Only) + +### Before (Access Token Only): +```javascript +// User logs in +const tokens = await exchangeAuthCode(code); +localStorage.setItem('access_token', tokens.access_token); + +// After 1 hour -> Token expires -> Redirect to login +if (isTokenExpired()) { + window.location = '/oauth/authorize'; +} +``` + +### After (With Refresh Tokens): +```javascript +// User logs in +const tokens = await exchangeAuthCode(code); +sessionStorage.setItem('access_token', tokens.access_token); +secureStorage.set('refresh_token', tokens.refresh_token); // Encrypted + +// After 1 hour -> Refresh silently +if (isTokenExpired()) { + const newTokens = await refreshAccessToken(); + sessionStorage.setItem('access_token', newTokens.access_token); + secureStorage.set('refresh_token', newTokens.refresh_token); +} +``` + +--- + +## Additional Resources + +- **RFC 6749 (OAuth 2.0):** https://datatracker.ietf.org/doc/html/rfc6749 +- **RFC 7009 (Token Revocation):** https://datatracker.ietf.org/doc/html/rfc7009 +- **OIDC Core Spec:** https://openid.net/specs/openid-connect-core-1_0.html +- **OAuth 2.0 Security Best Practices:** https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics + +--- + +## Support + +For issues or questions about refresh token implementation, contact your Clinch administrator or check the application documentation. + +**Version:** 1.0 +**Last Updated:** November 2025 diff --git a/docs/rodauth-oauth-analysis.md b/docs/rodauth-oauth-analysis.md new file mode 100644 index 0000000..cc6f336 --- /dev/null +++ b/docs/rodauth-oauth-analysis.md @@ -0,0 +1,913 @@ +# Rodauth-OAuth Analysis: Comprehensive Comparison with Clinch's Custom Implementation + +## Executive Summary + +**Rodauth-OAuth** is a production-ready Ruby gem that implements the OAuth 2.0 framework and OpenID Connect on top of the `rodauth` authentication library. It's architected as a modular feature-based system that integrates with Roda (a routing library) and provides extensive OAuth/OIDC capabilities. + +Your current Clinch implementation is a **custom, minimalist Rails-based OIDC provider** focusing on the authorization code grant with PKCE support. Switching to rodauth-oauth would provide significantly more features and standards compliance but requires architectural changes. + +--- + +## 1. What Rodauth-OAuth Is + +### Core Identity +- **Type**: Ruby gem providing OAuth 2.0 & OpenID Connect implementation +- **Framework**: Built on top of `rodauth` (a dedicated authentication library) +- **Web Framework**: Designed for Roda framework (lightweight, routing-focused) +- **Rails Support**: Available via `rodauth-rails` wrapper +- **Maturity**: Production-ready, OpenID-Certified for multiple profiles +- **Author**: Tiago Cardoso (tiago.cardoso@gmail.com) +- **License**: Apache 2.0 + +### Architecture Philosophy +- **Feature-based**: Modular "features" that can be enabled/disabled +- **Database-agnostic**: Uses Sequel ORM, works with any SQL database +- **Highly configurable**: Override methods to customize behavior +- **Standards-focused**: Implements RFCs and OpenID specs strictly + +--- + +## 2. File Structure and Organization + +### Directory Layout in `/tmp/rodauth-oauth` + +``` +rodauth-oauth/ +├── lib/ +│ └── rodauth/ +│ ├── oauth.rb # Main module entry point +│ ├── oauth/ +│ │ ├── version.rb +│ │ ├── database_extensions.rb +│ │ ├── http_extensions.rb +│ │ ├── jwe_extensions.rb +│ │ └── ttl_store.rb +│ └── features/ # 34 feature files! +│ ├── oauth_base.rb # Foundation +│ ├── oauth_authorization_code_grant.rb +│ ├── oauth_pkce.rb +│ ├── oauth_jwt*.rb # JWT support (5 files) +│ ├── oidc.rb # OpenID Core +│ ├── oidc_*logout.rb # Logout flows (3 files) +│ ├── oauth_client_credentials_grant.rb +│ ├── oauth_device_code_grant.rb +│ ├── oauth_token_revocation.rb +│ ├── oauth_token_introspection.rb +│ ├── oauth_dynamic_client_registration.rb +│ ├── oauth_dpop.rb # DPoP support +│ ├── oauth_tls_client_auth.rb +│ ├── oauth_pushed_authorization_request.rb +│ ├── oauth_assertion_base.rb +│ └── ... (more features) +├── test/ +│ ├── migrate/ # Database migrations +│ │ ├── 001_accounts.rb +│ │ ├── 003_oauth_applications.rb +│ │ ├── 004_oauth_grants.rb +│ │ ├── 005_pushed_requests.rb +│ │ ├── 006_saml_settings.rb +│ │ └── 007_dpop_proofs.rb +│ └── [multiple test directories with hundreds of tests] +├── examples/ # Full working examples +│ ├── authorization_server/ +│ ├── oidc/ +│ ├── jwt/ +│ ├── device_grant/ +│ ├── saml_assertion/ +│ └── mtls/ +├── templates/ # HTML/ERB templates +├── locales/ # i18n translations +├── doc/ +└── [Gemfile, README, MIGRATION-GUIDE, etc.] +``` + +### Feature Count: 34 Features! + +The gem is completely modular. Each feature can be independently enabled: + +**Core OAuth Features:** +- `oauth_base` - Foundation +- `oauth_authorization_code_grant` - Authorization Code Flow +- `oauth_implicit_grant` - Implicit Flow +- `oauth_client_credentials_grant` - Client Credentials Flow +- `oauth_device_code_grant` - Device Code Flow + +**Token Management:** +- `oauth_token_revocation` - RFC 7009 +- `oauth_token_introspection` - RFC 7662 +- `oauth_refresh_token` - Refresh tokens + +**Security & Advanced:** +- `oauth_pkce` - RFC 7636 (what Clinch is using!) +- `oauth_jwt` - JWT Access Tokens +- `oauth_jwt_bearer_grant` - RFC 7523 +- `oauth_saml_bearer_grant` - RFC 7522 +- `oauth_tls_client_auth` - Mutual TLS +- `oauth_dpop` - Demonstrating Proof-of-Possession +- `oauth_jwt_secured_authorization_request` - Request Objects +- `oauth_resource_indicators` - RFC 8707 +- `oauth_pushed_authorization_request` - RFC 9126 + +**OpenID Connect:** +- `oidc` - Core OpenID Connect +- `oidc_session_management` - Session Management +- `oidc_rp_initiated_logout` - RP-Initiated Logout +- `oidc_frontchannel_logout` - Front-Channel Logout +- `oidc_backchannel_logout` - Back-Channel Logout +- `oidc_dynamic_client_registration` - Dynamic Registration +- `oidc_self_issued` - Self-Issued Provider + +**Management & Discovery:** +- `oauth_application_management` - Client app dashboard +- `oauth_grant_management` - Grant management dashboard +- `oauth_dynamic_client_registration` - RFC 7591/7592 +- `oauth_jwt_jwks` - JWKS endpoint + +--- + +## 3. OIDC/OAuth Features Provided + +### Grant Types Supported (15 types!) + +| Grant Type | Status | RFC/Spec | +|-----------|--------|----------| +| Authorization Code | Yes | RFC 6749 | +| Implicit | Optional | RFC 6749 | +| Client Credentials | Optional | RFC 6749 | +| Device Code | Optional | RFC 8628 | +| Refresh Token | Yes | RFC 6749 | +| JWT Bearer | Optional | RFC 7523 | +| SAML Bearer | Optional | RFC 7522 | + +### Response Types & Modes + +**Response Types:** +- `code` (Authorization Code) - Default +- `id_token` (OIDC Implicit) - Optional +- `token` (Implicit) - Optional +- `id_token token` (Hybrid) - Optional +- `code id_token` (Hybrid) - Optional +- `code token` (Hybrid) - Optional +- `code id_token token` (Hybrid) - Optional + +**Response Modes:** +- `query` (URL parameters) +- `fragment` (URL fragment) +- `form_post` (HTML form) +- `jwt` (JWT-based response) + +### OpenID Connect Features + +✓ **Certified for:** +- Basic OP (OpenID Provider) +- Implicit OP +- Hybrid OP +- Config OP (Discovery) +- Dynamic OP (Dynamic Client Registration) +- Form Post OP +- 3rd Party-Init OP +- Session Management OP +- RP-Initiated Logout OP +- Front-Channel Logout OP +- Back-Channel Logout OP + +✓ **Standard Claims Support:** +- `openid`, `email`, `profile`, `address`, `phone` scopes +- Automatic claim mapping per OpenID spec +- Custom claims via extension + +✓ **Token Features:** +- JWT ID Tokens +- JWT Access Tokens +- Encrypted JWTs (JWE support) +- HMAC-SHA256 signing +- RSA/EC signing +- Custom token formats + +### Security Features + +| Feature | Details | +|---------|---------| +| PKCE | RFC 7636 - Proof Key for Public Clients | +| Token Hashing | Bcrypt-based token storage (plain text optional) | +| DPoP | RFC 9449 - Demonstrating Proof-of-Possession | +| TLS Client Auth | RFC 8705 - Mutual TLS authentication | +| Request Objects | JWT-signed/encrypted authorization requests | +| Pushed Auth Requests | RFC 9126 - Pushed Authorization Requests | +| Token Introspection | RFC 7662 - Token validation without DB lookup | +| Token Revocation | RFC 7009 - Revoke tokens on demand | + +### Scopes & Authorization + +- Configurable scope list per application +- Offline access support (refresh tokens) +- Scope-based access control +- Custom scope handlers +- Consent UI for user authorization + +--- + +## 4. Architecture: How It Works + +### As a Plugin System + +Rodauth-OAuth integrates with Roda as a **plugin**: + +```ruby +# This is how you configure it +class AuthServer < Roda + plugin :rodauth do + db database_connection + + # Enable features + enable :login, :logout, :create_account, :oidc, :oidc_session_management, + :oauth_pkce, :oauth_authorization_code_grant + + # Configure + oauth_application_scopes %w[openid email profile] + oauth_require_pkce true + hmac_secret "SECRET" + + # Customize with blocks + oauth_jwt_keys("RS256" => [private_key]) + oauth_jwt_public_keys("RS256" => [public_key]) + end +end +``` + +### Request Flow Architecture + +``` +1. Authorization Request + ↓ + rodauth validates params + ↓ + (if not auth'd) user logs in via rodauth + ↓ + (if first use) consent page rendered + ↓ + create oauth_grant (code, nonce, PKCE challenge, etc.) + ↓ + redirect with auth code + +2. Token Exchange + ↓ + rodauth validates client (Basic/POST auth) + ↓ + validates code, redirect_uri, PKCE verifier + ↓ + creates access token (plain or JWT) + ↓ + creates refresh token + ↓ + returns JSON with tokens + +3. UserInfo + ↓ + validate access token + ↓ + lookup grant/account + ↓ + return claims as JSON +``` + +### Feature Composition + +Features depend on each other. For example: +- `oidc` depends on: `active_sessions`, `oauth_jwt`, `oauth_jwt_jwks`, `oauth_authorization_code_grant`, `oauth_implicit_grant` +- `oauth_pkce` depends on: `oauth_authorization_code_grant` +- `oidc_rp_initiated_logout` depends on: `oidc` + +This is a **strong dependency injection pattern**. + +--- + +## 5. Database Schema Requirements + +### Rodauth-OAuth Tables + +#### `accounts` table (from rodauth) +```sql +CREATE TABLE accounts ( + id INTEGER PRIMARY KEY, + status_id INTEGER DEFAULT 1, -- unverified/verified/closed + email VARCHAR UNIQUE NOT NULL, + -- password-related columns (added by rodauth features) + password_hash VARCHAR, + -- other rodauth-managed columns +); +``` + +#### `oauth_applications` table (75+ columns!) +```sql +CREATE TABLE oauth_applications ( + id INTEGER PRIMARY KEY, + account_id INTEGER FOREIGN KEY, + + -- Basic info + name VARCHAR NOT NULL, + description VARCHAR, + homepage_url VARCHAR, + logo_uri VARCHAR, + tos_uri VARCHAR, + policy_uri VARCHAR, + + -- OAuth credentials + client_id VARCHAR UNIQUE NOT NULL, + client_secret VARCHAR UNIQUE NOT NULL, + registration_access_token VARCHAR, + + -- OAuth config + redirect_uri VARCHAR NOT NULL, + scopes VARCHAR NOT NULL, + token_endpoint_auth_method VARCHAR, + grant_types VARCHAR, + response_types VARCHAR, + response_modes VARCHAR, + + -- JWT/JWKS + jwks_uri VARCHAR, + jwks TEXT, + jwt_public_key TEXT, + + -- OIDC-specific + sector_identifier_uri VARCHAR, + application_type VARCHAR, + initiate_login_uri VARCHAR, + subject_type VARCHAR, + + -- Token encryption algorithms + id_token_signed_response_alg VARCHAR, + id_token_encrypted_response_alg VARCHAR, + id_token_encrypted_response_enc VARCHAR, + userinfo_signed_response_alg VARCHAR, + userinfo_encrypted_response_alg VARCHAR, + userinfo_encrypted_response_enc VARCHAR, + + -- Request object handling + request_object_signing_alg VARCHAR, + request_object_encryption_alg VARCHAR, + request_object_encryption_enc VARCHAR, + request_uris VARCHAR, + require_signed_request_object BOOLEAN, + + -- PAR (Pushed Auth Requests) + require_pushed_authorization_requests BOOLEAN DEFAULT FALSE, + + -- DPoP + dpop_bound_access_tokens BOOLEAN DEFAULT FALSE, + + -- TLS Client Auth + tls_client_auth_subject_dn VARCHAR, + tls_client_auth_san_dns VARCHAR, + tls_client_auth_san_uri VARCHAR, + tls_client_auth_san_ip VARCHAR, + tls_client_auth_san_email VARCHAR, + tls_client_certificate_bound_access_tokens BOOLEAN DEFAULT FALSE, + + -- Logout URIs + post_logout_redirect_uris VARCHAR, + frontchannel_logout_uri VARCHAR, + frontchannel_logout_session_required BOOLEAN DEFAULT FALSE, + backchannel_logout_uri VARCHAR, + backchannel_logout_session_required BOOLEAN DEFAULT FALSE, + + -- Response encryption + authorization_signed_response_alg VARCHAR, + authorization_encrypted_response_alg VARCHAR, + authorization_encrypted_response_enc VARCHAR, + + contact_info VARCHAR, + software_id VARCHAR, + software_version VARCHAR +); +``` + +#### `oauth_grants` table (everything in one table!) +```sql +CREATE TABLE oauth_grants ( + id INTEGER PRIMARY KEY, + account_id INTEGER FOREIGN KEY, -- nullable for client credentials + oauth_application_id INTEGER FOREIGN KEY, + sub_account_id INTEGER, -- for context-based ownership + + type VARCHAR, -- 'authorization_code', 'refresh_token', etc. + + -- Authorization code flow + code VARCHAR UNIQUE (per app), + redirect_uri VARCHAR, + + -- Tokens (stored hashed or plain) + token VARCHAR UNIQUE, + token_hash VARCHAR UNIQUE, + refresh_token VARCHAR UNIQUE, + refresh_token_hash VARCHAR UNIQUE, + + -- Expiry + expires_in TIMESTAMP NOT NULL, + revoked_at TIMESTAMP, + + -- Scopes + scopes VARCHAR NOT NULL, + access_type VARCHAR DEFAULT 'offline', -- 'offline' or 'online' + + -- PKCE + code_challenge VARCHAR, + code_challenge_method VARCHAR, -- 'plain' or 'S256' + + -- Device Code Grant + user_code VARCHAR UNIQUE, + last_polled_at TIMESTAMP, + + -- TLS Client Auth + certificate_thumbprint VARCHAR, + + -- Resource Indicators + resource VARCHAR, + + -- OpenID Connect + nonce VARCHAR, + acr VARCHAR, -- Authentication Context Class + claims_locales VARCHAR, + claims VARCHAR, -- custom OIDC claims + + -- DPoP + dpop_jkt VARCHAR -- DPoP key thumbprint +); +``` + +#### Optional Tables for Advanced Features + +```sql +-- For Pushed Authorization Requests +CREATE TABLE oauth_pushed_requests ( + request_uri VARCHAR UNIQUE PRIMARY KEY, + oauth_application_id INTEGER FOREIGN KEY, + params TEXT, -- JSON params + created_at TIMESTAMP +); + +-- For SAML Assertion Grant +CREATE TABLE oauth_saml_settings ( + id INTEGER PRIMARY KEY, + oauth_application_id INTEGER FOREIGN KEY, + idp_url VARCHAR, + certificate TEXT, + -- ... +); + +-- For DPoP +CREATE TABLE oauth_dpop_proofs ( + id INTEGER PRIMARY KEY, + oauth_grant_id INTEGER FOREIGN KEY, + jti VARCHAR UNIQUE, + created_at TIMESTAMP +); +``` + +### Key Differences from Your Implementation + +| Aspect | Your Implementation | Rodauth-OAuth | +|--------|-------------------|----------------| +| Authorization Codes | Separate table | In oauth_grants | +| Access Tokens | Separate table | In oauth_grants | +| Refresh Tokens | Not implemented | In oauth_grants | +| Token Hashing | Not done | Bcrypt (default) | +| Applications | Basic (name, client_id, secret) | 75+ columns for full spec | +| PKCE | Simple columns | Built-in feature | +| Account Data | In users table | In accounts table | +| Session Management | Session model | Rodauth's account_active_session_keys | +| User Consent | OidcUserConsent table | In memory or via hooks | + +--- + +## 6. Integration Points with Rails + +### Via Rodauth-Rails Wrapper + +Rodauth-OAuth can be used in Rails through the `rodauth-rails` gem: + +```bash +# Install generator +gem 'rodauth-rails' +bundle install +rails generate rodauth:install +rails generate rodauth:oauth:install # Generates OIDC tables/migrations +rails generate rodauth:oauth:views # Generates templates +``` + +### Generated Components + +1. **Migration**: `db/migrate/*_create_rodauth_oauth.rb` + - Creates all OAuth tables + - Customizable column names via config + +2. **Models**: `app/models/` + - `RodauthApp` (configuration) + - `OauthApplication` (client app) + - `OauthGrant` (grants/tokens) + - Customizable! + +3. **Views**: `app/views/rodauth/` + - Authorization consent form + - Application management dashboard + - Grant management dashboard + +4. **Lib**: `lib/rodauth_app.rb` + - Main rodauth configuration + +### Rails Controller Integration + +```ruby +class BooksController < ApplicationController + before_action :require_oauth_authorization, only: %i[create update] + before_action :require_oauth_authorization_scopes, only: %i[create update] + + private + + def require_oauth_authorization(scope = "books.read") + rodauth.require_oauth_authorization(scope) + end +end +``` + +Or for route protection: + +```ruby +# config/routes.rb +namespace :api do + resources :books, only: [:index] # protected by rodauth +end +``` + +--- + +## 7. Architectural Comparison + +### Your Custom Implementation + +**Pros:** +- Simple, easy to understand +- Minimal dependencies (just JWT, OpenSSL) +- Lightweight database (small tables) +- Direct Rails integration +- Minimal features = less surface area + +**Cons:** +- Only supports Authorization Code + PKCE +- No refresh tokens +- No token revocation/introspection +- No client credentials grant +- No JWT access tokens +- Manual consent management +- Not standards-compliant (missing many OIDC features) +- Will need continuous custom development + +**Architecture:** +``` +Rails Controller + ↓ +OidcController (450 lines) + ↓ +OidcAuthorizationCode Model +OidcAccessToken Model +OidcUserConsent Model + ↓ +Database +``` + +### Rodauth-OAuth Implementation + +**Pros:** +- 34 built-in features +- OpenID-Certified +- Production-tested +- Highly configurable +- Comprehensive token management +- Standards-compliant (RFCs & OpenID specs) +- Strong test coverage (hundreds of tests) +- Active maintenance + +**Cons:** +- More complex (needs Roda/Rodauth knowledge) +- Larger codebase to learn +- Rails integration via wrapper (extra layer) +- Different paradigm (Roda vs Rails) +- More database columns to manage + +**Architecture:** +``` +Roda App + ↓ +Rodauth Plugin (configurable) + ├── oauth_base (foundation) + ├── oauth_authorization_code_grant + ├── oauth_pkce + ├── oauth_jwt + ├── oidc (all OpenID features) + ├── [other optional features] + ↓ +Sequel ORM + ↓ +Database (flexible schema) +``` + +--- + +## 8. Feature Comparison Matrix + +| Feature | Your Impl | Rodauth-OAuth | Notes | +|---------|-----------|---------------|-------| +| **Authorization Code** | ✓ | ✓ | Both support | +| **PKCE** | ✓ | ✓ | Both support | +| **Refresh Tokens** | ✗ | ✓ | You'd need to add | +| **Implicit Flow** | ✗ | ✓ Optional | Legacy, not recommended | +| **Client Credentials** | ✗ | ✓ Optional | Machine-to-machine | +| **Device Code** | ✗ | ✓ Optional | IoT devices | +| **JWT Bearer Grant** | ✗ | ✓ Optional | Service accounts | +| **SAML Bearer Grant** | ✗ | ✓ Optional | Enterprise SAML | +| **JWT Access Tokens** | ✗ | ✓ Optional | Stateless tokens | +| **Token Revocation** | ✗ | ✓ | RFC 7009 | +| **Token Introspection** | ✗ | ✓ | RFC 7662 | +| **Pushed Auth Requests** | ✗ | ✓ Optional | RFC 9126 | +| **DPoP** | ✗ | ✓ Optional | RFC 9449 | +| **TLS Client Auth** | ✗ | ✓ Optional | RFC 8705 | +| **OpenID Connect** | ✓ Basic | ✓ Full | Yours is minimal | +| **ID Tokens** | ✓ | ✓ | Both support | +| **UserInfo Endpoint** | ✓ | ✓ | Both support | +| **Discovery** | ✓ | ✓ | Both support | +| **Session Management** | ✗ | ✓ Optional | Check session iframe | +| **RP-Init Logout** | ✓ | ✓ | Both support | +| **Front-Channel Logout** | ✗ | ✓ | Iframe-based | +| **Back-Channel Logout** | ✗ | ✓ | Server-to-server | +| **Dynamic Client Reg** | ✗ | ✓ Optional | RFC 7591/7592 | +| **Token Hashing** | ✗ | ✓ | Security best practice | +| **Scopes** | ✓ | ✓ | Both support | +| **Custom Claims** | ✓ Manual | ✓ Built-in | Yours via JWT service | +| **Consent UI** | ✓ | ✓ | Both support | +| **Client App Dashboard** | ✗ | ✓ Optional | Built-in | +| **Grant Management Dashboard** | ✗ | ✓ Optional | Built-in | + +--- + +## 9. Integration Complexity Analysis + +### Switching to Rodauth-OAuth + +#### Medium Complexity (Not Trivial, but Doable) + +**What you'd need to do:** + +1. **Learn Roda + Rodauth** + - Move from pure Rails to Roda-based architecture + - Understand rodauth feature system + - Time: 1-2 weeks for Rails developers + +2. **Migrate Database Schema** + - Consolidate tables: authorization codes + access tokens → oauth_grants + - Rename columns to match rodauth conventions + - Add many new columns for feature support + - Migration script needed: ~100-300 lines + - Time: 1 week development + testing + +3. **Replace Your OIDC Code** + - Replace your 450-line OidcController + - Remove your 3 model files + - Keep your OidcJwtService (mostly compatible) + - Add rodauth configuration + - Time: 1-2 weeks + +4. **Update Application/Client Model** + - Expand `Application` model properties + - Support all OAuth scopes, grant types, response types + - Time: 3-5 days + +5. **Create Migrations from Template** + - Use rodauth-oauth migration templates + - Customize for your database + - Time: 2-3 days + +6. **Testing** + - Write integration tests + - Verify all OAuth flows still work + - Check token validation logic + - Time: 2-3 weeks + +**Total Effort:** 4-8 weeks for experienced team + +### Keeping Your Implementation (Custom Path) + +#### What You'd Need to Add + +To reach feature parity with rodauth-oauth (for common use cases): + +1. **Refresh Token Support** (1-2 weeks) + - Database schema + - Token refresh endpoint + - Token validation logic + +2. **Token Revocation** (1 week) + - Revocation endpoint + - Token blacklist/invalidation + +3. **Token Introspection** (1 week) + - Introspection endpoint + - Token validation without DB lookup + +4. **Client Credentials Grant** (2 weeks) + - Endpoint logic + - Client authentication + - Token generation for apps + +5. **Improved Security** (ongoing) + - Token hashing (bcrypt) + - Rate limiting + - Additional validation + +6. **Advanced OIDC Features** + - Session Management + - Logout endpoints (front/back-channel) + - Dynamic client registration + - Device code flow + +**Total Effort:** 2-3 months ongoing + +--- + +## 10. Key Findings & Recommendations + +### What Rodauth-OAuth Does Better + +1. **Standards Compliance** + - Certified for 11 OpenID Connect profiles + - Implements 20+ RFCs and specs + - Regular spec updates + +2. **Security** + - Token hashing by default + - DPoP support (token binding) + - TLS client auth + - Proper scope enforcement + +3. **Features** + - 34 optional features (you get what you need) + - No bloat - only enable what you use + - Mature refresh token handling + +4. **Production Readiness** + - Thousands of test cases + - Open source (auditable) + - Active maintenance + - Real-world deployments + +5. **Flexibility** + - Works with any SQL database + - Highly configurable column names + - Custom behavior via overrides + - Multiple app types support + +### What Your Implementation Does Better + +1. **Simplicity** + - Fewer dependencies + - Smaller codebase + - Easier to reason about + +2. **Rails Integration** + - Direct Rails ActiveRecord + - No Roda learning curve + - Familiar patterns + +3. **Control** + - Full control of every line + - No surprises + - Easy to debug + +### Recommendation + +**Use Rodauth-OAuth IF:** +- You need a production OIDC/OAuth provider +- You want standards compliance +- You plan to support multiple grant types +- You need token revocation/introspection +- You want a maintained codebase + +**Keep Your Custom Implementation IF:** +- Authorization Code + PKCE only is sufficient +- You're avoiding Roda/Rodauth learning curve +- Your org standardizes on Rails patterns +- You have time to add features incrementally +- You need maximum control and simplicity + +**Hybrid Approach:** +- Use rodauth-oauth for OIDC/OAuth server components +- Keep your Rails app for other features +- They can coexist (separate services) + +--- + +## 11. Migration Path (If You Decide to Switch) + +### Phase 1: Preparation (Week 1-2) +- Set up separate Roda app with rodauth-oauth +- Run alongside your existing service +- Parallel user testing + +### Phase 2: Data Migration (Week 2-3) +- Create migration script for oauth_grants table +- Backfill existing auth codes and tokens +- Verify data integrity + +### Phase 3: Gradual Cutover (Week 4-6) +- Direct some OAuth clients to new server +- Monitor for issues +- Swap over when confident + +### Phase 4: Cleanup (Week 6+) +- Remove custom OIDC code +- Decommission old tables +- Document new architecture + +--- + +## 12. Code Examples + +### Rodauth-OAuth: Minimal Setup + +```ruby +# Gemfile +gem 'roda' +gem 'rodauth-oauth' +gem 'sequel' + +# lib/auth_server.rb +class AuthServer < Roda + plugin :render, views: 'views' + plugin :sessions, secret: 'SECRET' + + plugin :rodauth do + db DB + enable :login, :logout, :create_account, :oidc, :oauth_pkce, + :oauth_authorization_code_grant, :oauth_token_introspection + + oauth_application_scopes %w[openid email profile] + oauth_require_pkce true + hmac_secret 'HMAC_SECRET' + + oauth_jwt_keys('RS256' => [private_key]) + end + + route do |r| + r.rodauth # All OAuth routes automatically mounted + + # Your custom routes + r.get 'api' do + rodauth.require_oauth_authorization('api.read') + # return data + end + end +end +``` + +### Your Current Approach: Manual + +```ruby +# app/controllers/oidc_controller.rb +def authorize + validate_params + find_application + check_authentication + handle_consent + generate_code + redirect_with_code +end + +def token + extract_client_credentials + find_application + validate_code + check_pkce + generate_tokens + return_json +end +``` + +--- + +## Summary Table + +| Aspect | Your Implementation | Rodauth-OAuth | +|--------|-------------------|----------------| +| **Framework** | Rails | Roda | +| **Database ORM** | ActiveRecord | Sequel | +| **Grant Types** | 1 (Auth Code) | 7+ options | +| **Token Types** | Opaque | Opaque or JWT | +| **Security Features** | Basic | Advanced (DPoP, MTLS, etc.) | +| **OIDC Compliance** | Partial | Full (Certified) | +| **Lines of Code** | ~1000 | ~10,000+ | +| **Features** | 2-3 | 34 optional | +| **Maintenance Burden** | High | Low (OSS) | +| **Learning Curve** | Low | Medium (Roda) | +| **Production Ready** | Yes | Yes | +| **Community** | Just you | Active | + diff --git a/docs/rodauth-oauth-quick-reference.md b/docs/rodauth-oauth-quick-reference.md new file mode 100644 index 0000000..774b928 --- /dev/null +++ b/docs/rodauth-oauth-quick-reference.md @@ -0,0 +1,418 @@ +# Rodauth-OAuth: Quick Reference Guide + +## What Is It? +A production-ready Ruby gem implementing OAuth 2.0 and OpenID Connect. Think of it as a complete, standards-certified OAuth/OIDC server library for Ruby apps. + +## Key Stats +- **Framework**: Roda (not Rails, but works with Rails via wrapper) +- **Features**: 34 modular features you can enable/disable +- **Certification**: Officially certified for 11 OpenID Connect profiles +- **Test Coverage**: Hundreds of tests +- **Status**: Production-ready, actively maintained + +## Why Consider It? + +### Advantages Over Your Implementation +1. **Complete OAuth/OIDC Implementation** + - All major grant types supported + - Certified compliance with standards + - 20+ RFC implementations + +2. **Security Features** + - Token hashing (bcrypt) by default + - DPoP support (token binding) + - TLS mutual authentication + - Proper scope enforcement + +3. **Advanced Token Management** + - Refresh tokens (you don't have) + - Token revocation + - Token introspection + - Token rotation policies + +4. **Low Maintenance** + - Well-tested codebase + - Active community + - Regular spec updates + - Battle-tested in production + +5. **Extensible** + - Highly configurable + - Override any behavior you need + - Database-agnostic + - Works with any SQL DB + +### What Your Implementation Does Better +1. **Simplicity** - Fewer lines of code, easier to understand +2. **Rails Native** - No need to learn Roda +3. **Control** - Full ownership of the codebase +4. **Minimal Dependencies** - Just JWT and OpenSSL + +## Architecture Overview + +### Your Current Setup +``` +Rails App + └─ OidcController (450 lines) + ├─ /oauth/authorize + ├─ /oauth/token + ├─ /oauth/userinfo + └─ /logout + +Models: + ├─ OidcAuthorizationCode + ├─ OidcAccessToken + └─ OidcUserConsent + +Features Supported: + ├─ Authorization Code Flow ✓ + ├─ PKCE ✓ + └─ Basic OIDC ✓ + +NOT Supported: + ├─ Refresh Tokens + ├─ Token Revocation + ├─ Token Introspection + ├─ Client Credentials Grant + ├─ Device Code Flow + ├─ Session Management + ├─ Front/Back-Channel Logout + └─ Dynamic Client Registration +``` + +### Rodauth-OAuth Setup +``` +Roda App (web framework) + └─ Rodauth Plugin (authentication/authorization) + ├─ oauth_base (foundation) + ├─ oauth_authorization_code_grant + ├─ oauth_pkce + ├─ oauth_jwt (optional) + ├─ oidc (OpenID core) + ├─ oidc_session_management (optional) + ├─ oidc_rp_initiated_logout (optional) + ├─ oidc_frontchannel_logout (optional) + ├─ oidc_backchannel_logout (optional) + ├─ oauth_token_revocation (optional) + ├─ oauth_token_introspection (optional) + ├─ oauth_client_credentials_grant (optional) + └─ ... (28+ more optional features) + +Routes Generated Automatically: + ├─ /.well-known/openid-configuration ✓ + ├─ /.well-known/jwks.json ✓ + ├─ /oauth/authorize ✓ + ├─ /oauth/token ✓ + ├─ /oauth/userinfo ✓ + ├─ /oauth/introspect (optional) + ├─ /oauth/revoke (optional) + └─ /logout ✓ +``` + +## Database Schema Comparison + +### Your Current Tables +``` +oidc_authorization_codes + ├─ id + ├─ user_id + ├─ application_id + ├─ code (unique) + ├─ redirect_uri + ├─ scope + ├─ nonce + ├─ code_challenge + ├─ code_challenge_method + ├─ used (boolean) + ├─ expires_at + └─ created_at + +oidc_access_tokens + ├─ id + ├─ user_id + ├─ application_id + ├─ token (unique) + ├─ scope + ├─ expires_at + └─ created_at + +oidc_user_consents + ├─ user_id + ├─ application_id + ├─ scopes_granted + └─ granted_at + +applications + ├─ id + ├─ name + ├─ client_id (unique) + ├─ client_secret + ├─ redirect_uris (JSON) + ├─ app_type + └─ ... (few more fields) +``` + +### Rodauth-OAuth Tables +``` +accounts (from rodauth) + ├─ id + ├─ status_id + ├─ email + └─ password_hash + +oauth_applications (75+ columns!) + ├─ Basic: id, account_id, name, description + ├─ OAuth: client_id, client_secret, redirect_uri, scopes + ├─ Config: token_endpoint_auth_method, grant_types, response_types + ├─ JWT/JWKS: jwks_uri, jwks, jwt_public_key + ├─ OIDC: subject_type, id_token_signed_response_alg, etc. + ├─ PAR: require_pushed_authorization_requests + ├─ DPoP: dpop_bound_access_tokens + ├─ TLS: tls_client_auth_* fields + └─ Logout: post_logout_redirect_uris, frontchannel_logout_uri, etc. + +oauth_grants (consolidated - replaces your two tables!) + ├─ id, account_id, oauth_application_id + ├─ type (authorization_code, refresh_token, etc.) + ├─ code, token, refresh_token (with hashed versions) + ├─ expires_in, revoked_at + ├─ scopes, access_type + ├─ code_challenge, code_challenge_method (PKCE) + ├─ user_code, last_polled_at (Device code grant) + ├─ nonce, acr, claims (OIDC) + ├─ dpop_jkt (DPoP) + └─ certificate_thumbprint, resource (advanced) + +[Optional tables for features you enable] +``` + +## Feature Comparison Matrix + +| Feature | Your Code | Rodauth-OAuth | Effort to Add* | +|---------|-----------|---------------|--------| +| Authorization Code Flow | ✓ | ✓ | N/A | +| PKCE | ✓ | ✓ | N/A | +| Refresh Tokens | ✗ | ✓ | 1-2 weeks | +| Token Revocation | ✗ | ✓ | 1 week | +| Token Introspection | ✗ | ✓ | 1 week | +| Client Credentials Grant | ✗ | ✓ | 2 weeks | +| Device Code Flow | ✗ | ✓ | 3 weeks | +| JWT Access Tokens | ✗ | ✓ | 1 week | +| Session Management | ✗ | ✓ | 2-3 weeks | +| Front-Channel Logout | ✗ | ✓ | 1-2 weeks | +| Back-Channel Logout | ✗ | ✓ | 2 weeks | +| Dynamic Client Reg | ✗ | ✓ | 3-4 weeks | +| Token Hashing | ✗ | ✓ | 1 week | + +*Time estimates for adding to your implementation + +## Code Examples + +### Rodauth-OAuth: Minimal OAuth Server +```ruby +# Gemfile +gem 'roda' +gem 'rodauth-oauth' +gem 'sequel' + +# lib/auth_server.rb +class AuthServer < Roda + plugin :sessions, secret: ENV['SESSION_SECRET'] + plugin :rodauth do + db DB + enable :login, :logout, :create_account, + :oidc, :oauth_pkce, :oauth_authorization_code_grant, + :oauth_token_revocation + + oauth_application_scopes %w[openid email profile] + oauth_require_pkce true + end + + route do |r| + r.rodauth # All OAuth endpoints auto-mounted! + + # Your app logic here + end +end +``` + +That's it! All these endpoints are automatically available: +- GET /.well-known/openid-configuration +- GET /.well-known/jwks.json +- GET /oauth/authorize +- POST /oauth/token +- POST /oauth/revoke +- GET /oauth/userinfo +- GET /logout + +### Your Current Approach +```ruby +# app/controllers/oidc_controller.rb +class OidcController < ApplicationController + def authorize + # 150 lines of validation logic + end + + def token + # 100 lines of token generation logic + end + + def userinfo + # 50 lines of claims logic + end + + def logout + # 50 lines of logout logic + end + + private + + def validate_pkce(auth_code, code_verifier) + # 50 lines of PKCE validation + end +end +``` + +## Integration Paths + +### Option 1: Stick with Your Implementation +- Keep building features incrementally +- Effort: 2-3 months to reach feature parity +- Pro: Rails native, full control +- Con: Continuous maintenance burden + +### Option 2: Switch to Rodauth-OAuth +- Learn Roda/Rodauth (1-2 weeks) +- Migrate database (1 week) +- Replace 450 lines of code with config (1 week) +- Testing & validation (2-3 weeks) +- Effort: 4-8 weeks total +- Pro: Production-ready, certified, maintained +- Con: Different framework (Roda) + +### Option 3: Hybrid Approach +- Keep your Rails app for business logic +- Use rodauth-oauth as separate OAuth/OIDC service +- Services communicate via HTTP/APIs +- Effort: 2-3 weeks (independent services) +- Pro: Best of both worlds +- Con: Operational complexity + +## Decision Matrix + +### Use Rodauth-OAuth If You Need... +- [x] Standards compliance (OpenID certified) +- [x] Multiple grant types (Client Credentials, Device Code, etc.) +- [x] Token revocation/introspection +- [x] Refresh tokens +- [x] Advanced logout (front/back-channel) +- [x] Session management +- [x] Token hashing/security best practices +- [x] Hands-off maintenance +- [x] Production-battle-tested code + +### Keep Your Implementation If You... +- [x] Only need Authorization Code + PKCE +- [x] Want zero Roda/external framework learning +- [x] Value Rails patterns over standards +- [x] Like to understand every line of code +- [x] Can allocate time for ongoing maintenance +- [x] Prefer minimal dependencies + +## Key Differences You'll Notice + +### 1. Framework Paradigm +- **Your impl**: Rails (MVC, familiar) +- **Rodauth**: Roda (routing-focused, lightweight) + +### 2. Database ORM +- **Your impl**: ActiveRecord (Rails native) +- **Rodauth**: Sequel (lighter, more control) + +### 3. Configuration Style +- **Your impl**: Rails initializers, environment variables +- **Rodauth**: Plugin block with DSL + +### 4. Model Management +- **Your impl**: Rails models with validations, associations +- **Rodauth**: Minimal models, logic in database + +### 5. Testing Approach +- **Your impl**: RSpec, model/controller tests +- **Rodauth**: Request-based integration tests + +## File Locations (If You Switch) + +``` +Current Structure +├── app/controllers/oidc_controller.rb +├── app/models/ +│ ├── oidc_authorization_code.rb +│ ├── oidc_access_token.rb +│ └── oidc_user_consent.rb +├── app/services/oidc_jwt_service.rb +├── db/migrate/*oidc*.rb + +Rodauth-OAuth Equivalent +├── lib/rodauth_app.rb # Configuration (replaces most controllers) +├── app/views/rodauth/ # Templates (consent form, etc.) +├── config/routes.rb # Simple: routes mount rodauth +└── db/migrate/*rodauth_oauth*.rb +``` + +## Performance Considerations + +### Your Implementation +- Small tables → fast queries +- Fewer columns → less overhead +- Simple token validation +- Estimated: 5-10ms per token validation + +### Rodauth-OAuth +- More columns, but same queries +- Optional token hashing (slight overhead) +- More features = more options checked +- Estimated: 10-20ms per token validation +- Can be optimized: disable unused features + +## Getting Started (If You Want to Explore) + +1. **Review the code** + ```bash + cd /Users/dkam/Development/clinch/tmp/rodauth-oauth + ls -la lib/rodauth/features/ # See all features + cat examples/oidc/authentication_server.rb # Full working example + ``` + +2. **Run the example** + ```bash + cd /Users/dkam/Development/clinch/tmp/rodauth-oauth/examples + ruby oidc/authentication_server.rb # Starts server on http://localhost:9292 + ``` + +3. **Read the key files** + - README.md: Overview + - MIGRATION-GUIDE-v1.md: Version migration (shows architecture) + - test/migrate/*.rb: Database schema + - examples/oidc/*.rb: Complete working implementation + +## Next Steps + +1. **If keeping your implementation:** + - Prioritize refresh token support + - Add token revocation endpoint + - Consider token hashing + +2. **If exploring rodauth-oauth:** + - Run the example server + - Review the feature files + - Check if hybrid approach works for your org + +3. **For either path:** + - Document your decision + - Plan feature roadmap + - Set up appropriate monitoring + +--- + +**Bottom Line**: Rodauth-OAuth is the "production-grade" option if you need comprehensive OAuth/OIDC. Your implementation is fine if you keep features minimal and have maintenance bandwidth. diff --git a/docs/traefik-example.md b/docs/traefik-example.md new file mode 100644 index 0000000..b501e95 --- /dev/null +++ b/docs/traefik-example.md @@ -0,0 +1,330 @@ +# Traefik ForwardAuth Configuration Examples + +## Basic Configuration (Protecting MEtube) + +### docker-compose.yml with Traefik Labels + +```yaml +version: '3' + +services: + # Clinch SSO + clinch: + image: your-clinch-image + labels: + - "traefik.enable=true" + - "traefik.http.routers.clinch.rule=Host(`clinch.yourdomain.com`)" + - "traefik.http.routers.clinch.entrypoints=websecure" + - "traefik.http.routers.clinch.tls.certresolver=letsencrypt" + - "traefik.http.services.clinch.loadbalancer.server.port=3000" + + # MEtube - Protected by Clinch + metube: + image: ghcr.io/alexta69/metube + labels: + - "traefik.enable=true" + - "traefik.http.routers.metube.rule=Host(`metube.yourdomain.com`)" + - "traefik.http.routers.metube.entrypoints=websecure" + - "traefik.http.routers.metube.tls.certresolver=letsencrypt" + + # ForwardAuth middleware + - "traefik.http.routers.metube.middlewares=metube-auth" + - "traefik.http.middlewares.metube-auth.forwardauth.address=http://clinch:3000/api/verify?app=metube" + - "traefik.http.middlewares.metube-auth.forwardauth.authResponseHeaders=Remote-User,Remote-Email,Remote-Groups,Remote-Admin" + + - "traefik.http.services.metube.loadbalancer.server.port=8081" +``` + +## Traefik Static Configuration (File) + +### traefik.yml + +```yaml +entryPoints: + web: + address: ":80" + http: + redirections: + entryPoint: + to: websecure + scheme: https + + websecure: + address: ":443" + +certificatesResolvers: + letsencrypt: + acme: + email: your-email@example.com + storage: /letsencrypt/acme.json + tlsChallenge: {} + +providers: + docker: + exposedByDefault: false + file: + filename: /config/dynamic.yml + watch: true +``` + +## Traefik Dynamic Configuration (File) + +### dynamic.yml + +```yaml +http: + middlewares: + # Clinch ForwardAuth middleware for MEtube + metube-auth: + forwardAuth: + address: "http://clinch:3000/api/verify?app=metube" + authResponseHeaders: + - "Remote-User" + - "Remote-Email" + - "Remote-Groups" + - "Remote-Admin" + + # Clinch ForwardAuth for Sonarr (with group restriction) + sonarr-auth: + forwardAuth: + address: "http://clinch:3000/api/verify?app=sonarr" + authResponseHeaders: + - "Remote-User" + - "Remote-Email" + - "Remote-Groups" + - "Remote-Admin" + + routers: + clinch: + rule: "Host(`clinch.yourdomain.com`)" + service: clinch + entryPoints: + - websecure + tls: + certResolver: letsencrypt + + metube: + rule: "Host(`metube.yourdomain.com`)" + service: metube + middlewares: + - metube-auth + entryPoints: + - websecure + tls: + certResolver: letsencrypt + + sonarr: + rule: "Host(`sonarr.yourdomain.com`)" + service: sonarr + middlewares: + - sonarr-auth + entryPoints: + - websecure + tls: + certResolver: letsencrypt + + services: + clinch: + loadBalancer: + servers: + - url: "http://clinch:3000" + + metube: + loadBalancer: + servers: + - url: "http://metube:8081" + + sonarr: + loadBalancer: + servers: + - url: "http://sonarr:8989" +``` + +## How It Works + +1. User visits `https://metube.yourdomain.com` +2. Traefik intercepts and applies the `metube-auth` middleware +3. Traefik makes request to `http://clinch:3000/api/verify?app=metube` +4. Clinch checks if user is authenticated and authorized: + - If **200**: Traefik forwards request to MEtube with user headers + - If **401/403**: Traefik redirects to Clinch login page +5. User signs into Clinch (with TOTP if enabled) +6. Clinch redirects back to MEtube +7. User can now access MEtube! + +## Setup Steps + +### 1. Create Applications in Clinch + +Via Rails console: + +```ruby +# MEtube - No groups = everyone can access +Application.create!( + name: "MEtube", + slug: "metube", + app_type: "trusted_header", + active: true +) + +# Sonarr - Restricted to media-managers group +media_group = Group.find_by(name: "media-managers") +sonarr = Application.create!( + name: "Sonarr", + slug: "sonarr", + app_type: "trusted_header", + active: true +) +ApplicationGroup.create!(application: sonarr, group: media_group) +``` + +### 2. Update Traefik Configuration + +Add the ForwardAuth middlewares and labels shown above. + +### 3. Restart Traefik + +```bash +docker-compose restart traefik +``` + +### 4. Test + +Visit https://metube.yourdomain.com - you should be redirected to Clinch login! + +## Advanced: Custom Error Pages + +```yaml +http: + middlewares: + clinch-errors: + errors: + status: + - "401-403" + service: clinch + query: "/signin?redirect={url}" + + metube-auth: + forwardAuth: + address: "http://clinch:3000/api/verify?app=metube" + authResponseHeaders: + - "Remote-User" + - "Remote-Email" + - "Remote-Groups" + - "Remote-Admin" + + routers: + metube: + rule: "Host(`metube.yourdomain.com`)" + service: metube + middlewares: + - metube-auth + - clinch-errors # Add custom error handling + entryPoints: + - websecure + tls: + certResolver: letsencrypt +``` + +## Kubernetes Ingress Example + +```yaml +apiVersion: traefik.containo.us/v1alpha1 +kind: Middleware +metadata: + name: clinch-metube-auth +spec: + forwardAuth: + address: http://clinch.clinch-system.svc.cluster.local:3000/api/verify?app=metube + authResponseHeaders: + - Remote-User + - Remote-Email + - Remote-Groups + - Remote-Admin + +--- + +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: metube + annotations: + traefik.ingress.kubernetes.io/router.middlewares: default-clinch-metube-auth@kubernetescrd +spec: + rules: + - host: metube.yourdomain.com + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: metube + port: + number: 8081 +``` + +## Troubleshooting + +### Users not staying logged in + +Ensure Traefik preserves cookies and sets correct headers: + +```yaml +http: + routers: + clinch: + middlewares: + - clinch-headers + + middlewares: + clinch-headers: + headers: + customRequestHeaders: + X-Forwarded-Host: "clinch.yourdomain.com" + X-Forwarded-Proto: "https" +``` + +### Authentication loop + +1. Check that `/api/verify` is accessible from Traefik +2. Verify the ForwardAuth middleware address is correct +3. Check Clinch logs for errors + +### Check Clinch logs + +```bash +docker-compose logs -f clinch +``` + +You'll see ForwardAuth log messages like: +``` +ForwardAuth: User user@example.com granted access to metube +ForwardAuth: Unauthorized - No session cookie +``` + +### Debug Traefik + +Enable access logs in `traefik.yml`: + +```yaml +accessLog: + filePath: "/var/log/traefik/access.log" + format: json +``` + +## Comparison: Traefik vs. Caddy + +### Traefik +- ✅ Better for Docker/Kubernetes environments +- ✅ Automatic service discovery +- ✅ Rich middleware system +- ❌ More complex configuration + +### Caddy +- ✅ Simpler configuration +- ✅ Automatic HTTPS by default +- ✅ Better for static configurations +- ❌ Less dynamic than Traefik + +Both work great with Clinch ForwardAuth! diff --git a/docs/webauthn-implementation-summary.md b/docs/webauthn-implementation-summary.md new file mode 100644 index 0000000..2fdfcd1 --- /dev/null +++ b/docs/webauthn-implementation-summary.md @@ -0,0 +1,238 @@ +# WebAuthn/Passkeys Implementation - Quick Start + +This is a companion summary to the [full implementation plan](webauthn-passkeys-plan.md). + +## What We're Building + +Add modern passwordless authentication (passkeys) to Clinch, allowing users to sign in with Face ID, Touch ID, Windows Hello, or hardware security keys (YubiKey). + +## Quick Overview + +### Features +- **Passwordless login** - Sign in with biometrics, no password needed +- **Multi-device support** - Register passkeys on multiple devices +- **Synced passkeys** - Works with iCloud Keychain, Google Password Manager +- **2FA option** - Use passkeys as second factor instead of TOTP +- **Hardware keys** - Support for YubiKey and other FIDO2 devices +- **User management** - Register, name, and delete multiple passkeys + +### Tech Stack +- `webauthn` gem (~3.0) - Server-side WebAuthn implementation +- Browser WebAuthn API - Native browser support (no JS libraries needed) +- Stimulus controller - Frontend UX management + +## 5-Phase Implementation + +### Phase 1: Foundation (Week 1-2) +Core WebAuthn registration and authentication +- Database schema for credentials +- Registration ceremony (add passkey) +- Authentication ceremony (sign in with passkey) +- Basic JavaScript integration + +### Phase 2: User Experience (Week 2-3) +Polished UI and management +- Profile page: list/manage passkeys +- Login page: "Sign in with Passkey" button +- Nickname management +- First-run wizard update + +### Phase 3: Security (Week 3-4) +Advanced security features +- Sign count verification (clone detection) +- Attestation validation (optional) +- User verification requirements +- Admin controls and policies + +### Phase 4: Integration (Week 4) +Connect with existing features +- OIDC integration (AMR claims) +- WebAuthn as 2FA option +- ForwardAuth compatibility +- Account recovery flows + +### Phase 5: Testing & Docs (Week 4-5) +Quality assurance +- Unit, integration, and system tests +- Virtual authenticator testing +- User and admin documentation +- Security audit + +## Database Schema + +### New Table: `webauthn_credentials` +```ruby +create_table :webauthn_credentials do |t| + t.references :user, null: false, foreign_key: true + t.string :external_id, null: false # Credential ID + t.string :public_key, null: false # Public key + t.integer :sign_count, default: 0 # For clone detection + t.string :nickname # "MacBook Touch ID" + t.string :authenticator_type # platform/cross-platform + t.datetime :last_used_at + t.timestamps +end +``` + +### Update `users` table +```ruby +add_column :users, :webauthn_id, :string # User handle +add_column :users, :webauthn_required, :boolean # Policy enforcement +``` + +## Key User Flows + +### 1. Register Passkey +``` +User profile → "Add Passkey" → Browser prompt → +Touch ID/Face ID → Passkey saved → Can sign in +``` + +### 2. Sign In with Passkey +``` +Login page → Enter email → "Continue with Passkey" → +Browser prompt → Touch ID/Face ID → Signed in +``` + +### 3. WebAuthn as 2FA +``` +Enter password → Prompted for passkey → +Touch ID/Face ID → Signed in +``` + +## Security Highlights + +1. **Phishing-resistant** - Passkeys are bound to the domain +2. **No shared secrets** - Public key cryptography +3. **Clone detection** - Sign count verification +4. **User verification** - Biometric or PIN required +5. **Privacy-preserving** - Opaque user handles + +## Integration Points + +### OIDC +- Add `amr` claim: `["webauthn"]` +- Support `acr_values=webauthn` in authorization request +- Include authentication method in ID token + +### ForwardAuth +- WebAuthn creates standard sessions +- Works automatically with existing `/api/verify` endpoint +- Optional header: `Remote-Auth-Method: webauthn` + +### Admin Controls +- Require WebAuthn for specific users/groups +- View all registered passkeys +- Revoke compromised credentials +- Audit log of authentications + +## Files to Create/Modify + +### New Files (~12) +- `app/models/webauthn_credential.rb` +- `app/controllers/webauthn_controller.rb` +- `app/javascript/controllers/webauthn_controller.js` +- `config/initializers/webauthn.rb` +- Views for registration/management +- Tests (model, controller, integration, system) +- Documentation (user guide, admin guide) + +### Modified Files (~8) +- `Gemfile` - Add webauthn gem +- `app/models/user.rb` - Add associations/methods +- `app/controllers/sessions_controller.rb` - WebAuthn authentication +- `app/views/sessions/new.html.erb` - Add passkey button +- `app/views/profiles/show.html.erb` - Passkey management +- `config/routes.rb` - WebAuthn routes +- `README.md` - Document feature +- `app/controllers/oidc_controller.rb` - AMR claims + +## Browser Support + +### Supported (WebAuthn Level 2) +- Chrome/Edge 90+ +- Firefox 90+ +- Safari 14+ (macOS Big Sur / iOS 14+) + +### Platform Authenticators +- macOS: Touch ID +- iOS/iPadOS: Face ID, Touch ID +- Windows: Windows Hello (face, fingerprint, PIN) +- Android: Fingerprint, face unlock + +### Roaming Authenticators +- YubiKey 5 series +- SoloKeys +- Google Titan Security Key +- Any FIDO2-certified hardware key + +## Open Questions + +1. **Attestation**: Validate authenticator hardware? (Recommend: No for v1) +2. **Resident Keys**: Require discoverable credentials? (Recommend: Preferred, not required) +3. **Synced Passkeys**: Allow iCloud/Google sync? (Recommend: Yes) +4. **Recovery**: How to recover if all passkeys lost? (Recommend: Email verification) +5. **2FA**: Replace TOTP or offer both? (Recommend: Offer both) +6. **Enforcement**: When to require passkeys? (Recommend: 3 months after launch for admins) + +## Success Metrics + +### Adoption +- % of users with ≥1 passkey +- % of logins using passkey vs password +- Average registration time + +### Security +- Reduced password reset requests +- Reduced account takeover attempts +- Zero phishing success (passkeys can't be phished) + +### Performance +- Faster authentication time +- Low error rate (<5%) +- High browser compatibility (>95%) + +## Timeline + +- **Week 1-2**: Foundation (Phase 1) +- **Week 2-3**: UX & Testing (Phase 2 + Phase 5 start) +- **Week 3-4**: Security & Integration (Phase 3 + Phase 4) +- **Week 4-5**: Beta testing and documentation +- **Week 5+**: Production rollout + +**Total**: 4-6 weeks for full implementation and testing + +## Next Steps + +1. ✅ Review this plan +2. ⬜ Create Gitea issues for each phase +3. ⬜ Add `webauthn` gem to Gemfile +4. ⬜ Create database migrations +5. ⬜ Implement Phase 1 (registration ceremony) +6. ⬜ Implement Phase 1 (authentication ceremony) +7. ⬜ Add JavaScript frontend +8. ⬜ Test with virtual authenticators +9. ⬜ Continue through remaining phases + +## Resources + +- [Full Implementation Plan](webauthn-passkeys-plan.md) - Detailed 50+ page document +- [W3C WebAuthn Spec](https://www.w3.org/TR/webauthn-2/) +- [webauthn-ruby gem](https://github.com/cedarcode/webauthn-ruby) +- [WebAuthn Guide](https://webauthn.guide/) +- [MDN Web Authentication API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Authentication_API) + +## Questions? + +Refer to the [full implementation plan](webauthn-passkeys-plan.md) for: +- Detailed technical specifications +- Security considerations +- Code examples +- Testing strategies +- Migration strategies +- Complete API reference + +--- + +*Status: Ready for Review* +*See: [webauthn-passkeys-plan.md](webauthn-passkeys-plan.md) for full details* diff --git a/docs/webauthn-passkeys-plan.md b/docs/webauthn-passkeys-plan.md new file mode 100644 index 0000000..001f478 --- /dev/null +++ b/docs/webauthn-passkeys-plan.md @@ -0,0 +1,787 @@ +# 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*