Skip to content

Commit c9464de

Browse files
committed
Added credential provider factory methods
1 parent 689e6b8 commit c9464de

File tree

6 files changed

+236
-148
lines changed

6 files changed

+236
-148
lines changed

README.md

Lines changed: 25 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
The `redis-entra-id` Python package helps simplifying the authentication with [Azure Managed Redis](https://azure.microsoft.com/en-us/products/managed-redis) and Azure Cache for Redis using Microsoft Entra ID (formerly Azure Active Directory). It enables seamless integration with Azure's Redis services by fetching authentication tokens and managing the token renewal in the background. This package builds on top of `redis-py` and provides a structured way to authenticate by using a:
1+
from redis.auth.token_manager import RetryPolicyThe `redis-entra-id` Python package helps simplifying the authentication with [Azure Managed Redis](https://azure.microsoft.com/en-us/products/managed-redis) and Azure Cache for Redis using Microsoft Entra ID (formerly Azure Active Directory). It enables seamless integration with Azure's Redis services by fetching authentication tokens and managing the token renewal in the background. This package builds on top of `redis-py` and provides a structured way to authenticate by using a:
22

33
* System-assigned managed identity
44
* User-assigned managed identity
@@ -35,7 +35,7 @@ You need to install the `redis-py` Entra ID package via the following command:
3535
pip install redis-entra-id
3636
```
3737

38-
The package depends on [redis-py](https://github.com/redis/redis-py/tree/v5.3.0b4) version `5.3.0b4`.
38+
The package depends on [redis-py](https://github.com/redis/redis-py).
3939

4040
## Usage
4141

@@ -45,31 +45,38 @@ After having installed the package, you can import its modules:
4545

4646
```python
4747
import redis
48-
from redis_entraid import identity_provider
4948
from redis_entraid import cred_provider
5049
```
5150

52-
### Step 2 - Define your authority based on the tenant ID
51+
### Step 2 - Create the credential provider via the factory method
5352

5453
```python
55-
authority = "{}/{}".format("https://login.microsoftonline.com", "<TENANT_ID>")
54+
cred_provider = create_from_service_principal(
55+
client_credential="<CLIENT_SECRET>",
56+
client_id="<CLIENT_ID>",
57+
tenant_id="<TENANT_ID>"
58+
)
5659
```
5760

58-
> This step is going to be removed in the next pre-release version of `redis-py-entraid`. Instead, the factory method will allow to pass the tenant id direclty.
61+
### Step 3 - Provide optional token renewal configuration
5962

60-
### Step 3 - Create the identity provider via the factory method
61-
62-
```python
63-
idp = identity_provider.create_provider_from_service_principal("<CLIENT_SECRET>", "<CLIENT_ID>", authority=authority)
64-
```
65-
66-
### Step 4 - Initialize a credentials provider from the authentication configuration
67-
68-
You can use the default configuration or customize the background task for token renewal.
63+
The default configuration would be applied, but you're able to customise it.
6964

7065
```python
71-
auth_config = TokenAuthConfig(idp)
72-
cred_provider = EntraIdCredentialsProvider(auth_config)
66+
cred_provider = create_from_service_principal(
67+
client_credential="<CLIENT_SECRET>",
68+
client_id="<CLIENT_ID>",
69+
tenant_id="<TENANT_ID>",
70+
token_manager_config=TokenManagerConfig(
71+
expiration_refresh_ratio=0.9,
72+
lower_refresh_bound_millis=DEFAULT_LOWER_REFRESH_BOUND_MILLIS,
73+
token_request_execution_timeout_in_ms=DEFAULT_TOKEN_REQUEST_EXECUTION_TIMEOUT_IN_MS,
74+
retry_policy=RetryPolicy(
75+
max_attempts=5,
76+
delay_in_ms=50
77+
)
78+
)
79+
)
7380
```
7481

7582
You can test the credentials provider by obtaining a token. The following example demonstrates both, a synchronous and an asynchronous approach:
@@ -82,7 +89,7 @@ cred_provider.get_credentials()
8289
await cred_provider.get_credentials_async()
8390
```
8491

85-
### Step 5 - Connect to Redis
92+
### Step 4 - Connect to Redis
8693

8794
When using Entra ID, Azure enforces TLS on your Redis connection. Here is an example that shows how to **test** the connection in an insecure way:
8895

redis_entraid/cred_provider.py

Lines changed: 108 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,70 +1,50 @@
1-
from dataclasses import dataclass
2-
from typing import Union, Tuple, Callable, Any, Awaitable
1+
from typing import Union, Tuple, Callable, Any, Awaitable, Optional
32

43
from redis.credentials import StreamingCredentialProvider
54
from redis.auth.token_manager import TokenManagerConfig, RetryPolicy, TokenManager, CredentialsListener
65

7-
from redis_entraid.identity_provider import EntraIDIdentityProvider
8-
9-
10-
@dataclass
11-
class TokenAuthConfig:
12-
"""
13-
Configuration for token authentication.
14-
15-
Requires :class:`EntraIDIdentityProvider`. It's recommended to use an additional factory methods.
16-
See :class:`EntraIDIdentityProvider` for more information.
17-
"""
18-
DEFAULT_EXPIRATION_REFRESH_RATIO = 0.8
19-
DEFAULT_LOWER_REFRESH_BOUND_MILLIS = 0
20-
DEFAULT_TOKEN_REQUEST_EXECUTION_TIMEOUT_IN_MS = 100
21-
DEFAULT_MAX_ATTEMPTS = 3
22-
DEFAULT_DELAY_IN_MS = 3
23-
24-
idp: EntraIDIdentityProvider
25-
expiration_refresh_ratio: float = DEFAULT_EXPIRATION_REFRESH_RATIO
26-
lower_refresh_bound_millis: int = DEFAULT_LOWER_REFRESH_BOUND_MILLIS
27-
token_request_execution_timeout_in_ms: int = DEFAULT_TOKEN_REQUEST_EXECUTION_TIMEOUT_IN_MS
28-
max_attempts: int = DEFAULT_MAX_ATTEMPTS
29-
delay_in_ms: int = DEFAULT_DELAY_IN_MS
30-
31-
def get_token_manager_config(self) -> TokenManagerConfig:
32-
return TokenManagerConfig(
33-
self.expiration_refresh_ratio,
34-
self.lower_refresh_bound_millis,
35-
self.token_request_execution_timeout_in_ms,
36-
RetryPolicy(
37-
self.max_attempts,
38-
self.delay_in_ms
39-
)
40-
)
41-
42-
def get_identity_provider(self) -> EntraIDIdentityProvider:
43-
return self.idp
6+
from redis_entraid.identity_provider import ManagedIdentityType, ManagedIdentityIdType, \
7+
create_provider_from_managed_identity, ManagedIdentityProviderConfig, ServicePrincipalIdentityProviderConfig, \
8+
create_provider_from_service_principal
449

10+
DEFAULT_EXPIRATION_REFRESH_RATIO = 0.7
11+
DEFAULT_LOWER_REFRESH_BOUND_MILLIS = 0
12+
DEFAULT_TOKEN_REQUEST_EXECUTION_TIMEOUT_IN_MS = 100
13+
DEFAULT_MAX_ATTEMPTS = 3
14+
DEFAULT_DELAY_IN_MS = 3
4515

4616
class EntraIdCredentialsProvider(StreamingCredentialProvider):
4717
def __init__(
4818
self,
49-
config: TokenAuthConfig,
19+
idp_config: Union[ManagedIdentityProviderConfig, ServicePrincipalIdentityProviderConfig],
20+
token_manager_config: TokenManagerConfig,
5021
initial_delay_in_ms: float = 0,
5122
block_for_initial: bool = False,
5223
):
5324
"""
54-
:param config:
25+
:param idp_config: Identity provider specific configuration.
26+
:param token_manager_config: Token manager specific configuration.
5527
:param initial_delay_in_ms: Initial delay before run background refresh (valid for async only)
5628
:param block_for_initial: Block execution until initial token will be acquired (valid for async only)
5729
"""
30+
if isinstance(idp_config, ManagedIdentityProviderConfig):
31+
idp = create_provider_from_managed_identity(idp_config)
32+
else:
33+
idp = create_provider_from_service_principal(idp_config)
34+
5835
self._token_mgr = TokenManager(
59-
config.get_identity_provider(),
60-
config.get_token_manager_config()
36+
idp,
37+
token_manager_config
6138
)
6239
self._listener = CredentialsListener()
6340
self._is_streaming = False
6441
self._initial_delay_in_ms = initial_delay_in_ms
6542
self._block_for_initial = block_for_initial
6643

6744
def get_credentials(self) -> Union[Tuple[str], Tuple[str, str]]:
45+
"""
46+
Acquire token from the identity provider.
47+
"""
6848
init_token = self._token_mgr.acquire_token()
6949

7050
if self._is_streaming is False:
@@ -77,6 +57,9 @@ def get_credentials(self) -> Union[Tuple[str], Tuple[str, str]]:
7757
return init_token.get_token().try_get('oid'), init_token.get_token().get_value()
7858

7959
async def get_credentials_async(self) -> Union[Tuple[str], Tuple[str, str]]:
60+
"""
61+
Acquire token from the identity provider in async mode.
62+
"""
8063
init_token = await self._token_mgr.acquire_token_async()
8164

8265
if self._is_streaming is False:
@@ -98,3 +81,85 @@ def on_error(self, callback: Union[Callable[[Exception], None], Awaitable]):
9881

9982
def is_streaming(self) -> bool:
10083
return self._is_streaming
84+
85+
86+
def create_from_managed_identity(
87+
identity_type: ManagedIdentityType,
88+
resource: str,
89+
id_type: Optional[ManagedIdentityIdType] = None,
90+
id_value: Optional[str] = '',
91+
kwargs: Optional[dict] = None,
92+
token_manager_config: Optional[TokenManagerConfig] = TokenManagerConfig(
93+
DEFAULT_EXPIRATION_REFRESH_RATIO,
94+
DEFAULT_LOWER_REFRESH_BOUND_MILLIS,
95+
DEFAULT_TOKEN_REQUEST_EXECUTION_TIMEOUT_IN_MS,
96+
RetryPolicy(
97+
DEFAULT_MAX_ATTEMPTS,
98+
DEFAULT_DELAY_IN_MS
99+
)
100+
)
101+
) -> EntraIdCredentialsProvider:
102+
"""
103+
Create a credential provider from a managed identity type.
104+
105+
:param identity_type: Managed identity type.
106+
:param resource: Identity provider resource.
107+
:param id_type: Identity provider type.
108+
:param id_value: Identity provider value.
109+
:param kwargs: Optional keyword arguments to pass to identity provider. See: :class:`ManagedIdentityClient`
110+
:param token_manager_config: Token manager specific configuration.
111+
:return: EntraIdCredentialsProvider instance.
112+
"""
113+
managed_identity_config = ManagedIdentityProviderConfig(
114+
identity_type=identity_type,
115+
resource=resource,
116+
id_type=id_type,
117+
id_value=id_value,
118+
kwargs=kwargs
119+
)
120+
121+
return EntraIdCredentialsProvider(managed_identity_config, token_manager_config)
122+
123+
124+
def create_from_service_principal(
125+
client_credential: Any,
126+
client_id: str,
127+
scopes: Optional[list[str]] = None,
128+
timeout: Optional[float] = None,
129+
tenant_id: Optional[str] = None,
130+
token_kwargs: Optional[dict] = None,
131+
app_kwargs: Optional[dict] = None,
132+
token_manager_config: Optional[TokenManagerConfig] = TokenManagerConfig(
133+
DEFAULT_EXPIRATION_REFRESH_RATIO,
134+
DEFAULT_LOWER_REFRESH_BOUND_MILLIS,
135+
DEFAULT_TOKEN_REQUEST_EXECUTION_TIMEOUT_IN_MS,
136+
RetryPolicy(
137+
DEFAULT_MAX_ATTEMPTS,
138+
DEFAULT_DELAY_IN_MS
139+
)
140+
)
141+
) -> EntraIdCredentialsProvider:
142+
"""
143+
Create a credential provider from a service principal.
144+
145+
:param client_credential: Service principal credentials.
146+
:param client_id: Service principal client ID.
147+
:param scopes: Service principal scopes. Fallback to default scopes if None.
148+
:param timeout: Service principal timeout.
149+
:param tenant_id: Service principal tenant ID.
150+
:param token_kwargs: Optional token arguments to pass to service identity provider.
151+
:param app_kwargs: Optional keyword arguments to pass to service principal application.
152+
:param token_manager_config: Token manager specific configuration.
153+
:return: EntraIdCredentialsProvider instance.
154+
"""
155+
service_principal_config = ServicePrincipalIdentityProviderConfig(
156+
client_credential=client_credential,
157+
client_id=client_id,
158+
scopes=scopes,
159+
timeout=timeout,
160+
tenant_id=tenant_id,
161+
app_kwargs=app_kwargs,
162+
token_kwargs=token_kwargs,
163+
)
164+
165+
return EntraIdCredentialsProvider(service_principal_config, token_manager_config)

redis_entraid/identity_provider.py

Lines changed: 46 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1+
from dataclasses import dataclass
12
from enum import Enum
2-
from typing import Optional, Union, Callable
3+
from typing import Optional, Union, Callable, Any
34

45
import requests
56
from msal import (
@@ -24,6 +25,26 @@ class ManagedIdentityIdType(Enum):
2425
RESOURCE_ID = "resource_id"
2526

2627

28+
@dataclass
29+
class ManagedIdentityProviderConfig:
30+
identity_type: ManagedIdentityType
31+
resource: str
32+
id_type: Optional[ManagedIdentityIdType] = None
33+
id_value: Optional[str] = ''
34+
kwargs: Optional[dict] = None
35+
36+
37+
@dataclass
38+
class ServicePrincipalIdentityProviderConfig:
39+
client_credential: Any
40+
client_id: str
41+
scopes: Optional[list[str]] = None
42+
timeout: Optional[float] = None
43+
tenant_id: Optional[str] = None
44+
token_kwargs: Optional[dict] = None
45+
app_kwargs: Optional[dict] = None
46+
47+
2748
class EntraIDIdentityProvider(IdentityProviderInterface):
2849
"""
2950
EntraID Identity Provider implementation.
@@ -75,70 +96,54 @@ def _get_token(self, callback: Callable, **kwargs) -> JWToken:
7596
raise RequestTokenErr(e)
7697

7798

78-
def create_provider_from_managed_identity(
79-
identity_type: ManagedIdentityType,
80-
resource: str,
81-
id_type: Optional[ManagedIdentityIdType] = None,
82-
id_value: Optional[str] = '',
83-
**kwargs
84-
) -> EntraIDIdentityProvider:
99+
def create_provider_from_managed_identity(config: ManagedIdentityProviderConfig) -> EntraIDIdentityProvider:
85100
"""
86101
Create an EntraID identity provider following Managed Identity auth flow.
87102
88-
:param identity_type: User Assigned or System Assigned.
89-
:param resource: Resource for which token should be acquired.
90-
:param id_type: Required for User Assigned identity type only.
91-
:param id_value: Required for User Assigned identity type only.
92-
:param kwargs: Additional arguments you may need during specify to request token.
103+
:param config: Config for managed assigned identity provider
93104
See: :class:`ManagedIdentityClient` acquire_token_for_client method.
94105
95106
:return: :class:`EntraIDIdentityProvider`
96107
"""
97-
if identity_type == ManagedIdentityType.USER_ASSIGNED:
98-
if id_type is None or id_value == '':
108+
if config.identity_type == ManagedIdentityType.USER_ASSIGNED:
109+
if config.id_type is None or config.id_value == '':
99110
raise ValueError("Id_type and id_value are required for User Assigned identity auth")
100111

101112
kwargs = {
102-
id_type.value: id_value
113+
config.id_type.value: config.id_value
103114
}
104115

105-
managed_identity = identity_type.value(**kwargs)
116+
managed_identity = config.identity_type.value(**kwargs)
106117
else:
107-
managed_identity = identity_type.value()
118+
managed_identity = config.identity_type.value()
108119

109120
app = ManagedIdentityClient(managed_identity, http_client=requests.Session())
110-
return EntraIDIdentityProvider(app, [], resource, **kwargs)
121+
return EntraIDIdentityProvider(app, [], config.resource, **config.kwargs)
111122

112123

113-
def create_provider_from_service_principal(
114-
client_credential,
115-
client_id: str,
116-
scopes: list = [],
117-
timeout: Optional[float] = None,
118-
token_kwargs: dict = {},
119-
**app_kwargs
120-
) -> EntraIDIdentityProvider:
124+
def create_provider_from_service_principal(config: ServicePrincipalIdentityProviderConfig) -> EntraIDIdentityProvider:
121125
"""
122126
Create an EntraID identity provider following Service Principal auth flow.
123127
124-
:param client_credential: Can be secret string, PEM certificate and more.
125-
See: :class:`ConfidentialClientApplication`.
128+
:param config: Config for service principal identity provider
126129
127-
:param client_id: Application (Client) ID.
128-
:param scopes: If no scopes will be provided, default will be used.
129-
:param timeout: Timeout in seconds.
130-
:param token_kwargs: Additional arguments you may need during token request.
131-
:param app_kwargs: Additional arguments you may need to configure an application.
132130
:return: :class:`EntraIDIdentityProvider`
131+
See: :class:`ConfidentialClientApplication`.
133132
"""
134133

135-
if len(scopes) == 0:
136-
scopes.append("https://redis.azure.com/.default")
134+
if config.scopes is None:
135+
scopes = ["https://redis.azure.com/.default"]
136+
else:
137+
scopes = config.scopes
138+
139+
authority = f"https://login.microsoftonline.com/{config.tenant_id}" \
140+
if config.tenant_id is not None else config.tenant_id
137141

138142
app = ConfidentialClientApplication(
139-
client_id=client_id,
140-
client_credential=client_credential,
141-
timeout=timeout,
142-
**app_kwargs
143+
client_id=config.client_id,
144+
client_credential=config.client_credential,
145+
timeout=config.timeout,
146+
authority=authority,
147+
**config.app_kwargs
143148
)
144-
return EntraIDIdentityProvider(app, scopes, **token_kwargs)
149+
return EntraIDIdentityProvider(app, scopes, **config.token_kwargs)

0 commit comments

Comments
 (0)