CRITICAL SECURITY FIX: - Email code required EVERY login (authentication, not verification) - DNS TXT check cached separately (domain verification) - New auth_sessions table for per-login state - Codes hashed with SHA-256, constant-time comparison - Max 3 attempts, 10-minute session expiry - OAuth params stored server-side (security improvement) New files: - services/auth_session.py - migrations 004, 005 - ADR-010: domain verification vs user authentication 312 tests passing, 86.21% coverage 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
7.4 KiB
Authentication Flow Fix Design
Problem Statement
The current implementation conflates domain verification (one-time DNS check) with user authentication (per-login email verification). This creates a security vulnerability where only the first user needs to authenticate via email code, while subsequent users bypass authentication entirely.
Core Concepts
Domain Verification
- Purpose: Establish that a domain is configured to use this IndieAuth server
- Method: DNS TXT record containing server-specific verification string
- Frequency: Once per domain, results cached in database
- Storage:
domainstable with verification status and timestamp
User Authentication
- Purpose: Prove the current user owns the claimed identity
- Method: Time-limited 6-digit code sent to rel="me" email
- Frequency: EVERY authorization attempt
- Storage: Temporary session storage, expires after 5-10 minutes
Corrected Authorization Flow
Step 1: Authorization Request
Client initiates OAuth flow:
GET /authorize?
response_type=code&
client_id=https://app.example.com&
redirect_uri=https://app.example.com/callback&
state=xyz&
code_challenge=abc&
code_challenge_method=S256&
me=https://user.example.com
Step 2: Domain Verification Check
- Extract domain from
meparameter - Check
domainstable for existing verification:SELECT verified, last_checked FROM domains WHERE domain = 'user.example.com' - If not verified or stale (>24 hours):
- Check DNS TXT record at
_indieauth.user.example.com - Update database with verification status
- Check DNS TXT record at
- If domain not verified, reject with error
Step 3: Profile Discovery
- Fetch the user's homepage at
meURL - Parse for IndieAuth metadata:
- Authorization endpoint (must be this server)
- Token endpoint (if present)
- rel="me" links for authentication options
- Extract email from rel="me" links
Step 4: User Authentication (ALWAYS REQUIRED)
- Generate 6-digit code
- Store in session with expiration:
{ "session_id": "uuid", "me": "https://user.example.com", "email": "user@example.com", "code": "123456", "client_id": "https://app.example.com", "redirect_uri": "https://app.example.com/callback", "state": "xyz", "code_challenge": "abc", "expires_at": "2024-01-01T12:05:00Z" } - Send code via email
- Show code entry form
Step 5: Code Verification
- User submits code
- Validate against session storage
- If valid, mark session as authenticated
- If invalid, allow retry (max 3 attempts)
Step 6: Consent
- Show consent page with client details
- User approves/denies
- If approved, generate authorization code
Step 7: Authorization Code
- Generate authorization code
- Store with session binding:
{ "code": "auth_code_xyz", "session_id": "uuid", "me": "https://user.example.com", "client_id": "https://app.example.com", "redirect_uri": "https://app.example.com/callback", "code_challenge": "abc", "expires_at": "2024-01-01T12:10:00Z" } - Redirect to client with code
Data Models
domains table (persistent)
CREATE TABLE domains (
domain VARCHAR(255) PRIMARY KEY,
verified BOOLEAN DEFAULT FALSE,
verification_string VARCHAR(255),
last_checked TIMESTAMP,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
auth_sessions table (temporary, cleaned periodically)
CREATE TABLE auth_sessions (
session_id VARCHAR(255) PRIMARY KEY,
me VARCHAR(255) NOT NULL,
email VARCHAR(255),
verification_code VARCHAR(6),
code_verified BOOLEAN DEFAULT FALSE,
client_id VARCHAR(255) NOT NULL,
redirect_uri VARCHAR(255) NOT NULL,
state VARCHAR(255),
code_challenge VARCHAR(255),
code_challenge_method VARCHAR(10),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
expires_at TIMESTAMP NOT NULL,
INDEX idx_expires (expires_at)
);
authorization_codes table (temporary)
CREATE TABLE authorization_codes (
code VARCHAR(255) PRIMARY KEY,
session_id VARCHAR(255) NOT NULL,
me VARCHAR(255) NOT NULL,
client_id VARCHAR(255) NOT NULL,
redirect_uri VARCHAR(255) NOT NULL,
code_challenge VARCHAR(255),
used BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
expires_at TIMESTAMP NOT NULL,
FOREIGN KEY (session_id) REFERENCES auth_sessions(session_id),
INDEX idx_expires (expires_at)
);
Session Management
Session Creation
- Generate UUID for session_id
- Set expiration to 10 minutes for email verification
- Store all OAuth parameters in session
Session Validation
- Check expiration on every access
- Verify session_id matches throughout flow
- Clear expired sessions periodically (cron job)
Security Considerations
- Session IDs must be cryptographically random
- Email codes must be 6 random digits
- Authorization codes must be unguessable
- All temporary data expires and is cleaned up
Error Handling
Domain Not Verified
{
"error": "unauthorized_client",
"error_description": "Domain not configured for this IndieAuth server"
}
Invalid Email Code
{
"error": "access_denied",
"error_description": "Invalid verification code"
}
Session Expired
{
"error": "invalid_request",
"error_description": "Session expired, please start over"
}
Migration from Current Implementation
- Immediate: Disable caching of email verification
- Add auth_sessions table: Track per-login authentication state
- Modify verification flow: Always require email code
- Update domain verification: Separate from user authentication
- Clean up old code: Remove improper caching logic
Testing Requirements
Unit Tests
- Domain verification logic (DNS lookup, caching)
- Session management (creation, expiration, cleanup)
- Email code generation and validation
- Authorization code generation and exchange
Integration Tests
- Full authorization flow with email verification
- Multiple concurrent users for same domain
- Session expiration during flow
- Domain verification caching behavior
Security Tests
- Ensure email verification required every login
- Verify sessions properly isolated between users
- Test rate limiting on code attempts
- Verify all codes are single-use
Acceptance Criteria
- ✓ Domain verification via DNS TXT is cached appropriately
- ✓ Email verification code is required for EVERY login attempt
- ✓ Multiple users can authenticate for the same domain independently
- ✓ Sessions expire and are cleaned up properly
- ✓ Authorization codes are single-use
- ✓ Clear separation between domain verification and user authentication
- ✓ No security regression from current (broken) implementation
Implementation Priority
CRITICAL: This is a security vulnerability that must be fixed immediately. The current implementation allows unauthenticated access after the first user logs in for a domain.
Notes
The confusion between domain verification and user authentication is a fundamental architectural error. This fix properly separates these concerns:
- Domain verification establishes trust in the domain configuration (one-time)
- User authentication establishes trust in the current user (every time)
This aligns with the IndieAuth specification where the authorization endpoint MUST authenticate the user, not just verify the domain.