Skip to content

Strengthen TRE API authentication and clean up codebase#4989

Open
marrobi wants to merge 19 commits into
microsoft:mainfrom
marrobi:copilot/redesign-tre-api-auth
Open

Strengthen TRE API authentication and clean up codebase#4989
marrobi wants to merge 19 commits into
microsoft:mainfrom
marrobi:copilot/redesign-tre-api-auth

Conversation

@marrobi

@marrobi marrobi commented Jul 22, 2026

Copy link
Copy Markdown
Member

The TRE API auth layer was a single god-object (AzureADAuthorization) mixing JWT validation, JWKS key management, Graph API calls, and role checking, with an unused AccessService abstraction sitting on top.

New api_app/auth/ package

Introduces a clean, layered auth package:

  • models.pyAuthenticatedUser (frozen Pydantic; roles is an immutable tuple so roles cannot be reassigned or mutated in place), TRERole/WorkspaceAccessRole StrEnums, has_any_role()/is_tre_admin() helpers
  • exceptions.py — Typed exception hierarchy (TokenExpired, TokenSignatureInvalid, TokenInvalid, InsufficientPermissions, WorkspaceNotFound); no silent swallowing
  • token_validator.pyTokenValidator using PyJWKClient for automatic JWKS key management; validates issuer + audience + expiry + signature
  • registry.py@lru_cache singletons: get_core_validator() for the TRE core app, get_workspace_validator(client_id) per workspace, all sharing a single PyJWKClient
  • dependencies.pyrequire_bearer_credentials (HTTPBearer with auto_error=False → consistent 401 + WWW-Authenticate: Bearer on missing/malformed headers) and get_authenticated_user
  • rbac.pyrequire_roles() / require_workspace_roles() dependency factories + pre-built named checks. Workspace-scoped checks validate against the workspace app registration first and only fall back to the core app registration for TREAdmin (no cross-audience elevation for other core tokens)

Route migration to the new auth layer

  • All API routes now use the auth.rbac dependencies (require_tre_admin, require_workspace_owner, etc.) instead of the legacy services/authentication.py singletons
  • The legacy module-level auth singletons and the get_current_* dependencies have been removed

Removal of AccessService abstraction

  • services/access_service.py deleted — Entra ID is the only auth provider; the abstraction was never varied
  • AzureADAuthorization is now a plain Microsoft Graph service wrapper (no OAuth2AuthorizationCodeBearer base class and no __call__); it is used only for Graph API calls (role-assignment lookups) via get_aad_service()
  • AuthConfigValidationError / UserRoleAssignmentError moved into aad_authentication.py
  • get_access_service() replaced by get_aad_service() across routes, airlock service, and repositories
  • Dead function get_app_user_roles_assignments_emails (unreachable) removed from resource_helpers.py

Tests

  • New unit tests for TokenValidator, RBAC/AuthenticatedUser helpers, the workspace-validator + TREAdmin-only fallback paths, model immutability, and missing-credential handling
  • Full api_app unit test suite passes (706 tests)
  • conftest.py auth fixtures updated for the new validator/dependency structure

Notes

  • api_app/_version.py bumped to 0.26.0
  • CHANGELOG.md updated under ENHANCEMENTS

Copilot AI review requested due to automatic review settings July 22, 2026 10:53

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR refactors TRE API authentication by introducing a new layered api_app/auth/ package (token validation, typed user model, RBAC dependency factories) and removes the unused AccessService abstraction, while updating API routes and tests to use the new RBAC dependencies.

Changes:

  • Added api_app/auth/ with TokenValidator (JWKS via PyJWKClient), typed auth exceptions, an immutable AuthenticatedUser, and composable RBAC dependency factories.
  • Removed services/access_service.py and updated legacy AAD auth/service plumbing to use AzureADAuthorization directly for Graph calls (via get_aad_service()).
  • Migrated many route-level dependencies and tests from legacy services.authentication dependencies to auth.rbac dependencies.

Reviewed changes

