feat: Implement PKCE authentication for IndieLogin.com

This fixes critical IndieAuth authentication by implementing PKCE (Proof Key
for Code Exchange) as required by IndieLogin.com API specification.

Added:
- PKCE code_verifier and code_challenge generation (RFC 7636)
- Database column: auth_state.code_verifier for PKCE support
- Issuer validation for authentication callbacks
- Comprehensive PKCE unit tests (6 tests, all passing)
- Database migration script for code_verifier column

Changed:
- Corrected IndieLogin.com API endpoints (/authorize and /token)
- State token validation now returns code_verifier for token exchange
- Authentication flow follows IndieLogin.com API specification exactly
- Enhanced logging with code_verifier redaction

Removed:
- OAuth metadata endpoint (/.well-known/oauth-authorization-server)
  Added in v0.7.0 but not required by IndieLogin.com
- h-app microformats markup from templates
  Modified in v0.7.1 but not used by IndieLogin.com
- indieauth-metadata link from HTML head

Security:
- PKCE prevents authorization code interception attacks
- Issuer validation prevents token substitution attacks
- Code verifier securely stored, redacted in logs, and single-use

Documentation:
- Version: 0.8.0
- CHANGELOG updated with v0.8.0 entry and v0.7.x notes
- ADR-016 and ADR-017 marked as superseded by ADR-019
- Implementation report created in docs/reports/
- Test update guide created in TODO_TEST_UPDATES.md

Breaking Changes:
- Users mid-authentication will need to restart login after upgrade
- Database migration required before deployment

Related: ADR-019

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-19 15:43:38 -07:00
parent caabf0087e
commit 5e50330bdf
18 changed files with 4208 additions and 125 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, jsonify
from flask import Blueprint, abort, render_template, Response, current_app
from starpunk.notes import list_notes, get_note
from starpunk.feed import generate_feed
@@ -145,73 +145,3 @@ 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