Skip to content

feat: Add API key regenerate/revoke from admin dashboard#15

Merged
StuMason merged 7 commits intomainfrom
feature/create-reset-API-key
Jan 18, 2026
Merged

feat: Add API key regenerate/revoke from admin dashboard#15
StuMason merged 7 commits intomainfrom
feature/create-reset-API-key

Conversation

@StuMason
Copy link
Owner

Summary

  • Adds admin-level API key management using session authentication (no API key required)
  • Solves the chicken-and-egg problem where you needed an API key to regenerate your API key
  • Adds Regenerate and Revoke buttons to the API Keys table in the dashboard
  • Shows new key once after regeneration with copy-to-clipboard functionality

Changes

  • POST /admin/api-keys/{key_id}/regenerate - regenerate a key from admin panel
  • POST /admin/api-keys/{key_id}/revoke - revoke a key from admin panel
  • Action buttons in API Keys table with confirmation modals
  • Partial templates for success states

Test plan

  • Login to admin dashboard
  • Navigate to API Keys section
  • Click "Regenerate" on an active key
  • Verify confirmation modal appears with warning
  • Confirm regeneration and verify new key is displayed
  • Copy the new key and verify it works for API access
  • Test "Revoke" action similarly
  • Verify revoked keys show as "Revoked" in the table

Stu Mason + AI me@stumason.dev

Adds admin-level key management that uses session authentication,
solving the chicken-and-egg problem where you needed an API key
to regenerate your API key.

Changes:
- Add POST /admin/api-keys/{key_id}/regenerate endpoint
- Add POST /admin/api-keys/{key_id}/revoke endpoint
- Add Regenerate/Revoke action buttons to API Keys table
- Add confirmation modals with warnings about key invalidation
- Show new key once after regeneration with copy button
- Add partial templates for success/revoked states

Stu Mason + AI <me@stumason.dev>
@claude
Copy link

claude bot commented Jan 17, 2026

Pull Request Review: API Key Regenerate/Revoke from Admin Dashboard

Summary

This PR adds admin-level API key management functionality, solving a chicken-and-egg problem where users needed an API key to manage API keys. The implementation adds two new endpoints that use session-based admin authentication instead of API key authentication.

Detailed Analysis

1. Security Review

Strengths:

  • Proper authentication separation: Uses session-based is_authenticated() checks
  • Authorization checks: Both endpoints verify admin authentication and API key existence
  • Secure key generation: Reuses existing regenerate_api_key() function with secrets.token_hex()
  • One-time display: New API key shown only once after regeneration
  • HTML injection protection: Uses Jinja2 auto-escaping for all dynamic content

Concerns:

  • No CSRF protection visible - should verify Litestar session middleware includes CSRF protection
  • Missing rate limiting on admin endpoints
  • Missing authorization check for multi-tenant scenarios (CRITICAL for SaaS mode)

2. Code Quality Issues

Critical:

  1. Missing authorization check for multi-tenant scenarios
    • Code doesn't verify admin has access to the user_id associated with the API key
    • In DEPLOYMENT_MODE=saas, an admin could potentially manage another user's keys
    • Location: routes.py:1007-1019, 1071-1077

Important:
2. Inconsistent error handling - Generic Exception catch-all is too broad (routes.py:1031-1036, 1094-1099)
3. Transaction management issue - Manual commit/rollback instead of using context manager
4. Missing validation for key_id <= 0

3. Style & Consistency

  • Follows existing patterns from similar endpoints
  • Consistent type hints and docstrings
  • HTML/CSS follows Tailwind patterns
  • JavaScript follows camelCase convention

4. Functional Review

UI/UX Strengths:

  • Clear warning messages about key invalidation
  • Copy-to-clipboard functionality with visual feedback
  • Proper button state management
  • Escape key support for closing modals

Edge Cases Covered:

  • Attempting to regenerate already-revoked key
  • Attempting to revoke already-revoked key
  • API key not found

Edge Cases Missing:

  • Concurrent regeneration of the same key
  • Network failure during API call
  • Session expiration during modal operation

5. Testing Concerns

No automated tests for new endpoints. Should add:

  • Unit tests for regenerate_api_key() and revoke_api_key()
  • Integration tests for the admin endpoints
  • Tests for authentication bypass attempts
  • Tests for multi-tenant isolation

6. Recommendations

Required Changes:

  1. Add multi-tenant authorization check
  2. Verify CSRF protection is enabled
  3. Improve exception handling with specific exception types

Suggested Improvements:
4. Add audit logging for security events
5. Add automated tests
6. Consider rate limiting on admin endpoints
7. Use HTMX for page updates instead of full reload

Recommendation: Request Changes

This is a valuable feature with good implementation, but the multi-tenant authorization check is critical before merge. The security model is sound otherwise.

- Add "Create API Key" button for when no keys exist
- Add "Create New Key" button in header when all keys are revoked
- Add POST /admin/api-keys/create endpoint for creating keys
- Exclude /admin/api-keys/ from CSRF (uses session auth)
- Add CSRF token to fetch requests for key management

Stu Mason + AI <me@stumason.dev>
BREAKING CHANGE: API endpoints now ALWAYS require authentication.

Previously, if no API_KEY env var was set, all data endpoints were
publicly accessible. This was a dangerous default for health data.

Now authentication is required regardless of configuration:
- Use X-API-Key header with a per-user key (from OAuth flow)
- Or use X-API-Key header with the master API_KEY (if configured)

