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>
346 lines
8.9 KiB
Markdown
346 lines
8.9 KiB
Markdown
# 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. |