Skip to content

Commit 46761b7

Browse files
authored
Merge pull request #77 from redis/feature/entraid-auth
Add EntraID authentication support for Azure Managed Redis
2 parents 9834059 + 2a3de7d commit 46761b7

File tree

7 files changed

+643
-59
lines changed

7 files changed

+643
-59
lines changed

README.md

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ The Redis MCP Server is a **natural language interface** designed for agentic ap
3636
- [Redis ACL](#redis-acl)
3737
- [Configuration via command line arguments](#configuration-via-command-line-arguments)
3838
- [Configuration via Environment Variables](#configuration-via-environment-variables)
39+
- [EntraID Authentication for Azure Managed Redis](#entraid-authentication-for-azure-managed-redis)
3940
- [Logging](#logging)
4041
- [Integrations](#integrations)
4142
- [OpenAI Agents SDK](#openai-agents-sdk)
@@ -56,6 +57,7 @@ The Redis MCP Server is a **natural language interface** designed for agentic ap
5657
- **Full Redis Support**: Handles **hashes, lists, sets, sorted sets, streams**, and more.
5758
- **Search & Filtering**: Supports efficient data retrieval and searching in Redis.
5859
- **Scalable & Lightweight**: Designed for **high-performance** data operations.
60+
- **EntraID Authentication**: Native support for Azure Active Directory authentication with Azure Managed Redis.
5961
- The Redis MCP Server supports the `stdio` [transport](https://modelcontextprotocol.io/docs/concepts/transports#standard-input%2Foutput-stdio). Support to the `stremable-http` transport will be added in the future.
6062

6163
## Tools
@@ -349,6 +351,85 @@ If desired, you can use environment variables. Defaults are provided for all var
349351
| `REDIS_SSL_CA_CERTS` | Path to the trusted CA certificates file | None |
350352
| `REDIS_CLUSTER_MODE` | Enable Redis Cluster mode | `False` |
351353

354+
### EntraID Authentication for Azure Managed Redis
355+
356+
The Redis MCP Server supports **EntraID (Azure Active Directory) authentication** for Azure Managed Redis, enabling OAuth-based authentication with automatic token management.
357+
358+
#### Authentication Providers
359+
360+
**Service Principal Authentication** - Application-based authentication using client credentials:
361+
```bash
362+
export REDIS_ENTRAID_AUTH_FLOW=service_principal
363+
export REDIS_ENTRAID_CLIENT_ID=your-client-id
364+
export REDIS_ENTRAID_CLIENT_SECRET=your-client-secret
365+
export REDIS_ENTRAID_TENANT_ID=your-tenant-id
366+
```
367+
368+
**Managed Identity Authentication** - For Azure-hosted applications:
369+
```bash
370+
# System-assigned managed identity
371+
export REDIS_ENTRAID_AUTH_FLOW=managed_identity
372+
export REDIS_ENTRAID_IDENTITY_TYPE=system_assigned
373+
374+
# User-assigned managed identity
375+
export REDIS_ENTRAID_AUTH_FLOW=managed_identity
376+
export REDIS_ENTRAID_IDENTITY_TYPE=user_assigned
377+
export REDIS_ENTRAID_USER_ASSIGNED_CLIENT_ID=your-identity-client-id
378+
```
379+
380+
**Default Azure Credential** - Automatic credential discovery (recommended for development):
381+
```bash
382+
export REDIS_ENTRAID_AUTH_FLOW=default_credential
383+
export REDIS_ENTRAID_SCOPES=https://redis.azure.com/.default
384+
```
385+
386+
#### EntraID Configuration Variables
387+
388+
| Name | Description | Default Value |
389+
|-----------------------------------------|-----------------------------------------------------------|--------------------------------------|
390+
| `REDIS_ENTRAID_AUTH_FLOW` | Authentication flow type | None (EntraID disabled) |
391+
| `REDIS_ENTRAID_CLIENT_ID` | Service Principal client ID | None |
392+
| `REDIS_ENTRAID_CLIENT_SECRET` | Service Principal client secret | None |
393+
| `REDIS_ENTRAID_TENANT_ID` | Azure tenant ID | None |
394+
| `REDIS_ENTRAID_IDENTITY_TYPE` | Managed identity type | `"system_assigned"` |
395+
| `REDIS_ENTRAID_USER_ASSIGNED_CLIENT_ID` | User-assigned managed identity client ID | None |
396+
| `REDIS_ENTRAID_SCOPES` | OAuth scopes for Default Azure Credential | `"https://redis.azure.com/.default"` |
397+
| `REDIS_ENTRAID_RESOURCE` | Azure Redis resource identifier | `"https://redis.azure.com/"` |
398+
399+
#### Key Features
400+
401+
- **Automatic token renewal** - Background token refresh with no manual intervention
402+
- **Graceful fallback** - Falls back to standard Redis authentication when EntraID not configured
403+
- **Multiple auth flows** - Supports Service Principal, Managed Identity, and Default Azure Credential
404+
- **Enterprise ready** - Designed for Azure Managed Redis with centralized identity management
405+
406+
#### Example Configuration
407+
408+
For **local development** with Azure CLI:
409+
```bash
410+
# Login with Azure CLI
411+
az login
412+
413+
# Configure MCP server
414+
export REDIS_ENTRAID_AUTH_FLOW=default_credential
415+
export REDIS_URL=redis://your-azure-redis.redis.cache.windows.net:6379
416+
```
417+
418+
For **production** with Service Principal:
419+
```bash
420+
export REDIS_ENTRAID_AUTH_FLOW=service_principal
421+
export REDIS_ENTRAID_CLIENT_ID=your-app-client-id
422+
export REDIS_ENTRAID_CLIENT_SECRET=your-app-secret
423+
export REDIS_ENTRAID_TENANT_ID=your-tenant-id
424+
export REDIS_URL=redis://your-azure-redis.redis.cache.windows.net:6379
425+
```
426+
427+
For **Azure-hosted applications** with Managed Identity:
428+
```bash
429+
export REDIS_ENTRAID_AUTH_FLOW=managed_identity
430+
export REDIS_ENTRAID_IDENTITY_TYPE=system_assigned
431+
export REDIS_URL=redis://your-azure-redis.redis.cache.windows.net:6379
432+
```
352433

353434
There are several ways to set environment variables:
354435

@@ -471,6 +552,7 @@ You can also configure the Redis MCP Server in Augment manually by importing the
471552

472553
The simplest way to configure MCP clients is using `uvx`. Add the following JSON to your `claude_desktop_config.json`, remember to provide the full path to `uvx`.
473554

555+
**Basic Redis connection:**
474556
```json
475557
{
476558
"mcpServers": {
@@ -487,6 +569,26 @@ The simplest way to configure MCP clients is using `uvx`. Add the following JSON
487569
}
488570
```
489571

572+
**Azure Managed Redis with EntraID authentication:**
573+
```json
574+
{
575+
"mcpServers": {
576+
"redis-mcp-server": {
577+
"type": "stdio",
578+
"command": "/Users/mortensi/.local/bin/uvx",
579+
"args": [
580+
"--from", "redis-mcp-server@latest",
581+
"redis-mcp-server",
582+
"--url", "redis://your-azure-redis.redis.cache.windows.net:6379"
583+
],
584+
"env": {
585+
"REDIS_ENTRAID_AUTH_FLOW": "default_credential",
586+
"REDIS_ENTRAID_SCOPES": "https://redis.azure.com/.default"
587+
}
588+
}
589+
}
590+
}
591+
```
490592

491593
### VS Code with GitHub Copilot
492594

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ dependencies = [
2727
"dotenv>=0.9.9",
2828
"numpy>=2.2.4",
2929
"click>=8.0.0",
30+
"redis-entraid>=1.0.0",
3031
]
3132

3233
[project.scripts]

src/common/config.py

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,13 @@
55

66
load_dotenv()
77

8+
# Default values for Entra ID authentication
9+
DEFAULT_TOKEN_EXPIRATION_REFRESH_RATIO = 0.9
10+
DEFAULT_LOWER_REFRESH_BOUND_MILLIS = 30000 # 30 seconds
11+
DEFAULT_TOKEN_REQUEST_EXECUTION_TIMEOUT_MS = 10000 # 10 seconds
12+
DEFAULT_RETRY_MAX_ATTEMPTS = 3
13+
DEFAULT_RETRY_DELAY_MS = 100
14+
815
REDIS_CFG = {
916
"host": os.getenv("REDIS_HOST", "127.0.0.1"),
1017
"port": int(os.getenv("REDIS_PORT", 6379)),
@@ -20,6 +27,55 @@
2027
"db": int(os.getenv("REDIS_DB", 0)),
2128
}
2229

30+
# Entra ID Authentication Configuration
31+
ENTRAID_CFG = {
32+
# Authentication flow selection
33+
"auth_flow": os.getenv(
34+
"REDIS_ENTRAID_AUTH_FLOW", None
35+
), # service_principal, managed_identity, default_credential
36+
# Service Principal Authentication
37+
"client_id": os.getenv("REDIS_ENTRAID_CLIENT_ID", None),
38+
"client_secret": os.getenv("REDIS_ENTRAID_CLIENT_SECRET", None),
39+
"tenant_id": os.getenv("REDIS_ENTRAID_TENANT_ID", None),
40+
# Managed Identity Authentication
41+
"identity_type": os.getenv(
42+
"REDIS_ENTRAID_IDENTITY_TYPE", "system_assigned"
43+
), # system_assigned, user_assigned
44+
"user_assigned_identity_client_id": os.getenv(
45+
"REDIS_ENTRAID_USER_ASSIGNED_CLIENT_ID", None
46+
),
47+
# Default Azure Credential Authentication
48+
"scopes": os.getenv("REDIS_ENTRAID_SCOPES", "https://redis.azure.com/.default"),
49+
# Token lifecycle configuration
50+
"token_expiration_refresh_ratio": float(
51+
os.getenv(
52+
"REDIS_ENTRAID_TOKEN_EXPIRATION_REFRESH_RATIO",
53+
DEFAULT_TOKEN_EXPIRATION_REFRESH_RATIO,
54+
)
55+
),
56+
"lower_refresh_bound_millis": int(
57+
os.getenv(
58+
"REDIS_ENTRAID_LOWER_REFRESH_BOUND_MILLIS",
59+
DEFAULT_LOWER_REFRESH_BOUND_MILLIS,
60+
)
61+
),
62+
"token_request_execution_timeout_ms": int(
63+
os.getenv(
64+
"REDIS_ENTRAID_TOKEN_REQUEST_EXECUTION_TIMEOUT_MS",
65+
DEFAULT_TOKEN_REQUEST_EXECUTION_TIMEOUT_MS,
66+
)
67+
),
68+
# Retry configuration
69+
"retry_max_attempts": int(
70+
os.getenv("REDIS_ENTRAID_RETRY_MAX_ATTEMPTS", DEFAULT_RETRY_MAX_ATTEMPTS)
71+
),
72+
"retry_delay_ms": int(
73+
os.getenv("REDIS_ENTRAID_RETRY_DELAY_MS", DEFAULT_RETRY_DELAY_MS)
74+
),
75+
# Resource configuration
76+
"resource": os.getenv("REDIS_ENTRAID_RESOURCE", "https://redis.azure.com/"),
77+
}
78+
2379

2480
def parse_redis_uri(uri: str) -> dict:
2581
"""Parse a Redis URI and return connection parameters."""
@@ -99,3 +155,77 @@ def set_redis_config_from_cli(config: dict):
99155
else:
100156
# Convert other values to strings
101157
REDIS_CFG[key] = str(value) if value is not None else None
158+
159+
160+
def set_entraid_config_from_cli(config: dict):
161+
"""Update Entra ID configuration from CLI parameters."""
162+
for key, value in config.items():
163+
if value is not None:
164+
if key in ["token_expiration_refresh_ratio"]:
165+
# Keep float values as floats
166+
ENTRAID_CFG[key] = float(value)
167+
elif key in [
168+
"lower_refresh_bound_millis",
169+
"token_request_execution_timeout_ms",
170+
"retry_max_attempts",
171+
"retry_delay_ms",
172+
]:
173+
# Keep integer values as integers
174+
ENTRAID_CFG[key] = int(value)
175+
else:
176+
# Convert other values to strings
177+
ENTRAID_CFG[key] = str(value)
178+
179+
180+
def is_entraid_auth_enabled() -> bool:
181+
"""Check if Entra ID authentication is enabled."""
182+
return ENTRAID_CFG["auth_flow"] is not None
183+
184+
185+
def get_entraid_auth_flow() -> str:
186+
"""Get the configured Entra ID authentication flow."""
187+
return ENTRAID_CFG["auth_flow"]
188+
189+
190+
def validate_entraid_config() -> tuple[bool, str]:
191+
"""Validate Entra ID configuration based on the selected auth flow.
192+
193+
Returns:
194+
tuple: (is_valid, error_message)
195+
"""
196+
auth_flow = ENTRAID_CFG["auth_flow"]
197+
198+
if not auth_flow:
199+
return True, "" # No Entra ID auth configured, which is valid
200+
201+
if auth_flow == "service_principal":
202+
required_fields = ["client_id", "client_secret", "tenant_id"]
203+
missing_fields = [field for field in required_fields if not ENTRAID_CFG[field]]
204+
if missing_fields:
205+
return (
206+
False,
207+
f"Service principal authentication requires: {', '.join(missing_fields)}",
208+
)
209+
210+
elif auth_flow == "managed_identity":
211+
identity_type = ENTRAID_CFG["identity_type"]
212+
if (
213+
identity_type == "user_assigned"
214+
and not ENTRAID_CFG["user_assigned_identity_client_id"]
215+
):
216+
return (
217+
False,
218+
"User-assigned managed identity requires user_assigned_identity_client_id",
219+
)
220+
221+
elif auth_flow == "default_credential":
222+
# Default credential doesn't require specific configuration
223+
pass
224+
225+
else:
226+
return (
227+
False,
228+
f"Invalid auth_flow: {auth_flow}. Must be one of: service_principal, managed_identity, default_credential",
229+
)
230+
231+
return True, ""

src/common/connection.py

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,11 @@
55
from redis import Redis
66
from redis.cluster import RedisCluster
77

8-
from src.common.config import REDIS_CFG
8+
from src.common.config import REDIS_CFG, is_entraid_auth_enabled
9+
from src.common.entraid_auth import (
10+
create_credential_provider,
11+
EntraIDAuthenticationError,
12+
)
913
from src.version import __version__
1014

1115
_logger = logging.getLogger(__name__)
@@ -18,6 +22,17 @@ class RedisConnectionManager:
1822
def get_connection(cls, decode_responses=True) -> Redis:
1923
if cls._instance is None:
2024
try:
25+
# Create Entra ID credential provider if configured
26+
credential_provider = None
27+
if is_entraid_auth_enabled():
28+
try:
29+
credential_provider = create_credential_provider()
30+
except EntraIDAuthenticationError as e:
31+
_logger.error(
32+
"Failed to create Entra ID credential provider: %s", e
33+
)
34+
raise
35+
2136
if REDIS_CFG["cluster_mode"]:
2237
redis_class: Type[Union[Redis, RedisCluster]] = (
2338
redis.cluster.RedisCluster
@@ -37,6 +52,12 @@ def get_connection(cls, decode_responses=True) -> Redis:
3752
"lib_name": f"redis-py(mcp-server_v{__version__})",
3853
"max_connections_per_node": 10,
3954
}
55+
56+
# Add credential provider if available
57+
if credential_provider:
58+
connection_params["credential_provider"] = credential_provider
59+
# Note: Azure Redis Enterprise with EntraID uses plain text connections
60+
# SSL setting is controlled by REDIS_SSL environment variable
4061
else:
4162
redis_class: Type[Union[Redis, RedisCluster]] = redis.Redis
4263
connection_params = {
@@ -56,6 +77,12 @@ def get_connection(cls, decode_responses=True) -> Redis:
5677
"max_connections": 10,
5778
}
5879

80+
# Add credential provider if available
81+
if credential_provider:
82+
connection_params["credential_provider"] = credential_provider
83+
# Note: Azure Redis Enterprise with EntraID uses plain text connections
84+
# SSL setting is controlled by REDIS_SSL environment variable
85+
5986
cls._instance = redis_class(**connection_params)
6087

6188
except redis.exceptions.ConnectionError:

0 commit comments

Comments
 (0)