Skip to content

Installation Validation: OAuth Authentication#

System Criticality: Business-Critical Validation Approach: Risk-based Related Design: DESIGN:OAuthAuthentication (docs/design-oauth-authentication.md) Related URS: @URS:OAuthAuthentication (requirements/features/oauth-authentication.feature) Related Risks: RISK:OAuthAuthentication (docs/risks/risk-assessment-oauth-authentication.md)


Test Strategy#

Level Purpose Owner When
Unit Individual auth functions (OBO exchange, token storage) Developers During dev
Integration Auth middleware + storage backend + Azure AD QA After unit tests
System End-to-end OAuth2 flow in dev environment QA Before prod deploy
Security Token exposure, transport security, scope validation Security Before UAT

Traceability Matrix#

Requirement Summary Risk Test Case Type Status
URS — OAuth2Flow User redirected to Azure AD, MFA enforced RISK-002, RISK-003 TEST-001 Integration Not Started
URS — TokenExchange OBO flow exchanges user token for Graph token RISK-001, RISK-002 TEST-002 Integration Not Started
URS — TokenStorage Tokens stored encrypted, never logged RISK-001 TEST-003 Integration Not Started
URS — TokenRefresh Access token auto-refreshed when expired RISK-002, RISK-005 TEST-004 Integration Not Started
URS — SessionExpiry Re-auth required when refresh token expires RISK-005 TEST-005 System Not Started
URS — ConditionalAccess Azure AD conditional access policies enforced RISK-002, RISK-003 TEST-006 System Not Started
URS — SecretManagement Client secrets in SSM, encrypted, never logged RISK-001 TEST-007 Integration Not Started
URS — AuditTrail Auth events logged with identity, timestamp, IP RISK-006 TEST-008 Integration Not Started

Coverage: 8/8 requirements = 100% (Target: 100%)


Test Cases#

TEST-001: OAuth2 Authorization Flow Initiates Correctly#

Related: URS — OAuth2Flow | RISK-002 | RISK-003 Risk Level: High

Preconditions: - MCP server deployed in dev environment with ENABLE_OAUTH=true - Azure AD app registration configured with correct redirect_uri

Steps: 1. Connect to MCP server endpoint without a session token 2. Observe the server response

Expected: - Server returns HTTP 302 redirect to https://login.microsoftonline.com/{tenant_id}/oauth2/v2.0/authorize - Redirect URL includes client_id, redirect_uri, scope, response_type=code - Redirect URL does NOT include client_secret

Pass Criteria: All expected results met


TEST-002: OBO Token Exchange Succeeds#

Related: URS — TokenExchange | RISK-001 | RISK-002 Risk Level: High

Preconditions: - User has completed Azure AD authentication and holds a valid access token - MCP server configured with valid MCP_SERVER_AZURE_CLIENT_ID and MCP_SERVER_AZURE_CLIENT_SECRET from SSM

Steps: 1. Authenticate as a valid Novo Nordisk user via the OAuth2 flow 2. Issue a tool call that requires a Microsoft Graph API request (e.g., list_emails) 3. Observe CloudWatch logs for the OBO exchange event

Expected: - Graph API call succeeds (HTTP 200) - Log entry: OBO token exchange successful (no bearer token value in log) - Response contains user-scoped data only

Pass Criteria: All expected results met


TEST-003: Token Storage — Encrypted at Rest, Never Logged#

Related: URS — TokenStorage | RISK-001 Risk Level: Critical

Preconditions: - Production configuration: STORAGE_TYPE=dynamodb - User has authenticated and tokens are stored in DynamoDB

Steps: 1. Complete an OAuth2 authentication flow 2. Inspect the DynamoDB table mcp-oauth-storage-* for the stored token entry 3. Grep CloudWatch logs for the authenticated user's session for any token-like strings (Bearer, eyJ) 4. Make an HTTP GET request to any MCP API response — inspect the full response body

Expected: - DynamoDB item exists and is encrypted at rest (encryption confirmed in table settings — AWS managed key) - CloudWatch logs contain no Bearer token values or eyJ (JWT prefix) strings - API response body contains no token values

Pass Criteria: All three checks pass


TEST-004: Expired Access Token Auto-Refreshes#

Related: URS — TokenRefresh | RISK-002 | RISK-005 Risk Level: High

Preconditions: - User has an active session - A valid refresh token is present in the storage backend - Simulate or wait for access token expiry (1-hour Azure AD default)

Steps: 1. Allow the access token to expire (or simulate expiry by modifying the stored token TTL in a test environment) 2. Issue a tool call requiring a Graph API request 3. Observe CloudWatch logs

Expected: - Tool call succeeds — user is NOT prompted to re-authenticate - Log contains a token refresh event - No 401 Unauthorized errors returned to the client

Pass Criteria: All expected results met


TEST-005: Session Expiry Forces Re-Authentication#

Related: URS — SessionExpiry | RISK-005 Risk Level: High

