Skip to content

feat(api-keys): add API key authentication via FAB SecurityManager#37973

Open
aminghadersohi wants to merge 5 commits intoapache:masterfrom
aminghadersohi:amin/ch99414/api-key-auth
Open

feat(api-keys): add API key authentication via FAB SecurityManager#37973
aminghadersohi wants to merge 5 commits intoapache:masterfrom
aminghadersohi:amin/ch99414/api-key-auth

Conversation

@aminghadersohi
Copy link
Contributor

SUMMARY

Add API key authentication support to Superset by delegating to Flask-AppBuilder's SecurityManager. This enables programmatic access to Superset for MCP service integrations, CI/CD pipelines, and external applications without requiring browser-based login or frequent JWT refresh.

Architecture: API key auth is implemented at the FAB layer so @protect() works automatically, avoiding split auth management. This benefits the broader FAB ecosystem.

Key changes:

  • Update MCP auth to use FAB's SecurityManager.validate_api_key()
  • Add FAB_API_KEY_ENABLED and FAB_API_KEY_PREFIXES config options
  • Add frontend UI for API key management (list, create, revoke)
  • Add migration for ab_api_key table (FAB-managed)
  • Pin FAB to feature branch with API key support

Design decisions:

  • API keys validated via prefix-based lookup + hash verification (werkzeug check_password_hash) — no plaintext storage
  • Key prefix configurable (FAB_API_KEY_PREFIXES, default ["sst_"]) for identification and efficient DB lookup
  • @protect() checks API keys before JWT — deterministic auth path when prefix is detected
  • Keys inherit user's RBAC permissions via FAB's existing has_access() mechanism
  • UUID-based external references for key management (not auto-increment IDs)

Depends on: dpgaspar/Flask-AppBuilder#2431 (FAB feature branch — all CI green)
Related SIP: #36175

BEFORE/AFTER SCREENSHOTS OR ANIMATED GIF

N/A - New feature, no existing UI changes

TESTING INSTRUCTIONS

  1. Install FAB feature branch: pip install git+https://github.com/aminghadersohi/Flask-AppBuilder@amin/ch99414/api-key-auth
  2. Set FAB_API_KEY_ENABLED = True in superset_config.py
  3. Create an API key via POST /api/v1/security/api_keys/ with Bearer JWT auth
  4. Use the returned key with Authorization: Bearer sst_... on any protected endpoint
  5. Verify key can be revoked via DELETE /api/v1/security/api_keys/<uuid>

ADDITIONAL INFORMATION

Add API key authentication support to Superset by delegating to
Flask-AppBuilder's SecurityManager. This ensures @Protect() works
automatically with API keys and benefits the broader FAB ecosystem.

Changes:
- Update MCP auth to use FAB's SecurityManager.validate_api_key()
- Add FAB_API_KEY_ENABLED and FAB_API_KEY_PREFIXES config options
- Add frontend UI for API key management (list, create, revoke)
- Add migration for ab_api_key table (FAB-managed)
- Pin FAB to feature branch with API key support

Key design decisions:
- API keys validated via prefix-based lookup + hash verification
- Key prefix configurable (default: ["sst_"]) for identification
- Keys inherit user's RBAC permissions via FAB's has_access()
- UUID-based external references for key management

