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