Skip to content

Commit 9be81a4

Browse files
authored
Added support for DefaultAzureCredential (#43)
* Added support for DefaultAzureCredential * Import tuple object to be compatible with py 3.8 * Same as above * Updated configuration interface * Added ENV variable * Increased expiration refreshed ratio * Same as above * Added LICENSE * Added documentation and examples * Updated examples and README
1 parent 6c0f8ea commit 9be81a4

File tree

12 files changed

+337
-45
lines changed

12 files changed

+337
-45
lines changed

.github/workflows/tests.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ env:
1212
AZURE_CLIENT_SECRET: ${{ secrets.IDP_CLIENT_CREDENTIAL }}
1313
AZURE_CLIENT_ID: ${{ secrets.IDP_CLIENT_ID }}
1414
AZURE_TENANT_ID: ${{ secrets.IDP_TENANT_ID }}
15+
AZURE_REDIS_SCOPES: ${{ secrets.IDP_SCOPES }}
1516
jobs:
1617
tests:
1718
strategy:

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2021-2023, Redis, inc.
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,31 @@ from redis_entraid.cred_provider import *
5050

5151
### Step 2 - Create the credential provider via the factory method
5252

53+
Following factory methods are offered depends on authentication type you need:
54+
55+
`create_from_managed_identity` - Creates a credential provider based on a managed identity.
56+
Managed identities allow Azure services to authenticate without needing explicit credentials, as they are automatically assigned by Azure.
57+
58+
`create_from_service_principal` - Creates a credential provider using a service principal.
59+
A service principal is typically used when you want to authenticate as an application, rather than as a user, with Azure Active Directory.
60+
61+
`create_from_default_azure_credential` - Creates a credential provider from a Default Azure Credential.
62+
This method allows automatic selection of the appropriate credential mechanism based on the environment
63+
(e.g., environment variables, managed identities, service principal, interactive browser etc.).
64+
65+
#### Examples ####
66+
67+
**Managed Identity**
68+
69+
```python
70+
credential_provider = create_from_managed_identity(
71+
identity_type=ManagedIdentityType.SYSTEM_ASSIGNED,
72+
resource="https://redis.azure.com/"
73+
)
74+
```
75+
76+
**Service principal**
77+
5378
```python
5479
credential_provider = create_from_service_principal(
5580
CLIENT_ID,
@@ -58,6 +83,17 @@ credential_provider = create_from_service_principal(
5883
)
5984
```
6085

86+
**Default Azure Credential**
87+
88+
```python
89+
credential_provider = create_from_default_azure_credential(
90+
("https://redis.azure.com/.default",),
91+
)
92+
```
93+
94+
More examples available in [examples](https://github.com/redis/redis-py-entraid/tree/vv-default-azure-credentials/examples)
95+
folder.
96+
6197
### Step 3 - Provide optional token renewal configuration
6298

6399
The default configuration would be applied, but you're able to customise it.

examples/__init__.py

Whitespace-only changes.
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# Before run this example you need to configure your EntraID application the following way:
2+
#
3+
# 1. Enable "Allow public client flows" option, under "Authentication" section.
4+
# 2. Add the Redirect URL of the web server that DefaultAzureCredential runs
5+
# By default, uses port 8400, so the default Redirect URL looks like "http://localhost:8400".
6+
7+
import os
8+
9+
from redis import Redis
10+
from redis_entraid.cred_provider import create_from_default_azure_credential
11+
12+
def main():
13+
14+
# By default, interactive browser login is excluded so you need to enable it.
15+
credential_provider = create_from_default_azure_credential(
16+
scopes=("user.read",),
17+
app_kwargs={
18+
"exclude_interactive_browser_credential": False,
19+
"interactive_browser_client_id": os.getenv("AZURE_CLIENT_ID"),
20+
"interactive_browser_tenant_id": os.getenv("AZURE_TENANT_ID"),
21+
}
22+
)
23+
24+
# Opens a browser tab. After you'll enter your username/password you'll be authenticated.
25+
# When using Entra ID, Azure enforces TLS on your Redis connection.
26+
client = Redis(host=HOST, port=PORT, ssl=True, ssl_cert_reqs=None, credential_provider=credential_provider)
27+
print("The database size is: {}".format(client.dbsize()))
28+
29+
30+
if __name__ == "__main__":
31+
main()

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,9 @@ dependencies = [
1515
"redis~=5.3.0b3",
1616
"PyJWT~=2.9.0",
1717
"msal~=1.31.0",
18+
"azure-identity~=1.20.0"
1819
]
1920

2021
[tool.setuptools.packages.find]
2122
include = ["redis_entraid"]
22-
exclude = ["tests", ".github"]
23+
exclude = ["tests", "examples", ".github"]

redis_entraid/cred_provider.py

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66

77
from redis_entraid.identity_provider import ManagedIdentityType, ManagedIdentityIdType, \
88
_create_provider_from_managed_identity, ManagedIdentityProviderConfig, ServicePrincipalIdentityProviderConfig, \
9-
_create_provider_from_service_principal
9+
_create_provider_from_service_principal, DefaultAzureCredentialIdentityProviderConfig, \
10+
_create_provider_from_default_azure_credential
1011

1112
DEFAULT_EXPIRATION_REFRESH_RATIO = 0.7
1213
DEFAULT_LOWER_REFRESH_BOUND_MILLIS = 0
@@ -158,4 +159,42 @@ def create_from_service_principal(
158159
token_kwargs=token_kwargs,
159160
)
160161
idp = _create_provider_from_service_principal(service_principal_config)
162+
return EntraIdCredentialsProvider(idp, token_manager_config)
163+
164+
165+
def create_from_default_azure_credential(
166+
scopes: Tuple[str],
167+
tenant_id: Optional[str] = None,
168+
authority: Optional[str] = None,
169+
token_kwargs: Optional[dict] = {},
170+
app_kwargs: Optional[dict] = {},
171+
token_manager_config: Optional[TokenManagerConfig] = TokenManagerConfig(
172+
DEFAULT_EXPIRATION_REFRESH_RATIO,
173+
DEFAULT_LOWER_REFRESH_BOUND_MILLIS,
174+
DEFAULT_TOKEN_REQUEST_EXECUTION_TIMEOUT_IN_MS,
175+
RetryPolicy(
176+
DEFAULT_MAX_ATTEMPTS,
177+
DEFAULT_DELAY_IN_MS
178+
)
179+
)
180+
) -> EntraIdCredentialsProvider:
181+
"""
182+
Create a credential provider from a Default Azure credential.
183+
https://learn.microsoft.com/en-us/python/api/azure-identity/azure.identity.defaultazurecredential?view=azure-python
184+
185+
:param scopes: Service principal scopes. Fallback to default scopes if None.
186+
:param tenant_id: Optional tenant to include in the token request.
187+
:param authority: Custom authority, by default used 'login.microsoftonline.com'
188+
:param token_kwargs: Optional token arguments applied when retrieving tokens.
189+
:param app_kwargs: Optional keyword arguments to pass when instantiating application.
190+
:param token_manager_config: Token manager specific configuration.
191+
"""
192+
default_azure_credential_config = DefaultAzureCredentialIdentityProviderConfig(
193+
scopes=scopes,
194+
authority=authority,
195+
additional_tenant_id=tenant_id,
196+
token_kwargs=token_kwargs,
197+
app_kwargs=app_kwargs,
198+
)
199+
idp = _create_provider_from_default_azure_credential(default_azure_credential_config)
161200
return EntraIdCredentialsProvider(idp, token_manager_config)

redis_entraid/identity_provider.py

Lines changed: 105 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
from dataclasses import dataclass, field
22
from enum import Enum
3-
from typing import Optional, Union, Callable, Any, List
3+
from typing import Optional, Any, List, Tuple, Iterable
44

55
import requests
6+
from azure.identity import DefaultAzureCredential
67
from msal import (
78
ConfidentialClientApplication,
89
ManagedIdentityClient,
@@ -41,69 +42,120 @@ class ServicePrincipalIdentityProviderConfig:
4142
scopes: Optional[List[str]] = None
4243
timeout: Optional[float] = None
4344
tenant_id: Optional[str] = None
44-
token_kwargs: Optional[dict] = None
45+
token_kwargs: Optional[dict] = field(default_factory=dict)
4546
app_kwargs: Optional[dict] = field(default_factory=dict)
4647

4748

48-
class EntraIDIdentityProvider(IdentityProviderInterface):
49-
"""
50-
EntraID Identity Provider implementation.
51-
It's recommended to use an additional factory methods to simplify object instantiation.
49+
@dataclass
50+
class DefaultAzureCredentialIdentityProviderConfig:
51+
scopes: Iterable[str]
52+
additional_tenant_id: Optional[str] = None
53+
authority: Optional[str] = None
54+
token_kwargs: Optional[dict] = field(default_factory=dict)
55+
app_kwargs: Optional[dict] = field(default_factory=dict)
56+
5257

53-
Methods: create_provider_from_managed_identity, create_provider_from_service_principal.
58+
class ManagedIdentityProvider(IdentityProviderInterface):
59+
"""
60+
Identity Provider implementation for Azure Managed Identity auth type.
5461
"""
5562
def __init__(
5663
self,
57-
app: Union[ManagedIdentityClient, ConfidentialClientApplication],
58-
scopes : List = [],
59-
resource: str = '',
64+
app: ManagedIdentityClient,
65+
resource: str,
6066
**kwargs
6167
):
68+
"""
69+
:param kwargs: See: :class:`ManagedIdentityClient` for additional configuration.
70+
"""
6271
self._app = app
63-
self._scopes = scopes
6472
self._resource = resource
6573
self._kwargs = kwargs
6674

6775
def request_token(self, force_refresh=False) -> TokenInterface:
6876
"""
69-
Request token from identity provider.
70-
Force refresh argument is optional and works only with Service Principal auth method.
77+
Request token from identity provider. Force refresh isn't supported for this provider type.
78+
"""
79+
try:
80+
response = self._app.acquire_token_for_client(resource=self._resource, **self._kwargs)
81+
82+
if "error" in response:
83+
raise RequestTokenErr(response["error_description"])
84+
except Exception as e:
85+
raise RequestTokenErr(e)
86+
87+
return JWToken(response["access_token"])
7188

72-
:param force_refresh:
73-
:return: TokenInterface
89+
90+
class ServicePrincipalProvider(IdentityProviderInterface):
91+
"""
92+
Identity Provider implementation for Azure Service Principal auth type.
93+
"""
94+
def __init__(
95+
self,
96+
app: ConfidentialClientApplication,
97+
scopes: Optional[List[str]] = None,
98+
**kwargs
99+
):
100+
"""
101+
:param kwargs: See: :class:`ConfidentialClientApplication` for additional configuration.
74102
"""
75-
if isinstance(self._app, ManagedIdentityClient):
76-
return self._get_token(self._app.acquire_token_for_client, resource=self._resource)
103+
self._app = app
104+
self._scopes = scopes
105+
self._kwargs = kwargs
77106

107+
def request_token(self, force_refresh=False) -> TokenInterface:
108+
"""
109+
Request token from identity provider.
110+
"""
78111
if force_refresh:
79112
self._app.remove_tokens_for_client()
80113

81-
return self._get_token(
82-
self._app.acquire_token_for_client,
83-
scopes=self._scopes,
84-
**self._kwargs
85-
)
86-
87-
def _get_token(self, callback: Callable, **kwargs) -> JWToken:
88114
try:
89-
response = callback(**kwargs)
115+
response = self._app.acquire_token_for_client(scopes=self._scopes, **self._kwargs)
90116

91117
if "error" in response:
92118
raise RequestTokenErr(response["error_description"])
119+
except Exception as e:
120+
raise RequestTokenErr(e)
121+
122+
return JWToken(response["access_token"])
123+
124+
125+
class DefaultAzureCredentialProvider(IdentityProviderInterface):
126+
"""
127+
Identity Provider implementation for Default Azure Credential flow.
128+
"""
129+
130+
def __init__(
131+
self,
132+
app: DefaultAzureCredential,
133+
scopes: Tuple[str],
134+
additional_tenant_id: Optional[str] = None,
135+
**kwargs
136+
):
137+
self._app = app
138+
self._scopes = scopes
139+
self._additional_tenant_id = additional_tenant_id
140+
self._kwargs = kwargs
93141

94-
return JWToken(callback(**kwargs)["access_token"])
142+
def request_token(self, force_refresh=False) -> TokenInterface:
143+
try:
144+
response = self._app.get_token(*self._scopes, tenant_id=self._additional_tenant_id, **self._kwargs)
95145
except Exception as e:
96146
raise RequestTokenErr(e)
97147

148+
return JWToken(response.token)
149+
98150

99-
def _create_provider_from_managed_identity(config: ManagedIdentityProviderConfig) -> EntraIDIdentityProvider:
151+
def _create_provider_from_managed_identity(config: ManagedIdentityProviderConfig) -> ManagedIdentityProvider:
100152
"""
101-
Create an EntraID identity provider following Managed Identity auth flow.
153+
Create a Managed identity provider following Managed Identity auth flow.
102154
103155
:param config: Config for managed assigned identity provider
104156
See: :class:`ManagedIdentityClient` acquire_token_for_client method.
105157
106-
:return: :class:`EntraIDIdentityProvider`
158+
:return: :class:`ManagedIdentityProvider`
107159
"""
108160
if config.identity_type == ManagedIdentityType.USER_ASSIGNED:
109161
if config.id_type is None or config.id_value == '':
@@ -118,16 +170,16 @@ def _create_provider_from_managed_identity(config: ManagedIdentityProviderConfig
118170
managed_identity = config.identity_type.value()
119171

120172
app = ManagedIdentityClient(managed_identity, http_client=requests.Session())
121-
return EntraIDIdentityProvider(app, [], config.resource, **config.kwargs)
173+
return ManagedIdentityProvider(app, config.resource, **config.kwargs)
122174

123175

124-
def _create_provider_from_service_principal(config: ServicePrincipalIdentityProviderConfig) -> EntraIDIdentityProvider:
176+
def _create_provider_from_service_principal(config: ServicePrincipalIdentityProviderConfig) -> ServicePrincipalProvider:
125177
"""
126-
Create an EntraID identity provider following Service Principal auth flow.
178+
Create a Service Principal identity provider following Service Principal auth flow.
127179
128180
:param config: Config for service principal identity provider
129181
130-
:return: :class:`EntraIDIdentityProvider`
182+
:return: :class:`ServicePrincipalProvider`
131183
See: :class:`ConfidentialClientApplication`.
132184
"""
133185

@@ -146,4 +198,23 @@ def _create_provider_from_service_principal(config: ServicePrincipalIdentityProv
146198
authority=authority,
147199
**config.app_kwargs
148200
)
149-
return EntraIDIdentityProvider(app, scopes, **config.token_kwargs)
201+
return ServicePrincipalProvider(app, scopes, **config.token_kwargs)
202+
203+
204+
def _create_provider_from_default_azure_credential(
205+
config: DefaultAzureCredentialIdentityProviderConfig
206+
) -> DefaultAzureCredentialProvider:
207+
"""
208+
Create a Default Azure Credential identity provider following Default Azure Credential flow.
209+
210+
:param config: Config for default Azure Credential identity provider
211+
:return: :class:`DefaultAzureCredentialProvider`
212+
See: :class:`DefaultAzureCredential`.
213+
"""
214+
215+
app = DefaultAzureCredential(
216+
authority=config.authority,
217+
**config.app_kwargs
218+
)
219+
220+
return DefaultAzureCredentialProvider(app, config.scopes, config.additional_tenant_id, **config.token_kwargs)

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
PyJWT~=2.9.0
22
msal~=1.31.0
3+
azure-identity~=1.20.0
34
redis==5.3.0b4
45
requests~=2.32.3

0 commit comments

Comments
 (0)