- Updated 42 references from indieauth.spec.indieweb.org to www.w3.org/TR/indieauth - Ensures consistency across all documentation - Points to the authoritative W3C specification - No functional changes, documentation update only Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com>
20 KiB
ADR-029: Micropub IndieAuth Integration Strategy
Status
Accepted
Context
The developer review of our Micropub design (ADR-028) revealed critical issues and questions about how IndieAuth and Micropub integrate. This ADR addresses all architectural decisions needed to proceed with implementation.
Critical Issues Identified
- Token endpoint missing required
meparameter in the IndieAuth spec - PKCE confusion - it's not part of IndieAuth spec, but StarPunk uses it with IndieLogin.com
- Database security issue - tokens stored in plain text
- Missing
authorization_codestable for token exchange - Property mapping rules undefined for Micropub to StarPunk conversion
- Authorization endpoint location unclear
- Two authentication flows need clarification
V1 Scope Decision
The user has agreed to simplify V1 by:
- ✅ Omitting update operations from V1
- ✅ Omitting delete operations from V1
- ✅ Focusing on create-only for V1 release
- Post-V1 features will be tracked separately
Decision
We will implement a hybrid IndieAuth architecture that clearly separates admin authentication from Micropub authorization.
Architectural Decisions
1. Token Endpoint me Parameter (RESOLVED)
Issue: IndieAuth spec requires me parameter in token exchange, but our design missed it.
Decision: Add me parameter validation to token endpoint.
Implementation:
# Token exchange request MUST include:
POST /auth/token
grant_type=authorization_code
code={code}
client_id={client_url}
redirect_uri={redirect_url}
me={user_profile_url} # REQUIRED by IndieAuth spec
Validation:
- Verify
mematches the value stored with the authorization code - Return error if mismatch (prevents code hijacking)
2. PKCE Strategy (RESOLVED)
Issue: PKCE is not part of IndieAuth spec, but StarPunk uses it with IndieLogin.com.
Decision: Make PKCE optional but recommended.
Implementation:
- Check for
code_challengein authorization request - If present, require
code_verifierin token exchange - If absent, proceed without PKCE (spec-compliant)
- Document as security enhancement beyond spec
Rationale:
- IndieLogin.com supports PKCE as an extension
- Other IndieAuth providers may not support it
- Making it optional ensures broader compatibility
3. Token Storage Security (RESOLVED)
Issue: Current tokens table stores tokens in plain text (major security vulnerability).
Decision: Implement immediate migration to hashed token storage.
Migration Strategy:
-- Step 1: Create new secure tokens table
CREATE TABLE tokens_secure (
id INTEGER PRIMARY KEY AUTOINCREMENT,
token_hash TEXT UNIQUE NOT NULL, -- SHA256 hash
me TEXT NOT NULL,
client_id TEXT,
scope TEXT DEFAULT 'create',
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
expires_at TIMESTAMP NOT NULL,
last_used_at TIMESTAMP,
revoked_at TIMESTAMP
);
-- Step 2: Invalidate all existing tokens (security breach recovery)
-- Since we can't hash plain text tokens retroactively, all must be revoked
DROP TABLE IF EXISTS tokens;
-- Step 3: Rename secure table
ALTER TABLE tokens_secure RENAME TO tokens;
-- Step 4: Create indexes
CREATE INDEX idx_tokens_hash ON tokens(token_hash);
CREATE INDEX idx_tokens_me ON tokens(me);
CREATE INDEX idx_tokens_expires ON tokens(expires_at);
Security Notice: All existing tokens will be invalidated. Users must re-authenticate.
4. Authorization Codes Table (RESOLVED)
Issue: Design references authorization_codes table that doesn't exist.
Decision: Create the table as part of Micropub implementation.
Schema:
CREATE TABLE authorization_codes (
code TEXT PRIMARY KEY,
code_hash TEXT UNIQUE NOT NULL, -- SHA256 hash for security
me TEXT NOT NULL,
client_id TEXT NOT NULL,
redirect_uri TEXT NOT NULL,
scope TEXT DEFAULT 'create',
code_challenge TEXT, -- Optional PKCE
code_challenge_method TEXT, -- S256 if PKCE used
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
expires_at TIMESTAMP NOT NULL,
used_at TIMESTAMP -- Prevent replay attacks
);
CREATE INDEX idx_auth_codes_hash ON authorization_codes(code_hash);
CREATE INDEX idx_auth_codes_expires ON authorization_codes(expires_at);
5. Property Mapping Rules (RESOLVED)
Issue: Functions like extract_title() and extract_content() are undefined.
Decision: Define explicit mapping rules for V1.
Micropub → StarPunk Mapping:
# Content mapping (required)
content = properties.get('content', [''])[0] # First content value
if not content:
return error_response("invalid_request", "Content is required")
# Title mapping (optional)
# Option 1: Use 'name' property if provided
title = properties.get('name', [''])[0]
# Option 2: If no name, extract from content (first line up to 50 chars)
if not title and content:
first_line = content.split('\n')[0]
title = first_line[:50] + ('...' if len(first_line) > 50 else '')
# Tags mapping
tags = properties.get('category', []) # All category values become tags
# Published date (respect if provided, otherwise use current time)
published = properties.get('published', [''])[0]
if published:
# Parse ISO 8601 date
created_at = parse_iso8601(published)
else:
created_at = datetime.now()
# Slug generation
mp_slug = properties.get('mp-slug', [''])[0]
if mp_slug:
slug = slugify(mp_slug)
else:
slug = generate_slug(title or content[:30])
Q1: Authorization Endpoint Location (RESOLVED)
Issue: Design mentions /auth/authorization but it doesn't exist.
Decision: Create NEW /auth/authorization endpoint for Micropub clients.
Rationale:
- Keep admin login (
/auth/login) separate from Micropub authorization - Clear separation of concerns
- Follows IndieAuth spec naming conventions
Implementation:
@bp.route("/auth/authorization", methods=["GET", "POST"])
def authorization_endpoint():
"""
IndieAuth authorization endpoint for Micropub clients
GET: Display authorization form
POST: Process authorization and redirect with code
"""
if request.method == "GET":
# Parse IndieAuth parameters
response_type = request.args.get('response_type')
client_id = request.args.get('client_id')
redirect_uri = request.args.get('redirect_uri')
state = request.args.get('state')
scope = request.args.get('scope', 'create')
me = request.args.get('me')
code_challenge = request.args.get('code_challenge')
# Validate parameters
if response_type != 'code':
return error_response("unsupported_response_type")
# Check if user is logged in (via admin session)
if not verify_admin_session():
# Redirect to login, then back here
session['pending_auth'] = request.url
return redirect(url_for('auth.login_form'))
# Display authorization form
return render_template('auth/authorize.html',
client_id=client_id,
scope=scope,
redirect_uri=redirect_uri)
else: # POST
# User approved/denied authorization
# Generate authorization code
# Store in authorization_codes table
# Redirect to client with code
Q2: Two Authentication Flows Integration (RESOLVED)
Decision: Maintain two separate flows with clear boundaries.
Flow 1: Admin Login (Existing)
- Purpose: Admin access to StarPunk interface
- Path:
/auth/login→ IndieLogin.com →/auth/callback - Result: Session cookie for admin panel
- No changes needed
Flow 2: Micropub Authorization (New)
- Purpose: Micropub client authorization
- Path:
/auth/authorization→/auth/token - Result: Bearer token for API access
Integration Point: The authorization endpoint checks for admin session:
def authorization_endpoint():
# Check if admin is logged in
if not has_admin_session():
# Store authorization request
# Redirect to admin login
# After login, return to authorization
return redirect_to_login_with_return()
# Admin is logged in, show authorization form
return show_authorization_form()
Key Design Choice: We act as our own authorization server for Micropub, not delegating to IndieLogin.com for this flow. This is because:
- IndieLogin.com doesn't issue access tokens
- We need to control scopes and token lifetime
- We already have admin authentication to verify the user
Q3: Scope Validation Rules (RESOLVED)
Issue: What happens when client requests no scopes?
Decision: Implement Option C - Allow empty scope during authorization, reject at token endpoint.
Rationale: This matches the IndieAuth spec requirement exactly.
Implementation:
def handle_authorization():
scope = request.args.get('scope', '')
# Store whatever scope was requested (even empty)
authorization_code = create_authorization_code(
scope=scope, # Can be empty string
# ... other parameters
)
def handle_token_exchange():
auth_code = get_authorization_code(code)
# IndieAuth spec: MUST NOT issue token if no scope
if not auth_code.scope:
return error_response("invalid_scope",
"Authorization code was issued without scope")
# Issue token with the authorized scope
token = create_access_token(scope=auth_code.scope)
Q4: V1 Scope - Update/Delete Operations (RESOLVED)
Decision: Remove update/delete from V1 completely.
Changes Required:
- Remove
handle_update()andhandle_delete()from design doc - Remove update/delete from supported scopes in V1
- Return "invalid_request" if action=update or action=delete
- Document in project plan for post-V1
V1 Supported Actions:
- ✅ action=create (or no action - default)
- ❌ action=update → error response
- ❌ action=delete → error response
Q5: Token Storage Security Fix (RESOLVED)
Decision: Fix the security issue as part of Micropub implementation.
Implementation Plan:
- Create migration to new secure schema
- Hash all new tokens before storage
- Document that existing tokens will be invalidated
- Add security notice to changelog
Implementation Architecture
Complete Authorization Flow
┌─────────────────────────────────────────────────────────┐
│ Micropub Client │
└────────────────────┬────────────────────────────────────┘
│
│ 1. GET /auth/authorization?
│ response_type=code&
│ client_id=https://app.example&
│ redirect_uri=...&
│ state=...&
│ scope=create&
│ me=https://user.example
▼
┌─────────────────────────────────────────────────────────┐
│ StarPunk Authorization Endpoint │
│ /auth/authorization │
├─────────────────────────────────────────────────────────┤
│ if not admin_logged_in: │
│ redirect_to_login() │
│ else: │
│ show_authorization_form() │
└────────────────────┬────────────────────────────────────┘
│
│ 2. User approves
│ POST /auth/authorization
│
│ 3. Redirect with code
│ https://app.example/callback?
│ code=xxx&state=yyy
▼
┌─────────────────────────────────────────────────────────┐
│ Micropub Client │
└────────────────────┬────────────────────────────────────┘
│
│ 4. POST /auth/token
│ grant_type=authorization_code&
│ code=xxx&
│ client_id=https://app.example&
│ redirect_uri=...&
│ me=https://user.example&
│ code_verifier=... (if PKCE)
▼
┌─────────────────────────────────────────────────────────┐
│ StarPunk Token Endpoint │
│ /auth/token │
├─────────────────────────────────────────────────────────┤
│ 1. Verify authorization code │
│ 2. Check code not used │
│ 3. Verify client_id matches │
│ 4. Verify redirect_uri matches │
│ 5. Verify me matches │
│ 6. Verify PKCE if present │
│ 7. Check scope not empty │
│ 8. Generate access token │
│ 9. Store hashed token │
│ 10. Return token response │
└────────────────────┬────────────────────────────────────┘
│
│ 5. Response:
│ {
│ "access_token": "xxx",
│ "token_type": "Bearer",
│ "scope": "create",
│ "me": "https://user.example"
│ }
▼
┌─────────────────────────────────────────────────────────┐
│ Micropub Client │
└────────────────────┬────────────────────────────────────┘
│
│ 6. POST /micropub
│ Authorization: Bearer xxx
│ h=entry&content=Hello
▼
┌─────────────────────────────────────────────────────────┐
│ StarPunk Micropub Endpoint │
│ /micropub │
├─────────────────────────────────────────────────────────┤
│ 1. Extract bearer token │
│ 2. Hash token and lookup │
│ 3. Verify not expired │
│ 4. Check scope includes "create" │
│ 5. Parse Micropub properties │
│ 6. Create note via notes.py │
│ 7. Return 201 with Location header │
└─────────────────────────────────────────────────────────┘
Consequences
Positive
- ✅ All spec compliance issues resolved
- ✅ Clear separation between admin auth and Micropub auth
- ✅ Security vulnerability in token storage fixed
- ✅ Simplified V1 scope (create-only)
- ✅ PKCE optional for compatibility
- ✅ Clear property mapping rules
Negative
- ⚠️ Existing tokens will be invalidated (security fix)
- ⚠️ More complex than initially designed
- ⚠️ Two authorization flows to maintain
Neutral
- We become our own authorization server (for Micropub only)
- Admin must be logged in to authorize Micropub clients
- Update/delete deferred to post-V1
Migration Requirements
Database Migration Script
-- Migration: Fix token security and add authorization codes
-- Version: 0.10.0
-- 1. Create secure tokens table
CREATE TABLE tokens_new (
id INTEGER PRIMARY KEY AUTOINCREMENT,
token_hash TEXT UNIQUE NOT NULL,
me TEXT NOT NULL,
client_id TEXT,
scope TEXT DEFAULT 'create',
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
expires_at TIMESTAMP NOT NULL,
last_used_at TIMESTAMP,
revoked_at TIMESTAMP
);
-- 2. Drop insecure table (invalidates all tokens)
DROP TABLE IF EXISTS tokens;
-- 3. Rename to final name
ALTER TABLE tokens_new RENAME TO tokens;
-- 4. Create authorization codes table
CREATE TABLE authorization_codes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
code_hash TEXT UNIQUE NOT NULL,
me TEXT NOT NULL,
client_id TEXT NOT NULL,
redirect_uri TEXT NOT NULL,
scope TEXT,
state TEXT,
code_challenge TEXT,
code_challenge_method TEXT,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
expires_at TIMESTAMP NOT NULL,
used_at TIMESTAMP
);
-- 5. Create indexes
CREATE INDEX idx_tokens_hash ON tokens(token_hash);
CREATE INDEX idx_tokens_expires ON tokens(expires_at);
CREATE INDEX idx_auth_codes_hash ON authorization_codes(code_hash);
CREATE INDEX idx_auth_codes_expires ON authorization_codes(expires_at);
-- 6. Clean up expired auth state
DELETE FROM auth_state WHERE expires_at < datetime('now');
Implementation Checklist
Phase 1: Security & Database
- Create database migration script
- Implement token hashing functions
- Add authorization_codes table
- Update database.py schema
Phase 2: Authorization Endpoint
- Create
/auth/authorizationroute - Implement authorization form template
- Add scope approval UI
- Generate and store authorization codes
Phase 3: Token Endpoint
- Create
/auth/tokenroute - Implement code exchange logic
- Add
meparameter validation - Optional PKCE verification
- Generate and store hashed tokens
Phase 4: Micropub Endpoint (Create Only)
- Create
/micropubroute - Bearer token extraction
- Token verification (hash lookup)
- Property normalization
- Content/title/tags mapping
- Note creation via notes.py
- Location header response
Phase 5: Testing & Documentation
- Test with Indigenous app
- Test with Quill
- Update API documentation
- Security audit
- Performance testing
References
- IndieAuth Spec - Token Endpoint
- IndieAuth Spec - Authorization Code
- Micropub Spec - Authentication
- OAuth 2.0 Security Best Practices
Related ADRs
- ADR-021: IndieAuth Provider Strategy (understanding flows)
- ADR-028: Micropub Implementation Strategy (original design)
- ADR-005: IndieLogin Authentication (admin auth flow)
Date: 2024-11-24 Author: StarPunk Architecture Team Status: Accepted Version Impact: Requires 0.10.0 (breaking change - token invalidation)