Depends on: dpgaspar/Flask-AppBuilder#2431
Related: apache#36175
@github-actions github-actions bot added the risk:db-migration PRs that require a DB migration label Feb 14, 2026
@dosubot dosubot bot added api Related to the REST API authentication Related to authentication change:frontend Requires changing the frontend labels Feb 14, 2026
Comment on lines 63 to 67
const handleCopyKey = () => {
if (createdKey) {
navigator.clipboard.writeText(createdKey);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: The clipboard write operation is asynchronous but is neither awaited nor error-handled, so if navigator.clipboard.writeText fails (e.g., due to denied permissions or unsupported environment) it will reject with an unhandled promise while the UI still shows a successful "Copied!" state, misleading the user and potentially causing runtime errors. [possible bug]

Severity Level: Major ⚠️
- ⚠️ Clipboard copy failures silently ignored for API key modal.
- ⚠️ Users see "Copied!" despite clipboard write failure.
- ⚠️ Unhandled promise rejections clutter browser error logs.
Suggested change
const handleCopyKey = () => {
if (createdKey) {
navigator.clipboard.writeText(createdKey);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
const handleCopyKey = async () => {
if (!createdKey) {
return;
}
try {
await navigator.clipboard.writeText(createdKey);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
} catch {
addDangerToast(t('Failed to copy API key to clipboard'));
Steps of Reproduction ✅
1. Log into Superset and open the API key management UI that renders `ApiKeyCreateModal`
from `superset-frontend/src/features/apiKeys/ApiKeyCreateModal.tsx:33-150`.

2. Create a new API key via the modal so that `handleFormSubmit` at lines 50-58 sets
`createdKey` to a non-null value and the component re-renders into the "API Key Created"
branch at lines 80-127.

3. In that success modal, click the "Copy" button wired to `handleCopyKey` at lines 63-69,
in a browser/environment where `navigator.clipboard.writeText` rejects (for example,
clipboard permission denied or clipboard API unsupported for the current context).

4. Observe in browser dev tools that the Promise returned by
`navigator.clipboard.writeText` at line 65 is rejected without any `await`/`catch`,
causing an unhandled promise rejection, while the UI still switches the button label to
"Copied!" via `setCopied(true)` at line 66 even though the key was not copied.
Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** superset-frontend/src/features/apiKeys/ApiKeyCreateModal.tsx
**Line:** 63:67
**Comment:**
	*Possible Bug: The clipboard write operation is asynchronous but is neither awaited nor error-handled, so if `navigator.clipboard.writeText` fails (e.g., due to denied permissions or unsupported environment) it will reject with an unhandled promise while the UI still shows a successful "Copied!" state, misleading the user and potentially causing runtime errors.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
👍 | 👎

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch — the clipboard write is async and should be awaited with error handling. Fixed in d653271: handleCopyKey is now async, awaits the clipboard write, and shows a danger toast on failure instead of silently swallowing the rejected promise.

Comment on lines 119 to 121
# Try API key authentication via FAB SecurityManager
api_key_enabled = current_app.config.get("FAB_API_KEY_ENABLED", False)
if api_key_enabled:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: The API key extraction unconditionally calls a helper that likely relies on a Flask request context; when MCP tools run with only an application context (as mcp_auth_hook does for internal/non-HTTP operations), this will raise a RuntimeError ("working outside of request context") instead of gracefully skipping API-key auth, breaking flows that previously worked when API keys are enabled. [logic error]

Severity Level: Critical 🚨
- ❌ MCP tools crash when FAB_API_KEY_ENABLED is enabled.
- ❌ FastMCP internal operations fail with RuntimeError, breaking tooling.
- ⚠️ Development flows without HTTP requests become unusable.
Suggested change
# Try API key authentication via FAB SecurityManager
api_key_enabled = current_app.config.get("FAB_API_KEY_ENABLED", False)
if api_key_enabled:
from flask import has_request_context
# Try API key authentication via FAB SecurityManager
api_key_enabled = current_app.config.get("FAB_API_KEY_ENABLED", False)
if api_key_enabled and has_request_context():
Steps of Reproduction ✅
1. Enable FAB API key auth by setting `FAB_API_KEY_ENABLED = True` in
`superset_config.py`, causing the API-key branch in `get_user_from_request()`
(`superset/mcp_service/auth.py:119-131`) to execute.

2. Start the standalone MCP server which wraps tools with `mcp_auth_hook()` (defined in
`superset/mcp_service/auth.py`) so that each tool call enters `_setup_user_context()` and
then `get_user_from_request()` inside a Flask *application* context only.

3. Trigger an MCP operation that is not handling an HTTP request (e.g., FastMCP tool
discovery or background invocation), so `mcp_auth_hook._get_app_context_manager()` pushes
`app.app_context()` but no request context is created (no `test_request_context()` or view
handling).

4. During this call, `get_user_from_request()` reaches the API-key block and calls
`sm._extract_api_key_from_request()` at line 123, which accesses `flask.request` and
raises `RuntimeError("Working outside of request context")`; this bubbles out of
`_setup_user_context()` (which only treats "application context" errors as expected) and
crashes the MCP tool execution instead of falling back to `MCP_DEV_USERNAME` or returning
`None` for internal calls.
Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** superset/mcp_service/auth.py
**Line:** 119:121
**Comment:**
	*Logic Error: The API key extraction unconditionally calls a helper that likely relies on a Flask request context; when MCP tools run with only an application context (as mcp_auth_hook does for internal/non-HTTP operations), this will raise a RuntimeError ("working outside of request context") instead of gracefully skipping API-key auth, breaking flows that previously worked when API keys are enabled.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
👍 | 👎

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Valid point — when MCP tools run with only an application context (no HTTP request), _extract_api_key_from_request() would hit a RuntimeError trying to access flask.request. Fixed in d653271 by guarding with has_request_context() so API key auth is only attempted when there's an actual HTTP request.

@github-actions github-actions bot removed the api Related to the REST API label Feb 14, 2026
@codecov
Copy link

codecov bot commented Feb 14, 2026

Codecov Report

❌ Patch coverage is 16.66667% with 10 lines in your changes missing coverage. Please review.
✅ Project coverage is 66.42%. Comparing base (76d897e) to head (3f7711b).
⚠️ Report is 3749 commits behind head on master.

Files with missing lines Patch % Lines
superset/mcp_service/auth.py 0.00% 10 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master   #37973      +/-   ##
==========================================
+ Coverage   60.48%   66.42%   +5.93%     
==========================================
  Files        1931      668    -1263     
  Lines       76236    51334   -24902     
  Branches     8568     5779    -2789     
==========================================
- Hits        46114    34098   -12016     
+ Misses      28017    15850   -12167     
+ Partials     2105     1386     -719     
Flag Coverage Δ
hive 41.46% <16.66%> (-7.69%) ⬇️
javascript ?
mysql 64.52% <16.66%> (?)
postgres 64.60% <16.66%> (?)
presto 41.48% <16.66%> (-12.32%) ⬇️
python 66.39% <16.66%> (+2.89%) ⬆️
sqlite 64.19% <16.66%> (?)
unit 100.00% <ø> (+42.36%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

- Import t from @apache-superset/core instead of @superset-ui/core
- Add ApiKeyApi to expected CSRF exempt blueprints set
Copy link
Contributor

@bito-code-review bito-code-review bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review Agent Run #b5c2de

Actionable Suggestions - 1
  • requirements/base.txt - 1
Additional Suggestions - 5
  • superset-frontend/src/features/apiKeys/ApiKeyCreateModal.tsx - 2
    • Logic Error in Close Handler · Line 71-78
      The handleClose function sets createdKey to null before checking its value, which prevents onSuccess from being called after successful API key creation. This means the parent component won't refresh the API key list as intended.
      Code suggestion
       @@ -71,8 +71,8 @@
      -  const handleClose = () => {
      -    setCreatedKey(null);
      -    setCopied(false);
      -    onHide();
      -    if (createdKey) {
      -      onSuccess();
      -    }
      -  };
      +  const handleClose = () => {
      +    if (createdKey) {
      +      onSuccess();
      +    }
      +    setCreatedKey(null);
      +    setCopied(false);
      +    onHide();
      +  };
    • Missing Error Handling for Clipboard · Line 63-69
      The clipboard write operation lacks error handling. If the Clipboard API fails (e.g., due to permissions or browser limitations), users won't know the copy failed, leading to confusion.
  • superset-frontend/src/features/apiKeys/ApiKeyList.tsx - 3
    • Truncate API key prefix display · Line 142-152
      The key_prefix render appends '...' to the full string, potentially showing long values like 'sk-1234567890...'. Truncate to a fixed length (e.g., 10 chars) for better UX.
      Code suggestion
       diff --git a/superset-frontend/src/features/apiKeys/ApiKeyList.tsx b/superset-frontend/src/features/apiKeys/ApiKeyList.tsx
       index 0000000..0000000 100644
      --- a/superset-frontend/src/features/apiKeys/ApiKeyList.tsx
      +++ b/superset-frontend/src/features/apiKeys/ApiKeyList.tsx
       @@ -142,7 +142,7 @@
              render: (prefix: string) => (
                <code
                  css={css`
                    background: ${theme.colorFillSecondary};
                    padding: 2px 6px;
                    border-radius: 3px;
                  `}
                >
      -          {prefix}...
      +          {prefix.length > 10 ? prefix.substring(0, 10) + '...' : prefix}
                </code>
              ),
            },
    • Specify locale in date formatting · Line 91-91
      Using undefined locale may lead to inconsistent date formatting across browsers; specify 'en-US' for consistency.
    • Replace custom spans with antd Tags · Line 98-130
      Use antd Tag components for status badges to avoid custom CSS and follow best practices from AGENTS.md.
      Code suggestion
       diff --git a/superset-frontend/src/features/apiKeys/ApiKeyList.tsx b/superset-frontend/src/features/apiKeys/ApiKeyList.tsx
       index 0000000..0000000 100644
      --- a/superset-frontend/src/features/apiKeys/ApiKeyList.tsx
      +++ b/superset-frontend/src/features/apiKeys/ApiKeyList.tsx
       @@ -1,7 +1,7 @@
        import { useCallback, useEffect, useState } from 'react';
        import { t, SupersetClient } from '@superset-ui/core';
        import { css, useTheme } from '@apache-superset/core/ui';
      -import { Button, Table, Modal, Tooltip } from '@superset-ui/core/components';
      +import { Button, Table, Modal, Tooltip, Tag } from '@superset-ui/core/components';
        import { useToasts } from 'src/components/MessageToasts/withToasts';
        import { ApiKeyCreateModal } from './ApiKeyCreateModal';
       
       @@ -99,29 +99,15 @@
          const getStatusBadge = (key: ApiKey) => {
            if (key.revoked_on) {
      -      return (
      -        <span
      -          css={css`
      -            color: ${theme.colorError};
      -          `}
      -        >
      -          {t('Revoked')}
      -        </span>
      -      );
      +      return <Tag color="error">{t('Revoked')}</Tag>;
            }
            if (key.expires_on && new Date(key.expires_on) < new Date()) {
      -      return (
      -        <span
      -          css={css`
      -            color: ${theme.colorWarning};
      -          `}
      -        >
      -          {t('Expired')}
      -        </span>
      -      );
      +      return <Tag color="warning">{t('Expired')}</Tag>;
            }
      -    return (
      -      <span
      -        css={css`
      -          color: ${theme.colorSuccess};
      -        `}
      -      >
      -        {t('Active')}
      -      </span>
      -    );
      +    return <Tag color="success">{t('Active')}</Tag>;
          };
       
          const columns = [
Review Details
  • Files reviewed - 6 · Commit Range: 100c366..100c366
    • requirements/base.txt
    • superset-frontend/src/features/apiKeys/ApiKeyCreateModal.tsx
    • superset-frontend/src/features/apiKeys/ApiKeyList.tsx
    • superset/config.py
    • superset/mcp_service/auth.py
    • superset/migrations/versions/2026-02-14_12-00_f1a2b3c4d5e6_add_fab_api_key_table.py
  • Files skipped - 0
  • Tools
    • Whispers (Secret Scanner) - ✔︎ Successful
    • Detect-secrets (Secret Scanner) - ✔︎ Successful
    • MyPy (Static Code Analysis) - ✔︎ Successful
    • Astral Ruff (Static Code Analysis) - ✔︎ Successful

Bito Usage Guide

Commands

Type the following command in the pull request comment and save the comment.

  • /review - Manually triggers a full AI review.

  • /pause - Pauses automatic reviews on this pull request.

  • /resume - Resumes automatic reviews.

  • /resolve - Marks all Bito-posted review comments as resolved.

  • /abort - Cancels all in-progress reviews.

Refer to the documentation for additional commands.

Configuration

This repository uses Superset You can customize the agent settings here or contact your Bito workspace admin at evan@preset.io.

Documentation & Help

AI Code Review powered by Bito Logo

# flask-sqlalchemy
# flask-wtf
flask-appbuilder==5.0.2
flask-appbuilder @ git+https://github.com/aminghadersohi/Flask-AppBuilder@amin/ch99414/api-key-auth
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Supply Chain Security Risk

Switching to a git dependency from a third-party fork introduces supply chain risks, as the branch can change unpredictably and the fork may not be vetted. Flask-AppBuilder is central to authentication and RBAC in Superset, so this could affect security if the fork has untrusted code. Consider using an official release or a pinned commit instead.

Code Review Run #b5c2de


Should Bito avoid suggestions like this for future reviews? (Manage Rules)

  • Yes, avoid them

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Acknowledged — this is a temporary pin to the FAB feature branch during development. Once the FAB PR (dpgaspar/Flask-AppBuilder#2431) is merged and released to PyPI, this will be reverted to a standard version pin (e.g., flask-appbuilder>=5.2.0).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The change pins flask-appbuilder to a development branch temporarily, to be reverted to a standard version pin after the upstream PR merges and releases.

- Add async/await and error handling for clipboard write in ApiKeyCreateModal
- Reorder handleClose to call onSuccess before clearing state
- Use antd Tag components for status badges instead of custom styled spans
- Guard API key extraction with has_request_context() to prevent
  RuntimeError when MCP runs outside HTTP request context
@bito-code-review
Copy link
Contributor

bito-code-review bot commented Feb 14, 2026

Code Review Agent Run #bb1233

Actionable Suggestions - 0
Additional Suggestions - 1
  • requirements/development.txt - 1
    • Unofficial dependency fork · Line 265-265
      Switching to a personal fork of Flask-AppBuilder introduces supply chain risks, as the code isn't from an official or reviewed source. This could lead to security vulnerabilities or maintenance issues if the fork becomes unavailable. Consider using the official package or contributing the changes upstream.
Review Details
  • Files reviewed - 8 · Commit Range: 100c366..d653271
    • requirements/base.txt
    • requirements/development.txt
    • superset-frontend/src/features/apiKeys/ApiKeyCreateModal.tsx
    • superset-frontend/src/features/apiKeys/ApiKeyList.tsx
    • superset/config.py
    • superset/mcp_service/auth.py
    • superset/migrations/versions/2026-02-14_12-00_f1a2b3c4d5e6_add_fab_api_key_table.py
    • tests/unit_tests/security/api_test.py
  • Files skipped - 0
  • Tools
    • Whispers (Secret Scanner) - ✔︎ Successful
    • Detect-secrets (Secret Scanner) - ✔︎ Successful
    • MyPy (Static Code Analysis) - ✔︎ Successful
    • Astral Ruff (Static Code Analysis) - ✔︎ Successful

Bito Usage Guide

Commands

Type the following command in the pull request comment and save the comment.

  • /review - Manually triggers a full AI review.

  • /pause - Pauses automatic reviews on this pull request.

  • /resume - Resumes automatic reviews.

  • /resolve - Marks all Bito-posted review comments as resolved.

  • /abort - Cancels all in-progress reviews.

Refer to the documentation for additional commands.

Configuration

This repository uses Superset You can customize the agent settings here or contact your Bito workspace admin at evan@preset.io.

Documentation & Help

AI Code Review powered by Bito Logo

@aminghadersohi
Copy link
Contributor Author

Addressed all review feedback in d653271:

Fixed:

  • Clipboard error handling (CodeAnt): handleCopyKey is now async with try/catch — shows a danger toast on clipboard failure instead of silently showing "Copied!" on an unhandled promise rejection.
  • has_request_context() guard (CodeAnt): API key extraction now only runs when there's an actual HTTP request context, preventing RuntimeError in MCP internal operations (tool discovery, etc.) that run with only an application context.
  • handleClose ordering (Bito): Moved onSuccess() call before state clearing. While React closures mean the original code wasn't technically buggy (createdKey retains its value in the closure), the new ordering is clearer about intent.
  • antd Tag components (Bito): Replaced custom styled <span> elements with antd <Tag color="error|warning|success"> components for status badges, following Superset's convention of preferring antd components over custom CSS.

Acknowledged (no change needed):

  • Supply chain risk (Bito): The git URL dependency on FAB is temporary during development. Will revert to a standard PyPI version pin once FAB PR #2431 is merged and released.
  • Truncate key prefix (Bito): The key_prefix column is already limited to 16 chars in the database schema, so no truncation is needed in the UI.
  • Specify locale (Bito): Using undefined locale in toLocaleDateString() is intentional — it uses the user's browser locale, which is the correct behavior for an internationalized application.

@bito-code-review
Copy link
Contributor

bito-code-review bot commented Feb 14, 2026

Code Review Agent Run #a32c1b

Actionable Suggestions - 0
Review Details
  • Files reviewed - 8 · Commit Range: d653271..3f7711b
    • requirements/base.txt
    • requirements/development.txt
    • superset-frontend/src/features/apiKeys/ApiKeyCreateModal.tsx
    • superset-frontend/src/features/apiKeys/ApiKeyList.tsx
    • superset/config.py
    • superset/mcp_service/auth.py
    • superset/migrations/versions/2026-02-14_12-00_f1a2b3c4d5e6_add_fab_api_key_table.py
    • tests/unit_tests/security/api_test.py
  • Files skipped - 0
  • Tools
    • Whispers (Secret Scanner) - ✔︎ Successful
    • Detect-secrets (Secret Scanner) - ✔︎ Successful

Bito Usage Guide

Commands

Type the following command in the pull request comment and save the comment.

  • /review - Manually triggers a full AI review.

  • /pause - Pauses automatic reviews on this pull request.

  • /resume - Resumes automatic reviews.

  • /resolve - Marks all Bito-posted review comments as resolved.

  • /abort - Cancels all in-progress reviews.

Refer to the documentation for additional commands.

Configuration

This repository uses Superset You can customize the agent settings here or contact your Bito workspace admin at evan@preset.io.

Documentation & Help

AI Code Review powered by Bito Logo

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

authentication Related to authentication change:frontend Requires changing the frontend risk:db-migration PRs that require a DB migration size/XL

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant