diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 00000000..58bfecda --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,32 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: '' +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to our [off-the-shelf samples](https://github.com/AzureAD/microsoft-authentication-library-for-python/tree/dev/sample) and pick one that is closest to your usage scenario. You should not need to modify the sample. +2. Follow the description of the sample, typically at the beginning of it, to prepare a `config.json` containing your test configurations +3. Run such sample, typically by `python sample.py config.json` +4. See the error +5. In this bug report, tell us the sample you choose, paste the content of the config.json with your test setup (which you can choose to skip your credentials, and/or mail it to our developer's email). + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**What you see instead** +Paste the sample output, or add screenshots to help explain your problem. + +**The MSAL Python version you are using** +Paste the output of this +`python -c "import msal; print(msal.__version__)"` + +**Additional context** +Add any other context about the problem here. diff --git a/msal/application.py b/msal/application.py index cbeedef7..0d38a1ae 100644 --- a/msal/application.py +++ b/msal/application.py @@ -21,7 +21,7 @@ # The __init__.py will import this. Not the other way around. -__version__ = "1.4.1" +__version__ = "1.4.2" logger = logging.getLogger(__name__) @@ -82,6 +82,7 @@ def extract_certs(public_cert_content): class ClientApplication(object): ACQUIRE_TOKEN_SILENT_ID = "84" + ACQUIRE_TOKEN_BY_REFRESH_TOKEN = "85" ACQUIRE_TOKEN_BY_USERNAME_PASSWORD_ID = "301" ACQUIRE_TOKEN_ON_BEHALF_OF_ID = "523" ACQUIRE_TOKEN_BY_DEVICE_FLOW_ID = "622" @@ -551,6 +552,12 @@ def acquire_token_silent_with_error( return result final_result = result for alias in self._get_authority_aliases(self.authority.instance): + if not self.token_cache.find( + self.token_cache.CredentialType.REFRESH_TOKEN, + target=scopes, + query={"environment": alias}): + # Skip heavy weight logic when RT for this alias doesn't exist + continue the_authority = Authority( "https://" + alias + "/" + self.authority.tenant, self.http_client, @@ -727,6 +734,11 @@ def acquire_token_by_refresh_token(self, refresh_token, scopes): return self.client.obtain_token_by_refresh_token( refresh_token, scope=decorate_scope(scopes, self.client_id), + headers={ + CLIENT_REQUEST_ID: _get_new_correlation_id(), + CLIENT_CURRENT_TELEMETRY: _build_current_telemetry_request_header( + self.ACQUIRE_TOKEN_BY_REFRESH_TOKEN), + }, rt_getter=lambda rt: rt, on_updating_rt=False, on_removing_rt=lambda rt_item: None, # No OP diff --git a/msal/mex.py b/msal/mex.py index 684d50ed..a84f320b 100644 --- a/msal/mex.py +++ b/msal/mex.py @@ -41,9 +41,7 @@ def _xpath_of_root(route_to_leaf): def send_request(mex_endpoint, http_client, **kwargs): - mex_document = http_client.get( - mex_endpoint, headers={'Content-Type': 'application/soap+xml'}, - **kwargs).text + mex_document = http_client.get(mex_endpoint, **kwargs).text return Mex(mex_document).get_wstrust_username_password_endpoint() diff --git a/msal/oauth2cli/oauth2.py b/msal/oauth2cli/oauth2.py index 55fa0547..1d9c21d5 100644 --- a/msal/oauth2cli/oauth2.py +++ b/msal/oauth2cli/oauth2.py @@ -3,10 +3,10 @@ import json try: - from urllib.parse import urlencode, parse_qs + from urllib.parse import urlencode, parse_qs, quote_plus except ImportError: from urlparse import parse_qs - from urllib import urlencode + from urllib import urlencode, quote_plus import logging import warnings import time @@ -205,9 +205,14 @@ def _obtain_token( # The verb "obtain" is influenced by OAUTH2 RFC 6749 # client credentials in the request-body using the following # parameters: client_id, client_secret. if self.client_secret and self.client_id: - _headers["Authorization"] = "Basic " + base64.b64encode( - "{}:{}".format(self.client_id, self.client_secret) - .encode("ascii")).decode("ascii") + _headers["Authorization"] = "Basic " + base64.b64encode("{}:{}".format( + # Per https://tools.ietf.org/html/rfc6749#section-2.3.1 + # client_id and client_secret needs to be encoded by + # "application/x-www-form-urlencoded" + # https://www.w3.org/TR/html401/interact/forms.html#h-17.13.4.1 + # BEFORE they are fed into HTTP Basic Authentication + quote_plus(self.client_id), quote_plus(self.client_secret) + ).encode("ascii")).decode("ascii") if "token_endpoint" not in self.configuration: raise ValueError("token_endpoint not found in configuration") diff --git a/msal/oauth2cli/oidc.py b/msal/oauth2cli/oidc.py index 45861303..4a7df6a7 100644 --- a/msal/oauth2cli/oidc.py +++ b/msal/oauth2cli/oidc.py @@ -38,6 +38,11 @@ def decode_id_token(id_token, client_id=None, issuer=None, nonce=None, now=None) """ decoded = json.loads(decode_part(id_token.split('.')[1])) err = None # https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation + _now = now or time.time() + if _now < decoded.get("nbf", _now - 1): # nbf is optional per JWT specs + # This is not an ID token validation, but a JWT validation + # https://tools.ietf.org/html/rfc7519#section-4.1.5 + err = "0. The ID token is not yet valid" if issuer and issuer != decoded["iss"]: # https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationResponse err = ('2. The Issuer Identifier for the OpenID Provider, "%s", ' @@ -53,7 +58,7 @@ def decode_id_token(id_token, client_id=None, issuer=None, nonce=None, now=None) # the Client and the Token Endpoint (which it is in this flow), # the TLS server validation MAY be used to validate the issuer # in place of checking the token signature. - if (now or time.time()) > decoded["exp"]: + if _now > decoded["exp"]: err = "9. The current time MUST be before the time represented by the exp Claim." if nonce and nonce != decoded.get("nonce"): err = ("11. Nonce must be the same value " diff --git a/msal/wstrust_request.py b/msal/wstrust_request.py index b2898f76..bdfb57ef 100644 --- a/msal/wstrust_request.py +++ b/msal/wstrust_request.py @@ -79,7 +79,7 @@ def _build_rst(username, password, cloud_audience_urn, endpoint_address, soap_ac return """ {soap_action} - urn:uuid:{message_id} + urn:uuid:{message_id} http://www.w3.org/2005/08/addressing/anonymous diff --git a/tests/test_e2e.py b/tests/test_e2e.py index 28383cd6..957d01a4 100644 --- a/tests/test_e2e.py +++ b/tests/test_e2e.py @@ -302,7 +302,7 @@ def get_lab_app( # or it could be setup on Travis CI # https://docs.travis-ci.com/user/environment-variables/#defining-variables-in-repository-settings # Data came from here - # https://microsoft.sharepoint-df.com/teams/MSIDLABSExtended/SitePages/Rese.aspx#programmatic-access-info-for-lab-request-api + # https://docs.msidlab.com/accounts/confidentialclient.html logger.info("Using lab app defined by ENV variables %s and %s", env_client_id, env_client_secret) client_id = os.getenv(env_client_id) @@ -482,7 +482,6 @@ def test_ropc_adfs2019_onprem(self): # Configuration is derived from https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/blob/4.7.0/tests/Microsoft.Identity.Test.Common/TestConstants.cs#L250-L259 config = self.get_lab_user(usertype="onprem", federationProvider="ADFSv2019") config["authority"] = "https://fs.%s.com/adfs" % config["lab_name"] - config["client_id"] = "PublicClientId" config["scope"] = self.adfs2019_scopes config["password"] = self.get_lab_user_secret(config["lab_name"]) self._test_username_password(**config) @@ -497,7 +496,6 @@ def test_adfs2019_onprem_acquire_token_by_auth_code(self): """ config = self.get_lab_user(usertype="onprem", federationProvider="ADFSv2019") config["authority"] = "https://fs.%s.com/adfs" % config["lab_name"] - config["client_id"] = "PublicClientId" config["scope"] = self.adfs2019_scopes config["port"] = 8080 self._test_acquire_token_by_auth_code(**config) @@ -505,18 +503,24 @@ def test_adfs2019_onprem_acquire_token_by_auth_code(self): @unittest.skipUnless( os.getenv("LAB_OBO_CLIENT_SECRET"), "Need LAB_OBO_CLIENT SECRET from https://msidlabs.vault.azure.net/secrets/TodoListServiceV2-OBO/c58ba97c34ca4464886943a847d1db56") + @unittest.skipUnless( + os.getenv("LAB_OBO_CONFIDENTIAL_CLIENT_ID"), + "Confidential client id can be found here https://docs.msidlab.com/flows/onbehalfofflow.html") + @unittest.skipUnless( + os.getenv("LAB_OBO_PUBLIC_CLIENT_ID"), + "Public client id can be found here https://docs.msidlab.com/flows/onbehalfofflow.html") def test_acquire_token_obo(self): config = self.get_lab_user(usertype="cloud") config_cca = {} config_cca.update(config) - config_cca["client_id"] = "f4aa5217-e87c-42b2-82af-5624dd14ee72" + config_cca["client_id"] = os.getenv("LAB_OBO_CONFIDENTIAL_CLIENT_ID") config_cca["scope"] = ["https://graph.microsoft.com/.default"] config_cca["client_secret"] = os.getenv("LAB_OBO_CLIENT_SECRET") config_pca = {} config_pca.update(config) - config_pca["client_id"] = "c0485386-1e9a-4663-bc96-7ab30656de7f" + config_pca["client_id"] = os.getenv("LAB_OBO_PUBLIC_CLIENT_ID") config_pca["password"] = self.get_lab_user_secret(config_pca["lab_name"]) config_pca["scope"] = ["api://%s/read" % config_cca["client_id"]] @@ -535,20 +539,22 @@ def test_b2c_acquire_token_by_auth_code(self): # This won't work https://msidlab.com/api/user?usertype=b2c password="***" # From https://aka.ms/GetLabUserSecret?Secret=msidlabb2c """ + config = self.get_lab_app_object(azureenvironment="azureb2ccloud") self._test_acquire_token_by_auth_code( authority=self._build_b2c_authority("B2C_1_SignInPolicy"), - client_id="b876a048-55a5-4fc5-9403-f5d90cb1c852", + client_id=config["appId"], port=3843, # Lab defines 4 of them: [3843, 4584, 4843, 60000] - scope=["https://msidlabb2c.onmicrosoft.com/msaapp/user_impersonation"] + scope=config["defaultScopes"].split(','), ) def test_b2c_acquire_token_by_ropc(self): + config = self.get_lab_app_object(azureenvironment="azureb2ccloud") self._test_username_password( authority=self._build_b2c_authority("B2C_1_ROPC_Auth"), - client_id="e3b9ad76-9763-4827-b088-80c7a7888f79", + client_id=config["appId"], username="b2clocal@msidlabb2c.onmicrosoft.com", password=self.get_lab_user_secret("msidlabb2c"), - scope=["https://msidlabb2c.onmicrosoft.com/msidlabb2capi/read"], + scope=config["defaultScopes"].split(','), ) @@ -584,6 +590,17 @@ def test_acquire_token_device_flow(self): config["scope"] = ["user.read"] self._test_device_flow(**config) + def test_acquire_token_silent_with_an_empty_cache_should_return_none(self): + config = self.get_lab_user( + usertype="cloud", azureenvironment=self.environment, publicClient="no") + app = msal.ConfidentialClientApplication( + config['client_id'], authority=config['authority'], + http_client=MinimalHttpClient()) + result = app.acquire_token_silent(scopes=config['scope'], account=None) + self.assertEqual(result, None) + # Note: An alias in this region is no longer accepting HTTPS traffic. + # If this test case passes without exception, + # it means MSAL Python is not affected by that. if __name__ == "__main__": unittest.main()