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

@@ -89,11 +89,13 @@ def callback():
Handle IndieLogin callback
Processes the OAuth callback from IndieLogin.com, validates the
authorization code and state token, and creates an authenticated session.
authorization code, state token, and issuer, then creates an
authenticated session using PKCE verification.
Query parameters:
code: Authorization code from IndieLogin
state: CSRF state token
iss: Issuer identifier (should be https://indielogin.com/)
Returns:
Redirect to admin dashboard on success, login form on failure
@@ -103,14 +105,15 @@ def callback():
"""
code = request.args.get("code")
state = request.args.get("state")
iss = request.args.get("iss") # Extract issuer parameter
if not code or not state:
flash("Missing authentication parameters", "error")
return redirect(url_for("auth.login_form"))
try:
# Handle callback and create session
session_token = handle_callback(code, state)
# Handle callback and create session with PKCE verification
session_token = handle_callback(code, state, iss) # Pass issuer
# Create response with redirect
response = redirect(url_for("admin.dashboard"))

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