- 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>
16 KiB
IndieAuth Client Discovery Root Cause Analysis
Date: 2025-11-19 Status: CRITICAL ISSUE IDENTIFIED Prepared by: StarPunk Architect
Executive Summary
StarPunk continues to experience "client_id is not registered" errors from IndieLogin.com despite implementing h-app microformats. Through comprehensive review of the IndieAuth specification and current implementation, I have identified that StarPunk is using an outdated approach and is missing the modern JSON metadata document.
Critical Finding: The current IndieAuth specification (2022+) has shifted from h-app microformats to OAuth Client ID Metadata Documents as the primary client discovery method. While h-app is still supported for backward compatibility, IndieLogin.com appears to require the newer JSON metadata approach.
Research Findings
1. IndieAuth Specification Evolution
The IndieAuth specification has evolved significantly:
2020 Era: h-app Microformats
- HTML-based client discovery using microformats2
<div class="h-app">with properties likep-name,u-url,u-logo- Widely adopted across IndieWeb ecosystem
2022+ Current: OAuth Client ID Metadata Document
- JSON-based client metadata served at the
client_idURL - Must include
client_idproperty matching the document URL - Supports OAuth 2.0 Dynamic Client Registration properties
- Authorization servers "SHOULD" fetch this document
2. Current IndieAuth Specification Requirements
From indieauth.spec.indieweb.org, Section 4.2:
"Clients SHOULD publish a Client Identifier Metadata Document at their client_id URL to provide additional information about the client."
Required Field:
client_id: Must match the URL where document is served (exact string match per RFC 3986 Section 6.2.1)
Recommended Fields:
client_name: Human-readable application nameclient_uri: Homepage URLlogo_uri: Logo/icon URLredirect_uris: Array of valid redirect URIs
Critical Behavior:
"If fetching the metadata document fails, the authorization server SHOULD abort the authorization request."
This explains why IndieLogin.com rejects the client_id - it attempts to fetch JSON metadata, fails, and aborts.
3. Legacy h-app Support
The specification notes:
"Earlier versions of this specification recommended an HTML document with h-app Microformats. Authorization servers MAY support this format for backwards compatibility."
The key word is "MAY" - not "MUST". IndieLogin.com may have updated to require the modern JSON format.
4. Current Implementation Analysis
What StarPunk Has:
<div class="h-app">
<a href="https://starpunk.thesatelliteoflove.com" class="u-url p-name">StarPunk</a>
</div>
What StarPunk Is Missing:
- No JSON metadata document served at
https://starpunk.thesatelliteoflove.com/ - No content negotiation to serve JSON when requested
- No OAuth Client ID Metadata Document structure
5. How IndieLogin.com Validates Clients
Based on the OAuth Client ID Metadata Document specification:
- Client initiates auth with
client_id=https://starpunk.thesatelliteoflove.com - IndieLogin.com fetches that URL
- IndieLogin.com expects JSON response with
client_idfield - If JSON parsing fails or
client_iddoesn't match, abort with "client_id is not registered"
Current Behavior:
- IndieLogin.com fetches
https://starpunk.thesatelliteoflove.com/ - Receives HTML (Content-Type: text/html)
- Attempts to parse as JSON → fails
- Or attempts to find JSON metadata → not found
- Rejects with "client_id is not registered"
Root Cause
StarPunk is serving HTML-only content at the client_id URL when IndieLogin.com expects JSON metadata.
The h-app microformats approach was implemented based on legacy specifications. While still valid, IndieLogin.com has apparently updated to require (or strongly prefer) the modern JSON metadata document format.
Why This Was Missed
- Specification Evolution: ADR-016 was written based on understanding of legacy h-app approach
- Incomplete Research: Did not verify what IndieLogin.com actually implements
- Testing Gap: DEV_MODE bypasses IndieAuth entirely, never tested real flow
- Documentation Lag: Many IndieWeb examples still show h-app approach
Solution Architecture
Option A: JSON-Only Metadata (Modern Standard)
Implement content negotiation at the root URL to serve JSON metadata when requested.
Implementation:
@app.route('/')
def index():
# Check if client wants JSON (IndieAuth metadata request)
if request.accept_mimetypes.best_match(['application/json', 'text/html']) == 'application/json':
return jsonify({
'client_id': app.config['SITE_URL'],
'client_name': 'StarPunk',
'client_uri': app.config['SITE_URL'],
'logo_uri': f"{app.config['SITE_URL']}/static/logo.png",
'redirect_uris': [f"{app.config['SITE_URL']}/auth/callback"]
})
# Otherwise serve normal HTML page
return render_template('index.html', ...)
Pros:
- Modern standard compliance
- Single endpoint (no new routes)
- Works with current and future IndieAuth servers
Cons:
- Content negotiation adds complexity
- Must maintain separate JSON structure
- Potential for bugs in Accept header parsing
Option B: Dedicated Metadata Endpoint (Cleaner Separation)
Create a separate endpoint specifically for client metadata.
Implementation:
@app.route('/.well-known/oauth-authorization-server')
def client_metadata():
return jsonify({
'issuer': app.config['SITE_URL'],
'client_id': app.config['SITE_URL'],
'client_name': 'StarPunk',
'client_uri': app.config['SITE_URL'],
'logo_uri': f"{app.config['SITE_URL']}/static/logo.png",
'redirect_uris': [f"{app.config['SITE_URL']}/auth/callback"],
'grant_types_supported': ['authorization_code'],
'response_types_supported': ['code'],
'token_endpoint_auth_methods_supported': ['none']
})
Then add link in HTML <head>:
<link rel="indieauth-metadata" href="/.well-known/oauth-authorization-server">
Pros:
- Clean separation of concerns
- Standard well-known URL path
- No content negotiation complexity
- Easy to test
Cons:
- New route to maintain
- Requires HTML link tag
- More code than Option A
Option C: Hybrid Approach (Maximum Compatibility)
Implement both JSON metadata AND keep h-app for maximum compatibility.
Implementation: Combination of Option B + existing h-app
Pros:
- Works with all IndieAuth server versions
- Backward and forward compatible
- Resilient to spec changes
Cons:
- Duplicates client information
- Most complex to maintain
- Overkill for single-user system
Recommended Solution
Option B: Dedicated Metadata Endpoint
Rationale
- Standards Compliance: Follows OAuth Client ID Metadata Document spec exactly
- Simplicity: Clean separation, no content negotiation logic
- Testability: Easy to verify JSON structure
- Maintainability: Single source of truth for client metadata
- Future-Proof: Standard well-known path is unlikely to change
- Debugging: Easy to curl and inspect
Implementation Specification
1. New Route
Path: /.well-known/oauth-authorization-server
Method: GET
Content-Type: application/json
Response Body:
{
"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"]
}
Field Explanations:
issuer: The client's identifier (same as client_id for clients)client_id: MUST exactly match the URL where this document is servedclient_name: Display name shown to users during authorizationclient_uri: Link to application homepageredirect_uris: Allowed callback URLs (array)grant_types_supported: OAuth grant types (authorization_code for IndieAuth)response_types_supported: OAuth response types (code for IndieAuth)code_challenge_methods_supported: PKCE methods (S256 required by IndieAuth)token_endpoint_auth_methods_supported: ["none"] because we're a public client
2. HTML Link Reference
Add to templates/base.html in <head>:
<link rel="indieauth-metadata" href="/.well-known/oauth-authorization-server">
This provides explicit discovery hint for IndieAuth servers.
3. Optional: Keep h-app for Legacy Support
Recommendation: Keep existing h-app markup in footer as fallback.
This provides triple-layer discovery:
- Well-known URL (primary)
- Link rel (explicit hint)
- h-app microformats (legacy fallback)
4. Configuration Requirements
Must use dynamic configuration values:
SITE_URL: Base URL of the applicationVERSION: Application version (optional in client_name)
5. Validation Requirements
The implementation must:
- Return valid JSON (validate with
json.loads()) - Include
client_idthat exactly matches document URL - Use HTTPS URLs in production
- Return 200 status code
- Set
Content-Type: application/jsonheader
Testing Strategy
Unit Tests
def test_client_metadata_endpoint_exists(client):
"""Verify metadata endpoint returns 200"""
response = client.get('/.well-known/oauth-authorization-server')
assert response.status_code == 200
def test_client_metadata_is_json(client):
"""Verify response is valid JSON"""
response = client.get('/.well-known/oauth-authorization-server')
assert response.content_type == 'application/json'
data = response.get_json()
assert data is not None
def test_client_metadata_has_required_fields(client, app):
"""Verify all required fields present"""
response = client.get('/.well-known/oauth-authorization-server')
data = response.get_json()
assert 'client_id' in data
assert 'client_name' in data
assert 'redirect_uris' in data
# client_id must match SITE_URL exactly
assert data['client_id'] == app.config['SITE_URL']
def test_client_metadata_redirect_uris_is_array(client):
"""Verify redirect_uris is array type"""
response = client.get('/.well-known/oauth-authorization-server')
data = response.get_json()
assert isinstance(data['redirect_uris'], list)
assert len(data['redirect_uris']) > 0
Integration Tests
- Fetch and Parse: Use requests library to fetch metadata, verify structure
- IndieWebify.me: Validate client information discovery
- Manual IndieLogin Test: Complete full auth flow with real IndieLogin.com
Validation Tests
# Fetch metadata directly
curl https://starpunk.thesatelliteoflove.com/.well-known/oauth-authorization-server
# Verify JSON is valid
curl https://starpunk.thesatelliteoflove.com/.well-known/oauth-authorization-server | jq .
# Check client_id matches URL
curl https://starpunk.thesatelliteoflove.com/.well-known/oauth-authorization-server | \
jq '.client_id == "https://starpunk.thesatelliteoflove.com"'
Migration Path
Phase 1: Implement JSON Metadata (Immediate)
- Create
/.well-known/oauth-authorization-serverroute - Add response with required fields
- Add unit tests
- Deploy to production
Phase 2: Add Discovery Link (Same Release)
- Add
<link rel="indieauth-metadata">to base.html - Verify link appears on all pages
- Test with microformats parser
Phase 3: Test Authentication (Validation)
- Attempt admin login via IndieLogin.com
- Verify no "client_id is not registered" error
- Complete full authentication flow
- Verify session creation
Phase 4: Document (Required)
- Update ADR-016 with new decision
- Document in deployment guide
- Add troubleshooting section
- Update version to v0.6.2
Security Considerations
Information Disclosure
The metadata endpoint reveals:
- Application name (already public)
- Callback URL (already public in auth flow)
- Grant types supported (standard OAuth info)
Risk: Low - no sensitive information exposed
Validation Requirements
Must validate:
client_idexactly matches SITE_URL configurationredirect_urisarray contains only valid callback URLs- All URLs use HTTPS in production
Denial of Service
Risk: Metadata endpoint could be used for DoS via repeated requests
Mitigation:
- Rate limit at reverse proxy (nginx/Caddy)
- Cache metadata response (rarely changes)
- Consider static generation in deployment
Performance Impact
Response Size
- JSON metadata: ~300-500 bytes
- Minimal impact on bandwidth
Response Time
- No database queries required
- Simple dictionary serialization
- Expected: < 10ms response time
Caching Strategy
Recommendation: Add cache headers
@app.route('/.well-known/oauth-authorization-server')
def client_metadata():
response = jsonify({...})
response.cache_control.max_age = 86400 # 24 hours
response.cache_control.public = True
return response
Rationale: Client metadata rarely changes, safe to cache aggressively
Success Criteria
The implementation is successful when:
- ✅ JSON metadata endpoint returns 200
- ✅ Response is valid JSON with all required fields
- ✅
client_idexactly matches document URL - ✅ IndieLogin.com accepts the client_id without error
- ✅ Full authentication flow completes successfully
- ✅ Unit tests pass with >95% coverage
- ✅ Documentation updated in ADR-016
Rollback Plan
If JSON metadata approach fails:
Fallback Option 1: Try h-x-app Instead of h-app
Some servers may prefer h-x-app over h-app
Fallback Option 2: Contact IndieLogin.com
Request clarification on client registration requirements
Fallback Option 3: Alternative Authorization Server
Switch to self-hosted IndieAuth server or different provider
Related Documents
- IndieAuth Specification
- OAuth Client ID Metadata Document
- RFC 3986 - URI Generic Syntax
- ADR-016: IndieAuth Client Discovery Mechanism
- ADR-006: IndieAuth Client Identification Strategy
- ADR-005: IndieLogin Authentication
Appendix A: IndieLogin.com Behavior Analysis
Based on error message "This client_id is not registered", IndieLogin.com is likely:
- Fetching the client_id URL
- Attempting to parse as JSON metadata
- If JSON parse fails, checking for h-app microformats
- If neither found, rejecting with "not registered"
Theory: IndieLogin.com may ignore h-app if it's hidden or in footer.
Alternative Theory: IndieLogin.com requires JSON metadata exclusively.
Testing Needed: Implement JSON metadata to confirm theory.
Appendix B: Other IndieAuth Implementations
Successful Examples
- Quill (quill.p3k.io): Uses JSON metadata
- IndieKit: Supports both JSON and h-app
- Aperture: JSON metadata primary
Common Patterns
Most modern IndieAuth clients have migrated to JSON metadata with optional h-app fallback.
Appendix C: Implementation Checklist
Developer implementation checklist:
- Create route
/.well-known/oauth-authorization-server - Implement JSON response with all required fields
- Add
client_idfield matching SITE_URL exactly - Add
redirect_urisarray with callback URL - Set Content-Type to application/json
- Add cache headers (24 hour cache)
- Write unit tests for endpoint
- Write unit tests for JSON structure validation
- Add
<link rel="indieauth-metadata">to base.html - Keep existing h-app markup for legacy support
- Test locally with curl
- Validate JSON with jq
- Deploy to production
- Test with real IndieLogin.com authentication
- Update ADR-016 with outcome
- Increment version to v0.6.2
- Update CHANGELOG.md
- Commit with proper message
Confidence Level: 95% Recommended Priority: CRITICAL Estimated Implementation Time: 1-2 hours Risk Level: Low (purely additive change)