This ensures health data is never accidentally exposed publicly.

Stu Mason + AI <me@stumason.dev>
@claude
Copy link

claude bot commented Jan 17, 2026

Pull Request Review

This PR adds API key management from the admin dashboard. Overall well-designed but has critical security issues.

CRITICAL Security Issues

  1. CSRF Protection Inconsistency (app.py:120)

    • /admin/api-keys/ is in CSRF exclusion list
    • BUT JavaScript sends X-CSRF-Token headers (dashboard.html:396,434,506)
    • FIX: Remove from exclusion list - these endpoints modify sensitive data
  2. Error Handling Exposes Details (routes.py:74-79,128-133,194-199)

    • Generic except blocks return str(e) to users
    • Could expose database errors and stack traces
    • FIX: Log exceptions, return generic error messages
  3. Missing Authorization Check (routes.py:27-80)

    • Endpoints check authentication but not if key belongs to user
    • In multi-tenant: Admin A could revoke Admin Bs key
    • FIX: Add user_id authorization check for SaaS mode

Other Issues

  1. HTML Injection Risk (dashboard.html:368-373)

    • loadServerPartial uses insertAdjacentHTML
    • Could be XSS if error messages include user data
    • Currently safe (key_id is integer) but needs documentation
  2. Race Condition (routes.py:164-175)

    • Check for existing key not atomic
    • FIX: Add unique constraint on (user_id, is_active)
  3. No Tests

    • Need tests for regenerate, revoke, create endpoints
    • Need auth and authorization test cases

Positive Aspects

  • Solves real UX problem (chicken-and-egg with API keys)
  • Good JavaScript: loading states, modals, copy-to-clipboard
  • Proper crypto: SHA-256 hashing, secrets.token_hex
  • Follows codebase patterns well
  • Clear docstrings

Minor Issues

  • Template reuse: create uses api_key_regenerated.html (confusing name)
  • Hardcoded key names (routes.py:180)
  • No audit logging

Recommendation: REQUEST CHANGES

Fix critical issues 1-3 before merging. The feature is well-designed but security must be addressed.

Files reviewed:

  • admin/routes.py (3 endpoints)
  • app.py (CSRF config)
  • dashboard.html (UI + JS)
  • partials templates

@claude
Copy link

claude bot commented Jan 17, 2026

Pull Request Review: API Key Management from Admin Dashboard

Summary

This PR adds admin-level API key management (regenerate, revoke, create) using session authentication. It solves the chicken-and-egg problem where API keys were needed to manage API keys. The implementation includes UI buttons, confirmation modals, CSRF protection, and proper success/error feedback.


Detailed Analysis

1. Security Analysis

Strengths

  1. CSRF Protection: All POST endpoints correctly require CSRF tokens via X-CSRF-Token header (lines 419, 454, 527 in dashboard.html)
  2. Session Authentication: Properly validates admin session before any operation using is_authenticated(request)
  3. Constant-time Comparison: Uses secrets.compare_digest for API key validation (inherited from existing code)
  4. Hash Storage: API keys stored as SHA-256 hashes, raw keys shown only once
  5. Path Exclusion: API key management routes correctly excluded from API key guard in app.py:224

⚠️ Concerns & Recommendations

  1. Authorization Gap (src/polar_flow_server/admin/routes.py:45-107)

    • Issue: Routes check if user is authenticated but don't verify the admin owns the key being modified
    • Example: Line 45-62 in admin_regenerate_api_key - any authenticated admin can regenerate ANY key by ID
    • Risk: In self-hosted mode (single user) this is fine, but violates principle of least privilege
    • Recommendation: Add ownership check:
    # After line 47
    api_key = result.scalar_one_or_none()
    if not api_key:
        return Template(...)
    
    # ADD THIS CHECK:
    # Verify the key belongs to the current admin's user
    admin_user_id = request.session.get("admin_id")
    if api_key.user_id != admin_user_id:  # Or similar ownership check
        return Template(
            template_name="admin/partials/sync_error.html",
            context={"error": "Unauthorized to modify this key."}
        )
  2. Integer ID Exposure (routes.py:26, 82, 136)

    • Issue: Routes use sequential integer IDs in URLs: /api-keys/{key_id:int}
    • Risk: Predictable IDs enable enumeration attacks if authorization isn't perfect
    • Current Mitigation: Session authentication prevents unauthorized access
    • Best Practice: Consider UUIDs for key IDs in future (not blocking for this PR)
  3. Error Message Information Disclosure (routes.py:52, 58)

    • Issue: Detailed error messages like "Cannot regenerate a revoked key" could aid attackers
    • Current Risk: Low (requires valid session)
    • Note: Acceptable for admin panel, just noting for completeness
  4. BREAKING CHANGE - API Key Now Required (src/polar_flow_server/core/auth.py:236-243)

    • Critical Change: API key authentication is now mandatory (previously optional)
    • Old behavior: If API_KEY env var not set, endpoints were open
    • New behavior: Always requires API key in X-API-Key header
    • Impact: This will break existing deployments that relied on open access
    • Recommendation: This should be prominently documented in:
      • PR description (migration guide)
      • CHANGELOG.md
      • README.md security section
      • Consider a startup warning if no API keys exist

2. Bug Analysis

🐛 Potential Issues

  1. Race Condition in Create Key (admin/routes.py:163-175)

    # Check if user already has an active key
    key_stmt = select(APIKey).where(
        APIKey.user_id == user.polar_user_id,
        APIKey.is_active == True
    )
    existing_key = key_result.scalar_one_or_none()
    if existing_key:
        return Template(...)
    
    # Create the API key
    api_key, raw_key = await create_api_key_for_user(...)
    • Issue: Time-of-check-time-of-use (TOCTOU) race condition
    • Scenario: Two concurrent requests could both pass the check and create duplicate keys
    • Mitigation: Add unique constraint on (user_id, is_active) in database, or use SELECT FOR UPDATE
    • Severity: Low (admin panel, unlikely concurrent clicks)
  2. Modal State Management (dashboard.html:359-375)

    • Issue: Global variables currentKeyId, currentKeyPrefix, currentKeyName could theoretically leak between modals
    • Scenario: Rapidly opening/closing different modals might show wrong key info
    • Mitigation: Variables are properly reset in close functions and set in open functions
    • Severity: Very Low (UI-only, no security impact)
  3. Page Reload After Success (dashboard.html:492-494)

    • Issue: window.location.reload() loses the newly generated key if user hasn't copied it
    • Current Mitigation: Copy button is available before reload
    • Edge Case: User closes modal via ESC before copying (line 497-503)
    • Recommendation: Consider showing warning: "Have you copied the key? It won't be shown again"

Proper Error Handling

  • All async operations wrapped in try/catch with rollback (lines 74-79, 128-133, 194-199)
  • Loading states prevent double-submission (lines 410, 435, 447, 520)

3. Code Style & Consistency

Good Practices

  1. Follows Project Patterns: Matches existing admin route structure
  2. Template Reuse: Properly uses admin/partials/sync_error.html for errors
  3. Descriptive Comments: Clear section headers and function docs
  4. Consistent Naming: Follows admin_* prefix convention for route handlers

💅 Minor Style Notes

  1. SQL Boolean Comparison (routes.py:153, 166)

    • Uses == True which triggers noqa comments
    • Already acknowledged with # noqa: E712
    • Acceptable for SQLAlchemy compatibility
  2. JavaScript Style (dashboard.html:389-395)

    • Good: Uses loadServerPartial helper to safely inject trusted HTML
    • Good: Properly escapes HTML via template engine
    • Comment on line 388 correctly notes this is server-rendered trusted content

4. Suggested Improvements