Copilot reviewed 41 out of 43 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
CHANGELOG.md Documents the auth refactor and AccessService removal.
api_app/tests_ma/test_services/test_aad_access_service.py Updates imports after moving exceptions into aad_authentication.py.
api_app/tests_ma/test_db/test_repositories/test_airlock_request_repository.py Updates patches to get_aad_service in repository tests.
api_app/tests_ma/test_api/test_routes/test_workspaces.py Switches route auth overrides to new auth.rbac dependencies.
api_app/tests_ma/test_api/test_routes/test_workspace_users.py Switches route auth overrides to new auth.rbac dependencies.
api_app/tests_ma/test_api/test_routes/test_workspace_templates.py Updates admin/user dependency overrides to auth.rbac.
api_app/tests_ma/test_api/test_routes/test_workspace_service_templates.py Updates admin/user dependency overrides to auth.rbac.
api_app/tests_ma/test_api/test_routes/test_user_resource_templates.py Updates admin/user dependency overrides to auth.rbac.
api_app/tests_ma/test_api/test_routes/test_shared_services.py Updates admin/user dependency overrides to auth.rbac.
api_app/tests_ma/test_api/test_routes/test_shared_service_templates.py Updates admin/user dependency overrides to auth.rbac.
api_app/tests_ma/test_api/test_routes/test_requests.py Updates route auth overrides to auth.rbac.
api_app/tests_ma/test_api/test_routes/test_migrations.py Updates route auth overrides to auth.rbac.
api_app/tests_ma/test_api/test_routes/test_api_access.py Updates access-control tests to override auth.rbac dependencies.
api_app/tests_ma/test_api/test_routes/test_airlock.py Updates route auth overrides to auth.rbac and adjusts role param generation.
api_app/tests_ma/test_api/conftest.py Reworks global auth patching for new validator/dependency structure.
api_app/tests_ma/auth/test_token_validator.py Adds unit tests for TokenValidator.
api_app/tests_ma/auth/test_rbac.py Adds unit tests for new RBAC dependency factories and AuthenticatedUser helpers.
api_app/tests_ma/auth/init.py Adds auth tests package marker.
api_app/services/authentication.py Removes get_access_service; adds get_aad_service and updates extract_auth_information.
api_app/services/airlock.py Replaces get_access_service usage with get_aad_service.
api_app/services/access_service.py Removes unused AccessService abstraction and its exceptions.
api_app/services/aad_authentication.py Refactors JWT validation to use new auth.registry validators and keeps Graph role logic.
api_app/db/repositories/airlock_requests.py Uses get_aad_service for role assignment lookups.
api_app/auth/token_validator.py Implements TokenValidator backed by PyJWKClient.
api_app/auth/registry.py Adds cached validator registry (get_core_validator, get_workspace_validator).
api_app/auth/rbac.py Adds require_roles / require_workspace_roles factories and prebuilt role dependencies.
api_app/auth/models.py Introduces AuthenticatedUser and role enums.
api_app/auth/exceptions.py Adds typed auth exception hierarchy.
api_app/auth/dependencies.py Adds FastAPI deps for core/workspace token validation and HTTP exception mapping.
api_app/auth/init.py Adds auth package marker.
api_app/api/routes/workspaces.py Migrates router dependencies to auth.rbac; updates AAD service usage for workspace filtering.
api_app/api/routes/workspace_users.py Migrates router dependencies to auth.rbac and get_aad_service.
api_app/api/routes/workspace_templates.py Migrates admin router dependencies to auth.rbac.
api_app/api/routes/workspace_service_templates.py Migrates router dependencies to auth.rbac.
api_app/api/routes/user_resource_templates.py Migrates router dependencies to auth.rbac.
api_app/api/routes/shared_services.py Migrates router dependencies to auth.rbac.
api_app/api/routes/shared_service_templates.py Migrates router dependencies to auth.rbac.
api_app/api/routes/resource_helpers.py Switches role assignment lookup to get_aad_service and removes dead helper.
api_app/api/routes/requests.py Migrates router dependencies to auth.rbac.
api_app/api/routes/operations.py Migrates router dependencies to auth.rbac.
api_app/api/routes/migrations.py Migrates router dependencies to auth.rbac.
api_app/api/routes/costs.py Migrates router dependencies to auth.rbac.
api_app/api/routes/airlock.py Migrates router dependencies to auth.rbac.

