Files
StarPunk/docs/decisions/ADR-029-micropub-indieauth-integration.md
Phil Skentelbery 2eaf67279d docs: Standardize all IndieAuth spec references to W3C URL
- 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>
2025-11-24 11:54:04 -07:00

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

  1. Token endpoint missing required me parameter in the IndieAuth spec
  2. PKCE confusion - it's not part of IndieAuth spec, but StarPunk uses it with IndieLogin.com
  3. Database security issue - tokens stored in plain text
  4. Missing authorization_codes table for token exchange
  5. Property mapping rules undefined for Micropub to StarPunk conversion
  6. Authorization endpoint location unclear
  7. 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 me matches 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_challenge in authorization request
  • If present, require code_verifier in 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:

  1. IndieLogin.com doesn't issue access tokens
  2. We need to control scopes and token lifetime
  3. 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:

  1. Remove handle_update() and handle_delete() from design doc
  2. Remove update/delete from supported scopes in V1
  3. Return "invalid_request" if action=update or action=delete
  4. 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:

  1. Create migration to new secure schema
  2. Hash all new tokens before storage
  3. Document that existing tokens will be invalidated
  4. 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/authorization route
  • Implement authorization form template
  • Add scope approval UI
  • Generate and store authorization codes

Phase 3: Token Endpoint

  • Create /auth/token route
  • Implement code exchange logic
  • Add me parameter validation
  • Optional PKCE verification
  • Generate and store hashed tokens

Phase 4: Micropub Endpoint (Create Only)

  • Create /micropub route
  • 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

  • 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)