High Priority

  1. Add Migration Guide: Document the breaking change requiring API keys
  2. Add Ownership Check: Verify admin can only modify their own keys (see Security Add Claude Code GitHub Workflow #1)

Medium Priority

  1. Database Constraint: Add unique constraint for active user keys to prevent race condition
  2. Copy Confirmation: Warn before reload that key won't be shown again
  3. Test Coverage: Add tests for:
    • Regenerate with wrong key ID
    • Revoke already revoked key
    • Create when key exists (race condition)
    • CSRF token validation

Low Priority

  1. Consider UUIDs: Use UUIDs for key IDs instead of sequential integers
  2. Audit Logging: Log key regeneration/revocation for security audit trail

Recommendation

REQUEST CHANGES - Address these issues before merge:

Must Fix

  1. Document Breaking Change: The mandatory API key requirement (auth.py:236-243) will break existing deployments. Add migration notes to PR description and documentation.

  2. Add Authorization Check: Prevent admins from modifying keys they don't own (see Security concern Add Claude Code GitHub Workflow #1)

Should Fix

  1. Add Database Constraint: Unique constraint on (user_id, is_active) to prevent duplicate active keys
  2. Add Copy Confirmation: Prevent accidental modal close before copying key

Nice to Have

  1. Add test coverage for edge cases
  2. Consider audit logging for key operations

Test Plan Validation

The PR's test plan is good but should add:

  • Verify error when trying to regenerate someone else's key (after auth fix)
  • Test rapid double-click on create button (race condition)
  • Verify old key stops working after regeneration
  • Test CSRF token validation (remove token, verify request fails)

Overall Assessment: Well-implemented feature with good security fundamentals (CSRF, session auth, hash storage). The main concerns are the undocumented breaking change and missing authorization check. Once those are addressed, this will be a solid addition to the codebase.

Great work on solving the chicken-and-egg problem! 🎉

StuMason and others added 3 commits January 17, 2026 21:15
- Fix ruff formatting in routes.py
- Add confirmation dialog before closing modal if key not copied
- Update README to reflect that API auth is always required
- Remove references to old "open access" mode

Stu Mason + AI <me@stumason.dev>
The api_key_guard function (currently unused) still had the old
insecure behavior that skipped auth when no API_KEY was configured.
Removed this to prevent future accidents and ensure consistency.

Stu Mason + AI <me@stumason.dev>
@claude
Copy link

claude bot commented Jan 17, 2026

Pull Request Review: API Key Management from Admin Dashboard

Summary

This PR adds admin dashboard functionality for managing API keys (create, regenerate, revoke) without requiring an API key for authentication. This solves a critical UX problem where users needed an API key to manage their API keys (chicken-and-egg problem).

Overall Assessment: REQUEST CHANGES ⚠️

While the feature implementation is solid, there are several security concerns and potential bugs that should be addressed before merging.


Security Issues

🔴 Critical: Missing Authorization Check on Key Operations

Location: src/polar_flow_server/admin/routes.py:46-100

The regenerate/revoke/create endpoints check authentication (is_authenticated()) but do NOT verify that the authenticated admin owns the API key being modified. In a multi-user scenario:

  1. Admin A creates their account
  2. Admin B creates their account
  3. Admin A could regenerate/revoke Admin B's API keys by guessing the key_id

Fix needed:

# In admin_regenerate_api_key, admin_revoke_api_key
# After fetching api_key, before operating on it:
if not api_key:
    return Template(...)

# Get the authenticated user's polar_user_id from session
stmt = select(User).where(User.is_active == True).limit(1)
result = await session.execute(stmt)
user = result.scalar_one_or_none()

if user and api_key.user_id != user.polar_user_id:
    return Template(
        template_name="admin/partials/sync_error.html",
        context={"error": "You don't have permission to modify this API key."}
    )

Note: This is currently low-risk in self-hosted mode (single user) but becomes critical if DEPLOYMENT_MODE=saas is ever used.

🟡 Medium: Error Template Reuse

Location: src/polar_flow_server/admin/routes.py:59

The endpoints reuse sync_error.html template which displays "Sync failed" as the heading, even though these aren't sync operations. This could be confusing.

Recommendation: Create a generic admin/partials/error.html template or parameterize the heading.

🟡 Medium: XSS Risk in Error Messages

Location: src/polar_flow_server/admin/routes.py:98, 152, 218

Exception messages are directly interpolated into templates without escaping:

context={"error": f"Failed to regenerate key: {str(e)}"}

If an exception contains user-controlled data (unlikely but possible), this could lead to XSS. Jinja2 auto-escapes by default, but it's best practice to sanitize error messages shown to users.

Recommendation: Use generic error messages or explicitly escape exceptions.


Bugs & Logic Errors

🔴 High: Race Condition in Create API Key

Location: src/polar_flow_server/admin/routes.py:183-195

The check for existing active keys and creation are not atomic:

key_result = await session.execute(key_stmt)
existing_key = key_result.scalar_one_or_none()

if existing_key:
    return Template(...)

# Race window here - another request could create a key
api_key, raw_key = await create_api_key_for_user(...)

If a user clicks "Create API Key" twice rapidly, they could end up with two keys.

Fix: Add a unique constraint in the database (one active key per user) OR use a database lock/transaction isolation level.

🟡 Medium: Inconsistent Key Limit Logic

Location: src/polar_flow_server/admin/routes.py:183-195 & templates/admin/dashboard.html:306

The create endpoint prevents creating a second key if one exists:

if existing_key:
    return Template(..., context={"error": "User already has an active API key..."})

But the UI only shows the "Create New Key" button when has_active_key == False (line 306). This means:

  • Users with revoked keys can't create new ones (UI button hidden)
  • The backend would reject it anyway with a confusing error

Fix: Update the query to check for is_active == True to allow creating new keys after revocation:

key_stmt = select(APIKey).where(
    APIKey.user_id == user.polar_user_id,
    APIKey.is_active == True,  # Already correct
)

And update UI logic at line 306 to match.

🟢 Low: Copy Confirmation UX Issue

Location: src/polar_flow_server/templates/admin/dashboard.html:541-549

The closeAndRefresh() function warns users if they haven't copied the key. However:

  1. If user refreshes the page (F5), the warning doesn't show
  2. keyWasCopied is a global variable that persists across modal opens

Recommendation: Reset keyWasCopied = false when opening modals, and use session storage for page refresh detection.


Code Style & Quality

✅ Good: CSRF Protection

The JavaScript correctly includes CSRF tokens:

headers: {
    'X-CSRF-Token': getCsrfToken(),
}

And the routes are properly excluded from CSRF on admin session routes (app.py:244).

✅ Good: Session vs API Key Auth Separation

The admin routes correctly use session authentication (is_authenticated()) rather than API key guards, solving the chicken-and-egg problem.

✅ Good: Test Coverage

Existing tests in tests/test_api_keys.py cover the core regenerate_api_key and revoke_api_key functions well.

Recommendation: Add integration tests for the new admin endpoints to test the full flow including:

  • CSRF validation
  • Authentication checks
  • Authorization (when implemented)

🟡 Code Duplication

Location: src/polar_flow_server/admin/routes.py:46-219

The three endpoints share similar structure:

  1. Check auth
  2. Find key/user
  3. Validate state
  4. Perform action
  5. Return template

Recommendation: Extract common patterns into helper functions to reduce duplication and improve maintainability.


Documentation

✅ Good: README Updates

The README.md correctly documents the new authentication requirements and removes the misleading "Open Access" option.

🟡 Missing: Revoke Recovery Instructions

Location: src/polar_flow_server/templates/admin/partials/api_key_revoked.html:837

The revoke success message says "reconnect via OAuth to generate a new key" but this isn't accurate - users can click "Create API Key" button (if implemented correctly per bug above).

Recommendation: Update message to: "To restore access, create a new API key from the dashboard."


Testing Recommendations

Before merging, please test:

  1. Multi-key scenarios: Create key → revoke → create another → verify it works
  2. Error paths: Try regenerating a revoked key, try creating when one exists
  3. CSRF: Verify requests without X-CSRF-Token header are rejected
  4. Copy workflow: Ensure users can copy the key and see warnings appropriately
  5. Concurrent requests: Try clicking regenerate twice rapidly

Positive Aspects

  1. ✅ Solves real UX problem elegantly
  2. ✅ Proper use of modals for confirmation (good UX)
  3. ✅ Warning messages for destructive actions
  4. ✅ Copy-to-clipboard functionality
  5. ✅ Consistent with existing codebase patterns
  6. ✅ CSRF protection properly implemented
  7. ✅ Session auth separation is architecturally sound

Required Changes

  1. Add authorization check to verify admin owns the key being modified
  2. Fix race condition in create endpoint (DB constraint or locking)
  3. Fix key limit logic to allow creating keys after revocation
  4. Add integration tests for the new endpoints

Optional Improvements

  1. Create generic error template instead of reusing sync_error.html
  2. Extract common patterns to reduce duplication
  3. Improve copy confirmation UX with session storage
  4. Update revoke success message

Recommendation: Request changes for security and bug fixes before merging.

@claude
Copy link

claude bot commented Jan 17, 2026

Pull Request Review

Summary

This PR adds admin dashboard functionality for managing API keys (create, regenerate, revoke) using session authentication instead of API key authentication. This solves the chicken-and-egg problem where users needed an API key to regenerate their API key.


Detailed Analysis

1. Security Issues 🔴

Critical: CSRF Protection Verification Needed

Location: src/polar_flow_server/admin/routes.py:46-219

The new endpoints use CSRF tokens in the JavaScript client:

  • ✅ The endpoints use X-CSRF-Token header in JavaScript
  • ✅ CSRF token is retrieved from cookies via getCsrfToken()
  • ⚠️ Should be explicitly tested to ensure CSRF middleware is working

Recommendation: Test that CSRF protection is working correctly for these endpoints.

XSS Prevention - Good Implementation

Location: src/polar_flow_server/templates/admin/dashboard.html:403-407

GOOD: Uses .textContent instead of .innerHTML, which prevents XSS
GOOD: The loadServerPartial function properly sanitizes content

Potential IDOR (Insecure Direct Object Reference)

Location: src/polar_flow_server/admin/routes.py:64-73

⚠️ Issue: No authorization check to ensure the authenticated admin owns or has permission to manage this specific API key.

For future SaaS mode: This could allow one user to regenerate/revoke another user's keys by guessing key IDs.

Recommendation: Add authorization check after fetching the api_key to verify ownership or admin status.


2. Logic & Bug Issues 🟡

Prevents Multiple API Keys Per User

Location: src/polar_flow_server/admin/routes.py:183-195

The admin_create_api_key endpoint checks if a user already has an active key and rejects creation. This prevents users from having multiple API keys, which may be a legitimate use case (e.g., different keys for different applications).

Recommendation: Either:

  1. Allow multiple keys per user (remove this check)
  2. Clearly document this limitation in the UI
  3. Make this configurable

Error Handling May Expose Sensitive Information

Location: src/polar_flow_server/admin/routes.py:94-99, 148-153, 214-219

⚠️ Issue: Catching bare Exception can hide programming errors. Also, str(e) might expose sensitive information to the user.

Recommendation:

  • Add logger.exception() calls for debugging
  • Show generic error messages to users
  • Don't expose exception details

Minor: keyWasCopied State Management

Location: src/polar_flow_server/templates/admin/dashboard.html:541-548

GOOD: Warns users before closing without copying
⚠️ Minor: keyWasCopied is a global variable that persists across modals.

Recommendation: Reset keyWasCopied = false when opening modals, not just when closing.


3. Code Style & Consistency ✅

Consistent with Codebase Patterns

  • ✅ Uses existing Template response pattern
  • ✅ Follows async/await conventions
  • ✅ Uses SQLAlchemy correctly with async session
  • ✅ Error handling matches other admin routes
  • ✅ HTMX pattern for partial updates

Good Practices

  • ✅ Session auth check via is_authenticated(request)
  • ✅ Proper session rollback on errors
  • ✅ Uses sync_to_thread=False correctly for async handlers
  • ✅ Clear separation of concerns

Documentation

  • ✅ Good docstrings on new route handlers
  • ✅ Clear comments in JavaScript
  • ✅ Updated README

4. Improvements & Suggestions 💡

Add Tests - HIGH PRIORITY

No tests were added for the new endpoints. Given that these handle security-critical operations (API key management), tests are essential.

Recommendation: Add tests for:

  • Unauthenticated access rejection
  • Successful regeneration
  • Key revocation
  • Key creation

Better UX for Key Prefix Display

Location: src/polar_flow_server/templates/admin/dashboard.html:322-367

The old prefix is shown in the modal when regenerating, which might confuse users since the prefix will change.

Recommendation: Update modal text to clarify that the new key will have a different prefix.

Add Audit Logging - MEDIUM PRIORITY

API key operations (create, regenerate, revoke) are security-sensitive and should be logged for audit trails.

Modal Accessibility

The modals don't have ARIA attributes for accessibility.

Recommendation: Add role="dialog", aria-modal="true", aria-labelledby attributes.

Make Key Name Editable

Currently auto-generated. Consider allowing users to provide custom names like "Production Server" or "Local Dev".


5. Breaking Changes ⚠️

API Now Requires Authentication by Default

Location: src/polar_flow_server/core/auth.py:268-271

🔴 BREAKING CHANGE: This removes the "open access" mode. Users upgrading will need to:

  1. Create an API key via the admin dashboard
  2. Update their API clients to include the key

Documentation: ✅ README was updated to reflect this change


Overall Assessment

Summary:

  • ✅ Solves a real problem (chicken-and-egg key management)
  • ✅ Well-structured code following existing patterns
  • ✅ Good UX with modals and copy functionality
  • ⚠️ Missing authorization checks for SAAS mode
  • ⚠️ No automated tests
  • ⚠️ Error handling could expose sensitive info
  • ⚠️ Breaking change (removes open access mode)

Recommendation: Request Changes

Required Before Merge:

  1. Add authorization check to verify API key ownership/admin status
  2. Improve error handling to avoid exposing exception details
  3. Add automated tests for the new endpoints
  4. Add audit logging for key operations

Nice to Have:

  1. Reset keyWasCopied when opening modals
  2. Allow custom key names
  3. Add accessibility attributes to modals

Files Changed:

  • README.md - Documentation updates ✅
  • src/polar_flow_server/admin/routes.py - New endpoints ⚠️
  • src/polar_flow_server/app.py - Route exclusions ✅
  • src/polar_flow_server/core/auth.py - Breaking change ⚠️
  • src/polar_flow_server/templates/admin/dashboard.html - UI updates ✅
  • src/polar_flow_server/templates/admin/partials/*.html - New templates ✅

Great work on the implementation! With the suggested security improvements and tests, this will be a solid feature. 🚀

@claude
Copy link

claude bot commented Jan 17, 2026

Pull Request Review: API Key Management from Admin Dashboard

Summary

This PR adds admin panel functionality for creating, regenerating, and revoking API keys without requiring API key authentication. This solves the chicken-and-egg problem where users needed an API key to manage their API keys. The implementation uses session-based admin authentication with CSRF protection and provides a clean UI with confirmation modals.


Security Analysis

✅ Strengths

  1. Session-based authentication: All three endpoints properly check is_authenticated(request) before executing
  2. CSRF protection: Implemented correctly with X-CSRF-Token header validation
  3. Proper exclusion from API key guard: Added /admin/api-keys/ to the exclusion list in app.py:244
  4. Key display security: New keys are only shown once and require explicit copy action with confirmation before modal close
  5. No timing attacks: Uses constant-time comparison in underlying auth code

⚠️ Security Concerns

HIGH: Missing Authorization Check

The admin endpoints don't verify which user the admin is authorized to manage. In self-hosted mode this is fine, but the multi-tenancy support means this could be a vulnerability:

  • admin/routes.py:173: admin_create_api_key uses .limit(1) to get the connected user, but doesn't verify the admin should have access to that user
  • admin/routes.py:64-73, 119-128: The regenerate/revoke endpoints only check if the key exists, not if the admin should have access to it

Recommendation: Add admin-to-user authorization checks in all three endpoints.

MEDIUM: XSS Risk in Error Messages

  • admin/routes.py:72, 98, 127, 152, 218: Error messages include user input (key_id, str(e)) which are rendered in templates
  • The templates use Jinja2 which auto-escapes by default, but this should be verified

LOW: No Audit Trail

  • Key regeneration and revocation don't log admin actions, making it difficult to audit who performed sensitive operations
  • Consider adding structured logging for these sensitive operations

Bug Analysis

🐛 Issues Found

1. Race Condition on Key Creation (admin/routes.py:183-196)

Between checking for existing key and creating, another process could create a key. Should use database-level uniqueness constraint or transaction isolation.

2. Revoke Warning Message Inconsistency (admin/partials/api_key_revoked.html:837)

The message says To restore access, reconnect via OAuth to generate a new key. This conflicts with the new Create API Key button functionality.

Recommendation: Change to: To restore access, create a new API key from the dashboard or reconnect via OAuth.

3. Inconsistent Error Handling (admin/routes.py:94-99)

All three endpoints use generic except Exception as e which catches everything including database connection errors. This could mask serious issues.

Recommendation: Catch specific exceptions (like SQLAlchemyError) and handle them appropriately.

4. Modal State Management Bug (dashboard.html:552-558)

The keyWasCopied flag is global but only reset in closeAndRefresh(). If a user opens multiple modals without refreshing, the flag persists incorrectly.

Recommendation: Reset keyWasCopied = false at the start of each confirm function.


Code Style & Consistency

✅ Good Patterns

  1. Follows existing admin route patterns (Template returns, session auth)
  2. Consistent modal structure across all three actions
  3. Proper use of HTMX patterns with partial templates
  4. Clear separation of concerns

🔧 Style Issues

1. Inconsistent SQL Query Style

  • admin/routes.py:173, 186: Uses == True (noqa: E712) for boolean comparison

Most Python style guides recommend is True or just the boolean value directly.

2. Duplicate Template for Different Actions

Both admin_create_api_key and admin_regenerate_api_key return the same template (api_key_regenerated.html). While this works, the template name is misleading for creation.

Recommendation: Rename to something generic like api_key_created.html.


Improvements & Suggestions

1. Input Validation

The key_id path parameter isn't validated beyond type checking. Add bounds checking for negative or zero values.

2. Better User Feedback

The success messages could include more context:

  • Show when the old key was created
  • Display rate limit information

3. Accessibility

The modals lack proper ARIA attributes and keyboard navigation. Add:

  • role=dialog
  • aria-labelledby pointing to modal title
  • Focus trap when modal opens

4. Testing

The PR includes a manual test plan but no automated tests. Add tests for:

  • Authorization verification
  • CSRF token validation
  • Edge cases (regenerating revoked key, invalid key IDs, etc.)
  • Session expiry handling

Files Changed Analysis

README.md

  • ✅ Correctly updates authentication documentation
  • ✅ Removes misleading Open Access option

src/polar_flow_server/admin/routes.py

  • ✅ Clean separation of new functionality
  • ⚠️ Missing authorization checks (HIGH security issue)
  • ⚠️ Overly broad exception handling

src/polar_flow_server/app.py

  • ✅ Properly adds API key management routes to CSRF exclusion
  • ✅ Maintains existing patterns

src/polar_flow_server/core/auth.py

  • ✅ Security improvement: removes dangerous open access mode
  • ✅ Clearer code with better comments

templates/admin/dashboard.html

  • ✅ Good UX with confirmation modals
  • ✅ Copy-to-clipboard functionality
  • ⚠️ Modal state management bug
  • ⚠️ Missing accessibility features

templates/admin/partials/*.html

  • ✅ Clean, reusable partial templates
  • ⚠️ Misleading OAuth reconnection message in revoked template

Overall Recommendation: REQUEST CHANGES

This PR implements valuable functionality and generally follows good security practices, but has issues that should be addressed before merging:

Required Changes:

  1. Add admin-to-user authorization checks in all three new endpoints (especially important for SaaS deployment mode)
  2. Fix the revoke warning message about OAuth reconnection
  3. Fix modal state bug with keyWasCopied flag

Suggested Improvements:

  1. Add specific exception handling instead of catching all exceptions
  2. Add automated tests for the new endpoints
  3. Add audit logging for sensitive operations
  4. Improve accessibility of modals
  5. Add input validation for key_id

Positive Notes:

  • Excellent UX with confirmation modals and one-time key display
  • Proper CSRF protection implementation
  • Clean code organization following existing patterns
  • Solves a real usability problem elegantly

Once the required changes are addressed, this will be a solid addition to the codebase. Great work overall!

- Reset keyWasCopied flag in confirmRegenerate() and confirmCreateKey()
  to prevent false copy warnings when generating multiple keys
- Update revoke message to reference dashboard instead of OAuth
- Add auth model comment explaining admin vs per-key authorization

Stu Mason + AI <me@stumason.dev>
@claude
Copy link

claude bot commented Jan 18, 2026

Pull Request Review - API Key Management

Summary

This PR adds admin-level API key management functionality (create, regenerate, revoke) via the admin dashboard using session-based authentication. This solves the chicken-and-egg problem where users needed an API key to manage their API keys.


1. Security Analysis 🔒

✅ Strengths

  • Authentication Model: Uses session-based admin auth instead of requiring API key auth for key management, which is the correct approach to avoid circular dependency
  • Timing Attack Prevention: Uses existing constant_time_compare and secrets.compare_digest for key validation
  • Secure Key Generation: Uses secrets.token_hex() for cryptographically secure random keys
  • Hash Storage: Keys stored as SHA-256 hashes, never in plaintext
  • One-time Display: Raw keys shown only once after generation with copy confirmation UX
  • CSRF Protection: All POST endpoints include CSRF token validation (X-CSRF-Token header)

⚠️ Concerns

CRITICAL - Missing Authorization Check (src/polar_flow_server/admin/routes.py)

All three new endpoints (admin_regenerate_api_key, admin_revoke_api_key, admin_create_api_key) only check if the user is authenticated but don't verify if they should have access to manage keys:

# Line 68-72
if not is_authenticated(request):
    return Template(...)
# No check if user has permission to manage THIS specific key

Issue: In a multi-tenant scenario (even self-hosted with multiple admin users), any authenticated admin can manage ANY user's API keys. While the comment on line 47-54 states this is intentional for the admin role, there's a potential issue:

  1. admin_create_api_key (line 184-186) selects ANY active user: select(User).where(User.is_active == True).limit(1)
  2. This is problematic if you have multiple users - it will create a key for a random user
  3. For self-hosted single-user deployments, this works, but the code doesn't enforce the single-user constraint

Recommendation:

  • Either add deployment mode checks to ensure single-user mode, OR
  • Add a user selection parameter for multi-user scenarios
  • Consider adding an explicit admin permission model if supporting multi-admin setups

Medium - XSS Risk in Dynamic HTML (src/polar_flow_server/templates/admin/dashboard.html)

Line 354, 364, 419, 432:

onclick="openRegenerateModal({{ key.id }}, '{{ key.key_prefix }}', '{{ key.name }}')"

The key.name is inserted into JavaScript without escaping. If an attacker could control the API key name (which they currently can't through the OAuth flow, but could if manual creation is added), they could inject JavaScript.

Recommendation: Use Jinja2 escaping:

onclick="openRegenerateModal({{ key.id }}, '{{ key.key_prefix | tojson }}', '{{ key.name | tojson }}')"

2. Bugs & Logic Issues 🐛

❌ User Selection Logic Issue

Location: src/polar_flow_server/admin/routes.py:184-186

stmt = select(User).where(User.is_active == True).limit(1)
result = await session.execute(stmt)
user = result.scalar_one_or_none()

Problem: This selects an arbitrary active user. In a multi-user deployment:

  • Which user gets the key is undefined
  • If you have users A, B, C, the key might go to any of them
  • No way for admin to specify which user should get the key

Recommendation:

  • Add user_id parameter to the endpoint
  • Or, if truly single-user only, add a check that enforces only one user exists

⚠️ Error Handling - Rollback After Exception

Location: Lines 105-110, 159-164, 225-230

All three endpoints call await session.rollback() in the exception handler, but:

  1. The session is already in a failed state after an exception
  2. The transaction will auto-rollback on exception anyway with SQLAlchemy async
  3. More importantly, returning the error template after rollback means the user sees an error but the modal stays open - might want to consider UX here

Recommendation: The rollback is fine, but consider logging the actual exception for debugging:

except Exception as e:
    logger.error(f"Failed to regenerate key {key_id}: {e}", exc_info=True)
    await session.rollback()
    # ...

⚠️ Rate Limit Reset Not Ideal

Location: src/polar_flow_server/core/api_keys.py:262-264

api_key.rate_limit_remaining = api_key.rate_limit_requests
api_key.rate_limit_reset_at = None

When regenerating a key, the rate limit resets immediately. This could be exploited by an attacker who compromises an admin account - they could regenerate keys repeatedly to bypass rate limits.

Recommendation: Consider whether to reset rate limits on regeneration or keep them as-is.


3. Code Quality & Style 📝

✅ Good Patterns

  • Consistent error handling with partial templates
  • Good separation of concerns (modals, handlers, templates)
  • Clear comments explaining authorization model
  • Proper use of HTMX patterns for dynamic updates

⚠️ Suggestions

JavaScript: Avoid Global State

Lines 410-412:

let currentKeyId = null;
let currentKeyPrefix = null;
let currentKeyName = null;

Global mutable state can lead to race conditions if user rapidly clicks buttons. Consider passing data via HTML data attributes instead:

function openRegenerateModal(button) {
    const keyId = button.dataset.keyId;
    const keyPrefix = button.dataset.keyPrefix;
    // ...
}

Missing Import

Line 76 in routes.py uses select(APIKey) but there's no import for APIKey model. I see imports for create/regenerate/revoke functions but not the model itself.

Recommendation: Add from polar_flow_server.models.api_key import APIKey at the top of the file.

Duplicate Template Usage

The same template admin/partials/api_key_regenerated.html is used for both regenerate and create operations (lines 96-103 and 216-223). While this works, the messaging is slightly misleading for creation ("API Key Regenerated" when it's actually created).

Recommendation: Consider a separate template or make the template message dynamic.


4. Testing Concerns 🧪

Missing Test Coverage

The PR doesn't include tests for:

  • Regenerating a key that doesn't exist
  • Regenerating a revoked key
  • Revoking a key twice
  • Creating a key when one already exists
  • Creating a key with no users in the system
  • CSRF token validation
  • Session authentication requirements

Recommendation: Add test coverage for these edge cases.


5. Documentation & UX 📚

✅ Good UX Decisions

  • Confirmation modals with clear warnings
  • Copy-to-clipboard functionality
  • Warning when closing without copying key
  • Clear visual feedback (loading states, success/error messages)

⚠️ Improvements

Confusing Message (line 702):

<p>To restore access, you will need to reconnect via OAuth.</p>

This is misleading - according to the code, you can also click "Create New Key" or "Regenerate" (if you create first). The OAuth reconnection is not the only way.

Missing Info in README

The README changes remove the "Open Access" option but don't clearly document:

  • How to create the initial API key
  • What happens if you revoke all keys
  • The admin UI key management flow

6. Breaking Changes ⚠️

CRITICAL - Removed Open Access Mode

Location: src/polar_flow_server/core/auth.py:280-283

The PR removes the open access mode:

# OLD (removed):
# if not settings.api_key:
#     logger.debug("No API_KEY configured - authentication disabled")
#     return

# NEW:
# API key is ALWAYS required - health data should never be public
raise NotAuthorizedException("Missing API key. Use X-API-Key header.")

Impact:

  • Existing deployments without API_KEY configured will suddenly require authentication
  • This is a BREAKING CHANGE that should be clearly documented in release notes
  • Users upgrading will need to create an API key before they can access data endpoints

Recommendation:

  • Add migration documentation
  • Consider deprecation warning in previous version
  • Ensure the admin UI provides clear path to create first key

Overall Assessment

Recommendation: REQUEST CHANGES ⚠️

Critical Issues to Address:

  1. User selection logic in admin_create_api_key - either add user selection or enforce single-user mode
  2. XSS risk in modal JavaScript - escape user-provided data
  3. Breaking change documentation - clearly document removal of open access mode
  4. Missing import - Add APIKey model import

Nice-to-Have Improvements:

  1. Add comprehensive test coverage
  2. Improve error logging
  3. Consider rate limit reset behavior on regeneration
  4. Refine UX messaging
  5. Add deployment mode validation

Security Rating: 7/10

The core security is solid (crypto, hashing, CSRF), but the authorization model needs clarification and the XSS risk should be addressed.

Code Quality: 8/10

Well-structured, consistent patterns, good UX, but needs test coverage and minor refactoring.


Would you like me to provide specific code fixes for any of these issues?

@StuMason StuMason merged commit 2d41a5c into main Jan 18, 2026
5 checks passed
@StuMason StuMason deleted the feature/create-reset-API-key branch January 18, 2026 05:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant