Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package python-msal for openSUSE:Factory checked in at 2023-10-13 23:14:01 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python-msal (Old) and /work/SRC/openSUSE:Factory/.python-msal.new.20540 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-msal" Fri Oct 13 23:14:01 2023 rev:16 rq:1116468 version:1.24.1 Changes: -------- --- /work/SRC/openSUSE:Factory/python-msal/python-msal.changes 2023-08-14 22:36:28.504689044 +0200 +++ /work/SRC/openSUSE:Factory/.python-msal.new.20540/python-msal.changes 2023-10-13 23:14:18.362220889 +0200 @@ -1,0 +2,28 @@ +Mon Oct 9 09:52:17 UTC 2023 - John Paul Adrian Glaubitz <adrian.glaub...@suse.com> + +- Update to version 1.24.1 + + Includes minor adjustments on handling acquire_token_interactive(). + The scope of the issue being addressed was limited to a short-lived + sign-in attempt. The potential misuse vector complexity was high, + therefore it is unlikely to be reproduced in standard usage scenarios; + however, out of abundance of caution, this fix is shipped to align + ourselves with Microsoft's policy of secure-by-default. +- from version 1.24.0 + + Enhancement: There may be a new msal_telemetry key available in MSAL's + acquire token response, currently observed when broker is enabled. Its + content and format are opaque to caller. This telemetry blob allows + participating apps to collect them via telemetry, and it may help + future troubleshooting. (#575) + + Enhancement: A new enable_pii_log parameter is added into ClientApplication + constructor. When enabled, the broker component may include PII (Personal + Identifiable Information) in logs. This may help troubleshooting. (#568, #590) +- Remove temporary version override + +------------------------------------------------------------------- +Wed Oct 4 10:11:52 UTC 2023 - John Paul Adrian Glaubitz <adrian.glaub...@suse.com> + +- Update to version 1.24.0b2 + + Experimental: Building on top of 1.24.0b1 and includes + some adjustment on handling acquire_token_interactive(). + +------------------------------------------------------------------- Old: ---- msal-1.24.0b1.tar.gz New: ---- msal-1.24.1.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-msal.spec ++++++ --- /var/tmp/diff_new_pack.pc7Evg/_old 2023-10-13 23:14:19.202251357 +0200 +++ /var/tmp/diff_new_pack.pc7Evg/_new 2023-10-13 23:14:19.202251357 +0200 @@ -16,20 +16,18 @@ # -%define realversion 1.24.0b1 - %{?!python_module:%define python_module() python-%{**} python3-%{**}} %if 0%{?suse_version} >= 1500 %define skip_python2 1 %endif Name: python-msal -Version: 1.24.0~b1 +Version: 1.24.1 Release: 0 Summary: Microsoft Authentication Library (MSAL) for Python License: MIT Group: Development/Languages/Python URL: https://github.com/AzureAD/microsoft-authentication-library-for-python -Source: https://files.pythonhosted.org/packages/source/m/msal/msal-%{realversion}.tar.gz +Source: https://files.pythonhosted.org/packages/source/m/msal/msal-%{version}.tar.gz BuildRequires: %{python_module devel} BuildRequires: %{python_module setuptools} BuildRequires: fdupes @@ -51,7 +49,7 @@ standard OAuth2 and OpenID Connect. %prep -%setup -q -n msal-%{realversion} +%setup -q -n msal-%{version} %build %python_build ++++++ msal-1.24.0b1.tar.gz -> msal-1.24.1.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/msal-1.24.0b1/PKG-INFO new/msal-1.24.1/PKG-INFO --- old/msal-1.24.0b1/PKG-INFO 2023-07-24 11:38:52.279546500 +0200 +++ new/msal-1.24.1/PKG-INFO 2023-09-29 09:54:15.598715300 +0200 @@ -1,6 +1,6 @@ Metadata-Version: 2.1 Name: msal -Version: 1.24.0b1 +Version: 1.24.1 Summary: The Microsoft Authentication Library (MSAL) for Python library Home-page: https://github.com/AzureAD/microsoft-authentication-library-for-python Author: Microsoft Corporation @@ -26,14 +26,19 @@ Classifier: Operating System :: OS Independent Requires-Python: >=2.7 Description-Content-Type: text/markdown -Provides-Extra: broker License-File: LICENSE +Requires-Dist: requests<3,>=2.0.0 +Requires-Dist: PyJWT[crypto]<3,>=1.0.0 +Requires-Dist: cryptography<44,>=0.6 +Requires-Dist: mock; python_version < "3.3" +Provides-Extra: broker +Requires-Dist: pymsalruntime<0.14,>=0.13.2; (python_version >= "3.6" and platform_system == "Windows") and extra == "broker" # Microsoft Authentication Library (MSAL) for Python -| `dev` branch | Reference Docs | # of Downloads per different platforms | # of Downloads per recent MSAL versions | -|---------------|---------------|----------------------------------------|-----------------------------------------| - [](https://github.com/AzureAD/microsoft-authentication-library-for-python/actions) | [](https://msal-python.readthedocs.io/en/latest/?badge=latest) | [](https://pypistats.org/packages/msal) | [](https://pepy.tech/project/msal) +| `dev` branch | Reference Docs | # of Downloads per different platforms | # of Downloads per recent MSAL versions | Benchmark Diagram | +|:------------:|:--------------:|:--------------------------------------:|:---------------------------------------:|:-----------------:| + [](https://github.com/AzureAD/microsoft-authentication-library-for-python/actions) | [](https://msal-python.readthedocs.io/en/latest/?badge=latest) | [](https://pypistats.org/packages/msal) | [](https://pepy.tech/project/msal) | [ð](https://azuread.github.io/microsoft-authentication-library-for-python/dev/bench/) The Microsoft Authentication Library for Python enables applications to integrate with the [Microsoft identity platform](https://aka.ms/aaddevv2). It allows you to sign in users or apps with Microsoft identities ([Azure AD](https://azure.microsoft.com/services/active-directory/), [Microsoft Accounts](https://account.microsoft.com) and [Azure AD B2C](https://azure.microsoft.com/services/active-directory-b2c/) accounts) and obtain tokens to call Microsoft APIs such as [Microsoft Graph](https://graph.microsoft.io/) or your own APIs registered with the Microsoft identity platform. It is built using industry standard OAuth2 and OpenID Connect protocols diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/msal-1.24.0b1/README.md new/msal-1.24.1/README.md --- old/msal-1.24.0b1/README.md 2023-07-24 11:38:42.000000000 +0200 +++ new/msal-1.24.1/README.md 2023-09-29 09:54:06.000000000 +0200 @@ -1,8 +1,8 @@ # Microsoft Authentication Library (MSAL) for Python -| `dev` branch | Reference Docs | # of Downloads per different platforms | # of Downloads per recent MSAL versions | -|---------------|---------------|----------------------------------------|-----------------------------------------| - [](https://github.com/AzureAD/microsoft-authentication-library-for-python/actions) | [](https://msal-python.readthedocs.io/en/latest/?badge=latest) | [](https://pypistats.org/packages/msal) | [](https://pepy.tech/project/msal) +| `dev` branch | Reference Docs | # of Downloads per different platforms | # of Downloads per recent MSAL versions | Benchmark Diagram | +|:------------:|:--------------:|:--------------------------------------:|:---------------------------------------:|:-----------------:| + [](https://github.com/AzureAD/microsoft-authentication-library-for-python/actions) | [](https://msal-python.readthedocs.io/en/latest/?badge=latest) | [](https://pypistats.org/packages/msal) | [](https://pepy.tech/project/msal) | [ð](https://azuread.github.io/microsoft-authentication-library-for-python/dev/bench/) The Microsoft Authentication Library for Python enables applications to integrate with the [Microsoft identity platform](https://aka.ms/aaddevv2). It allows you to sign in users or apps with Microsoft identities ([Azure AD](https://azure.microsoft.com/services/active-directory/), [Microsoft Accounts](https://account.microsoft.com) and [Azure AD B2C](https://azure.microsoft.com/services/active-directory-b2c/) accounts) and obtain tokens to call Microsoft APIs such as [Microsoft Graph](https://graph.microsoft.io/) or your own APIs registered with the Microsoft identity platform. It is built using industry standard OAuth2 and OpenID Connect protocols diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/msal-1.24.0b1/msal/__main__.py new/msal-1.24.1/msal/__main__.py --- old/msal-1.24.0b1/msal/__main__.py 1970-01-01 01:00:00.000000000 +0100 +++ new/msal-1.24.1/msal/__main__.py 2023-09-29 09:54:06.000000000 +0200 @@ -0,0 +1,232 @@ +# It is currently shipped inside msal library. +# Pros: It is always available wherever msal is installed. +# Cons: Its 3rd-party dependencies (if any) may become msal's dependency. +"""MSAL Python Tester + +Usage 1: Run it on the fly. + python -m msal + +Usage 2: Build an all-in-one executable file for bug bash. + shiv -e msal.__main__._main -o msaltest-on-os-name.pyz . + Note: We choose to not define a console script to avoid name conflict. +""" +import base64, getpass, json, logging, sys, msal + +_AZURE_CLI = "04b07795-8ddb-461a-bbee-02f9e1bf7b46" +_VISUAL_STUDIO = "04f0c124-f2bc-4f59-8241-bf6df9866bbd" + +def print_json(blob): + print(json.dumps(blob, indent=2, sort_keys=True)) + +def _input_boolean(message): + return input( + "{} (N/n/F/f or empty means False, otherwise it is True): ".format(message) + ) not in ('N', 'n', 'F', 'f', '') + +def _input(message, default=None): + return input(message.format(default=default)).strip() or default + +def _select_options( + options, header="Your options:", footer=" Your choice? ", option_renderer=str, + accept_nonempty_string=False, + ): + assert options, "options must not be empty" + if header: + print(header) + for i, o in enumerate(options, start=1): + print(" {}: {}".format(i, option_renderer(o))) + if accept_nonempty_string: + print(" Or you can just type in your input.") + while True: + raw_data = input(footer) + try: + choice = int(raw_data) + if 1 <= choice <= len(options): + return options[choice - 1] + except ValueError: + if raw_data and accept_nonempty_string: + return raw_data + +def _input_scopes(): + scopes = _select_options([ + "https://graph.microsoft.com/.default", + "https://management.azure.com/.default", + "User.Read", + "User.ReadBasic.All", + ], + header="Select a scope (multiple scopes can only be input by manually typing them, delimited by space):", + accept_nonempty_string=True, + ).split() # It also converts the input string(s) into a list + if "https://pas.windows.net/CheckMyAccess/Linux/.default" in scopes: + raise ValueError("SSH Cert scope shall be tested by its dedicated functions") + return scopes + +def _select_account(app): + accounts = app.get_accounts() + if accounts: + return _select_options( + accounts, + option_renderer=lambda a: a["username"], + header="Account(s) already signed in inside MSAL Python:", + ) + else: + print("No account available inside MSAL Python. Use other methods to acquire token first.") + +def _acquire_token_silent(app): + """acquire_token_silent() - with an account already signed into MSAL Python.""" + account = _select_account(app) + if account: + print_json(app.acquire_token_silent( + _input_scopes(), + account=account, + force_refresh=_input_boolean("Bypass MSAL Python's token cache?"), + )) + +def _acquire_token_interactive(app, scopes=None, data=None): + """acquire_token_interactive() - User will be prompted if app opts to do select_account.""" + scopes = scopes or _input_scopes() # Let user input scope param before less important prompt and login_hint + prompt = _select_options([ + {"value": None, "description": "Unspecified. Proceed silently with a default account (if any), fallback to prompt."}, + {"value": "none", "description": "none. Proceed silently with a default account (if any), or error out."}, + {"value": "select_account", "description": "select_account. Prompt with an account picker."}, + ], + option_renderer=lambda o: o["description"], + header="Prompt behavior?")["value"] + if prompt == "select_account": + login_hint = None # login_hint is unnecessary when prompt=select_account + else: + raw_login_hint = _select_options( + [None] + [a["username"] for a in app.get_accounts()], + header="login_hint? (If you have multiple signed-in sessions in browser/broker, and you specify a login_hint to match one of them, you will bypass the account picker.)", + accept_nonempty_string=True, + ) + login_hint = raw_login_hint["username"] if isinstance(raw_login_hint, dict) else raw_login_hint + result = app.acquire_token_interactive( + scopes, + parent_window_handle=app.CONSOLE_WINDOW_HANDLE, # This test app is a console app + enable_msa_passthrough=app.client_id in [ # Apps are expected to set this right + _AZURE_CLI, _VISUAL_STUDIO, + ], # Here this test app mimics the setting for some known MSA-PT apps + prompt=prompt, login_hint=login_hint, data=data or {}, + ) + if login_hint and "id_token_claims" in result: + signed_in_user = result.get("id_token_claims", {}).get("preferred_username") + if signed_in_user != login_hint: + logging.warning('Signed-in user "%s" does not match login_hint', signed_in_user) + print_json(result) + return result + +def _acquire_token_by_username_password(app): + """acquire_token_by_username_password() - See constraints here: https://docs.microsoft.com/en-us/azure/active-directory/develop/msal-authentication-flows#constraints-for-ropc""" + print_json(app.acquire_token_by_username_password( + _input("username: "), getpass.getpass("password: "), scopes=_input_scopes())) + +_JWK1 = """{"kty":"RSA", "n":"2tNr73xwcj6lH7bqRZrFzgSLj7OeLfbn8216uOMDHuaZ6TEUBDN8Uz0ve8jAlKsP9CQFCSVoSNovdE-fs7c15MxEGHjDcNKLWonznximj8pDGZQjVdfK-7mG6P6z-lgVcLuYu5JcWU_PeEqIKg5llOaz-qeQ4LEDS4T1D2qWRGpAra4rJX1-kmrWmX_XIamq30C9EIO0gGuT4rc2hJBWQ-4-FnE1NXmy125wfT3NdotAJGq5lMIfhjfglDbJCwhc8Oe17ORjO3FsB5CLuBRpYmP7Nzn66lRY3Fe11Xz8AEBl3anKFSJcTvlMnFtu3EpD-eiaHfTgRBU7CztGQqVbiQ", "e":"AQAB"}""" +_SSH_CERT_DATA = {"token_type": "ssh-cert", "key_id": "key1", "req_cnf": _JWK1} +_SSH_CERT_SCOPE = ["https://pas.windows.net/CheckMyAccess/Linux/.default"] + +def _acquire_ssh_cert_silently(app): + """Acquire an SSH Cert silently- This typically only works with Azure CLI""" + account = _select_account(app) + if account: + result = app.acquire_token_silent( + _SSH_CERT_SCOPE, + account, + data=_SSH_CERT_DATA, + force_refresh=_input_boolean("Bypass MSAL Python's token cache?"), + ) + print_json(result) + if result and result.get("token_type") != "ssh-cert": + logging.error("Unable to acquire an ssh-cert.") + +def _acquire_ssh_cert_interactive(app): + """Acquire an SSH Cert interactively - This typically only works with Azure CLI""" + result = _acquire_token_interactive(app, scopes=_SSH_CERT_SCOPE, data=_SSH_CERT_DATA) + if result.get("token_type") != "ssh-cert": + logging.error("Unable to acquire an ssh-cert") + +_POP_KEY_ID = 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA-AAAAAAAA' # Fake key with a certain format and length +_RAW_REQ_CNF = json.dumps({"kid": _POP_KEY_ID, "xms_ksl": "sw"}) +_POP_DATA = { # Sampled from Azure CLI's plugin connectedk8s + 'token_type': 'pop', + 'key_id': _POP_KEY_ID, + "req_cnf": base64.urlsafe_b64encode(_RAW_REQ_CNF.encode('utf-8')).decode('utf-8').rstrip('='), + # Note: Sending _RAW_REQ_CNF without base64 encoding would result in an http 500 error +} # See also https://github.com/Azure/azure-cli-extensions/blob/main/src/connectedk8s/azext_connectedk8s/_clientproxyutils.py#L86-L92 + +def _acquire_pop_token_interactive(app): + """Acquire a POP token interactively - This typically only works with Azure CLI""" + POP_SCOPE = ['6256c85f-0aad-4d50-b960-e6e9b21efe35/.default'] # KAP 1P Server App Scope, obtained from https://github.com/Azure/azure-cli-extensions/pull/4468/files#diff-a47efa3186c7eb4f1176e07d0b858ead0bf4a58bfd51e448ee3607a5b4ef47f6R116 + result = _acquire_token_interactive(app, scopes=POP_SCOPE, data=_POP_DATA) + print_json(result) + if result.get("token_type") != "pop": + logging.error("Unable to acquire a pop token") + +def _remove_account(app): + """remove_account() - Invalidate account and/or token(s) from cache, so that acquire_token_silent() would be reset""" + account = _select_account(app) + if account: + app.remove_account(account) + print('Account "{}" and/or its token(s) are signed out from MSAL Python'.format(account["username"])) + +def _exit(app): + """Exit""" + bug_link = ( + "https://identitydivision.visualstudio.com/Engineering/_queries/query/79b3a352-a775-406f-87cd-a487c382a8ed/" + if app._enable_broker else + "https://github.com/AzureAD/microsoft-authentication-library-for-python/issues/new/choose" + ) + print("Bye. If you found a bug, please report it here: {}".format(bug_link)) + sys.exit() + +def _main(): + print("Welcome to the Msal Python {} Tester (Experimental)\n".format(msal.__version__)) + chosen_app = _select_options([ + {"client_id": _AZURE_CLI, "name": "Azure CLI (Correctly configured for MSA-PT)"}, + {"client_id": _VISUAL_STUDIO, "name": "Visual Studio (Correctly configured for MSA-PT)"}, + {"client_id": "95de633a-083e-42f5-b444-a4295d8e9314", "name": "Whiteboard Services (Non MSA-PT app. Accepts AAD & MSA accounts.)"}, + ], + option_renderer=lambda a: a["name"], + header="Impersonate this app (or you can type in the client_id of your own app)", + accept_nonempty_string=True) + allow_broker = _input_boolean("Allow broker?") + enable_debug_log = _input_boolean("Enable MSAL Python's DEBUG log?") + enable_pii_log = _input_boolean("Enable PII in broker's log?") if allow_broker and enable_debug_log else False + app = msal.PublicClientApplication( + chosen_app["client_id"] if isinstance(chosen_app, dict) else chosen_app, + authority=_select_options([ + "https://login.microsoftonline.com/common", + "https://login.microsoftonline.com/organizations", + "https://login.microsoftonline.com/microsoft.onmicrosoft.com", + "https://login.microsoftonline.com/msidlab4.onmicrosoft.com", + "https://login.microsoftonline.com/consumers", + ], + header="Input authority (Note that MSA-PT apps would NOT use the /common authority)", + accept_nonempty_string=True, + ), + allow_broker=allow_broker, + enable_pii_log=enable_pii_log, + ) + if enable_debug_log: + logging.basicConfig(level=logging.DEBUG) + while True: + func = _select_options([ + _acquire_token_silent, + _acquire_token_interactive, + _acquire_token_by_username_password, + _acquire_ssh_cert_silently, + _acquire_ssh_cert_interactive, + _acquire_pop_token_interactive, + _remove_account, + _exit, + ], option_renderer=lambda f: f.__doc__, header="MSAL Python APIs:") + try: + func(app) + except ValueError as e: + logging.error("Invalid input: %s", e) + except KeyboardInterrupt: # Useful for bailing out a stuck interactive flow + print("Aborted") + +if __name__ == "__main__": + _main() + diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/msal-1.24.0b1/msal/application.py new/msal-1.24.1/msal/application.py --- old/msal-1.24.0b1/msal/application.py 2023-07-24 11:38:42.000000000 +0200 +++ new/msal-1.24.1/msal/application.py 2023-09-29 09:54:06.000000000 +0200 @@ -25,7 +25,7 @@ # The __init__.py will import this. Not the other way around. -__version__ = "1.24.0b1" # When releasing, also check and bump our dependencies's versions if needed +__version__ = "1.24.1" # When releasing, also check and bump our dependencies's versions if needed logger = logging.getLogger(__name__) _AUTHORITY_TYPE_CLOUDSHELL = "CLOUDSHELL" @@ -193,6 +193,7 @@ http_cache=None, instance_discovery=None, allow_broker=None, + enable_pii_log=None, ): """Create an instance of application. @@ -205,12 +206,19 @@ or an X509 certificate container in this form:: { - "private_key": "...-----BEGIN PRIVATE KEY-----...", + "private_key": "...-----BEGIN PRIVATE KEY-----... in PEM format", "thumbprint": "A1B2C3D4E5F6...", "public_certificate": "...-----BEGIN CERTIFICATE-----... (Optional. See below.)", "passphrase": "Passphrase if the private_key is encrypted (Optional. Added in version 1.6.0)", } + MSAL Python requires a "private_key" in PEM format. + If your cert is in a PKCS12 (.pfx) format, you can also + `convert it to PEM and get the thumbprint <https://github.com/Azure/azure-sdk-for-python/blob/07d10639d7e47f4852eaeb74aef5d569db499d6e/sdk/identity/azure-identity/azure/identity/_credentials/certificate.py#L101-L123>`_. + + The thumbprint is available in your app's registration in Azure Portal. + Alternatively, you can `calculate the thumbprint <https://github.com/Azure/azure-sdk-for-python/blob/07d10639d7e47f4852eaeb74aef5d569db499d6e/sdk/identity/azure-identity/azure/identity/_credentials/certificate.py#L94-L97>`_. + *Added in version 0.5.0*: public_certificate (optional) is public key certificate which will be sent through 'x5c' JWT header only for @@ -500,6 +508,13 @@ * AAD and MSA accounts (i.e. Non-ADFS, non-B2C) New in version 1.20.0. + + :param boolean enable_pii_log: + When enabled, logs may include PII (Personal Identifiable Information). + This can be useful in troubleshooting broker behaviors. + The default behavior is False. + + New in version 1.24.0. """ self.client_id = client_id self.client_credential = client_credential @@ -576,6 +591,8 @@ try: from . import broker # Trigger Broker's initialization self._enable_broker = True + if enable_pii_log: + broker._enable_pii_log() except RuntimeError: logger.exception( "Broker is unavailable on this platform. " diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/msal-1.24.0b1/msal/authority.py new/msal-1.24.1/msal/authority.py --- old/msal-1.24.0b1/msal/authority.py 2023-07-24 11:38:42.000000000 +0200 +++ new/msal-1.24.1/msal/authority.py 2023-09-29 09:54:06.000000000 +0200 @@ -163,9 +163,9 @@ raise ValueError( "Your given address (%s) should consist of " "an https url with a minimum of one segment in a path: e.g. " - "https://login.microsoftonline.com/<tenant> " - "or https://<tenant_name>.ciamlogin.com/<tenant> " - "or https://<tenant_name>.b2clogin.com/<tenant_name>.onmicrosoft.com/policy" + "https://login.microsoftonline.com/{tenant} " + "or https://{tenant_name}.ciamlogin.com/{tenant} " + "or https://{tenant_name}.b2clogin.com/{tenant_name}.onmicrosoft.com/policy" % authority_or_auth_endpoint) def _instance_discovery(url, http_client, instance_discovery_endpoint, **kwargs): diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/msal-1.24.0b1/msal/broker.py new/msal-1.24.1/msal/broker.py --- old/msal-1.24.0b1/msal/broker.py 2023-07-24 11:38:42.000000000 +0200 +++ new/msal-1.24.1/msal/broker.py 2023-09-29 09:54:06.000000000 +0200 @@ -236,3 +236,6 @@ if error: return _convert_error(error, client_id) +def _enable_pii_log(): + pymsalruntime.set_is_pii_enabled(1) # New in PyMsalRuntime 0.13.0 + diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/msal-1.24.0b1/msal/oauth2cli/authcode.py new/msal-1.24.1/msal/oauth2cli/authcode.py --- old/msal-1.24.0b1/msal/oauth2cli/authcode.py 2023-07-24 11:38:42.000000000 +0200 +++ new/msal-1.24.1/msal/oauth2cli/authcode.py 2023-09-29 09:54:06.000000000 +0200 @@ -15,10 +15,12 @@ try: # Python 3 from http.server import HTTPServer, BaseHTTPRequestHandler from urllib.parse import urlparse, parse_qs, urlencode + from html import escape except ImportError: # Fall back to Python 2 from BaseHTTPServer import HTTPServer, BaseHTTPRequestHandler from urlparse import urlparse, parse_qs from urllib import urlencode + from cgi import escape logger = logging.getLogger(__name__) @@ -77,25 +79,42 @@ for k, v in qs.items()} +def _is_html(text): + return text.startswith("<") # Good enough for our purpose + + +def _escape(key_value_pairs): + return {k: escape(v) for k, v in key_value_pairs.items()} + + class _AuthCodeHandler(BaseHTTPRequestHandler): def do_GET(self): # For flexibility, we choose to not check self.path matching redirect_uri #assert self.path.startswith('/THE_PATH_REGISTERED_BY_THE_APP') qs = parse_qs(urlparse(self.path).query) if qs.get('code') or qs.get("error"): # So, it is an auth response - self.server.auth_response = _qs2kv(qs) - logger.debug("Got auth response: %s", self.server.auth_response) - template = (self.server.success_template - if "code" in qs else self.server.error_template) - self._send_full_response( - template.safe_substitute(**self.server.auth_response)) - # NOTE: Don't do self.server.shutdown() here. It'll halt the server. + auth_response = _qs2kv(qs) + logger.debug("Got auth response: %s", auth_response) + if self.server.auth_state and self.server.auth_state != auth_response.get("state"): + # OAuth2 successful and error responses contain state when it was used + # https://www.rfc-editor.org/rfc/rfc6749#section-4.2.2.1 + self._send_full_response("State mismatch") # Possibly an attack + else: + template = (self.server.success_template + if "code" in qs else self.server.error_template) + if _is_html(template.template): + safe_data = _escape(auth_response) # Foiling an XSS attack + else: + safe_data = auth_response + self._send_full_response(template.safe_substitute(**safe_data)) + self.server.auth_response = auth_response # Set it now, after the response is likely sent else: self._send_full_response(self.server.welcome_page) + # NOTE: Don't do self.server.shutdown() here. It'll halt the server. def _send_full_response(self, body, is_ok=True): self.send_response(200 if is_ok else 400) - content_type = 'text/html' if body.startswith('<') else 'text/plain' + content_type = 'text/html' if _is_html(body) else 'text/plain' self.send_header('Content-type', content_type) self.end_headers() self.wfile.write(body.encode("utf-8")) @@ -281,16 +300,14 @@ self._server.timeout = timeout # Otherwise its handle_timeout() won't work self._server.auth_response = {} # Shared with _AuthCodeHandler + self._server.auth_state = state # So handler will check it before sending response while not self._closing: # Otherwise, the handle_request() attempt # would yield noisy ValueError trace # Derived from # https://docs.python.org/2/library/basehttpserver.html#more-examples self._server.handle_request() if self._server.auth_response: - if state and state != self._server.auth_response.get("state"): - logger.debug("State mismatch. Ignoring this noise.") - else: - break + break result.update(self._server.auth_response) # Return via writable result param def close(self): @@ -318,6 +335,7 @@ default="https://login.microsoftonline.com/common/oauth2/v2.0/authorize") p.add_argument('client_id', help="The client_id of your application") p.add_argument('--port', type=int, default=0, help="The port in redirect_uri") + p.add_argument('--timeout', type=int, default=60, help="Timeout value, in second") p.add_argument('--host', default="127.0.0.1", help="The host of redirect_uri") p.add_argument('--scope', default=None, help="The scope list") args = parser.parse_args() @@ -331,8 +349,8 @@ auth_uri=flow["auth_uri"], welcome_template= "<a href='$auth_uri'>Sign In</a>, or <a href='$abort_uri'>Abort</a", - error_template="Oh no. $error", + error_template="<html>Oh no. $error</html>", success_template="Oh yeah. Got $code", - timeout=60, + timeout=args.timeout, state=flow["state"], # Optional ), indent=4)) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/msal-1.24.0b1/msal/oauth2cli/oauth2.py new/msal-1.24.1/msal/oauth2cli/oauth2.py --- old/msal-1.24.0b1/msal/oauth2cli/oauth2.py 2023-07-24 11:38:42.000000000 +0200 +++ new/msal-1.24.1/msal/oauth2cli/oauth2.py 2023-09-29 09:54:06.000000000 +0200 @@ -666,7 +666,7 @@ **(auth_params or {})) auth_response = auth_code_receiver.get_auth_response( auth_uri=flow["auth_uri"], - state=flow["state"], # Optional but we choose to do it upfront + state=flow["state"], # So receiver can check it early timeout=timeout, welcome_template=welcome_template, success_template=success_template, diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/msal-1.24.0b1/msal.egg-info/PKG-INFO new/msal-1.24.1/msal.egg-info/PKG-INFO --- old/msal-1.24.0b1/msal.egg-info/PKG-INFO 2023-07-24 11:38:52.000000000 +0200 +++ new/msal-1.24.1/msal.egg-info/PKG-INFO 2023-09-29 09:54:15.000000000 +0200 @@ -1,6 +1,6 @@ Metadata-Version: 2.1 Name: msal -Version: 1.24.0b1 +Version: 1.24.1 Summary: The Microsoft Authentication Library (MSAL) for Python library Home-page: https://github.com/AzureAD/microsoft-authentication-library-for-python Author: Microsoft Corporation @@ -26,14 +26,19 @@ Classifier: Operating System :: OS Independent Requires-Python: >=2.7 Description-Content-Type: text/markdown -Provides-Extra: broker License-File: LICENSE +Requires-Dist: requests<3,>=2.0.0 +Requires-Dist: PyJWT[crypto]<3,>=1.0.0 +Requires-Dist: cryptography<44,>=0.6 +Requires-Dist: mock; python_version < "3.3" +Provides-Extra: broker +Requires-Dist: pymsalruntime<0.14,>=0.13.2; (python_version >= "3.6" and platform_system == "Windows") and extra == "broker" # Microsoft Authentication Library (MSAL) for Python -| `dev` branch | Reference Docs | # of Downloads per different platforms | # of Downloads per recent MSAL versions | -|---------------|---------------|----------------------------------------|-----------------------------------------| - [](https://github.com/AzureAD/microsoft-authentication-library-for-python/actions) | [](https://msal-python.readthedocs.io/en/latest/?badge=latest) | [](https://pypistats.org/packages/msal) | [](https://pepy.tech/project/msal) +| `dev` branch | Reference Docs | # of Downloads per different platforms | # of Downloads per recent MSAL versions | Benchmark Diagram | +|:------------:|:--------------:|:--------------------------------------:|:---------------------------------------:|:-----------------:| + [](https://github.com/AzureAD/microsoft-authentication-library-for-python/actions) | [](https://msal-python.readthedocs.io/en/latest/?badge=latest) | [](https://pypistats.org/packages/msal) | [](https://pepy.tech/project/msal) | [ð](https://azuread.github.io/microsoft-authentication-library-for-python/dev/bench/) The Microsoft Authentication Library for Python enables applications to integrate with the [Microsoft identity platform](https://aka.ms/aaddevv2). It allows you to sign in users or apps with Microsoft identities ([Azure AD](https://azure.microsoft.com/services/active-directory/), [Microsoft Accounts](https://account.microsoft.com) and [Azure AD B2C](https://azure.microsoft.com/services/active-directory-b2c/) accounts) and obtain tokens to call Microsoft APIs such as [Microsoft Graph](https://graph.microsoft.io/) or your own APIs registered with the Microsoft identity platform. It is built using industry standard OAuth2 and OpenID Connect protocols diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/msal-1.24.0b1/msal.egg-info/SOURCES.txt new/msal-1.24.1/msal.egg-info/SOURCES.txt --- old/msal-1.24.0b1/msal.egg-info/SOURCES.txt 2023-07-24 11:38:52.000000000 +0200 +++ new/msal-1.24.1/msal.egg-info/SOURCES.txt 2023-09-29 09:54:15.000000000 +0200 @@ -3,6 +3,7 @@ setup.cfg setup.py msal/__init__.py +msal/__main__.py msal/application.py msal/authority.py msal/broker.py @@ -31,9 +32,11 @@ tests/test_assertion.py tests/test_authcode.py tests/test_authority.py +tests/test_benchmark.py tests/test_broker.py tests/test_ccs.py tests/test_client.py +tests/test_cryptography.py tests/test_e2e.py tests/test_individual_cache.py tests/test_mex.py diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/msal-1.24.0b1/tests/test_application.py new/msal-1.24.1/tests/test_application.py --- old/msal-1.24.0b1/tests/test_application.py 2023-07-24 11:38:42.000000000 +0200 +++ new/msal-1.24.1/tests/test_application.py 2023-09-29 09:54:06.000000000 +0200 @@ -627,7 +627,7 @@ sys.version_info[0] >= 3 and sys.version_info[1] >= 2, "assertWarns() is only available in Python 3.2+") class TestClientCredentialGrant(unittest.TestCase): - def _test_certain_authority_should_emit_warnning(self, authority): + def _test_certain_authority_should_emit_warning(self, authority): app = ConfidentialClientApplication( "client_id", client_credential="secret", authority=authority) def mock_post(url, headers=None, *args, **kwargs): @@ -636,12 +636,12 @@ with self.assertWarns(DeprecationWarning): app.acquire_token_for_client(["scope"], post=mock_post) - def test_common_authority_should_emit_warnning(self): - self._test_certain_authority_should_emit_warnning( + def test_common_authority_should_emit_warning(self): + self._test_certain_authority_should_emit_warning( authority="https://login.microsoftonline.com/common") - def test_organizations_authority_should_emit_warnning(self): - self._test_certain_authority_should_emit_warnning( + def test_organizations_authority_should_emit_warning(self): + self._test_certain_authority_should_emit_warning( authority="https://login.microsoftonline.com/organizations") diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/msal-1.24.0b1/tests/test_authcode.py new/msal-1.24.1/tests/test_authcode.py --- old/msal-1.24.0b1/tests/test_authcode.py 2023-07-24 11:38:42.000000000 +0200 +++ new/msal-1.24.1/tests/test_authcode.py 2023-09-29 09:54:06.000000000 +0200 @@ -2,6 +2,8 @@ import socket import sys +import requests + from msal.oauth2cli.authcode import AuthCodeReceiver @@ -17,10 +19,24 @@ self.assertNotEqual(port, receiver.get_port()) def test_no_two_concurrent_receivers_can_listen_on_same_port(self): - port = 12345 # Assuming this port is available - with AuthCodeReceiver(port=port) as receiver: + with AuthCodeReceiver() as receiver: expected_error = OSError if sys.version_info[0] > 2 else socket.error with self.assertRaises(expected_error): - with AuthCodeReceiver(port=port) as receiver2: + with AuthCodeReceiver(port=receiver.get_port()): pass + def test_template_should_escape_input(self): + with AuthCodeReceiver() as receiver: + receiver._scheduled_actions = [( # Injection happens here when the port is known + 1, # Delay it until the receiver is activated by get_auth_response() + lambda: self.assertEqual( + "<html><tag>foo</tag></html>", + requests.get("http://localhost:{}?error=<tag>foo</tag>".format( + receiver.get_port())).text, + "Unsafe data in HTML should be escaped", + ))] + receiver.get_auth_response( # Starts server and hang until timeout + timeout=3, + error_template="<html>$error</html>", + ) + diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/msal-1.24.0b1/tests/test_benchmark.py new/msal-1.24.1/tests/test_benchmark.py --- old/msal-1.24.0b1/tests/test_benchmark.py 1970-01-01 01:00:00.000000000 +0100 +++ new/msal-1.24.1/tests/test_benchmark.py 2023-09-29 09:54:06.000000000 +0200 @@ -0,0 +1,28 @@ +from tests.simulator import ClientCredentialGrantSimulator as CcaTester +from perf_baseline import Baseline + + +baseline = Baseline(".perf.baseline", threshold=1.5) # Up to 1.5x slower than baseline + +# Here come benchmark test cases, powered by pytest-benchmark +# Func names will become diag names. +def test_cca_1_tenant_with_10_tokens_per_tenant_and_cache_hit(benchmark): + tester = CcaTester(tokens_per_tenant=10, cache_hit=True) + baseline.set_or_compare(tester.run) + benchmark(tester.run) + +def test_cca_many_tenants_with_10_tokens_per_tenant_and_cache_hit(benchmark): + tester = CcaTester(number_of_tenants=1000, tokens_per_tenant=10, cache_hit=True) + baseline.set_or_compare(tester.run) + benchmark(tester.run) + +def test_cca_1_tenant_with_10_tokens_per_tenant_and_cache_miss(benchmark): + tester = CcaTester(tokens_per_tenant=10, cache_hit=False) + baseline.set_or_compare(tester.run) + benchmark(tester.run) + +def test_cca_many_tenants_with_10_tokens_per_tenant_and_cache_miss(benchmark): + tester = CcaTester(number_of_tenants=1000, tokens_per_tenant=10, cache_hit=False) + baseline.set_or_compare(tester.run) + benchmark(tester.run) + diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/msal-1.24.0b1/tests/test_cryptography.py new/msal-1.24.1/tests/test_cryptography.py --- old/msal-1.24.0b1/tests/test_cryptography.py 1970-01-01 01:00:00.000000000 +0100 +++ new/msal-1.24.1/tests/test_cryptography.py 2023-09-29 09:54:06.000000000 +0200 @@ -0,0 +1,63 @@ +import configparser +import os +import re +from unittest import TestCase +import warnings +import xml.etree.ElementTree as ET + +import requests + +from msal.application import _str2bytes + + +latest_cryptography_version = ET.fromstring( + requests.get("https://pypi.org/rss/project/cryptography/releases.xml").text + ).findall("./channel/item/title")[0].text + + +def get_current_ceiling(): + parser = configparser.ConfigParser() + parser.read("setup.cfg") + for line in parser["options"]["install_requires"].splitlines(): + if line.startswith("cryptography"): + match = re.search(r"<(\d+)", line) + if match: + return int(match.group(1)) + raise RuntimeError("Unable to find cryptography info from setup.cfg") + + +class CryptographyTestCase(TestCase): + + def test_should_be_run_with_latest_version_of_cryptography(self): + import cryptography + self.assertEqual( + cryptography.__version__, latest_cryptography_version, + "We are using cryptography {} but we should test with latest {} instead. " + "Run 'pip install -U cryptography'.".format( + cryptography.__version__, latest_cryptography_version)) + + def test_latest_cryptography_should_support_our_usage_without_warnings(self): + with open(os.path.join( + os.path.dirname(__file__), "certificate-with-password.pem")) as f: + cert = f.read() + with warnings.catch_warnings(record=True) as encountered_warnings: + # The usage was copied from application.py + from cryptography.hazmat.primitives import serialization + from cryptography.hazmat.backends import default_backend + unencrypted_private_key = serialization.load_pem_private_key( + _str2bytes(cert), + _str2bytes("password"), + backend=default_backend(), # It was a required param until 2020 + ) + self.assertEqual(0, len(encountered_warnings), + "Did cryptography deprecate the functions that we used?") + + def test_ceiling_should_be_latest_cryptography_version_plus_three(self): + expected_ceiling = int(latest_cryptography_version.split(".")[0]) + 3 + self.assertEqual( + expected_ceiling, get_current_ceiling(), + "Test passed with latest cryptography, so we shall bump ceiling to N+3={}, " + "based on their latest deprecation policy " + "https://cryptography.io/en/latest/api-stability/#deprecation".format( + expected_ceiling)) + diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/msal-1.24.0b1/tests/test_e2e.py new/msal-1.24.1/tests/test_e2e.py --- old/msal-1.24.0b1/tests/test_e2e.py 2023-07-24 11:38:42.000000000 +0200 +++ new/msal-1.24.1/tests/test_e2e.py 2023-09-29 09:54:06.000000000 +0200 @@ -10,7 +10,7 @@ load_dotenv() # take environment variables from .env. except: pass - +import base64 import logging import os import json @@ -277,48 +277,6 @@ return result # For further testing -class SshCertTestCase(E2eTestCase): - _JWK1 = """{"kty":"RSA", "n":"2tNr73xwcj6lH7bqRZrFzgSLj7OeLfbn8216uOMDHuaZ6TEUBDN8Uz0ve8jAlKsP9CQFCSVoSNovdE-fs7c15MxEGHjDcNKLWonznximj8pDGZQjVdfK-7mG6P6z-lgVcLuYu5JcWU_PeEqIKg5llOaz-qeQ4LEDS4T1D2qWRGpAra4rJX1-kmrWmX_XIamq30C9EIO0gGuT4rc2hJBWQ-4-FnE1NXmy125wfT3NdotAJGq5lMIfhjfglDbJCwhc8Oe17ORjO3FsB5CLuBRpYmP7Nzn66lRY3Fe11Xz8AEBl3anKFSJcTvlMnFtu3EpD-eiaHfTgRBU7CztGQqVbiQ", "e":"AQAB"}""" - _JWK2 = """{"kty":"RSA", "n":"72u07mew8rw-ssw3tUs9clKstGO2lvD7ZNxJU7OPNKz5PGYx3gjkhUmtNah4I4FP0DuF1ogb_qSS5eD86w10Wb1ftjWcoY8zjNO9V3ph-Q2tMQWdDW5kLdeU3-EDzc0HQeou9E0udqmfQoPbuXFQcOkdcbh3eeYejs8sWn3TQprXRwGh_TRYi-CAurXXLxQ8rp-pltUVRIr1B63fXmXhMeCAGwCPEFX9FRRs-YHUszUJl9F9-E0nmdOitiAkKfCC9LhwB9_xKtjmHUM9VaEC9jWOcdvXZutwEoW2XPMOg0Ky-s197F9rfpgHle2gBrXsbvVMvS0D-wXg6vsq6BAHzQ", "e":"AQAB"}""" - DATA1 = {"token_type": "ssh-cert", "key_id": "key1", "req_cnf": _JWK1} - DATA2 = {"token_type": "ssh-cert", "key_id": "key2", "req_cnf": _JWK2} - _SCOPE_USER = ["https://pas.windows.net/CheckMyAccess/Linux/user_impersonation"] - _SCOPE_SP = ["https://pas.windows.net/CheckMyAccess/Linux/.default"] - SCOPE = _SCOPE_SP # Historically there was a separation, at 2021 it is unified - - def test_ssh_cert_for_service_principal(self): - # Any SP can obtain an ssh-cert. Here we use the lab app. - result = get_lab_app().acquire_token_for_client(self.SCOPE, data=self.DATA1) - self.assertIsNotNone(result.get("access_token"), "Encountered {}: {}".format( - result.get("error"), result.get("error_description"))) - self.assertEqual("ssh-cert", result["token_type"]) - - def test_ssh_cert_for_user_should_work_with_any_account(self): - result = self._test_acquire_token_interactive( - client_id="04b07795-8ddb-461a-bbee-02f9e1bf7b46", # Azure CLI is one - # of the only 2 clients that are PreAuthz to use ssh cert feature - authority="https://login.microsoftonline.com/common", - scope=self.SCOPE, - data=self.DATA1, - username_uri="https://msidlab.com/api/user?usertype=cloud", - prompt="none" if msal.application._is_running_in_cloud_shell() else None, - ) # It already tests reading AT from cache, and using RT to refresh - # acquire_token_silent() would work because we pass in the same key - self.assertIsNotNone(result.get("access_token"), "Encountered {}: {}".format( - result.get("error"), result.get("error_description"))) - self.assertEqual("ssh-cert", result["token_type"]) - logger.debug("%s.cache = %s", - self.id(), json.dumps(self.app.token_cache._cache, indent=4)) - - # refresh_token grant can fetch an ssh-cert bound to a different key - account = self.app.get_accounts()[0] - refreshed_ssh_cert = self.app.acquire_token_silent( - self.SCOPE, account=account, data=self.DATA2) - self.assertIsNotNone(refreshed_ssh_cert) - self.assertEqual(refreshed_ssh_cert["token_type"], "ssh-cert") - self.assertNotEqual(result["access_token"], refreshed_ssh_cert['access_token']) - - @unittest.skipUnless( msal.application._is_running_in_cloud_shell(), "Manually run this test case from inside Cloud Shell") @@ -465,7 +423,7 @@ def get_lab_app( env_client_id="LAB_APP_CLIENT_ID", - env_client_secret="LAB_APP_CLIENT_SECRET", + env_name2="LAB_APP_CLIENT_SECRET", # A var name that hopefully avoids false alarm authority="https://login.microsoftonline.com/" "72f988bf-86f1-41af-91ab-2d7cd011db47", # Microsoft tenant ID timeout=None, @@ -477,18 +435,17 @@ logger.info( "Reading ENV variables %s and %s for lab app defined at " "https://docs.msidlab.com/accounts/confidentialclient.html", - env_client_id, env_client_secret) - if os.getenv(env_client_id) and os.getenv(env_client_secret): + env_client_id, env_name2) + if os.getenv(env_client_id) and os.getenv(env_name2): # A shortcut mainly for running tests on developer's local development machine # 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://docs.msidlab.com/accounts/confidentialclient.html client_id = os.getenv(env_client_id) - client_secret = os.getenv(env_client_secret) + client_secret = os.getenv(env_name2) else: - logger.info("ENV variables %s and/or %s are not defined. Fall back to MSI.", - env_client_id, env_client_secret) + logger.info("ENV variables are not defined. Fall back to MSI.") # See also https://microsoft.sharepoint-df.com/teams/MSIDLABSExtended/SitePages/Programmatically-accessing-LAB-API's.aspx raise unittest.SkipTest("MSI-based mechanism has not been implemented yet") return msal.ConfidentialClientApplication( @@ -698,6 +655,85 @@ self.assertCacheWorksForApp(result, scope) +class PopWithExternalKeyTestCase(LabBasedTestCase): + def _test_service_principal(self): + # Any SP can obtain an ssh-cert. Here we use the lab app. + result = get_lab_app().acquire_token_for_client(self.SCOPE, data=self.DATA1) + self.assertIsNotNone(result.get("access_token"), "Encountered {}: {}".format( + result.get("error"), result.get("error_description"))) + self.assertEqual(self.EXPECTED_TOKEN_TYPE, result["token_type"]) + + def _test_user_account(self): + lab_user = self.get_lab_user(usertype="cloud") + result = self._test_acquire_token_interactive( + client_id="04b07795-8ddb-461a-bbee-02f9e1bf7b46", # Azure CLI is one + # of the only 2 clients that are PreAuthz to use ssh cert feature + authority="https://login.microsoftonline.com/common", + scope=self.SCOPE, + data=self.DATA1, + username=lab_user["username"], + lab_name=lab_user["lab_name"], + prompt="none" if msal.application._is_running_in_cloud_shell() else None, + ) # It already tests reading AT from cache, and using RT to refresh + # acquire_token_silent() would work because we pass in the same key + self.assertIsNotNone(result.get("access_token"), "Encountered {}: {}".format( + result.get("error"), result.get("error_description"))) + self.assertEqual(self.EXPECTED_TOKEN_TYPE, result["token_type"]) + logger.debug("%s.cache = %s", + self.id(), json.dumps(self.app.token_cache._cache, indent=4)) + + # refresh_token grant can fetch an ssh-cert bound to a different key + account = self.app.get_accounts()[0] + refreshed_ssh_cert = self.app.acquire_token_silent( + self.SCOPE, account=account, data=self.DATA2) + self.assertIsNotNone(refreshed_ssh_cert) + self.assertEqual(self.EXPECTED_TOKEN_TYPE, refreshed_ssh_cert["token_type"]) + self.assertNotEqual(result["access_token"], refreshed_ssh_cert['access_token']) + + +class SshCertTestCase(PopWithExternalKeyTestCase): + EXPECTED_TOKEN_TYPE = "ssh-cert" + _JWK1 = """{"kty":"RSA", "n":"2tNr73xwcj6lH7bqRZrFzgSLj7OeLfbn8216uOMDHuaZ6TEUBDN8Uz0ve8jAlKsP9CQFCSVoSNovdE-fs7c15MxEGHjDcNKLWonznximj8pDGZQjVdfK-7mG6P6z-lgVcLuYu5JcWU_PeEqIKg5llOaz-qeQ4LEDS4T1D2qWRGpAra4rJX1-kmrWmX_XIamq30C9EIO0gGuT4rc2hJBWQ-4-FnE1NXmy125wfT3NdotAJGq5lMIfhjfglDbJCwhc8Oe17ORjO3FsB5CLuBRpYmP7Nzn66lRY3Fe11Xz8AEBl3anKFSJcTvlMnFtu3EpD-eiaHfTgRBU7CztGQqVbiQ", "e":"AQAB"}""" + _JWK2 = """{"kty":"RSA", "n":"72u07mew8rw-ssw3tUs9clKstGO2lvD7ZNxJU7OPNKz5PGYx3gjkhUmtNah4I4FP0DuF1ogb_qSS5eD86w10Wb1ftjWcoY8zjNO9V3ph-Q2tMQWdDW5kLdeU3-EDzc0HQeou9E0udqmfQoPbuXFQcOkdcbh3eeYejs8sWn3TQprXRwGh_TRYi-CAurXXLxQ8rp-pltUVRIr1B63fXmXhMeCAGwCPEFX9FRRs-YHUszUJl9F9-E0nmdOitiAkKfCC9LhwB9_xKtjmHUM9VaEC9jWOcdvXZutwEoW2XPMOg0Ky-s197F9rfpgHle2gBrXsbvVMvS0D-wXg6vsq6BAHzQ", "e":"AQAB"}""" + DATA1 = {"token_type": "ssh-cert", "key_id": "key1", "req_cnf": _JWK1} + DATA2 = {"token_type": "ssh-cert", "key_id": "key2", "req_cnf": _JWK2} + _SCOPE_USER = ["https://pas.windows.net/CheckMyAccess/Linux/user_impersonation"] + _SCOPE_SP = ["https://pas.windows.net/CheckMyAccess/Linux/.default"] + SCOPE = _SCOPE_SP # Historically there was a separation, at 2021 it is unified + + def test_service_principal(self): + self._test_service_principal() + + def test_user_account(self): + self._test_user_account() + + +def _data_for_pop(key): + raw_req_cnf = json.dumps({"kid": key, "xms_ksl": "sw"}) + return { # Sampled from Azure CLI's plugin connectedk8s + 'token_type': 'pop', + 'key_id': key, + "req_cnf": base64.urlsafe_b64encode(raw_req_cnf.encode('utf-8')).decode('utf-8').rstrip('='), + # Note: Sending raw_req_cnf without base64 encoding would result in an http 500 error + } # See also https://github.com/Azure/azure-cli-extensions/blob/main/src/connectedk8s/azext_connectedk8s/_clientproxyutils.py#L86-L92 + + +class AtPopWithExternalKeyTestCase(PopWithExternalKeyTestCase): + EXPECTED_TOKEN_TYPE = "pop" + DATA1 = _data_for_pop('AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA-AAAAAAAA') # Fake key with a certain format and length + DATA2 = _data_for_pop('BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB-BBBBBBBB') # Fake key with a certain format and length + SCOPE = [ + '6256c85f-0aad-4d50-b960-e6e9b21efe35/.default', # Azure CLI's connectedk8s plugin uses this + # https://github.com/Azure/azure-cli-extensions/pull/4468/files#diff-a47efa3186c7eb4f1176e07d0b858ead0bf4a58bfd51e448ee3607a5b4ef47f6R116 + ] + + def test_service_principal(self): + self._test_service_principal() + + def test_user_account(self): + self._test_user_account() + + class WorldWideTestCase(LabBasedTestCase): def test_aad_managed_user(self): # Pure cloud