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:
@@ -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"))
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user