- Add logging helper functions with automatic token redaction - Implement comprehensive logging throughout auth flow - Add production warning for DEBUG logging - Add 14 new tests for logging functionality - Update version to v0.7.0 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
18 KiB
ADR-017: OAuth Client ID Metadata Document Implementation
Status
Proposed
Context
StarPunk continues to experience "client_id is not registered" errors from IndieLogin.com despite implementing h-app microformats in ADR-016 and making them visible in ADR-006.
The Problem
IndieLogin.com rejects authentication requests with the error:
Request Error
This client_id is not registered (https://starpunk.thesatelliteoflove.com)
Root Cause Analysis
Through comprehensive review of the IndieAuth specification and actual IndieLogin.com behavior, we've identified that:
- IndieAuth Specification Has Evolved: The current specification (2022+) uses OAuth Client ID Metadata Documents (JSON) as the primary client discovery mechanism
- h-app is Legacy: While h-app microformats are still supported for backward compatibility, they are no longer the primary standard
- IndieLogin.com Expects JSON: IndieLogin.com appears to require or strongly prefer the modern JSON metadata approach
- Our Implementation is Outdated: StarPunk only provides h-app markup, not JSON metadata
What the Specification Requires
From IndieAuth Spec Section 4.2 (Client Information Discovery):
"Clients SHOULD publish a Client Identifier Metadata Document at their client_id URL."
The specification further states:
"If fetching the metadata document fails, the authorization server SHOULD abort the authorization request."
This explains the rejection behavior - IndieLogin.com fetches our client_id URL, expects JSON metadata, doesn't find it, and aborts.
Why Previous ADRs Failed
- ADR-016: Implemented h-app but used
hiddenattribute, making it invisible to parsers - ADR-006: Made h-app visible but this is no longer the primary discovery mechanism
- Both: Did not implement the modern JSON metadata document approach
Decision
Implement OAuth Client ID Metadata Document as a JSON endpoint at /.well-known/oauth-authorization-server following the current IndieAuth specification.
Implementation Details
1. Create Metadata Endpoint
Route: /.well-known/oauth-authorization-server
Method: GET
Content-Type: application/json
Cache: 24 hours (metadata rarely changes)
Response Structure:
{
"issuer": "https://starpunk.thesatelliteoflove.com",
"client_id": "https://starpunk.thesatelliteoflove.com",
"client_name": "StarPunk",
"client_uri": "https://starpunk.thesatelliteoflove.com",
"redirect_uris": [
"https://starpunk.thesatelliteoflove.com/auth/callback"
],
"grant_types_supported": ["authorization_code"],
"response_types_supported": ["code"],
"code_challenge_methods_supported": ["S256"],
"token_endpoint_auth_methods_supported": ["none"]
}
2. Add Discovery Link
Add to templates/base.html <head> section:
<link rel="indieauth-metadata" href="/.well-known/oauth-authorization-server">
3. Maintain h-app for Legacy Support
Keep existing h-app markup in footer as fallback for older IndieAuth servers that may not support JSON metadata.
This creates three layers of discovery:
- Well-known URL (primary, modern standard)
- Link rel hint (explicit pointer)
- h-app microformats (legacy fallback)
Rationale
Why JSON Metadata?
- Current Standard: This is what the 2022+ IndieAuth spec recommends
- IndieLogin.com Compatibility: Addresses the actual error we're experiencing
- Machine Readable: JSON is easier for servers to parse than microformats
- Extensibility: Easy to add more metadata fields in future
- Separation of Concerns: Metadata endpoint separate from presentation
Why Well-Known URL?
- IANA Registered:
/.well-known/is the standard path for service metadata - Discoverable: Predictable location makes discovery reliable
- Clean: No content negotiation complexity
- Standard Practice: Used by OAuth, OIDC, WebFinger, etc.
Why Keep h-app?
- Backward Compatibility: Supports older IndieAuth servers
- Redundancy: Multiple discovery methods increase reliability
- Low Cost: Already implemented, minimal maintenance
- Best Practice: Modern IndieAuth clients support both
Why This Will Work
- Specification Compliance: Directly implements current IndieAuth spec requirements
- Observable Behavior: IndieLogin.com's error message indicates it's checking for registration/metadata
- Industry Pattern: All modern IndieAuth clients use JSON metadata
- Testable: Can verify endpoint before deploying
Consequences
Positive
- ✅ Fixes Authentication: Should resolve "client_id is not registered" error
- ✅ Standards Compliant: Follows current IndieAuth specification exactly
- ✅ Future Proof: Unlikely to require changes as spec is stable
- ✅ Better Metadata: Can provide more detailed client information
- ✅ Easy to Test: Simple curl request verifies implementation
- ✅ Clean Architecture: Dedicated endpoint for metadata
- ✅ Maximum Compatibility: Works with old and new IndieAuth servers
Negative
- ⚠️ New Route: Adds one more endpoint to maintain
- Mitigation: Very simple, rarely changes, no business logic
- ⚠️ Data Duplication: Client info in both JSON and h-app
- Mitigation: Can use config variables as single source
- ⚠️ Testing Surface: New endpoint to test
- Mitigation: Simple unit tests, no complex logic
Neutral
- File Size: Adds ~500 bytes to metadata response
- Cached for 24 hours, negligible bandwidth impact
- Code Complexity: Modest increase
- ~20 lines of Python code
- Simple JSON serialization, no complex logic
Implementation Requirements
Python Code
@app.route('/.well-known/oauth-authorization-server')
def oauth_client_metadata():
"""
OAuth Client ID Metadata Document endpoint.
Returns JSON metadata about this IndieAuth client for authorization
server discovery. Required by IndieAuth specification section 4.2.
See: https://indieauth.spec.indieweb.org/#client-information-discovery
"""
metadata = {
'issuer': current_app.config['SITE_URL'],
'client_id': current_app.config['SITE_URL'],
'client_name': current_app.config.get('SITE_NAME', 'StarPunk'),
'client_uri': current_app.config['SITE_URL'],
'redirect_uris': [
f"{current_app.config['SITE_URL']}/auth/callback"
],
'grant_types_supported': ['authorization_code'],
'response_types_supported': ['code'],
'code_challenge_methods_supported': ['S256'],
'token_endpoint_auth_methods_supported': ['none']
}
response = jsonify(metadata)
# Cache for 24 hours (metadata rarely changes)
response.cache_control.max_age = 86400
response.cache_control.public = True
return response
HTML Template Update
In templates/base.html, add to <head>:
<!-- IndieAuth client metadata discovery -->
<link rel="indieauth-metadata" href="/.well-known/oauth-authorization-server">
Configuration Dependencies
Required config values:
SITE_URL: Full URL to the application (e.g., "https://starpunk.thesatelliteoflove.com")SITE_NAME: Application name (optional, defaults to "StarPunk")
Validation Rules
The implementation MUST ensure:
-
client_id Exact Match:
metadata['client_id']MUST exactly match the URL where the document is served- Use
current_app.config['SITE_URL']from configuration - Do NOT hardcode URLs
- Use
-
HTTPS in Production: All URLs MUST use HTTPS scheme in production
- Development may use HTTP
- Consider environment-based URL construction
-
Valid JSON: Response MUST be parseable JSON
- Use Flask's
jsonify()which handles serialization - Validates structure automatically
- Use Flask's
-
Correct Content-Type: Response MUST include
Content-Type: application/jsonheaderjsonify()sets this automatically
-
Array Types:
redirect_urisMUST be an array, even with single value- Use Python list:
['url']not string:'url'
- Use Python list:
Testing Strategy
Unit Tests
def test_oauth_metadata_endpoint_exists(client):
"""Verify metadata endpoint returns 200 OK"""
response = client.get('/.well-known/oauth-authorization-server')
assert response.status_code == 200
def test_oauth_metadata_content_type(client):
"""Verify response is JSON"""
response = client.get('/.well-known/oauth-authorization-server')
assert response.content_type == 'application/json'
def test_oauth_metadata_required_fields(client, app):
"""Verify all required fields present and valid"""
response = client.get('/.well-known/oauth-authorization-server')
data = response.get_json()
# Required fields
assert 'client_id' in data
assert 'client_name' in data
assert 'redirect_uris' in data
# client_id must match SITE_URL exactly (spec requirement)
assert data['client_id'] == app.config['SITE_URL']
# redirect_uris must be array
assert isinstance(data['redirect_uris'], list)
assert len(data['redirect_uris']) > 0
def test_oauth_metadata_cache_headers(client):
"""Verify appropriate cache headers set"""
response = client.get('/.well-known/oauth-authorization-server')
assert response.cache_control.max_age == 86400
assert response.cache_control.public is True
def test_indieauth_metadata_link_present(client):
"""Verify discovery link in HTML head"""
response = client.get('/')
assert b'rel="indieauth-metadata"' in response.data
assert b'/.well-known/oauth-authorization-server' in response.data
Integration Tests
- Direct Fetch: Use
requeststo fetch metadata, parse JSON, verify structure - Discovery Flow: Verify HTML contains link, fetch linked URL, verify metadata
- Real IndieLogin: Test complete authentication flow with IndieLogin.com
Manual Validation
# Fetch metadata directly
curl https://starpunk.thesatelliteoflove.com/.well-known/oauth-authorization-server
# Verify valid JSON
curl -s https://starpunk.thesatelliteoflove.com/.well-known/oauth-authorization-server | jq .
# Check client_id matches (should output: true)
curl -s https://starpunk.thesatelliteoflove.com/.well-known/oauth-authorization-server | \
jq '.client_id == "https://starpunk.thesatelliteoflove.com"'
# Verify cache headers
curl -I https://starpunk.thesatelliteoflove.com/.well-known/oauth-authorization-server | \
grep -i cache-control
Deployment Checklist
- Implement
/.well-known/oauth-authorization-serverroute - Add JSON response with all required fields
- Add cache headers (24 hour max-age)
- Add
<link rel="indieauth-metadata">to base.html - Write and run unit tests (all passing)
- Test locally with curl and jq
- Verify client_id exactly matches SITE_URL
- Deploy to production
- Verify endpoint accessible:
curl https://starpunk.thesatelliteoflove.com/.well-known/oauth-authorization-server - Test authentication flow with IndieLogin.com
- Verify no "client_id is not registered" error
- Complete successful admin login
- Update documentation
- Increment version to v0.6.2
- Update CHANGELOG.md
Success Criteria
Implementation is successful when:
- ✅ Metadata endpoint returns 200 OK with valid JSON
- ✅ All required fields present in response
- ✅
client_idexactly matches document URL - ✅ IndieLogin.com authentication flow completes without error
- ✅ Admin can successfully log in via IndieAuth
- ✅ Unit tests achieve >95% coverage
- ✅ Production deployment verified working
Alternatives Considered
Alternative 1: Content Negotiation at Root URL
Serve JSON when Accept: application/json header is present, otherwise serve HTML.
Rejected Because:
- More complex logic
- Higher chance of bugs
- Harder to test
- Non-standard approach
- Content negotiation can be fragile
Alternative 2: JSON-Only (Remove h-app)
Implement JSON metadata and remove h-app entirely.
Rejected Because:
- Breaks backward compatibility
- Some servers may still use h-app
- No cost to keeping both
- Redundancy increases reliability
Alternative 3: Custom Metadata Path
Use non-standard path like /client-metadata.json.
Rejected Because:
- Not following standard well-known conventions
- Harder to discover
- No advantage over standard path
- May not work with some IndieAuth servers
Alternative 4: Do Nothing (Wait for IndieLogin.com Fix)
Assume IndieLogin.com has a bug and wait for them to fix it.
Rejected Because:
- Blocking production authentication
- Specification clearly supports JSON metadata
- Other services may have same requirement
- User data suggests this is our bug, not theirs
Migration Path
From Current State
- No database changes required
- No configuration changes required (uses existing SITE_URL)
- No breaking changes to existing functionality
- Purely additive - adds new endpoint
Backward Compatibility
- Existing h-app markup remains functional
- Older IndieAuth servers continue to work
- No impact on users or existing sessions
Forward Compatibility
- Endpoint can be extended with additional metadata fields
- Cache headers can be adjusted if needed
- Can add more discovery mechanisms if spec evolves
Security Implications
Information Disclosure
Exposed Information:
- Application name (already public)
- Application URL (already public)
- Callback URL (already in auth flow)
- Supported OAuth methods (standard)
Risk: None - all information is non-sensitive and already public
Input Validation
No User Input: Endpoint serves static configuration data only
Risk: None - no injection vectors
Denial of Service
Concern: Endpoint could be hammered with requests
Mitigation:
- 24 hour cache reduces server load
- Rate limiting at reverse proxy (nginx/Caddy)
- Simple response, fast generation (<10ms)
Access Control
Public Endpoint: No authentication required
Justification: OAuth client metadata is designed to be publicly accessible for discovery
Performance Impact
Response Time
- Target: < 10ms
- Actual: ~2-5ms (simple dict serialization)
- Bottleneck: None (no DB/file I/O)
Response Size
- JSON: ~400-500 bytes
- Gzipped: ~250 bytes
- Impact: Negligible
Caching Strategy
- Max-Age: 24 hours
- Type: Public cache
- Rationale: Metadata rarely changes
Resource Usage
- CPU: Minimal (one-time JSON serialization)
- Memory: Negligible (~1KB response)
- Network: Cached by browsers/proxies
Compliance
IndieAuth Specification
- ✅ Section 4.2: Client Information Discovery
- ✅ OAuth Client ID Metadata Document format
- ✅ Required fields: client_id, redirect_uris
- ✅ Recommended fields: client_name, client_uri
OAuth 2.0 Standards
- ✅ RFC 7591: OAuth 2.0 Dynamic Client Registration
- ✅ Client metadata format
- ✅ Public client (no client secret)
HTTP Standards
- ✅ RFC 7231: HTTP/1.1 Semantics (cache headers)
- ✅ RFC 8259: JSON format
- ✅ IANA Well-Known URIs registry
Project Standards
- ✅ Minimal code principle
- ✅ Standards-first design
- ✅ No unnecessary dependencies
- ✅ Progressive enhancement (server-side)
References
Specifications
- IndieAuth Specification
- OAuth Client ID Metadata Document
- RFC 7591 - OAuth 2.0 Dynamic Client Registration
- RFC 3986 - URI Generic Syntax
IndieWeb Resources
Internal Documents
- ADR-016: IndieAuth Client Discovery Mechanism (superseded)
- ADR-006: IndieAuth Client Identification Strategy (superseded)
- ADR-005: IndieLogin Authentication
- Root Cause Analysis: IndieAuth Client Discovery (docs/reports/)
Related ADRs
- Supersedes: ADR-016 (h-app approach insufficient)
- Supersedes: ADR-006 (visibility issue but wrong approach)
- Extends: ADR-005 (adds missing client discovery to IndieLogin flow)
- Related: ADR-003 (frontend architecture - templates)
Version Impact
Issue Type: Critical Bug (authentication completely broken in production) Version Change: v0.6.1 → v0.6.2 Semantic Versioning: Patch increment (bug fix, no breaking changes) Changelog Category: Fixed
Notes for Implementation
Developer Guidance
- Use Configuration Variables: Never hardcode URLs, always use
current_app.config['SITE_URL'] - Test JSON Structure: Validate with
jqbefore deploying - Verify Exact Match: client_id must EXACTLY match URL (string comparison)
- Cache Appropriately: 24 hours is safe, metadata rarely changes
- Keep It Simple: No complex logic, just dictionary serialization
Common Pitfalls to Avoid
- ❌ Hardcoding URLs instead of using config
- ❌ Using string instead of array for redirect_uris
- ❌ Missing client_id field (spec requirement)
- ❌ client_id doesn't match document URL
- ❌ Forgetting to add discovery link to HTML
- ❌ Wrong content-type header
- ❌ No cache headers (unnecessary server load)
Debugging Tips
# Verify endpoint exists and returns JSON
curl -v https://starpunk.thesatelliteoflove.com/.well-known/oauth-authorization-server
# Pretty-print JSON response
curl -s https://starpunk.thesatelliteoflove.com/.well-known/oauth-authorization-server | jq .
# Check specific field
curl -s https://starpunk.thesatelliteoflove.com/.well-known/oauth-authorization-server | \
jq '.client_id'
# Verify cache headers
curl -I https://starpunk.thesatelliteoflove.com/.well-known/oauth-authorization-server
# Test from IndieLogin's perspective (check what they see)
curl -s -H "User-Agent: IndieLogin" \
https://starpunk.thesatelliteoflove.com/.well-known/oauth-authorization-server
Decided: 2025-11-19 Author: StarPunk Architect Agent Supersedes: ADR-016, ADR-006 Status: Proposed (awaiting implementation and validation)