Skip to content

Commit 25832df

Browse files
authored
Merge branch 'dev' into dharshanb/brokerSupportLinux
2 parents 89ef887 + 4d168cf commit 25832df

File tree

8 files changed

+155
-87
lines changed

8 files changed

+155
-87
lines changed
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
name: Run issue sentinel
2+
on:
3+
issues:
4+
types: [opened, edited, closed]
5+
6+
jobs:
7+
Issue:
8+
permissions:
9+
issues: write
10+
runs-on: ubuntu-latest
11+
steps:
12+
- name: Run Issue Sentinel
13+
uses: Azure/issue-sentinel@v1
14+
with:
15+
password: ${{secrets.ISSUE_SENTINEL_PASSWORD}}
16+
enable-similar-issues-scanning: true # Scan for similar issues
17+
enable-security-issues-scanning: true # Scan for security issues

.travis.yml

Lines changed: 0 additions & 46 deletions
This file was deleted.

msal/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,12 @@
2626
#------------------------------------------------------------------------------
2727

2828
from .application import (
29-
__version__,
3029
ClientApplication,
3130
ConfidentialClientApplication,
3231
PublicClientApplication,
3332
)
3433
from .oauth2cli.oidc import Prompt, IdTokenError
34+
from .sku import __version__
3535
from .token_cache import TokenCache, SerializableTokenCache
3636
from .auth_scheme import PopAuthScheme
3737
from .managed_identity import (

msal/application.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import warnings
77
from threading import Lock
88
from typing import Optional # Needed in Python 3.7 & 3.8
9+
from urllib.parse import urlparse
910
import os
1011

1112
from .oauth2cli import Client, JwtAssertionCreator
@@ -19,10 +20,9 @@
1920
from .region import _detect_region
2021
from .throttled_http_client import ThrottledHttpClient
2122
from .cloudshell import _is_running_in_cloud_shell
23+
from .sku import SKU, __version__
2224

2325

24-
# The __init__.py will import this. Not the other way around.
25-
__version__ = "1.31.1" # When releasing, also check and bump our dependencies's versions if needed
2626

2727
logger = logging.getLogger(__name__)
2828
_AUTHORITY_TYPE_CLOUDSHELL = "CLOUDSHELL"
@@ -623,6 +623,9 @@ def __init__(
623623
# Here the self.authority will not be the same type as authority in input
624624
if oidc_authority and authority:
625625
raise ValueError("You can not provide both authority and oidc_authority")
626+
if isinstance(authority, str) and urlparse(authority).path.startswith(
627+
"/dstsv2"): # dSTS authority's path always starts with "/dstsv2"
628+
oidc_authority = authority # So we treat it as if an oidc_authority
626629
try:
627630
authority_to_use = authority or "https://{}/common/".format(WORLD_WIDE)
628631
self.authority = Authority(
@@ -770,7 +773,7 @@ def _build_client(self, client_credential, authority, skip_regional_client=False
770773
client_assertion = None
771774
client_assertion_type = None
772775
default_headers = {
773-
"x-client-sku": "MSAL.Python", "x-client-ver": __version__,
776+
"x-client-sku": SKU, "x-client-ver": __version__,
774777
"x-client-os": sys.platform,
775778
"x-ms-lib-capability": "retry-after, h429",
776779
}

msal/broker.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import time
88
import uuid
99

10+
from .sku import __version__, SKU
1011

1112
logger = logging.getLogger(__name__)
1213
try:
@@ -136,13 +137,18 @@ def _get_new_correlation_id():
136137
def _enable_msa_pt(params):
137138
params.set_additional_parameter("msal_request_type", "consumer_passthrough") # PyMsalRuntime 0.8+
138139

140+
def _build_msal_runtime_auth_params(client_id, authority):
141+
params = pymsalruntime.MSALRuntimeAuthParameters(client_id, authority)
142+
params.set_additional_parameter("msal_client_sku", SKU)
143+
params.set_additional_parameter("msal_client_ver", __version__)
144+
return params
139145

140146
def _signin_silently(
141147
authority, client_id, scopes, correlation_id=None, claims=None,
142148
enable_msa_pt=False,
143149
auth_scheme=None,
144150
**kwargs):
145-
params = pymsalruntime.MSALRuntimeAuthParameters(client_id, authority)
151+
params = _build_msal_runtime_auth_params(client_id, authority)
146152
params.set_requested_scopes(scopes)
147153
if claims:
148154
params.set_decoded_claims(claims)
@@ -175,7 +181,7 @@ def _signin_interactively(
175181
enable_msa_pt=False,
176182
auth_scheme=None,
177183
**kwargs):
178-
params = pymsalruntime.MSALRuntimeAuthParameters(client_id, authority)
184+
params = _build_msal_runtime_auth_params(client_id, authority)
179185
params.set_requested_scopes(scopes)
180186
params.set_redirect_uri(
181187
_redirect_uri_on_mac if sys.platform == "darwin" else
@@ -231,7 +237,7 @@ def _acquire_token_silently(
231237
account = _read_account_by_id(account_id, correlation_id)
232238
if account is None:
233239
return
234-
params = pymsalruntime.MSALRuntimeAuthParameters(client_id, authority)
240+
params = _build_msal_runtime_auth_params(client_id, authority)
235241
params.set_requested_scopes(scopes)
236242
if claims:
237243
params.set_decoded_claims(claims)

msal/sku.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
"""This module is from where we recieve the client sku name and version.
2+
"""
3+
4+
# The __init__.py will import this. Not the other way around.
5+
__version__ = "1.31.1" # When releasing, also check and bump our dependencies's versions if needed
6+
SKU = "MSAL.Python"

tests/test_authority.py

Lines changed: 41 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -104,32 +104,63 @@ def test_authority_with_path_should_be_used_as_is(self, oidc_discovery):
104104
"authorization_endpoint": "https://contoso.com/authorize",
105105
"token_endpoint": "https://contoso.com/token",
106106
})
107-
class TestOidcAuthority(unittest.TestCase):
107+
class OidcAuthorityTestCase(unittest.TestCase):
108+
authority = "https://contoso.com/tenant"
109+
110+
def setUp(self):
111+
# setUp() gives subclass a dynamic setup based on their authority
112+
self.oidc_discovery_endpoint = (
113+
# MSAL Python always does OIDC Discovery,
114+
# not to be confused with Instance Discovery
115+
# Here the test is to confirm the OIDC endpoint contains no "/v2.0"
116+
self.authority + "/.well-known/openid-configuration")
117+
108118
def test_authority_obj_should_do_oidc_discovery_and_skip_instance_discovery(
109119
self, oidc_discovery, instance_discovery):
110120
c = MinimalHttpClient()
111-
a = Authority(None, c, oidc_authority_url="https://contoso.com/tenant")
121+
a = Authority(None, c, oidc_authority_url=self.authority)
112122
instance_discovery.assert_not_called()
113-
oidc_discovery.assert_called_once_with(
114-
"https://contoso.com/tenant/.well-known/openid-configuration", c)
123+
oidc_discovery.assert_called_once_with(self.oidc_discovery_endpoint, c)
115124
self.assertEqual(a.authorization_endpoint, 'https://contoso.com/authorize')
116125
self.assertEqual(a.token_endpoint, 'https://contoso.com/token')
117126

118127
def test_application_obj_should_do_oidc_discovery_and_skip_instance_discovery(
119128
self, oidc_discovery, instance_discovery):
120129
app = msal.ClientApplication(
121-
"id",
122-
authority=None,
123-
oidc_authority="https://contoso.com/tenant",
124-
)
130+
"id", authority=None, oidc_authority=self.authority)
125131
instance_discovery.assert_not_called()
126132
oidc_discovery.assert_called_once_with(
127-
"https://contoso.com/tenant/.well-known/openid-configuration",
128-
app.http_client)
133+
self.oidc_discovery_endpoint, app.http_client)
129134
self.assertEqual(
130135
app.authority.authorization_endpoint, 'https://contoso.com/authorize')
131136
self.assertEqual(app.authority.token_endpoint, 'https://contoso.com/token')
132137

138+
139+
class DstsAuthorityTestCase(OidcAuthorityTestCase):
140+
# Inherits OidcAuthority's test cases and run them with a dSTS authority
141+
authority = ( # dSTS is single tenanted with a tenant placeholder
142+
'https://test-instance1-dsts.dsts.core.azure-test.net/dstsv2/common')
143+
authorization_endpoint = (
144+
"https://some.url.dsts.core.azure-test.net/dstsv2/common/oauth2/authorize")
145+
token_endpoint = (
146+
"https://some.url.dsts.core.azure-test.net/dstsv2/common/oauth2/token")
147+
148+
@patch("msal.authority._instance_discovery")
149+
@patch("msal.authority.tenant_discovery", return_value={
150+
"authorization_endpoint": authorization_endpoint,
151+
"token_endpoint": token_endpoint,
152+
}) # We need to create new patches (i.e. mocks) for non-inherited test cases
153+
def test_application_obj_should_accept_dsts_url_as_an_authority(
154+
self, oidc_discovery, instance_discovery):
155+
app = msal.ClientApplication("id", authority=self.authority)
156+
instance_discovery.assert_not_called()
157+
oidc_discovery.assert_called_once_with(
158+
self.oidc_discovery_endpoint, app.http_client)
159+
self.assertEqual(
160+
app.authority.authorization_endpoint, self.authorization_endpoint)
161+
self.assertEqual(app.authority.token_endpoint, self.token_endpoint)
162+
163+
133164
class TestAuthorityInternalHelperCanonicalize(unittest.TestCase):
134165

135166
def test_canonicalize_tenant_followed_by_extra_paths(self):

tests/test_e2e.py

Lines changed: 75 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -477,9 +477,10 @@ def get_lab_app(
477477
if os.getenv(env_client_id) and os.getenv(env_client_cert_path):
478478
# id came from https://docs.msidlab.com/accounts/confidentialclient.html
479479
client_id = os.getenv(env_client_id)
480-
# Cert came from https://ms.portal.azure.com/#@microsoft.onmicrosoft.com/asset/Microsoft_Azure_KeyVault/Certificate/https://msidlabs.vault.azure.net/certificates/LabVaultAccessCert
481480
client_credential = {
482-
"private_key_pfx_path": os.getenv(env_client_cert_path),
481+
"private_key_pfx_path":
482+
# Cert came from https://ms.portal.azure.com/#@microsoft.onmicrosoft.com/asset/Microsoft_Azure_KeyVault/Certificate/https://msidlabs.vault.azure.net/certificates/LabAuth
483+
os.getenv(env_client_cert_path),
483484
"public_certificate": True, # Opt in for SNI
484485
}
485486
elif os.getenv(env_client_id) and os.getenv(env_name2):
@@ -649,19 +650,27 @@ def _test_acquire_token_obo(self, config_pca, config_cca,
649650
# Here we just test regional apps won't adversely break OBO
650651
http_client=None,
651652
):
652-
# 1. An app obtains a token representing a user, for our mid-tier service
653-
pca = msal.PublicClientApplication(
654-
config_pca["client_id"], authority=config_pca["authority"],
655-
azure_region=azure_region,
656-
http_client=http_client or MinimalHttpClient())
657-
pca_result = pca.acquire_token_by_username_password(
658-
config_pca["username"],
659-
config_pca["password"],
660-
scopes=config_pca["scope"],
661-
)
662-
self.assertIsNotNone(
663-
pca_result.get("access_token"),
664-
"PCA failed to get AT because %s" % json.dumps(pca_result, indent=2))
653+
if "client_secret" not in config_pca:
654+
# 1.a An app obtains a token representing a user, for our mid-tier service
655+
result = msal.PublicClientApplication(
656+
config_pca["client_id"], authority=config_pca["authority"],
657+
azure_region=azure_region,
658+
http_client=http_client or MinimalHttpClient(),
659+
).acquire_token_by_username_password(
660+
config_pca["username"], config_pca["password"],
661+
scopes=config_pca["scope"],
662+
)
663+
else: # We repurpose the config_pca to contain client_secret for cca app 1
664+
# 1.b An app obtains a token representing itself, for our mid-tier service
665+
result = msal.ConfidentialClientApplication(
666+
config_pca["client_id"], authority=config_pca["authority"],
667+
client_credential=config_pca["client_secret"],
668+
azure_region=azure_region,
669+
http_client=http_client or MinimalHttpClient(),
670+
).acquire_token_for_client(scopes=config_pca["scope"])
671+
assertion = result.get("access_token")
672+
self.assertIsNotNone(assertion, "First app failed to get AT. {}".format(
673+
json.dumps(result, indent=2)))
665674

666675
# 2. Our mid-tier service uses OBO to obtain a token for downstream service
667676
cca = msal.ConfidentialClientApplication(
@@ -674,9 +683,9 @@ def _test_acquire_token_obo(self, config_pca, config_cca,
674683
# That's fine if OBO app uses short-lived msal instance per session.
675684
# Otherwise, the OBO app need to implement a one-cache-per-user setup.
676685
)
677-
cca_result = cca.acquire_token_on_behalf_of(
678-
pca_result['access_token'], config_cca["scope"])
679-
self.assertNotEqual(None, cca_result.get("access_token"), str(cca_result))
686+
cca_result = cca.acquire_token_on_behalf_of(assertion, config_cca["scope"])
687+
self.assertIsNotNone(cca_result.get("access_token"), "OBO call failed: {}".format(
688+
json.dumps(cca_result, indent=2)))
680689

681690
# 3. Now the OBO app can simply store downstream token(s) in same session.
682691
# Alternatively, if you want to persist the downstream AT, and possibly
@@ -685,13 +694,27 @@ def _test_acquire_token_obo(self, config_pca, config_cca,
685694
# Assuming you already did that (which is not shown in this test case),
686695
# the following part shows one of the ways to obtain an AT from cache.
687696
username = cca_result.get("id_token_claims", {}).get("preferred_username")
688-
if username: # It means CCA have requested an IDT w/ "profile" scope
689-
self.assertEqual(config_cca["username"], username)
690697
accounts = cca.get_accounts(username=username)
691-
assert len(accounts) == 1, "App is expected to partition token cache per user"
692-
account = accounts[0]
698+
if username is not None: # It means CCA have requested an IDT w/ "profile" scope
699+
assert config_cca["username"] == username, "Incorrect test case configuration"
700+
self.assertEqual(1, len(accounts), "App is supposed to partition token cache per user")
701+
account = accounts[0] # Alternatively, cca app could just loop through each account
693702
result = cca.acquire_token_silent(config_cca["scope"], account)
694-
self.assertEqual(cca_result["access_token"], result["access_token"])
703+
self.assertTrue(
704+
result and result.get("access_token") == cca_result["access_token"],
705+
"CCA should hit an access token from cache: {}".format(
706+
json.dumps(cca.token_cache._cache, indent=2)))
707+
if "refresh_token" in cca_result:
708+
result = cca.acquire_token_silent(
709+
config_cca["scope"], account=account, force_refresh=True)
710+
self.assertTrue(
711+
result and "access_token" in result,
712+
"CCA should get an AT silently, but we got this instead: {}".format(result))
713+
self.assertNotEqual(
714+
result["access_token"], cca_result["access_token"],
715+
"CCA should get a new AT")
716+
else:
717+
logger.info("AAD did not issue a RT for OBO flow")
695718

696719
def _test_acquire_token_by_client_secret(
697720
self, client_id=None, client_secret=None, authority=None, scope=None,
@@ -933,6 +956,31 @@ def test_acquire_token_obo(self):
933956

934957
self._test_acquire_token_obo(config_pca, config_cca)
935958

959+
@unittest.skipUnless(
960+
os.path.exists("tests/sp_obo.pem"),
961+
"Need a 'tests/sp_obo.pem' private to run OBO for SP test")
962+
def test_acquire_token_obo_for_sp(self):
963+
authority = "https://login.windows-ppe.net/f686d426-8d16-42db-81b7-ab578e110ccd"
964+
with open("tests/sp_obo.pem") as pem:
965+
client_secret = {
966+
"private_key": pem.read(),
967+
"thumbprint": "378938210C976692D7F523B8C4FFBB645D17CE92",
968+
}
969+
midtier_app = {
970+
"authority": authority,
971+
"client_id": "c84e9c32-0bc9-4a73-af05-9efe9982a322",
972+
"client_secret": client_secret,
973+
"scope": ["23d08a1e-1249-4f7c-b5a5-cb11f29b6923/.default"],
974+
#"username": "OBO-Client-PPE", # We do NOT attempt locating initial_app by name
975+
}
976+
initial_app = {
977+
"authority": authority,
978+
"client_id": "9793041b-9078-4942-b1d2-babdc472cc0c",
979+
"client_secret": client_secret,
980+
"scope": [midtier_app["client_id"] + "/.default"],
981+
}
982+
self._test_acquire_token_obo(initial_app, midtier_app)
983+
936984
def test_acquire_token_by_client_secret(self):
937985
# Vastly different than ArlingtonCloudTestCase.test_acquire_token_by_client_secret()
938986
_app = self.get_lab_app_object(
@@ -1323,7 +1371,7 @@ def test_at_pop_calling_pattern(self):
13231371
# We skip it here because this test case has not yet initialize self.app
13241372
# assert self.app.is_pop_supported()
13251373
api_endpoint = "https://20.190.132.47/beta/me"
1326-
resp = requests.get(api_endpoint, verify=False)
1374+
resp = requests.get(api_endpoint, verify=False) # @suppress py/bandit/requests-ssl-verify-disabled
13271375
self.assertEqual(resp.status_code, 401, "Initial call should end with an http 401 error")
13281376
result = self._get_shr_pop(**dict(
13291377
self.get_lab_user(usertype="cloud"), # This is generally not the current laptop's default AAD account
@@ -1334,6 +1382,9 @@ def test_at_pop_calling_pattern(self):
13341382
nonce=self._extract_pop_nonce(resp.headers.get("WWW-Authenticate")),
13351383
),
13361384
))
1385+
# The api_endpoint is for test only and has no proper SSL certificate,
1386+
# so we suppress the CodeQL warning for disabling SSL certificate checks
1387+
# @suppress py/bandit/requests-ssl-verify-disabled
13371388
resp = requests.get(api_endpoint, verify=False, headers={
13381389
"Authorization": "pop {}".format(result["access_token"]),
13391390
})

0 commit comments

Comments
 (0)