Comment thread api_app/api/routes/workspaces.py Outdated
Comment thread api_app/services/aad_authentication.py Outdated
Comment thread api_app/tests_ma/test_api/conftest.py Outdated
Comment thread api_app/tests_ma/auth/test_rbac.py
Comment thread api_app/auth/models.py Outdated
Comment thread api_app/auth/models.py Outdated
@marrobi

marrobi commented Jul 22, 2026

Copy link
Copy Markdown
Member Author

@copilot address PR review comments (and replies) and fix merge conflicts.

Copilot AI review requested due to automatic review settings July 22, 2026 13:46
@github-actions

github-actions Bot commented Jul 22, 2026

Copy link
Copy Markdown

Unit Test Results

711 tests   711 ✅  10s ⏱️
  1 suites    0 💤
  1 files      0 ❌

Results for commit 67bc00b.

♻️ This comment has been updated with latest results.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 41 out of 43 changed files in this pull request and generated 3 comments.

Comment thread CHANGELOG.md Outdated
Comment thread api_app/auth/registry.py
Comment thread api_app/tests_ma/test_api/conftest.py
- Remove unused get_workspace_authenticated_user (had broken Depends(lambda: None))
- Remove dead AzureADAuthorization.__call__ and helpers; make it a plain Graph service class
- Share a single PyJWKClient across token validators via registry
Copilot AI review requested due to automatic review settings July 22, 2026 13:56

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 41 out of 43 changed files in this pull request and generated 1 comment.

Comments suppressed due to low confidence (1)

CHANGELOG.md:12

  • Changelog entries in this file consistently include an issue/PR reference link (e.g. "(#4950)"). This new entry is missing a reference, which makes it harder to trace the change back to the discussion/PR.
* Strengthen TRE API authentication: introduce layered `auth/` package with typed exceptions, `PyJWKClient`-backed token validation with issuer checking, immutable `AuthenticatedUser` model, and composable RBAC factories; remove the `AccessService` abstraction that is no longer needed now that Entra ID is the only auth provider.

Comment thread api_app/auth/dependencies.py
- Store AuthenticatedUser.roles as an immutable tuple so roles cannot be
  escalated via in-place mutation (frozen only blocked reassignment)
- Ignore E231 in flake8 config (Python 3.12 f-string tokenisation false positives)
- Fix E306 nested-def blank line in test_migrations.py
Copilot AI review requested due to automatic review settings July 22, 2026 14:17

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 43 out of 45 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (2)

CHANGELOG.md:12

  • The new CHANGELOG entry does not include an issue/PR reference link, while other entries in this section consistently include one (e.g. "(#4920)"). The contribution guidelines in this repo require changelog entries to include an issue and/or PR reference.
* Strengthen TRE API authentication: introduce layered `auth/` package with typed exceptions, `PyJWKClient`-backed token validation with issuer checking, immutable `AuthenticatedUser` model, and composable RBAC factories; remove the `AccessService` abstraction that is no longer needed now that Entra ID is the only auth provider.

api_app/auth/dependencies.py:17

  • 401 responses raised from the auth dependency currently omit the WWW-Authenticate: Bearer header. The previous auth layer included this header on 401s, and omitting it can break standards-compliant clients and auth middleware that relies on it.
def _to_http_exception(exc: AuthError) -> HTTPException:
    if isinstance(exc, TokenExpired):
        return HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail=strings.EXPIRED_SIGNATURE,

- Add PR reference to CHANGELOG auth entry
- Add WWW-Authenticate: Bearer header to 401 responses in auth dependencies
- Guard get_required_roles against endpoint.__defaults__ being None
Copilot AI review requested due to automatic review settings July 22, 2026 15:09

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 44 out of 46 changed files in this pull request and generated 2 comments.

Comment thread api_app/auth/dependencies.py Outdated
Comment thread api_app/services/aad_authentication.py
Copilot AI review requested due to automatic review settings July 22, 2026 15:16

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 44 out of 46 changed files in this pull request and generated 1 comment.

Comments suppressed due to low confidence (1)

api_app/services/aad_authentication.py:42

  • PR description says AzureADAuthorization now inherits directly from OAuth2AuthorizationCodeBearer, but in the actual code it is now a plain service wrapper (no FastAPI security base class / __call__). This is a meaningful behavioral change (it can no longer be used as a FastAPI security dependency) and the PR description should be updated to match the implementation so reviewers/operators aren’t misled.
