Authentication and Authorization Design¶
Version: 1.0 Last Updated: 2026-01-18
Related Documentation¶
- Multi-Provider IdP Support - Architecture for supporting multiple identity providers
- Authentication & Authorization Guide - Operational guide with setup instructions
- Microsoft Entra ID Integration - Entra ID-specific setup and configuration
Overview¶
The MCP Gateway Registry implements a comprehensive authentication and authorization system supporting three distinct identity scenarios:
- Human Users - Interactive users accessing the Registry UI and generating API tokens
- Programmatic Access (API Tokens) - Self-signed JWT tokens for CLI tools and AI coding assistants
- Workload Identity (M2M) - Service accounts for AI agents and automated systems
Identity Types¶
+------------------+------------------+------------------+
| Human Users | Programmatic | Workload |
| | Access | Identity (M2M) |
+------------------+------------------+------------------+
| | | |
| - Interactive | - CLI tools | - AI Agents |
| browser login | - AI coding | - Automated |
| - OAuth2 flow | assistants | pipelines |
| - Session-based | - Scripts | - Service-to- |
| | | service |
+------------------+------------------+------------------+
| | | |
| Auth Method: | Auth Method: | Auth Method: |
| Authorization | Self-signed | OAuth2 Client |
| Code Flow | JWT (HS256) | Credentials Flow |
| | | (RS256) |
+------------------+------------------+------------------+
Part 1: Human User Authentication¶
1.1 OAuth2 Authorization Code Flow¶
Human users authenticate via the configured identity provider (Keycloak or Entra ID) using the standard OAuth2 Authorization Code flow.
+-------------+ +--------------+ +--------------+ +-------------+
| Browser | | Registry | | Auth Server | | Identity |
| (User) | | Frontend | | | | Provider |
+------+------+ +------+-------+ +------+-------+ +------+------+
| | | |
| 1. Click "Login" | | |
+------------------>| | |
| | | |
| 2. Redirect to | | |
| Auth Server | | |
|<------------------+ | |
| | | |
| 3. GET /oauth2/login/entra | |
+--------------------------------------->| |
| | | |
| 4. Redirect to IdP authorize endpoint |
|<-----------------------------------------------------------+
| | | |
| 5. User authenticates with IdP |
+------------------------------------------------------------>|
|<------------------------------------------------------------+
| | | |
| 6. Redirect with authorization code | |
+--------------------------------------->| |
| | | |
| | 7. Exchange code | |
| | for tokens | |
| | +------------------->|
| | |<-------------------+
| | | (ID token + |
| | | access token) |
| | | |
| 8. Set session cookie + redirect | |
|<---------------------------------------+ |
| | | |
| 9. Access Registry with session cookie | |
+------------------>| | |
| | | |
1.2 Session Data¶
After successful authentication, the auth server creates a session containing:
{
"user_id": "user@example.onmicrosoft.com",
"email": "user@example.com",
"groups": ["5f605d68-06bc-4208-b992-bb378eee12c5"],
"provider": "entra",
"scopes": ["public-mcp-users"],
"is_admin": false,
"ui_permissions": {
"list_service": ["all"],
"list_agents": ["/flight-booking"],
"get_agent": ["/flight-booking"]
}
}
1.3 UI Permission Enforcement¶
The Registry UI enforces feature access based on ui_permissions from the user's mapped scopes.
Example: public-mcp-users Scope¶
From cli/examples/public-mcp-users.json:
{
"scope_name": "public-mcp-users",
"ui_permissions": {
"list_service": ["all"],
"list_agents": ["/flight-booking"],
"get_agent": ["/flight-booking"]
}
}
What this user CAN do: - View all MCP servers in the dashboard (list_service: ["all"]) - View the flight-booking agent details (list_agents, get_agent for /flight-booking) - Access public MCP servers: context7, cloudflare-docs (via server_access rules)
What this user CANNOT do: - Publish, modify, or delete agents - Register or modify MCP servers - Toggle services on/off - Access health check for all servers (only context7, cloudflare-docs) - Access IAM management features
Example: registry-admins Scope¶
Admins have unrestricted access to all UI features:
# From scopes.yml
UI-Scopes:
registry-admins:
list_agents: [all]
get_agent: [all]
publish_agent: [all]
modify_agent: [all]
delete_agent: [all]
list_service: [all]
register_service: [all]
health_check_service: [all]
toggle_service: [all]
modify_service: [all]
1.4 Frontend Permission Checks¶
The frontend checks ui_permissions before rendering features:
// From Dashboard.tsx
const hasUiPermission = useCallback((permission: string, servicePath: string): boolean => {
const permissions = user?.ui_permissions?.[permission];
if (!permissions) return false;
const serviceName = servicePath.replace(/^\//, '');
return permissions.includes('all') || permissions.includes(serviceName);
}, [user?.ui_permissions]);
// Usage in JSX
<ServerCard
canHealthCheck={hasUiPermission('health_check_service', server.path)}
canToggle={hasUiPermission('toggle_service', server.path)}
// ...
/>
Part 2: Programmatic Access (Self-Signed JWT Tokens)¶
Human users can generate API tokens for programmatic access (CLI tools, AI coding assistants) via the "Get JWT Token" button in the UI.
2.1 Token Generation Flow¶
+-------------+ +--------------+ +--------------+ +-------------+
| Browser | | Registry | | Auth Server | | MongoDB |
| (User) | | Backend | | | | (Scopes) |
+------+------+ +------+-------+ +------+-------+ +------+------+
| | | |
| 1. Click "Get JWT Token" | |
+------------------>| | |
| | | |
| | 2. POST /api/tokens/generate |
| | (with session cookie) |
| +------------------->| |
| | | |
| | 3. Validate session |
| | Extract: username, groups, provider |
| | | |
| | | 4. Query group |
| | | mappings |
| | +------------------->|
| | |<-------------------+
| | | (scopes for |
| | | user's groups) |
| | | |
| | 5. Build JWT claims: |
| | - iss: mcp-auth-server |
| | - aud: mcp-registry |
| | - sub: username |
| | - groups: [group IDs] |
| | - scope: mapped scopes |
| | - exp: 8 hours |
| | | |
| | 6. Sign JWT with SECRET_KEY (HS256) |
| | | |
| | 7. Return JWT | |
| |<-------------------+ |
| | | |
| 8. Display token | | |
|<------------------+ | |
2.2 Self-Signed JWT Structure¶
{
"iss": "mcp-auth-server",
"aud": "mcp-registry",
"sub": "user@example.onmicrosoft.com",
"preferred_username": "user@example.onmicrosoft.com",
"email": "user@example.com",
"groups": ["5f605d68-06bc-4208-b992-bb378eee12c5"],
"scope": "public-mcp-users",
"token_use": "access",
"auth_method": "oauth2",
"provider": "entra",
"iat": 1768685565,
"exp": 1768714365,
"description": "Generated via sidebar"
}
2.3 Using the Token with CLI Tools¶
# Save token to file
echo "eyJhbGciOiJIUzI1NiIs..." > .token
# Use with registry_management.py
uv run python api/registry_management.py \
--token-file .token \
--registry-url http://localhost \
server-search --query "documentation"
# Use with curl
curl -H "Authorization: Bearer $(cat .token)" \
http://localhost/api/servers
2.4 Token Validation Flow (API Usage)¶
+-------------+ +--------------+ +--------------+ +-------------+
| CLI / | | NGINX | | Auth Server | | MCP Server |
| Client | | Gateway | | | | |
+------+------+ +------+-------+ +------+-------+ +------+------+
| | | |
| 1. API Request | | |
| Authorization: Bearer <JWT> | |
+------------------>| | |
| | | |
| | 2. auth_request /validate |
| +------------------->| |
| | | |
| | 3. Check token issuer |
| | iss == "mcp-auth-server"? |
| | | |
| | 4. If yes: validate with |
| | SECRET_KEY (HS256) |
| | | |
| | 5. If no: try IdP JWKS |
| | validation (RS256) |
| | | |
| | 6. Extract scopes, validate |
| | server/tool access |
| | | |
| | 7. 200 OK + X-User headers |
| |<-------------------+ |
| | | |
| | 8. Proxy request | |
| +--------------------------------------->|
| | | |
| 9. Response | | |
|<------------------+ | |
Part 3: Workload Identity (M2M / Service Accounts)¶
AI agents and automated systems use service accounts with client credentials for authentication.
3.1 M2M Identity in Identity Providers¶
+---------------------------+---------------------------+
| Keycloak | Microsoft Entra ID |
+---------------------------+---------------------------+
| | |
| Service Account Client: | App Registration: |
| - Client ID | - Application (client) ID |
| - Client Secret | - Client Secret |
| - Service Account User | - Service Principal |
| - Group Memberships | - Group Memberships |
| | |
+---------------------------+---------------------------+
3.2 M2M Account Creation Flow (Entra ID)¶
+-------------+ +--------------+ +--------------+ +-------------+
| Admin | | Registry | | Entra ID | | MongoDB |
| CLI | | Backend | | Graph API | | |
+------+------+ +------+-------+ +------+-------+ +------+------+
| | | |
| 1. user-create-m2m --name pub-m2m-bot --groups public-mcp-users
+------------------>| | |
| | | |
| | 2. Create App Registration |
| +------------------->| |
| |<-------------------+ |
| | (app_id, object_id) |
| | | |
| | 3. Create Service Principal |
| +------------------->| |
| |<-------------------+ |
| | (service_principal_id) |
| | | |
| | 4. Create Client Secret |
| +------------------->| |
| |<-------------------+ |
| | (client_secret) |
| | | |
| | 5. Add SP to group (with retry) |
| +------------------->| |
| |<-------------------+ |
| | | |
| 6. Return credentials | |
| client_id, client_secret | |
|<------------------+ | |
3.3 M2M Token Request Flow¶
AI agents use OAuth2 Client Credentials flow to obtain access tokens:
+-------------+ +--------------+ +--------------+
| AI Agent | | Identity | | Registry |
| (M2M) | | Provider | | API |
+------+------+ +------+-------+ +------+-------+
| | |
| 1. POST /oauth2/v2.0/token |
| grant_type=client_credentials |
| client_id=... |
| client_secret=... |
| scope=api://.../.default |
+------------------>| |
| | |
| 2. Access Token (RS256, 1 hour) |
|<------------------+ |
| | |
| 3. API Request with token |
+--------------------------------------->|
| | |
| | 4. Validate via |
| | IdP JWKS |
| | |
| 5. Response | |
|<--------------------------------------+
3.4 Generating M2M Tokens¶
Use the credentials provider script to generate tokens:
Identities File (.oauth-tokens/entra-identities.json):
[
{
"identity_name": "pub-m2m-bot",
"tenant_id": "6e6ee81b-6bf3-495d-a7fc-d363a551f765",
"client_id": "c50b03cf-6f7b-4fae-846e-7910a4100020",
"client_secret": "your-client-secret",
"scope": "api://1bd17ba1-aad3-447f-be0b-26f8f9ee859f/.default"
}
]
Generate Token:
cd credentials-provider
uv run python entra/generate_tokens.py \
--identities-file ../.oauth-tokens/entra-identities.json \
--output-dir ../.oauth-tokens
Output (.oauth-tokens/pub-m2m-bot.json):
{
"identity_name": "pub-m2m-bot",
"access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIs...",
"token_type": "Bearer",
"expires_in": 3599,
"generated_at": "2026-01-18T02:30:56.123456+00:00",
"expires_at": "2026-01-18T03:30:55.123456+00:00",
"provider": "entra",
"tenant_id": "6e6ee81b-6bf3-495d-a7fc-d363a551f765",
"client_id": "c50b03cf-6f7b-4fae-846e-7910a4100020",
"scope": "api://1bd17ba1-aad3-447f-be0b-26f8f9ee859f/.default"
}
3.5 M2M Token Structure (from Entra ID)¶
{
"aud": "api://1bd17ba1-aad3-447f-be0b-26f8f9ee859f",
"iss": "https://sts.windows.net/6e6ee81b-6bf3-495d-a7fc-d363a551f765/",
"iat": 1768703056,
"nbf": 1768703056,
"exp": 1768706956,
"appid": "c50b03cf-6f7b-4fae-846e-7910a4100020",
"appidacr": "1",
"groups": ["5f605d68-06bc-4208-b992-bb378eee12c5"],
"idp": "https://sts.windows.net/6e6ee81b-6bf3-495d-a7fc-d363a551f765/",
"oid": "5d3d562c-4449-413a-9791-86920d4bf75f",
"sub": "5d3d562c-4449-413a-9791-86920d4bf75f",
"tid": "6e6ee81b-6bf3-495d-a7fc-d363a551f765",
"ver": "1.0"
}
Key Differences from Self-Signed Tokens:
| Aspect | Self-Signed (Human) | IdP Token (M2M) |
|---|---|---|
| Issuer | mcp-auth-server | https://sts.windows.net/{tenant}/ |
| Algorithm | HS256 (symmetric) | RS256 (asymmetric) |
| Validation | SECRET_KEY | IdP JWKS endpoint |
| Expiry | 8 hours | 1 hour |
| Subject | username/email | Service principal object ID |
Part 4: Authorization - Scope-Based Access Control¶
4.1 Scope Storage in MongoDB-CE/Amazon DocumentDB¶
Scopes are stored in the mcp_scopes_default collection:
+-----------------------------------------------------------------------+
| mcp_scopes_default collection |
+-----------------------------------------------------------------------+
| { |
| "_id": "public-mcp-users", |
| "group_mappings": [ |
| "public-mcp-users", <-- Keycloak group |
| "5f605d68-06bc-4208-b992-bb378eee12c5" <-- Entra ID Object ID |
| ], |
| "server_access": [ |
| { |
| "server": "context7", |
| "methods": ["initialize", "tools/list", "tools/call"], |
| "tools": ["*"] |
| }, |
| { |
| "server": "api", |
| "methods": ["initialize", "GET", "POST", "servers", ...], |
| "tools": [] |
| } |
| ], |
| "ui_permissions": { |
| "list_service": ["all"], |
| "list_agents": ["/flight-booking"], |
| "get_agent": ["/flight-booking"] |
| } |
| } |
+-----------------------------------------------------------------------+
4.2 Group-to-Scope Mapping Query¶
The auth server queries MongoDB-CE/Amazon DocumentDB to find scopes for a given group:
# From scope_repository.py
async def get_group_mappings(self, keycloak_group: str) -> List[str]:
"""Find all scopes where group_mappings array contains this group."""
collection = await self._get_collection()
cursor = collection.find({"group_mappings": keycloak_group})
scope_names = [doc["_id"] async for doc in cursor]
return scope_names
4.3 Server/Tool Access Validation¶
+------------------+ +------------------+ +------------------+
| | | | | |
| Request: | | User Scopes: | | Access |
| POST /context7 | | [public-mcp- | | Decision: |
| tools/call | | users] | | GRANTED |
| | | | | |
+--------+---------+ +--------+---------+ +--------+---------+
| | |
| 1. Extract server | |
| and method | |
+----------------------->| |
| | |
| 2. For each scope, | |
| check server_ | |
| access rules | |
| +----------------------->|
| | |
| 3. public-mcp-users | |
| allows context7 | |
| with tools/call | |
| | |
Validation Logic (from server.py):
def validate_server_tool_access(
server_name: str,
method: str,
tool_name: Optional[str],
user_scopes: List[str]
) -> bool:
"""Check if user has access to server/method/tool."""
for scope_name in user_scopes:
scope_config = get_scope_config(scope_name)
for server_rule in scope_config.get("server_access", []):
# Check server name match (exact or wildcard)
if server_rule["server"] in (server_name, "*"):
# Check method is allowed
if method in server_rule["methods"] or "all" in server_rule["methods"]:
# Check tool access if specified
if tool_name is None or server_rule["tools"] == "*":
return True
if tool_name in server_rule["tools"]:
return True
return False
Summary Comparison¶
| Aspect | Human User | Programmatic (API Token) | M2M (Workload) |
|---|---|---|---|
| Use Case | Browser UI | CLI, AI assistants | AI agents, automation |
| Auth Flow | OAuth2 Authorization Code | N/A (derived from session) | OAuth2 Client Credentials |
| Token Issuer | IdP (Keycloak/Entra) | mcp-auth-server | IdP (Keycloak/Entra) |
| Token Signing | RS256 (IdP) | HS256 (SECRET_KEY) | RS256 (IdP) |
| Token Lifetime | Session-based | 8 hours | 1 hour |
| Validation | Session cookie | SECRET_KEY | IdP JWKS |
| Groups Source | IdP groups claim | Copied from session | IdP groups claim |
| Scope Mapping | Groups -> Datastore scopes | Embedded in token | Groups -> Datastore scopes |
| Credential Storage | Browser session | Token file | Client ID/Secret |