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>
This commit is contained in:
2025-11-25 08:10:47 -07:00
parent 526a21d3fb
commit 6bb2a4033f
8 changed files with 1168 additions and 26 deletions

View File

@@ -2,7 +2,7 @@
import logging
from typing import Optional
from fastapi import APIRouter, Depends, Form, HTTPException, Response
from fastapi import APIRouter, Depends, Form, Header, HTTPException, Response
from pydantic import BaseModel
from gondulf.dependencies import get_code_storage, get_token_service
@@ -232,3 +232,105 @@ async def token_exchange(
me=me,
scope=scope
)
@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("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", "")
}