class AzureADAuthorization:
    """Service wrapper for Microsoft Graph calls related to workspace auth.

    Handles workspace app-registration validation and role-assignment lookups
    via the Microsoft Graph API. JWT validation for incoming API requests is
    handled separately by the :mod:`auth` package.
    """

Comment thread api_app/auth/rbac.py Outdated
…ng creds

- require_workspace_roles core fallback now only grants access to TREAdmin;
  any other valid core token is rejected with 401 (no cross-audience elevation)
- HTTPBearer(auto_error=False) + require_bearer_credentials returns 401 +
  WWW-Authenticate for missing/malformed Authorization headers (was FastAPI 403)
- Update workspace-roles tests to exercise the workspace-validator path and
  the hardened fallback; add missing-credentials tests
Copilot AI review requested due to automatic review settings July 22, 2026 15:34

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 44 out of 46 changed files in this pull request and generated 10 comments.

Comment thread api_app/tests_ma/auth/test_rbac.py Outdated
Comment thread api_app/tests_ma/auth/test_rbac.py Outdated
Comment thread api_app/tests_ma/auth/test_rbac.py Outdated
Comment thread api_app/tests_ma/auth/test_rbac.py Outdated
Comment thread api_app/tests_ma/auth/test_rbac.py Outdated
Comment thread api_app/tests_ma/auth/test_rbac.py Outdated
Comment thread api_app/tests_ma/auth/test_rbac.py Outdated
Comment thread api_app/tests_ma/auth/test_rbac.py Outdated
Comment thread api_app/tests_ma/auth/test_rbac.py Outdated
Comment thread api_app/tests_ma/auth/test_rbac.py Outdated
The migration inadvertently granted TREAdmin access to ALL workspace-scoped
endpoints (require_workspace_roles always allowed TREAdmin). Pre-PR, only
endpoints explicitly using the *_or_tre_admin dependencies allowed TREAdmin;
owner/researcher/airlock-manager-only endpoints did not.

- Make TREAdmin access opt-in via require_workspace_roles(..., allow_tre_admin)
- When allow_tre_admin=False, no core-token fallback occurs (wrong-audience
  token -> 401), preserving separation of platform admin vs workspace access
- Add require_workspace_owner_or_tre_admin and
  require_workspace_owner_or_researcher_or_airlock_manager_or_tre_admin, and
  re-map the exact endpoints that used *_or_tre_admin in main (costs,
  workspace_users router, workspaces shared router + operations/history +
  template listing)
- Update route tests' dependency overrides and rbac tests accordingly
Copilot AI review requested due to automatic review settings July 22, 2026 15:56

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 44 out of 46 changed files in this pull request and generated 1 comment.

Comments suppressed due to low confidence (1)

api_app/tests_ma/auth/test_rbac.py:32

  • asyncio.get_event_loop().run_until_complete(...) can raise RuntimeError on Python 3.12+ when no current loop is set (and it’s a deprecated pattern in newer asyncio usage). Using asyncio.run(...) (or making these tests async def with @pytest.mark.asyncio) avoids relying on a global event loop.

Comment thread api_app/auth/token_validator.py
- token_validator: add require=[exp,iss,aud] so tokens missing these claims are
  rejected (PyJWT only validates a claim when present; a token without exp was
  otherwise accepted indefinitely)
- test_rbac: use asyncio.run() instead of get_event_loop().run_until_complete()
- add tests for the required-claim behavior
Copilot AI review requested due to automatic review settings July 22, 2026 16:18

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 44 out of 46 changed files in this pull request and generated no new comments.

@marrobi

marrobi commented Jul 22, 2026

Copy link
Copy Markdown
Member Author

/test-extended 67bc00b

@github-actions

Copy link
Copy Markdown

🤖 pr-bot 🤖

🏃 Running extended tests: https://github.com/microsoft/AzureTRE/actions/runs/29953944587 (with refid 28fe96b6)

(in response to this comment from @marrobi)

@marrobi
marrobi marked this pull request as ready for review July 22, 2026 20:10
@marrobi
marrobi requested a review from a team as a code owner July 22, 2026 20:10
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.

3 participants