Repository: AzureAD/microsoft-authentication-library-for-python
Issue category: Performance Issue

Issue:
Instance metadata caching
This issue is inspired by an improvement made in MSAL .Net 4.1:
* documented in [its release blog post here](https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/wiki/msal-net-4.1#getaccounts-and-acquiretokensilent-are-now-less-network-chatty),
* its original issue [this one](https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/issues/1174) and [that one](https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/issues/1040),
* the [internal design review](https://identitydivision.visualstudio.com/DevEx/_git/AuthLibrariesApiReview/pullrequest/859?_a=files),
* and also [its implementation part1](https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/pull/1221) and [part 2](https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/pull/1229).
`openid-configuration` HTTP request slows down MSAL
**Describe the bug**

MSAL makes an HTTP request for every `Authority` creation:

```
https://login.microsoftonline.com/54826b22-38d6-4fb2-bad9-b7b93a3e9c5a/v2.0/.well-known/openid-configuration
```

The code responsible for the HTTP request is

https://github.com/AzureAD/microsoft-authentication-library-for-python/blob/84cb2cf6776ade5b03586a5a31d35f618246e54c/msal/authority.py#L83-L85

**Impact**

This extra network request doubles the number of network requests for Azure CLI commands and makes each Azure CLI command almost x1 slower (ARM -> AAD+ARM). 

As each Azure CLI command runs in a separate process, it is not possible for Azure CLI to reuse `Authority`.

**Possible solution**

- MSAL can cache the result so that the HTTP request is not always invoked.
- MSAL can have a switch to turn off `tenant_discovery` and use the default `authorization_endpoint` and `token_endpoint` to avoid this network call. https://github.com/AzureAD/microsoft-authentication-library-for-python/blob/84cb2cf6776ade5b03586a5a31d35f618246e54c/msal/authority.py#L93-L94


`openid-configuration` HTTP request slows down MSAL
**Describe the bug**

MSAL makes an HTTP request for every `Authority` creation:

```
https://login.microsoftonline.com/54826b22-38d6-4fb2-bad9-b7b93a3e9c5a/v2.0/.well-known/openid-configuration
```

The code responsible for the HTTP request is

https://github.com/AzureAD/microsoft-authentication-library-for-python/blob/84cb2cf6776ade5b03586a5a31d35f618246e54c/msal/authority.py#L83-L85

**Impact**

This extra network request doubles the number of network requests for Azure CLI commands and makes each Azure CLI command almost x1 slower (ARM -> AAD+ARM). 

As each Azure CLI command runs in a separate process, it is not possible for Azure CLI to reuse `Authority`.

**Possible solution**

- MSAL can cache the result so that the HTTP request is not always invoked.
- MSAL can have a switch to turn off `tenant_discovery` and use the default `authorization_endpoint` and `token_endpoint` to avoid this network call. https://github.com/AzureAD/microsoft-authentication-library-for-python/blob/84cb2cf6776ade5b03586a5a31d35f618246e54c/msal/authority.py#L93-L94




Hints:

Patch:
diff --git a/msal/application.py b/msal/application.py
index 686cc95d..3651d216 100644
--- a/msal/application.py
+++ b/msal/application.py
@@ -170,6 +170,7 @@ def __init__(
                 # This way, it holds the same positional param place for PCA,
                 # when we would eventually want to add this feature to PCA in future.
             exclude_scopes=None,
+            http_cache=None,
             ):
         """Create an instance of application.
 
@@ -336,6 +337,60 @@ def __init__(
             If that is unnecessary or undesirable for your app,
             now you can use this parameter to supply an exclusion list of scopes,
             such as ``exclude_scopes = ["offline_access"]``.
+
+        :param dict http_cache:
+            MSAL has long been caching tokens in the ``token_cache``.
+            Recently, MSAL also introduced a concept of ``http_cache``,
+            by automatically caching some finite amount of non-token http responses,
+            so that *long-lived*
+            ``PublicClientApplication`` and ``ConfidentialClientApplication``
+            would be more performant and responsive in some situations.
+
+            This ``http_cache`` parameter accepts any dict-like object.
+            If not provided, MSAL will use an in-memory dict.
+
+            If your app is a command-line app (CLI),
+            you would want to persist your http_cache across different CLI runs.
+            The following recipe shows a way to do so::
+
+                # Just add the following lines at the beginning of your CLI script
+                import sys, atexit, pickle
+                http_cache_filename = sys.argv[0] + ".http_cache"
+                try:
+                    with open(http_cache_filename, "rb") as f:
+                        persisted_http_cache = pickle.load(f)  # Take a snapshot
+                except (
+                        IOError,  # A non-exist http cache file
+                        pickle.UnpicklingError,  # A corrupted http cache file
+                        EOFError,  # An empty http cache file
+                        AttributeError, ImportError, IndexError,  # Other corruption
+                        ):
+                    persisted_http_cache = {}  # Recover by starting afresh
+                atexit.register(lambda: pickle.dump(
+                    # When exit, flush it back to the file.
+                    # It may occasionally overwrite another process's concurrent write,
+                    # but that is fine. Subsequent runs will reach eventual consistency.
+                    persisted_http_cache, open(http_cache_file, "wb")))
+
+                # And then you can implement your app as you normally would
+                app = msal.PublicClientApplication(
+                    "your_client_id",
+                    ...,
+                    http_cache=persisted_http_cache,  # Utilize persisted_http_cache
+                    ...,
+                    #token_cache=...,  # You may combine the old token_cache trick
+                        # Please refer to token_cache recipe at
+                        # https://msal-python.readthedocs.io/en/latest/#msal.SerializableTokenCache
+                    )
+                app.acquire_token_interactive(["your", "scope"], ...)
+
+            Content inside ``http_cache`` are cheap to obtain.
+            There is no need to share them among different apps.
+
+            Content inside ``http_cache`` will contain no tokens nor
+            Personally Identifiable Information (PII). Encryption is unnecessary.
+
+            New in version 1.16.0.
         """
         self.client_id = client_id
         self.client_credential = client_credential
@@ -370,7 +425,7 @@ def __init__(
             self.http_client.mount("https://", a)
         self.http_client = ThrottledHttpClient(
             self.http_client,
-            {}  # Hard code an in-memory cache, for now
+            {} if http_cache is None else http_cache,  # Default to an in-memory dict
             )
 
         self.app_name = app_name


Test patch: