Files
Gondulf/docs/designs/token-verification-endpoint.md
Phil Skentelbery 6bb2a4033f feat(token): implement GET /token for token verification
Implements W3C IndieAuth Section 6.3 token verification endpoint.
The token endpoint now supports both:
- POST: Issue new tokens (authorization code exchange)
- GET: Verify existing tokens (resource server validation)

Changes:
- Added GET handler to /token endpoint
- Extracts Bearer token from Authorization header (RFC 6750)
- Returns JSON with me, client_id, scope
- Returns 401 with WWW-Authenticate for invalid tokens
- 11 new tests covering all verification scenarios

All 533 tests passing. Resolves critical P0 blocker for v1.0.0.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-25 08:10:47 -07:00

8.9 KiB

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):

{
  "me": "https://example.com",
  "client_id": "https://client.example.com",
  "scope": ""
}

Error Response (401 Unauthorized):

{
  "error": "invalid_token"
}

Implementation

File: /src/gondulf/routers/token.py (UPDATE EXISTING)

Add this handler:

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?

DESIGN READY: Token Verification Endpoint - CRITICAL FIX REQUIRED

This must be implemented immediately to achieve IndieAuth compliance.