Preconditions: - User has an active session - Simulate refresh token expiry or admin revocation in Azure AD

Steps: 1. Revoke the refresh token in Azure AD (or simulate by clearing it from the storage backend) 2. Issue a tool call requiring authentication 3. Observe the server response

Expected: - Server returns HTTP 401 or HTTP 302 redirect to Azure AD login - No cached credentials or stale tokens are used - Storage backend no longer contains a token entry for that user

Pass Criteria: All expected results met


TEST-006: Azure AD Conditional Access Enforced#

Related: URS — ConditionalAccess | RISK-002 | RISK-003 Risk Level: High

Preconditions: - Azure AD has a conditional access policy requiring MFA for the app registration - A test user account that does NOT satisfy conditional access (e.g., not enrolled in MFA, or outside trusted IP range)

Steps: 1. Attempt to authenticate as the non-compliant test user 2. Observe Azure AD behaviour during the OAuth2 flow 3. Check whether the MCP server receives a token

Expected: - Azure AD blocks authentication with an interaction_required or conditional_access_policy error - The MCP server does NOT receive a valid token for the non-compliant user - MCP server returns HTTP 401 to the client

Pass Criteria: All expected results met


TEST-007: Client Secrets Secured in SSM — Never in Logs or Code#

Related: URS — SecretManagement | RISK-001 Risk Level: Critical

Steps: 1. Inspect the ECS task definition JSON for the outlook-mcp service — check environment variable declarations 2. Search the codebase for any hardcoded client secrets: grep -r "client_secret" connectors/ --include="*.py" -l 3. Inspect CloudWatch log streams for the outlook-mcp service: search for the string client_secret 4. Verify the SSM Parameter Store parameter /aiconnectors/prod/outlook-mcp/client_secret has Type = SecureString

Expected: - Task definition references SSM ARN, not plaintext secret value - No .py file contains a hardcoded secret value (only references to settings.mcp_server_azure_client_secret) - CloudWatch logs contain no string matching the actual client secret - SSM parameter type is SecureString (KMS-encrypted)

Pass Criteria: All four checks pass


TEST-008: Authentication Events Logged — No Sensitive Data Included#

Related: URS — AuditTrail | RISK-006 Risk Level: Medium

Preconditions: - MCP server running and accessible - CloudWatch log group for the service is available

Steps: 1. Complete a successful authentication as a known test user 2. Trigger a failed OBO exchange (e.g., supply an invalid user token) 3. Query CloudWatch Logs for both events

Expected (Success): - Log entry contains: user identity (UPN/email), ISO 8601 UTC timestamp, result = success, IP address - Log entry does NOT contain: access token, refresh token, client secret, authorization code

Expected (Failure): - Log entry contains: failure reason, timestamp - Log entry does NOT contain any token values

Pass Criteria: Both events logged; no sensitive data present in either log entry


Security Tests#

TEST-SEC-001: Expired Token Rejected#

Test: Send a request to an MCP tool endpoint with a manually expired or tampered access token Expected: HTTP 401 Unauthorized Pass: Token rejected; no data returned


TEST-SEC-002: TLS Enforcement on OAuth Endpoints#

Tool: curl --tlsv1.1 (downgrade attempt) Test: Attempt HTTPS connection to MCP server using TLS 1.1 Expected: Connection refused Pass: Only TLS 1.2+ accepted


TEST-SEC-003: Scope Boundary — Read-Only Enforced#

Test: Authenticate with a valid token and attempt a Graph API write operation (e.g., DELETE /me/messages/{id}) using the obtained OBO token Expected: HTTP 403 Forbidden from Graph API Pass: OBO token does not carry write permissions


TEST-SEC-004: No Token in HTTP Response Body#

Tool: HTTP proxy (e.g., mitmproxy) or response body inspection Test: Make authenticated MCP tool calls; capture all HTTP responses Expected: No JWT strings (eyJ) or bearer token values appear in any response body Pass: Zero token values in response payloads


Quality Gates#

Before go-live:

  • TEST-001 through TEST-008 all passed
  • TEST-SEC-001 through TEST-SEC-004 all passed
  • Zero critical defects open
  • No client secrets or token values found in logs or code (TEST-003, TEST-007, TEST-008)
  • DynamoDB encryption at rest confirmed (TEST-003)
  • Azure AD conditional access enforcement confirmed (TEST-006)
  • QA sign-off obtained
  • Security team sign-off obtained

Test Environment#

Dev: https://outlook.dev.connectors.novo-genai.com Prod: https://outlook.connectors.novo-genai.com Token Storage: DynamoDB table mcp-oauth-storage-outlook-mcp (eu-central-1) Logs: AWS CloudWatch — log group /ecs/outlook-mcp SSM Path: /aiconnectors/{env}/outlook-mcp/client_secret Test Users: Dedicated test accounts in Novo Nordisk Azure AD tenant


Version: 1.0 Date: 2026-04-22 Author: AILab Approved by: Pending PR review