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:
346
docs/designs/token-verification-endpoint.md
Normal file
346
docs/designs/token-verification-endpoint.md
Normal file
@@ -0,0 +1,346 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user