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:
@@ -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", "")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user