Skip to content

feat(microsoft-defender-o365): mvp1/chk4 - implement microsoft defender api authentication (msal) (#497) - #498

Draft
Megafredo wants to merge 12 commits into
feat/493-mvp1-chk2-define-business-configurationfrom
feat/497-mvp1-chk4-msal-authentication
Draft

feat(microsoft-defender-o365): mvp1/chk4 - implement microsoft defender api authentication (msal) (#497)#498
Megafredo wants to merge 12 commits into
feat/493-mvp1-chk2-define-business-configurationfrom
feat/497-mvp1-chk4-msal-authentication

Conversation

@Megafredo

@Megafredo Megafredo commented Jul 16, 2026

Copy link
Copy Markdown
Member

Parent Issue: #497
Parent PR (MVP): #492

Contracts

Input Contract

config: DefenderO365Config  # O365-specific extension of ConfigBaseSettings — declared in CHK.2
# config.tenant_id, config.client_id              — Entra app identity
# config.use_certificate_auth → bool              — determines mode
# config.client_secret                            — secret mode only
# config.client_cert_path, config.client_cert_thumbprint  — cert mode only

Output Contract

class AuthenticationError(CollectorError):
    """Raised when MSAL returns an error response or a result with no access_token.

    Extends CollectorError from the template's exception hierarchy.
    Defined in the O365 collector (e.g. src/auth/exceptions.py) — not a template exception.
    """
    ...

class MSGraphAuthClient:
    def __init__(self, config: DefenderO365Config) -> None: ...

    def get_access_token(self) -> str:
        """Returns a valid bearer token string."""
        ...

Consumed by: DefenderO365DataFetcher (CHK.6), which implements the template's DataFetcherProtocol. The fetcher instantiates MSGraphAuthClient and calls get_access_token() to build the Authorization header on every Graph API request.

Acceptance Criteria

@oauth2 @oaev @microsoft-defender-o365

Feature: MSGraphAuthClient acquires Microsoft Graph access tokens via MSAL
  As a collector process
  I want to obtain a valid access token for the Microsoft Graph API
  So that all downstream API calls can be authenticated

  Background:
    Given a valid DefenderO365Config is available
    And msal.ConfidentialClientApplication is patched for all tests
    And the scope is ["https://graph.microsoft.com/.default"]

  Scenario Outline: Client credentials mode returns an access token on first call
    Given use_certificate_auth is False
    And client_secret is a non-empty string
    And acquire_token_silent returns None (no cached token)
    And acquire_token_for_client returns {"access_token": "<token>", "token_type": "Bearer"}
    When get_access_token is called
    Then the returned value is "<token>"
    And acquire_token_for_client was called once

    Examples:
      | token  |
      | tok123 |

  Scenario Outline: Silent refresh returns the cached token without re-authentication
    Given acquire_token_silent returns {"access_token": "<cached_token>", "token_type": "Bearer"}
    When get_access_token is called
    Then acquire_token_for_client is NOT called
    And the returned value is "<cached_token>"

    Examples:
      | cached_token |
      | cached_tok   |

  Scenario Outline: Certificate mode initializes MSAL with private_key and thumbprint
    Given use_certificate_auth is True
    And client_cert_path points to a readable PEM file containing "<pem_content>"
    And client_cert_thumbprint is "<thumbprint>"
    And acquire_token_for_client returns {"access_token": "<token>", "token_type": "Bearer"}
    When get_access_token is called
    Then ConfidentialClientApplication was initialized with client_credential containing "thumbprint" and "private_key" keys
    And the returned value is "<token>"

    Examples:
      | pem_content         | thumbprint       | token    |
      | -----BEGIN RSA...   | AABBCCDD112233   | cert_tok |

  Scenario Outline: MSAL error response raises AuthenticationError
    Given acquire_token_silent returns None
    And acquire_token_for_client returns {"error": "<error_code>", "error_description": "<error_desc>"}
    When get_access_token is called
    Then an AuthenticationError is raised
    And the exception message contains "<error_code>"

    Examples:
      | error_code     | error_desc  |
      | invalid_client | Bad secret. |

  Scenario Outline: MSAL result missing access_token key raises AuthenticationError
    Given acquire_token_for_client returns {"token_type": "Bearer"} with no "access_token" key
    When get_access_token is called
    Then an AuthenticationError is raised

    Examples:
      | missing_key    |
      | access_token   |

Done Checklist

  • Create src/auth/ms_graph_auth_client.py with MSGraphAuthClient class
  • Define custom AuthenticationError(CollectorError) in src/auth/exceptions.py
  • Implement secret mode: ConfidentialClientApplication(client_credential=secret)
  • Implement cert mode: load PEM file, pass {private_key, thumbprint} to MSAL
  • Implement get_access_token() using acquire_token_for_client
  • Add error handling: check "error" key in MSAL response, raise AuthenticationError
  • Write unit tests with mocked MSAL responses (both modes + error case)
  • Create a dedicated PR linked to the Umbrella issue

Closes #497

@Megafredo Megafredo added the filigran team Item from the Filigran team. label Jul 16, 2026
@Megafredo
Megafredo force-pushed the feat/493-mvp1-chk2-define-business-configuration branch from ee1400d to cb5580c Compare July 17, 2026 15:06
@Megafredo Megafredo self-assigned this Jul 21, 2026
@guzmud guzmud self-assigned this Jul 28, 2026
@guzmud
guzmud force-pushed the feat/497-mvp1-chk4-msal-authentication branch from 66620f2 to 2a8f163 Compare July 28, 2026 11:09
@codecov

codecov Bot commented Jul 28, 2026

Copy link
Copy Markdown

❌ 1 Tests Failed:

Tests completed Failed Passed Skipped
283 1 282 0
View the top 1 failed test(s) by shortest run time
::tests.functional.test_msal_payload_processing
Stack Traces | 0s run time
ImportError while importing test module '.../tests/functional/test_msal_payload_processing.py'.
Hint: make sure your test modules/packages have valid Python names.
Traceback:
.../hostedtoolcache/Python/3.13.14.../x64/lib/python3.13/importlib/__init__.py:88: in import_module
    return _bootstrap._gcd_import(name[level:], package, level)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/functional/test_msal_payload_processing.py:5: in <module>
    import orjson
E   ModuleNotFoundError: No module named 'orjson'

To view more test analytics, go to the Test Analytics Dashboard
📋 Got 3 mins? Take this short survey to help us improve Test Analytics.

@guzmud
guzmud force-pushed the feat/497-mvp1-chk4-msal-authentication branch from ecc6831 to dc72caf Compare July 28, 2026 13:03
@guzmud

guzmud commented Jul 28, 2026

Copy link
Copy Markdown
Member

Note for the reviewer/other devs: the content of the payloads in tests/functional/ have been changed, all UUID were regenerated using python uuid and the access token has been replaced with url-safe data provided by python secrets.

@guzmud
guzmud force-pushed the feat/497-mvp1-chk4-msal-authentication branch from dc72caf to 70d73e2 Compare July 29, 2026 09:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

filigran team Item from the Filigran team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(microsoft-defender-o365): mvp1/chk4 - implement microsoft defender api authentication (msal)

3 participants