Skip to content

feat(security): add Entra ID token verification and OBO exchange#92

Merged
miguelgfierro merged 6 commits into
mainfrom
feat/entra-security
Apr 30, 2026
Merged

feat(security): add Entra ID token verification and OBO exchange#92
miguelgfierro merged 6 commits into
mainfrom
feat/entra-security

Conversation

@miguelgfierro
Copy link
Copy Markdown
Contributor

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 via DefaultAzureCredential; tests inject a stub.
  • 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.

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 — EntraTokenVerifier is a plain class, EntraOBOClient accepts injectable providers, and both are opt-in via the entra extra.

Test plan

  • pytest tests/security/test_entra.py — 11 tests, all passing. No network access (JWKS resolver and MSAL app are injected).
  • ruff check and ruff format --check clean on changed files.
  • pyright clean on security/entra.py and config.py.
  • Full PR-gate suite (pytest -m "not nightly") — 1012 passed, 1 skipped. The single failure (test_custom_timezone) reproduces on main and is an environment-level tzdata issue unrelated to this PR.

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.
Comment thread src/fireflyframework_agentic/security/azure.py Fixed
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`.
Comment thread src/fireflyframework_agentic/security/azure.py Fixed
miguelgfierro and others added 2 commits April 30, 2026 13:57
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.
feat(exposure): add MCP server module + firefly-mcp CLI
@miguelgfierro miguelgfierro merged commit 33820cc into main Apr 30, 2026
9 checks passed
@miguelgfierro miguelgfierro deleted the feat/entra-security branch April 30, 2026 12:53
@miguelgfierro miguelgfierro mentioned this pull request Apr 30, 2026
3 tasks
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
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.

2 participants