- 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>
493 lines
16 KiB
Markdown
493 lines
16 KiB
Markdown
# 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 like `p-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_id` URL
|
|
- Must include `client_id` property 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](https://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 name
|
|
- `client_uri`: Homepage URL
|
|
- `logo_uri`: Logo/icon URL
|
|
- `redirect_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**:
|
|
```html
|
|
<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:
|
|
|
|
1. Client initiates auth with `client_id=https://starpunk.thesatelliteoflove.com`
|
|
2. IndieLogin.com fetches that URL
|
|
3. IndieLogin.com expects JSON response with `client_id` field
|
|
4. If JSON parsing fails or `client_id` doesn'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
|
|
|
|
1. **Specification Evolution**: ADR-016 was written based on understanding of legacy h-app approach
|
|
2. **Incomplete Research**: Did not verify what IndieLogin.com actually implements
|
|
3. **Testing Gap**: DEV_MODE bypasses IndieAuth entirely, never tested real flow
|
|
4. **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**:
|
|
```python
|
|
@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**:
|
|
```python
|
|
@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>`:
|
|
```html
|
|
<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
|
|
|
|
1. **Standards Compliance**: Follows OAuth Client ID Metadata Document spec exactly
|
|
2. **Simplicity**: Clean separation, no content negotiation logic
|
|
3. **Testability**: Easy to verify JSON structure
|
|
4. **Maintainability**: Single source of truth for client metadata
|
|
5. **Future-Proof**: Standard well-known path is unlikely to change
|
|
6. **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**:
|
|
```json
|
|
{
|
|
"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 served
|
|
- `client_name`: Display name shown to users during authorization
|
|
- `client_uri`: Link to application homepage
|
|
- `redirect_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>`:
|
|
```html
|
|
<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:
|
|
1. Well-known URL (primary)
|
|
2. Link rel (explicit hint)
|
|
3. h-app microformats (legacy fallback)
|
|
|
|
#### 4. Configuration Requirements
|
|
|
|
Must use dynamic configuration values:
|
|
- `SITE_URL`: Base URL of the application
|
|
- `VERSION`: Application version (optional in client_name)
|
|
|
|
#### 5. Validation Requirements
|
|
|
|
The implementation must:
|
|
- Return valid JSON (validate with `json.loads()`)
|
|
- Include `client_id` that exactly matches document URL
|
|
- Use HTTPS URLs in production
|
|
- Return 200 status code
|
|
- Set `Content-Type: application/json` header
|
|
|
|
## Testing Strategy
|
|
|
|
### Unit Tests
|
|
|
|
```python
|
|
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
|
|
|
|
1. **Fetch and Parse**: Use requests library to fetch metadata, verify structure
|
|
2. **IndieWebify.me**: Validate client information discovery
|
|
3. **Manual IndieLogin Test**: Complete full auth flow with real IndieLogin.com
|
|
|
|
### Validation Tests
|
|
|
|
```bash
|
|
# 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)
|
|
1. Create `/.well-known/oauth-authorization-server` route
|
|
2. Add response with required fields
|
|
3. Add unit tests
|
|
4. Deploy to production
|
|
|
|
### Phase 2: Add Discovery Link (Same Release)
|
|
1. Add `<link rel="indieauth-metadata">` to base.html
|
|
2. Verify link appears on all pages
|
|
3. Test with microformats parser
|
|
|
|
### Phase 3: Test Authentication (Validation)
|
|
1. Attempt admin login via IndieLogin.com
|
|
2. Verify no "client_id is not registered" error
|
|
3. Complete full authentication flow
|
|
4. Verify session creation
|
|
|
|
### Phase 4: Document (Required)
|
|
1. Update ADR-016 with new decision
|
|
2. Document in deployment guide
|
|
3. Add troubleshooting section
|
|
4. 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_id` exactly matches SITE_URL configuration
|
|
- `redirect_uris` array 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
|
|
```python
|
|
@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:
|
|
|
|
1. ✅ JSON metadata endpoint returns 200
|
|
2. ✅ Response is valid JSON with all required fields
|
|
3. ✅ `client_id` exactly matches document URL
|
|
4. ✅ IndieLogin.com accepts the client_id without error
|
|
5. ✅ Full authentication flow completes successfully
|
|
6. ✅ Unit tests pass with >95% coverage
|
|
7. ✅ 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](https://indieauth.spec.indieweb.org/)
|
|
- [OAuth Client ID Metadata Document](https://www.ietf.org/archive/id/draft-parecki-oauth-client-id-metadata-document-00.html)
|
|
- [RFC 3986 - URI Generic Syntax](https://www.rfc-editor.org/rfc/rfc3986)
|
|
- 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:
|
|
|
|
1. Fetching the client_id URL
|
|
2. Attempting to parse as JSON metadata
|
|
3. If JSON parse fails, checking for h-app microformats
|
|
4. 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_id` field matching SITE_URL exactly
|
|
- [ ] Add `redirect_uris` array 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)
|