# Design Fix: Authorization Endpoint Domain Verification **Date**: 2025-11-22 **Architect**: Claude (Architect Agent) **Status**: CRITICAL - Ready for Immediate Implementation **Priority**: P0 - Security Fix ## Problem Statement The authorization endpoint (`GET /authorize`) is bypassing domain verification entirely. This allows anyone to authenticate as any domain without proving ownership, which is a critical security vulnerability. ### Current Behavior (BROKEN) ``` 1. GET /authorize?me=https://example.com/&... -> 200 OK (consent page shown) 2. POST /authorize/consent -> 302 redirect with code ``` ### Expected Behavior (Per Design) ``` 1. GET /authorize?me=https://example.com/&... 2. Check if domain is verified in database 3a. If NOT verified: - Verify DNS TXT record for _gondulf.{domain} - Fetch user's homepage - Discover email from rel="me" link - Send 6-digit verification code to email - Show code entry form 4. POST /authorize/verify-code with code 5. Validate code -> Store verified domain in database 6. Show consent page 7. POST /authorize/consent -> 302 redirect with authorization code ``` ## Root Cause In `/src/gondulf/routers/authorization.py`, lines 191-193: ```python # Check if domain is verified # For Phase 2, we'll show consent form immediately (domain verification happens separately) # In Phase 3, we'll check database for verified domains ``` The implementation shows the consent form directly without any verification checks. The `DomainVerificationService` exists and has the required methods, but they are never called in the authorization flow. ## Design Fix ### Overview The fix requires modifying the `GET /authorize` endpoint to: 1. Extract domain from `me` parameter 2. Check if domain is already verified (in database) 3. If not verified, initiate verification and show code entry form 4. After verification, show consent page Additionally, a new endpoint `POST /authorize/verify-code` must be implemented to handle code submission during the authorization flow. ### Modified Authorization Flow #### Step 1: Modify `GET /authorize` (authorization.py) **Location**: `/src/gondulf/routers/authorization.py`, `authorize_get` function **After line 189** (after me URL validation), insert domain verification logic: ```python # Extract domain from me URL domain = extract_domain_from_url(me) # Check if domain is already verified verification_service = Depends(get_verification_service) # NOTE: Need to add verification_service to function parameters # Query database for verified domain is_verified = await check_domain_verified(database, domain) if not is_verified: # Start two-factor verification result = verification_service.start_verification(domain, me) if not result["success"]: # Verification cannot start (DNS failed, no rel=me, etc) return templates.TemplateResponse( "verification_error.html", { "request": request, "error": result["error"], "domain": domain, # Pass through auth params for retry "client_id": normalized_client_id, "redirect_uri": redirect_uri, "response_type": effective_response_type, "state": state, "code_challenge": code_challenge, "code_challenge_method": code_challenge_method, "scope": scope, "me": me }, status_code=200 ) # Verification started - show code entry form return templates.TemplateResponse( "verify_code.html", { "request": request, "masked_email": result["email"], "domain": domain, # Pass through auth params "client_id": normalized_client_id, "redirect_uri": redirect_uri, "response_type": effective_response_type, "state": state, "code_challenge": code_challenge, "code_challenge_method": code_challenge_method, "scope": scope, "me": me, "client_metadata": client_metadata } ) # Domain is verified - show consent form (existing code from line 205) ``` #### Step 2: Add Database Check Function **Location**: Add to `/src/gondulf/routers/authorization.py` or `/src/gondulf/utils/validation.py` ```python async def check_domain_verified(database: Database, domain: str) -> bool: """ Check if domain is verified in the database. Args: database: Database service domain: Domain to check (e.g., "example.com") Returns: True if domain is verified, False otherwise """ async with database.get_session() as session: result = await session.execute( "SELECT verified FROM domains WHERE domain = ? AND verified = 1", (domain,) ) row = result.fetchone() return row is not None ``` #### Step 3: Add New Endpoint `POST /authorize/verify-code` **Location**: `/src/gondulf/routers/authorization.py` ```python @router.post("/authorize/verify-code") async def authorize_verify_code( request: Request, domain: str = Form(...), code: str = Form(...), client_id: str = Form(...), redirect_uri: str = Form(...), response_type: str = Form("id"), state: str = Form(...), code_challenge: str = Form(...), code_challenge_method: str = Form(...), scope: str = Form(""), me: str = Form(...), database: Database = Depends(get_database), verification_service: DomainVerificationService = Depends(get_verification_service), happ_parser: HAppParser = Depends(get_happ_parser) ) -> HTMLResponse: """ Handle verification code submission during authorization flow. This endpoint is called when user submits the 6-digit email verification code. On success, shows consent page. On failure, shows code entry form with error. Args: domain: Domain being verified code: 6-digit verification code from email client_id, redirect_uri, etc: Authorization parameters (passed through) Returns: HTML response: consent page on success, code form with error on failure """ logger.info(f"Verification code submission for domain={domain}") # Verify the code result = verification_service.verify_email_code(domain, code) if not result["success"]: # Code invalid - show form again with error # Need to get masked email again email = verification_service.code_storage.get(f"email_addr:{domain}") masked_email = mask_email(email) if email else "unknown" return templates.TemplateResponse( "verify_code.html", { "request": request, "error": result["error"], "masked_email": masked_email, "domain": domain, "client_id": client_id, "redirect_uri": redirect_uri, "response_type": response_type, "state": state, "code_challenge": code_challenge, "code_challenge_method": code_challenge_method, "scope": scope, "me": me }, status_code=200 ) # Code valid - store verified domain in database await store_verified_domain(database, domain, result.get("email", "")) logger.info(f"Domain verified successfully: {domain}") # Fetch client metadata for consent page client_metadata = None try: client_metadata = await happ_parser.fetch_and_parse(client_id) except Exception as e: logger.warning(f"Failed to fetch client metadata: {e}") # Show consent form return templates.TemplateResponse( "authorize.html", { "request": request, "client_id": client_id, "redirect_uri": redirect_uri, "response_type": response_type, "state": state, "code_challenge": code_challenge, "code_challenge_method": code_challenge_method, "scope": scope, "me": me, "client_metadata": client_metadata } ) ``` #### Step 4: Add Store Verified Domain Function ```python async def store_verified_domain(database: Database, domain: str, email: str) -> None: """ Store verified domain in database. Args: database: Database service domain: Verified domain email: Email used for verification (for audit) """ from datetime import datetime async with database.get_session() as session: await session.execute( """ INSERT OR REPLACE INTO domains (domain, verification_method, verified, verified_at, last_dns_check) VALUES (?, 'two_factor', 1, ?, ?) """, (domain, datetime.utcnow(), datetime.utcnow()) ) await session.commit() logger.info(f"Stored verified domain: {domain}") ``` #### Step 5: Create New Template `verify_code.html` **Location**: `/src/gondulf/templates/verify_code.html` ```html {% extends "base.html" %} {% block title %}Verify Your Identity - Gondulf{% endblock %} {% block content %}
To sign in as {{ domain }}, please enter the verification code sent to {{ masked_email }}.
{% if error %}{{ error }}
Did not receive a code? Check your spam folder. Request a new code
{% endblock %} ``` #### Step 6: Create Error Template `verification_error.html` **Location**: `/src/gondulf/templates/verification_error.html` ```html {% extends "base.html" %} {% block title %}Verification Failed - Gondulf{% endblock %} {% block content %}{{ error }}
Add the following DNS TXT record to your domain:
Type: TXT
Name: _gondulf.{{ domain }}
Value: gondulf-verify-domain
DNS changes may take up to 24 hours to propagate.
Add a rel="me" link to your homepage pointing to your email:
<link rel="me" href="mailto:you@example.com">
Or as an anchor tag:
<a rel="me" href="mailto:you@example.com">Email me</a>