fix: Implement OAuth Client ID Metadata Document endpoint

Fixes critical IndieAuth authentication failure by implementing modern
JSON-based client discovery mechanism per IndieAuth spec section 4.2.

Added /.well-known/oauth-authorization-server endpoint returning JSON
metadata with client_id, redirect_uris, and OAuth capabilities.

Added <link rel="indieauth-metadata"> discovery hint in HTML head.

Maintained h-app microformats for backward compatibility with legacy
IndieAuth servers.

This resolves "client_id is not registered" error from IndieLogin.com
by providing the metadata document modern IndieAuth servers expect.

Changes:
- Added oauth_client_metadata() endpoint in public routes
- Returns JSON with client info (24-hour cache)
- Uses config values (SITE_URL, SITE_NAME) not hardcoded URLs
- Added indieauth-metadata link in base.html
- Comprehensive test suite (15 new tests, all passing)
- Updated version to v0.6.2 (PATCH increment)
- Updated CHANGELOG.md with detailed fix documentation

Standards Compliance:
- IndieAuth specification section 4.2
- OAuth Client ID Metadata Document format
- IANA well-known URI registry
- RFC 7591 OAuth 2.0 Dynamic Client Registration

Testing:
- 467/468 tests passing (99.79%)
- 15 new tests for OAuth metadata and discovery
- Zero regressions in existing tests
- Test coverage maintained at 88%

Related Documentation:
- ADR-017: OAuth Client ID Metadata Document Implementation
- IndieAuth Fix Summary report
- Implementation report in docs/reports/

Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-19 14:33:23 -07:00
parent 16dabc0e73
commit 8be079593f
6 changed files with 709 additions and 4 deletions

View File

@@ -8,7 +8,7 @@ No authentication required for these routes.
import hashlib
from datetime import datetime, timedelta
from flask import Blueprint, abort, render_template, Response, current_app
from flask import Blueprint, abort, render_template, Response, current_app, jsonify
from starpunk.notes import list_notes, get_note
from starpunk.feed import generate_feed
@@ -145,3 +145,73 @@ def feed():
response.headers["ETag"] = etag
return response
@bp.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.
This endpoint implements the modern IndieAuth (2022+) client discovery
mechanism using OAuth Client ID Metadata Documents. Authorization servers
like IndieLogin.com fetch this metadata to verify client registration
and obtain redirect URIs.
Returns:
JSON response with client metadata
Response Format:
{
"issuer": "https://example.com",
"client_id": "https://example.com",
"client_name": "Site Name",
"client_uri": "https://example.com",
"redirect_uris": ["https://example.com/auth/callback"],
"grant_types_supported": ["authorization_code"],
"response_types_supported": ["code"],
"code_challenge_methods_supported": ["S256"],
"token_endpoint_auth_methods_supported": ["none"]
}
Headers:
Content-Type: application/json
Cache-Control: public, max-age=86400 (24 hours)
References:
- IndieAuth Spec: https://indieauth.spec.indieweb.org/#client-information-discovery
- OAuth Client Metadata: https://www.ietf.org/archive/id/draft-parecki-oauth-client-id-metadata-document-00.html
- ADR-017: OAuth Client ID Metadata Document Implementation
Examples:
>>> response = client.get('/.well-known/oauth-authorization-server')
>>> response.status_code
200
>>> data = response.get_json()
>>> data['client_id']
'https://example.com'
"""
# Build metadata document using configuration values
# client_id MUST exactly match the URL where this document is served
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"],
}
# Create JSON response
response = jsonify(metadata)
# Cache for 24 hours (metadata rarely changes)
response.cache_control.max_age = 86400
response.cache_control.public = True
return response