# 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**:
```python
# 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**:
```sql
-- 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**:
```sql
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**:
```python
# 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**:
```python
@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:
```python
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**:
```python
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
```sql
-- 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

- [IndieAuth Spec - Token Endpoint](https://www.w3.org/TR/indieauth/#token-endpoint)
- [IndieAuth Spec - Authorization Code](https://www.w3.org/TR/indieauth/#authorization-code)
- [Micropub Spec - Authentication](https://www.w3.org/TR/micropub/#authentication)
- [OAuth 2.0 Security Best Practices](https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics)

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