# Design: Token Verification Endpoint (Critical Compliance Fix) **Date**: 2025-11-25 **Architect**: Claude (Architect Agent) **Status**: Ready for Immediate Implementation **Priority**: P0 - CRITICAL BLOCKER **Design Version**: 1.0 ## Executive Summary **CRITICAL COMPLIANCE BUG**: Gondulf's token endpoint does not support GET requests for token verification, violating the W3C IndieAuth specification. This prevents resource servers (like Micropub endpoints) from verifying tokens, making our access tokens useless. **Fix Required**: Add GET handler to `/token` endpoint that verifies Bearer tokens per specification. ## Problem Statement ### What's Broken 1. **Current State**: - POST `/token` works (issues tokens) - GET `/token` returns 405 Method Not Allowed - Resource servers cannot verify our tokens - Micropub/Microsub integration fails 2. **Specification Requirement** (W3C IndieAuth Section 6.3): > "If an external endpoint needs to verify that an access token is valid, it MUST make a GET request to the token endpoint containing an HTTP Authorization header with the Bearer Token" 3. **Impact**: - Gondulf is NOT IndieAuth-compliant - Access tokens are effectively useless - Integration with any resource server fails ## Solution Design ### API Endpoint **GET /token** **Purpose**: Verify access token validity for resource servers **Headers Required**: ``` Authorization: Bearer {access_token} ``` **Success Response (200 OK)**: ```json { "me": "https://example.com", "client_id": "https://client.example.com", "scope": "" } ``` **Error Response (401 Unauthorized)**: ```json { "error": "invalid_token" } ``` ### Implementation **File**: `/src/gondulf/routers/token.py` (UPDATE EXISTING) **Add this handler**: ```python from fastapi import Header @router.get("/token") async def verify_token( authorization: Optional[str] = Header(None), token_service: TokenService = Depends(get_token_service) ) -> dict: """ Verify access token per W3C IndieAuth specification. Per https://www.w3.org/TR/indieauth/#token-verification: "If an external endpoint needs to verify that an access token is valid, it MUST make a GET request to the token endpoint containing an HTTP Authorization header with the Bearer Token" Request: GET /token Authorization: Bearer {access_token} Response (200 OK): { "me": "https://example.com", "client_id": "https://client.example.com", "scope": "" } Error Response (401 Unauthorized): { "error": "invalid_token" } Args: authorization: Authorization header with Bearer token token_service: Token validation service Returns: Token metadata if valid Raises: HTTPException: 401 for invalid/missing token """ # Log verification attempt logger.debug("Token verification request received") # STEP 1: Extract Bearer token from Authorization header if not authorization: logger.warning("Token verification failed: Missing Authorization header") raise HTTPException( status_code=401, detail={"error": "invalid_token"}, headers={"WWW-Authenticate": "Bearer"} ) # Check for Bearer prefix (case-insensitive per RFC 6750) if not authorization.lower().startswith("bearer "): logger.warning(f"Token verification failed: Invalid auth scheme (expected Bearer)") raise HTTPException( status_code=401, detail={"error": "invalid_token"}, headers={"WWW-Authenticate": "Bearer"} ) # Extract token (everything after "Bearer ") # Handle both "Bearer " and "bearer " per RFC 6750 token = authorization[7:].strip() if not token: logger.warning("Token verification failed: Empty token") raise HTTPException( status_code=401, detail={"error": "invalid_token"}, headers={"WWW-Authenticate": "Bearer"} ) # STEP 2: Validate token using existing service try: metadata = token_service.validate_token(token) except Exception as e: logger.error(f"Token verification error: {e}") raise HTTPException( status_code=401, detail={"error": "invalid_token"}, headers={"WWW-Authenticate": "Bearer"} ) # STEP 3: Check if token is valid if not metadata: logger.info(f"Token verification failed: Invalid or expired token (prefix: {token[:8]}...)") raise HTTPException( status_code=401, detail={"error": "invalid_token"}, headers={"WWW-Authenticate": "Bearer"} ) # STEP 4: Return token metadata per specification logger.info(f"Token verified successfully for {metadata['me']}") return { "me": metadata["me"], "client_id": metadata["client_id"], "scope": metadata.get("scope", "") } ``` ### No Other Changes Required The existing `TokenService.validate_token()` method already: - Hashes the token - Looks it up in the database - Checks expiration - Checks revocation status - Returns metadata or None No changes needed to the service layer. ## Data Flow ``` Resource Server (e.g., Micropub) │ │ GET /token │ Authorization: Bearer abc123... ▼ Token Endpoint (GET) │ │ Extract token from header ▼ Token Service │ │ Hash token │ Query database │ Check expiration ▼ Return Metadata │ │ 200 OK │ { │ "me": "https://example.com", │ "client_id": "https://client.com", │ "scope": "" │ } ▼ Resource Server (Allows/denies access) ``` ## Testing Requirements ### Unit Tests (5 tests) 1. **Valid Token**: - Input: Valid Bearer token - Expected: 200 OK with metadata 2. **Invalid Token**: - Input: Non-existent token - Expected: 401 Unauthorized 3. **Expired Token**: - Input: Expired token - Expected: 401 Unauthorized 4. **Missing Header**: - Input: No Authorization header - Expected: 401 Unauthorized 5. **Invalid Header Format**: - Input: "Basic xyz" or malformed - Expected: 401 Unauthorized ### Integration Tests (3 tests) 1. **Full Flow**: - POST /token to get token - GET /token to verify it - Verify metadata matches 2. **Revoked Token**: - Create token, revoke it - GET /token should fail 3. **Cross-Client Verification**: - Token from client A - Verify returns client_id A ### Manual Testing Test with real Micropub client: 1. Authenticate with Gondulf 2. Get access token 3. Configure Micropub client 4. Verify it can post successfully ## Security Considerations ### RFC 6750 Compliance - Accept both "Bearer" and "bearer" (case-insensitive) - Return WWW-Authenticate header on 401 - Don't leak token details in errors - Log only token prefix (8 chars) ### Error Handling All errors return 401 with `{"error": "invalid_token"}`: - Missing header - Wrong auth scheme - Invalid token - Expired token - Revoked token This prevents token enumeration attacks. ### Rate Limiting Consider adding rate limiting in future: - Per IP: 100 requests/minute - Per token: 10 requests/minute Not critical for v1.0.0 but recommended for v1.1.0. ## Implementation Checklist - [ ] Add GET handler to `/src/gondulf/routers/token.py` - [ ] Import Header from fastapi - [ ] Implement Bearer token extraction - [ ] Call existing validate_token() method - [ ] Return required JSON format - [ ] Add unit tests (5) - [ ] Add integration tests (3) - [ ] Test with real Micropub client - [ ] Update API documentation ## Effort Estimate **Total**: 1-2 hours - Implementation: 30 minutes - Testing: 45 minutes - Documentation: 15 minutes - Manual verification: 30 minutes ## Acceptance Criteria ### Mandatory for v1.0.0 - [ ] GET /token accepts Bearer token - [ ] Returns correct JSON format - [ ] Returns 401 for invalid tokens - [ ] All tests passing - [ ] Micropub client can verify tokens ### Success Metrics - StarPunk's Micropub works with Gondulf - Any IndieAuth resource server accepts our tokens - Full W3C specification compliance ## Why This is Critical Without token verification: 1. **Access tokens are useless** - No way to verify them 2. **Not IndieAuth-compliant** - Violates core specification 3. **No Micropub/Microsub** - Integration impossible 4. **Defeats the purpose** - Why issue tokens that can't be verified? ## Related Documents - ADR-013: Token Verification Endpoint Missing - W3C IndieAuth: https://www.w3.org/TR/indieauth/#token-verification - RFC 6750: https://datatracker.ietf.org/doc/html/rfc6750 - Existing Token Service: `/src/gondulf/services/token_service.py` --- **DESIGN READY: Token Verification Endpoint - CRITICAL FIX REQUIRED** This must be implemented immediately to achieve IndieAuth compliance.