feat(security): add Entra ID token verification and OBO exchange#92
Merged
Conversation
Foundation for the firefly-mcp server. Adds: - security/entra.py with EntraTokenVerifier (RS256 + JWKS, validates signature, expiry, audience, issuer) and EntraOBOClient (OAuth 2.0 On-Behalf-Of using a federated client assertion — no client secrets anywhere in the call path). - entra_tenant_id / entra_client_id / entra_audience / entra_obo_scopes config fields, env-driven via the existing FIREFLY_AGENTIC_ prefix. - entra optional-dependency extra (azure-identity, msal, pyjwt[crypto], httpx). - Plain-function tests covering signature, expiry, audience and issuer rejection, plus OBO success/failure paths with injected MSAL stubs so the suite never touches the network. The existing RBACManager (HS256, internal tokens) is left untouched; this module is its asymmetric, externally-issued counterpart for Entra ID-issued user tokens at the MCP front door.
Renames the optional-dependency extra and updates the typecheck and test jobs in pr-gate.yml to install it. Without the extra, pyright cannot resolve azure.identity / msal in security/entra.py and pytest collection fails on the same imports.
Two changes per review feedback: 1. Rename `security/entra.py` → `security/azure.py` to match the optional dependency extra (`azure`) and the underlying SDK family (azure-identity + msal). Class names stay `EntraTokenVerifier` / `EntraOBOClient` since they describe the protocol (Entra ID OAuth / OBO), not just the SDK. 2. `EntraTokenVerifier` now inherits from `RBACManager`, overriding only `validate_token` to use RS256 + JWKS. `has_permission`, `check_tenant_access`, `get_user_id`, `get_roles`, `get_permissions` are reused unchanged — Entra-validated claims plug straight into the existing authorization machinery. Backward-compatible RBACManager change: `jwt_secret` is now optional. When ``None``, ``create_token``/``validate_token`` raise a clear ``ValueError`` while permission/role methods continue to work. This lets the Entra subclass — and any caller that only needs role checks — skip the symmetric secret entirely. A `verify()` alias is kept on `EntraTokenVerifier` for OAuth-style call sites; it just delegates to `validate_token`.
Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
Wraps the framework's tool registry with a FastMCP server so any MCP client (Claude Code, Claude Desktop, Claude.ai, Cursor, custom Pydantic AI agents) can call into Firefly natively. - exposure/mcp/server.py:create_mcp_app() — enumerates the tool registry at construction time and registers each BaseTool-derived tool's pydantic_handler() on a FastMCP instance. Tools that don't expose a pydantic handler are skipped with a logged warning. - exposure/mcp/transports.py — run_stdio() for the local-subprocess transport that Claude Code/Desktop spawn, and mount_http() for attaching the Streamable HTTP transport to an existing FastAPI from exposure/rest/app.py (one container, one image, both protocols). - cli/mcp_server.py + [project.scripts] firefly-mcp — stdio entry point for the CLI. - mcp optional-dependency extra (fastmcp>=3.2). PR gate now installs --extra mcp --extra rest alongside dev and azure. - Tests cover: tool enumeration round-trip, custom registry injection, stdio invocation, HTTP mount at default and custom paths, CLI wiring. Depends on PR #92 (security/azure.py) for the Entra auth that will guard the HTTP transport in a follow-up.
4 tasks
feat(exposure): add MCP server module + firefly-mcp CLI
javier-alvarez
approved these changes
Apr 30, 2026
ancongui
pushed a commit
that referenced
this pull request
May 31, 2026
Wraps the framework's tool registry with a FastMCP server so any MCP client (Claude Code, Claude Desktop, Claude.ai, Cursor, custom Pydantic AI agents) can call into Firefly natively. - exposure/mcp/server.py:create_mcp_app() — enumerates the tool registry at construction time and registers each BaseTool-derived tool's pydantic_handler() on a FastMCP instance. Tools that don't expose a pydantic handler are skipped with a logged warning. - exposure/mcp/transports.py — run_stdio() for the local-subprocess transport that Claude Code/Desktop spawn, and mount_http() for attaching the Streamable HTTP transport to an existing FastAPI from exposure/rest/app.py (one container, one image, both protocols). - cli/mcp_server.py + [project.scripts] firefly-mcp — stdio entry point for the CLI. - mcp optional-dependency extra (fastmcp>=3.2). PR gate now installs --extra mcp --extra rest alongside dev and azure. - Tests cover: tool enumeration round-trip, custom registry injection, stdio invocation, HTTP mount at default and custom paths, CLI wiring. Depends on PR #92 (security/azure.py) for the Entra auth that will guard the HTTP transport in a follow-up.
ancongui
pushed a commit
that referenced
this pull request
May 31, 2026
feat(security): add Entra ID token verification and OBO exchange
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds the security foundation for an Entra ID-authenticated exposure layer (e.g. an MCP server, REST endpoint, or queue worker that requires user identity).
security/entra.py:EntraTokenVerifier— RS256 verification against the tenant's JWKS endpoint. Validates signature, expiry, audience, issuer.security/entra.py:EntraOBOClient— OAuth 2.0 On-Behalf-Of exchange. Uses a federated client assertion (workload identity federation) so the server holds no client secret. The default assertion provider mints the assertion viaDefaultAzureCredential; tests inject a stub.entra_tenant_id/entra_client_id/entra_audience/entra_obo_scopesconfig fields, env-driven via the existingFIREFLY_AGENTIC_prefix.entraoptional-dependency extra:azure-identity,msal,pyjwt[crypto],httpx.The existing
RBACManager(HS256, internally-issued tokens) is untouched. This module is its asymmetric, externally-issued counterpart for Entra ID user tokens.Why
The framework already exposes agents over REST and queues, but auth on those edges is limited to static API keys / bearer tokens. This change unlocks Entra ID OAuth for any exposure layer without locking the framework to a specific deployment shape —
EntraTokenVerifieris a plain class,EntraOBOClientaccepts injectable providers, and both are opt-in via theentraextra.Test plan
pytest tests/security/test_entra.py— 11 tests, all passing. No network access (JWKS resolver and MSAL app are injected).ruff checkandruff format --checkclean on changed files.pyrightclean onsecurity/entra.pyandconfig.py.pytest -m "not nightly") — 1012 passed, 1 skipped. The single failure (test_custom_timezone) reproduces onmainand is an environment-level tzdata issue unrelated to this PR.