# Cloud Deployment Guide
This guide covers deploying the SSO MCP Server in **cloud mode** where the server validates incoming Bearer tokens instead of acquiring tokens via browser SSO.
## Overview
In cloud mode, the MCP server acts as an **OAuth 2.1 Resource Server**:
- Clients obtain tokens from an identity provider (e.g., Azure Entra ID)
- Clients send tokens in the `Authorization: Bearer <token>` header
- The server validates tokens using JWKS (JSON Web Key Sets)
- No browser interaction required
This mode is suitable for:
- Multi-tenant deployments
- Server-to-server communication
- Containerized/cloud-hosted deployments
- Integration with existing OAuth infrastructure
## Architecture
```
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
│ AI Client │ │ MCP Server │ │ Azure Entra ID │
│ (Copilot/Claude)│ │ (Cloud Mode) │ │ (IdP) │
└────────┬────────┘ └────────┬─────────┘ └────────┬────────┘
│ │ │
│ 1. Get token │ │
│───────────────────────┼───────────────────────>│
│ │ │
│ 2. Token response │ │
│<──────────────────────┼────────────────────────│
│ │ │
│ 3. MCP request │ │
│ Authorization: Bearer│ │
│──────────────────────>│ │
│ │ │
│ │ 4. Fetch JWKS │
│ │───────────────────────>│
│ │ │
│ │ 5. JWKS response │
│ │<───────────────────────│
│ │ │
│ │ 6. Validate token │
│ │ (signature, aud, iss) │
│ │ │
│ 7. MCP response │ │
│<──────────────────────│ │
```
## Prerequisites
1. **Azure App Registration** configured as an API (Resource Server)
2. **Client App Registration** configured to request tokens for your API
3. Python 3.11+ and uv package manager
## Azure Configuration
### Step 1: Create API App Registration
1. Go to Azure Portal > Azure Active Directory > App registrations
2. Click "New registration"
3. Configure:
- Name: `SSO MCP Server API`
- Supported account types: Choose based on your needs
- Redirect URI: Not needed for API
4. After creation, note the **Application (client) ID** - this becomes your `RESOURCE_IDENTIFIER`
### Step 2: Expose an API
1. Go to your API app registration > "Expose an API"
2. Set the **Application ID URI** (e.g., `api://sso-mcp-server`)
3. Add scopes:
- `checklist.read` - Read checklists
- `checklist.list` - List checklists
- `process.read` - Read processes
- `process.search` - Search processes
### Step 3: Configure Token Settings
1. Go to "Manifest" and ensure:
```json
{
"accessTokenAcceptedVersion": 2
}
```
This ensures v2.0 tokens are issued.
### Step 4: Note Your Issuer URL
For Azure Entra ID, the issuer URL format is:
```
https://login.microsoftonline.com/{tenant-id}/v2.0
```
For multi-tenant apps, you may also allow:
```
https://login.microsoftonline.com/common/v2.0
https://login.microsoftonline.com/organizations/v2.0
```
## Server Configuration
### Environment Variables
```bash
# Required: Set cloud mode
AUTH_MODE=cloud
# Required: Your API's Application ID URI or Client ID
# This MUST match the 'aud' claim in incoming tokens
RESOURCE_IDENTIFIER=api://sso-mcp-server
# Required: Allowed token issuers (comma-separated)
# Include all tenants that should be able to access your API
ALLOWED_ISSUERS=https://login.microsoftonline.com/your-tenant-id/v2.0
# Required: Path to content files
CHECKLIST_DIR=/app/checklists
PROCESS_DIR=/app/processes
# Optional: Server port (default: 8080)
MCP_PORT=8080
# Optional: JWKS cache TTL in seconds (default: 3600)
JWKS_CACHE_TTL=3600
# Optional: Advertised scopes for Protected Resource Metadata
SCOPES_SUPPORTED=checklist.read,checklist.list,process.read,process.search
# Optional: Log level
LOG_LEVEL=INFO
```
### Docker Deployment
```dockerfile
FROM python:3.11-slim
WORKDIR /app
# Install uv
RUN pip install uv
# Copy application
COPY pyproject.toml uv.lock ./
COPY src/ ./src/
COPY checklists/ ./checklists/
COPY processes/ ./processes/
# Install dependencies
RUN uv sync --frozen
# Set environment variables
ENV AUTH_MODE=cloud
ENV CHECKLIST_DIR=/app/checklists
ENV PROCESS_DIR=/app/processes
ENV MCP_PORT=8080
EXPOSE 8080
CMD ["uv", "run", "sso-mcp-server"]
```
```bash
# Build and run
docker build -t sso-mcp-server .
docker run -p 8080:8080 \
-e RESOURCE_IDENTIFIER=api://sso-mcp-server \
-e ALLOWED_ISSUERS=https://login.microsoftonline.com/tenant-id/v2.0 \
sso-mcp-server
```
### Kubernetes Deployment
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: sso-mcp-server
spec:
replicas: 2
selector:
matchLabels:
app: sso-mcp-server
template:
metadata:
labels:
app: sso-mcp-server
spec:
containers:
- name: sso-mcp-server
image: your-registry/sso-mcp-server:latest
ports:
- containerPort: 8080
env:
- name: AUTH_MODE
value: "cloud"
- name: RESOURCE_IDENTIFIER
valueFrom:
secretKeyRef:
name: sso-mcp-secrets
key: resource-identifier
- name: ALLOWED_ISSUERS
valueFrom:
secretKeyRef:
name: sso-mcp-secrets
key: allowed-issuers
- name: CHECKLIST_DIR
value: "/app/checklists"
- name: PROCESS_DIR
value: "/app/processes"
volumeMounts:
- name: checklists
mountPath: /app/checklists
- name: processes
mountPath: /app/processes
volumes:
- name: checklists
configMap:
name: checklists-config
- name: processes
configMap:
name: processes-config
---
apiVersion: v1
kind: Service
metadata:
name: sso-mcp-server
spec:
selector:
app: sso-mcp-server
ports:
- port: 80
targetPort: 8080
```
## Client Configuration
Clients must obtain tokens with the correct audience and send them in the Authorization header.
### Token Requirements
The access token must have:
- `aud` claim matching `RESOURCE_IDENTIFIER`
- `iss` claim matching one of `ALLOWED_ISSUERS`
- Valid signature (verifiable via issuer's JWKS)
- Not expired (`exp` claim in the future)
### Example: Obtaining a Token (MSAL Python)
```python
from msal import ConfidentialClientApplication
app = ConfidentialClientApplication(
client_id="your-client-app-id",
client_credential="your-client-secret",
authority="https://login.microsoftonline.com/your-tenant-id"
)
# Request token for your API
result = app.acquire_token_for_client(
scopes=["api://sso-mcp-server/.default"]
)
access_token = result["access_token"]
```
### Example: Calling the MCP Server
```python
import httpx
async def call_mcp_tool(token: str, tool_name: str, arguments: dict):
async with httpx.AsyncClient() as client:
response = await client.post(
"http://your-server:8080/mcp",
headers={
"Authorization": f"Bearer {token}",
"Content-Type": "application/json"
},
json={
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": tool_name,
"arguments": arguments
}
}
)
return response.json()
# Usage
result = await call_mcp_tool(
token=access_token,
tool_name="list_checklists",
arguments={}
)
```
## Token Validation Details
The server performs the following validations:
| Check | Description | Error |
|-------|-------------|-------|
| Signature | Verified using issuer's JWKS | `INVALID_SIGNATURE` |
| Expiration | `exp` claim must be in the future | `TOKEN_EXPIRED` |
| Not Before | `nbf` claim (if present) must be in the past | `INVALID_TOKEN` |
| Audience | `aud` must contain `RESOURCE_IDENTIFIER` | `INVALID_AUDIENCE` |
| Issuer | `iss` must be in `ALLOWED_ISSUERS` | `INVALID_ISSUER` |
### JWKS Caching
The server caches JWKS from each issuer to improve performance:
- Default TTL: 3600 seconds (1 hour)
- Automatic refresh on cache expiry
- Automatic retry with cache refresh on key lookup failure (handles key rotation)
## Error Responses
Authentication errors return MCP protocol errors:
```json
{
"jsonrpc": "2.0",
"id": 1,
"error": {
"code": -32002,
"message": "The access token has expired.\n\nAction: Obtain a new access token from the authorization server."
}
}
```
### Error Codes
| HTTP Status | Error Code | Description |
|-------------|------------|-------------|
| 401 | `MISSING_AUTHORIZATION` | No Authorization header |
| 401 | `INVALID_TOKEN` | Malformed or unverifiable token |
| 401 | `INVALID_SIGNATURE` | Signature verification failed |
| 401 | `TOKEN_EXPIRED` | Token has expired |
| 401 | `INVALID_AUDIENCE` | Wrong audience claim |
| 401 | `INVALID_ISSUER` | Untrusted issuer |
| 403 | `INSUFFICIENT_SCOPE` | Missing required scope |
## Security Best Practices
1. **Use HTTPS in production** - Always deploy behind TLS termination
2. **Restrict allowed issuers** - Only allow trusted identity providers
3. **Use short-lived tokens** - Configure IdP for short token lifetimes
4. **Monitor authentication failures** - Set up alerting for auth errors
5. **Rotate secrets regularly** - If using client credentials flow
6. **Network isolation** - Deploy in private subnet if possible
## Monitoring
The server logs authentication events using structured logging:
```json
{"event": "token_validated", "sub": "user@example.com", "iss": "https://login.microsoftonline.com/...", "scopes": ["checklist.read"]}
{"event": "cloud_auth_failed", "error_code": "TOKEN_EXPIRED", "error": "invalid_token"}
```
Set `LOG_LEVEL=DEBUG` for detailed JWKS and validation logs.
## Troubleshooting
### "Invalid audience" Error
1. Check the `aud` claim in your token (decode at jwt.io)
2. Ensure it matches `RESOURCE_IDENTIFIER` exactly
3. For Azure, the audience is usually `api://your-app-id` or the client ID
### "Invalid issuer" Error
1. Check the `iss` claim in your token
2. Ensure it's in `ALLOWED_ISSUERS` (watch for trailing slashes)
3. Azure v2.0 tokens have issuer: `https://login.microsoftonline.com/{tenant}/v2.0`
### "Invalid signature" Error
1. Ensure token is from the expected issuer
2. Check JWKS endpoint is accessible from the server
3. Try clearing JWKS cache by restarting the server
### JWKS Fetch Failures
1. Check network connectivity to issuer's OIDC endpoints
2. Verify `.well-known/openid-configuration` is accessible
3. Check for firewall rules blocking outbound HTTPS
## Related Documentation
- [Quickstart Guide](../specs/001-mcp-sso-checklist/quickstart.md)
- [OAuth 2.1 Resource Server Design](../specs/002-oauth21-resource-server/design.md)
- [Architecture Overview](./architecture.md)