Skip to content

Commit 5df8693

Browse files
authored
Add support for regional STS (Azure#19392)
1 parent add1479 commit 5df8693

File tree

13 files changed

+176
-22
lines changed

13 files changed

+176
-22
lines changed

sdk/identity/azure-identity/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@
55
- `InteractiveBrowserCredential` keyword argument `login_hint` enables
66
pre-filling the username/email address field on the login page
77
([#19225](https://github.com/Azure/azure-sdk-for-python/issues/19225))
8+
- `CertificateCredential` and `ClientSecretCredential` support regional STS
9+
on Azure VMs by either keyword argument `regional_authority` or environment
10+
variable `AZURE_REGIONAL_AUTHORITY_NAME`. See `azure.identity.RegionalAuthority`
11+
for possible values.
12+
([#19301](https://github.com/Azure/azure-sdk-for-python/issues/19301))
813

914
## 1.7.0b1 (2021-06-08)
1015
Beginning with this release, this library requires Python 2.7 or 3.6+.

sdk/identity/azure-identity/azure/identity/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
"""Credentials for Azure SDK clients."""
66

77
from ._auth_record import AuthenticationRecord
8+
from ._enums import RegionalAuthority
89
from ._exceptions import AuthenticationRequiredError, CredentialUnavailableError
910
from ._constants import AzureAuthorityHosts, KnownAuthorities
1011
from ._credentials import (
@@ -42,6 +43,7 @@
4243
"EnvironmentCredential",
4344
"InteractiveBrowserCredential",
4445
"KnownAuthorities",
46+
"RegionalAuthority",
4547
"ManagedIdentityCredential",
4648
"SharedTokenCacheCredential",
4749
"TokenCachePersistenceOptions",

sdk/identity/azure-identity/azure/identity/_constants.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,3 +44,4 @@ class EnvironmentVariables:
4444
MSI_SECRET = "MSI_SECRET"
4545

4646
AZURE_AUTHORITY_HOST = "AZURE_AUTHORITY_HOST"
47+
AZURE_REGIONAL_AUTHORITY_NAME = "AZURE_REGIONAL_AUTHORITY_NAME"

sdk/identity/azure-identity/azure/identity/_credentials/certificate.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,10 @@
2121
class CertificateCredential(ClientCredentialBase):
2222
"""Authenticates as a service principal using a certificate.
2323
24-
The certificate must have an RSA private key, because this credential signs assertions using RS256.
25-
26-
See Azure Active Directory documentation for more information on configuring certificate authentication:
27-
https://docs.microsoft.com/azure/active-directory/develop/active-directory-certificate-credentials#register-your-certificate-with-microsoft-identity-platform
24+
The certificate must have an RSA private key, because this credential signs assertions using RS256. See
25+
`Azure Active Directory documentation
26+
<https://docs.microsoft.com/azure/active-directory/develop/active-directory-certificate-credentials#register-your-certificate-with-microsoft-identity-platform>`_
27+
for more information on configuring certificate authentication.
2828
2929
:param str tenant_id: ID of the service principal's tenant. Also called its 'directory' ID.
3030
:param str client_id: the service principal's client ID
@@ -44,6 +44,9 @@ class CertificateCredential(ClientCredentialBase):
4444
:keyword cache_persistence_options: configuration for persistent token caching. If unspecified, the credential
4545
will cache tokens in memory.
4646
:paramtype cache_persistence_options: ~azure.identity.TokenCachePersistenceOptions
47+
:keyword ~azure.identity.RegionalAuthority regional_authority: a :class:`~azure.identity.RegionalAuthority` to
48+
which the credential will authenticate. This argument should be used only by applications deployed to Azure
49+
VMs.
4750
"""
4851

4952
def __init__(self, tenant_id, client_id, certificate_path=None, **kwargs):

sdk/identity/azure-identity/azure/identity/_credentials/client_secret.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ class ClientSecretCredential(ClientCredentialBase):
2424
:keyword cache_persistence_options: configuration for persistent token caching. If unspecified, the credential
2525
will cache tokens in memory.
2626
:paramtype cache_persistence_options: ~azure.identity.TokenCachePersistenceOptions
27+
:keyword ~azure.identity.RegionalAuthority regional_authority: a :class:`~azure.identity.RegionalAuthority` to
28+
which the credential will authenticate. This argument should be used only by applications deployed to Azure
29+
VMs.
2730
"""
2831

2932
def __init__(self, tenant_id, client_id, client_secret, **kwargs):
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
# ------------------------------------
2+
# Copyright (c) Microsoft Corporation.
3+
# Licensed under the MIT License.
4+
# ------------------------------------
5+
# pylint:skip-file (avoids crash due to six.with_metaclass https://github.com/PyCQA/astroid/issues/713)
6+
from enum import Enum
7+
from six import with_metaclass
8+
9+
from azure.core import CaseInsensitiveEnumMeta
10+
from msal import ConfidentialClientApplication
11+
12+
13+
class RegionalAuthority(with_metaclass(CaseInsensitiveEnumMeta, str, Enum)):
14+
"""Identifies a regional authority for authentication"""
15+
16+
#: Attempt to discover the appropriate authority. This works on some Azure hosts, such as VMs and
17+
#: Azure Functions. The non-regional authority is used when discovery fails.
18+
AUTO_DISCOVER_REGION = ConfidentialClientApplication.ATTEMPT_REGION_DISCOVERY
19+
20+
ASIA_EAST = "eastasia"
21+
ASIA_SOUTHEAST = "southeastasia"
22+
AUSTRALIA_CENTRAL = "australiacentral"
23+
AUSTRALIA_CENTRAL_2 = "australiacentral2"
24+
AUSTRALIA_EAST = "australiaeast"
25+
AUSTRALIA_SOUTHEAST = "australiasoutheast"
26+
BRAZIL_SOUTH = "brazilsouth"
27+
CANADA_CENTRAL = "canadacentral"
28+
CANADA_EAST = "canadaeast"
29+
CHINA_EAST = "chinaeast"
30+
CHINA_EAST_2 = "chinaeast2"
31+
CHINA_NORTH = "chinanorth"
32+
CHINA_NORTH_2 = "chinanorth2"
33+
EUROPE_NORTH = "northeurope"
34+
EUROPE_WEST = "westeurope"
35+
FRANCE_CENTRAL = "francecentral"
36+
FRANCE_SOUTH = "francesouth"
37+
GERMANY_CENTRAL = "germanycentral"
38+
GERMANY_NORTH = "germanynorth"
39+
GERMANY_NORTHEAST = "germanynortheast"
40+
GERMANY_WEST_CENTRAL = "germanywestcentral"
41+
GOVERNMENT_US_ARIZONA = "usgovarizona"
42+
GOVERNMENT_US_DOD_CENTRAL = "usdodcentral"
43+
GOVERNMENT_US_DOD_EAST = "usdodeast"
44+
GOVERNMENT_US_IOWA = "usgoviowa"
45+
GOVERNMENT_US_TEXAS = "usgovtexas"
46+
GOVERNMENT_US_VIRGINIA = "usgovvirginia"
47+
INDIA_CENTRAL = "centralindia"
48+
INDIA_SOUTH = "southindia"
49+
INDIA_WEST = "westindia"
50+
JAPAN_EAST = "japaneast"
51+
JAPAN_WEST = "japanwest"
52+
KOREA_CENTRAL = "koreacentral"
53+
KOREA_SOUTH = "koreasouth"
54+
NORWAY_EAST = "norwayeast"
55+
NORWAY_WEST = "norwaywest"
56+
SOUTH_AFRICA_NORTH = "southafricanorth"
57+
SOUTH_AFRICA_WEST = "southafricawest"
58+
SWITZERLAND_NORTH = "switzerlandnorth"
59+
SWITZERLAND_WEST = "switzerlandwest"
60+
UAE_CENTRAL = "uaecentral"
61+
UAE_NORTH = "uaenorth"
62+
UK_SOUTH = "uksouth"
63+
UK_WEST = "ukwest"
64+
US_CENTRAL = "centralus"
65+
US_EAST = "eastus"
66+
US_EAST_2 = "eastus2"
67+
US_NORTH_CENTRAL = "northcentralus"
68+
US_SOUTH_CENTRAL = "southcentralus"
69+
US_WEST = "westus"
70+
US_WEST_2 = "westus2"
71+
US_WEST_CENTRAL = "westcentralus"

sdk/identity/azure-identity/azure/identity/_internal/msal_credentials.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,14 @@
33
# Licensed under the MIT License.
44
# ------------------------------------
55
import abc
6+
import os
67

78
import msal
89

910
from .msal_client import MsalClient
11+
from .._constants import EnvironmentVariables
1012
from .._internal import get_default_authority, normalize_authority, validate_tenant_id
11-
from .._persistent_cache import _load_persistent_cache, TokenCachePersistenceOptions
13+
from .._persistent_cache import _load_persistent_cache
1214

1315
try:
1416
ABC = abc.ABC
@@ -22,7 +24,7 @@
2224

2325
if TYPE_CHECKING:
2426
# pylint:disable=ungrouped-imports,unused-import
25-
from typing import Any, Mapping, Optional, Type, Union
27+
from typing import Any, Optional, Type, Union
2628

2729

2830
class MsalCredential(ABC):
@@ -32,6 +34,9 @@ def __init__(self, client_id, client_credential=None, **kwargs):
3234
# type: (str, Optional[Union[str, dict]], **Any) -> None
3335
authority = kwargs.pop("authority", None)
3436
self._authority = normalize_authority(authority) if authority else get_default_authority()
37+
self._regional_authority = kwargs.pop(
38+
"regional_authority", os.environ.get(EnvironmentVariables.AZURE_REGIONAL_AUTHORITY_NAME)
39+
)
3540
self._tenant_id = kwargs.pop("tenant_id", None) or "organizations"
3641
validate_tenant_id(self._tenant_id)
3742

@@ -63,6 +68,7 @@ def _create_app(self, cls, **kwargs):
6368
client_id=self._client_id,
6469
client_credential=self._client_credential,
6570
authority="{}/{}".format(self._authority, self._tenant_id),
71+
azure_region=self._regional_authority,
6672
token_cache=self._cache,
6773
http_client=self._client,
6874
**kwargs

sdk/identity/azure-identity/azure/identity/aio/_credentials/certificate.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,10 @@
2020
class CertificateCredential(AsyncContextManager, GetTokenMixin):
2121
"""Authenticates as a service principal using a certificate.
2222
23-
The certificate must have an RSA private key, because this credential signs assertions using RS256.
24-
25-
See Azure Active Directory documentation for more information on configuring certificate authentication:
26-
https://docs.microsoft.com/azure/active-directory/develop/active-directory-certificate-credentials#register-your-certificate-with-microsoft-identity-platform
23+
The certificate must have an RSA private key, because this credential signs assertions using RS256. See
24+
`Azure Active Directory documentation
25+
<https://docs.microsoft.com/azure/active-directory/develop/active-directory-certificate-credentials#register-your-certificate-with-microsoft-identity-platform>`_
26+
for more information on configuring certificate authentication.
2727
2828
:param str tenant_id: ID of the service principal's tenant. Also called its 'directory' ID.
2929
:param str client_id: the service principal's client ID

sdk/identity/azure-identity/setup.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,9 +72,9 @@
7272
]
7373
),
7474
install_requires=[
75-
"azure-core<2.0.0,>=1.0.0",
75+
"azure-core<2.0.0,>=1.11.0",
7676
"cryptography>=2.1.4",
77-
"msal<2.0.0,>=1.7.0",
77+
"msal<2.0.0,>=1.12.0",
7878
"msal-extensions~=0.3.0",
7979
"six>=1.12.0",
8080
],

sdk/identity/azure-identity/tests/test_certificate_credential.py

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import os
77

88
from azure.core.pipeline.policies import ContentDecodePolicy, SansIOHTTPPolicy
9-
from azure.identity import CertificateCredential, TokenCachePersistenceOptions
9+
from azure.identity import CertificateCredential, RegionalAuthority, TokenCachePersistenceOptions
1010
from azure.identity._constants import EnvironmentVariables
1111
from azure.identity._internal.user_agent import USER_AGENT
1212
from cryptography import x509
@@ -25,7 +25,6 @@
2525
mock_response,
2626
msal_validating_transport,
2727
Request,
28-
validating_transport,
2928
)
3029

3130
try:
@@ -78,7 +77,7 @@ def test_policies_configurable():
7877
policy = Mock(spec_set=SansIOHTTPPolicy, on_request=Mock())
7978

8079
transport = msal_validating_transport(
81-
requests=[Request()], responses=[mock_response(json_payload=build_aad_response(access_token="**"))],
80+
requests=[Request()], responses=[mock_response(json_payload=build_aad_response(access_token="**"))]
8281
)
8382

8483
credential = CertificateCredential(
@@ -135,6 +134,38 @@ def test_authority(authority):
135134
assert kwargs["authority"] == expected_authority
136135

137136

137+
def test_regional_authority():
138+
"""the credential should configure MSAL with a regional authority specified via kwarg or environment variable"""
139+
140+
mock_confidential_client = Mock(
141+
return_value=Mock(acquire_token_silent_with_error=lambda *_, **__: {"access_token": "**", "expires_in": 3600}),
142+
)
143+
144+
for region in RegionalAuthority:
145+
mock_confidential_client.reset_mock()
146+
147+
with patch.dict("os.environ", {}, clear=True):
148+
credential = CertificateCredential("tenant", "client-id", CERT_PATH, regional_authority=region)
149+
with patch("msal.ConfidentialClientApplication", mock_confidential_client):
150+
# must call get_token because the credential constructs the MSAL application lazily
151+
credential.get_token("scope")
152+
153+
assert mock_confidential_client.call_count == 1
154+
_, kwargs = mock_confidential_client.call_args
155+
assert kwargs["azure_region"] == region
156+
mock_confidential_client.reset_mock()
157+
158+
# region can be configured via environment variable
159+
with patch.dict("os.environ", {EnvironmentVariables.AZURE_REGIONAL_AUTHORITY_NAME: region}, clear=True):
160+
credential = CertificateCredential("tenant", "client-id", CERT_PATH)
161+
with patch("msal.ConfidentialClientApplication", mock_confidential_client):
162+
credential.get_token("scope")
163+
164+
assert mock_confidential_client.call_count == 1
165+
_, kwargs = mock_confidential_client.call_args
166+
assert kwargs["azure_region"] == region
167+
168+
138169
def test_requires_certificate():
139170
"""the credential should raise ValueError when not given a certificate"""
140171

0 commit comments

Comments
 (0)