Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

client_assertion can accept a callback #747

Open
wants to merge 867 commits into
base: dev
Choose a base branch
from
Open

client_assertion can accept a callback #747

wants to merge 867 commits into from

Conversation

rayluo
Copy link
Collaborator

@rayluo rayluo commented Sep 19, 2024

  • client_assertion can accept a callback.
  • Also expose a AutoRefresher and document their usage. The new doc is staged here, just search the keyword "Supporting raw assertion obtained from elsewhere"
  • Deprecate client_assertion as a string

This will close #705 and close #746

rayluo added 30 commits August 25, 2022 18:21
Disabled SSH Cert when using broker
Bump version number
Merge Release 1.19.0 back to dev branch
Apply the refactor to similar code path
Bumping version number
Merge Release 1.20.0 back to dev
Test matrix covers Python 3.11
Fallback to expires_on when expires_in is absent
rayluo and others added 22 commits July 16, 2024 20:51
CAE team and MSI team are working on turning on CAE by default for MSI
v1. So what that means is, App developers will start seeing CAE even
without setting the capability - "CP1".

Update msal/application.py

Co-authored-by: Den Delimarsky <53200638+localden@users.noreply.github.com>

Update msal/application.py

Co-authored-by: Den Delimarsky <53200638+localden@users.noreply.github.com>

Update msal/application.py

Co-authored-by: Den Delimarsky <53200638+localden@users.noreply.github.com>

Update msal/managed_identity.py

Co-authored-by: Den Delimarsky <53200638+localden@users.noreply.github.com>

Update msal/managed_identity.py

Co-authored-by: Den Delimarsky <53200638+localden@users.noreply.github.com>

Update msal/managed_identity.py

Co-authored-by: Den Delimarsky <53200638+localden@users.noreply.github.com>

Update msal/managed_identity.py

Co-authored-by: Den Delimarsky <53200638+localden@users.noreply.github.com>
* Add/update broker integration doc

* Move the doc components from .md file to broker-test.py
Old ciam2 test tenant is obsolete
Resource ID as mi_res_id in App Service, as msi_res_id in other flavors
@rayluo rayluo requested a review from a team as a code owner September 19, 2024 19:25
@@ -757,6 +780,11 @@ def _build_client(self, client_credential, authority, skip_regional_client=False
# Use client_credential.get("...") rather than "..." in client_credential
# so that we can ignore an empty string came from an empty ENV VAR.
if client_credential.get("client_assertion"):
if isinstance(client_credential["client_assertion"], str):
warnings.warn(
"Please use a callback instead of a string. See also "
Copy link
Member

@bgavrilMS bgavrilMS Sep 20, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"Please use a callback instead of a string. See also "
"Use a callback instead of a string. MSAL will invoke the callback every time it needs to fetch a token from the identity provider. The implementer is responsible for returning non-expired assertion and for caching assertions. See also "

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will accept this suggestion in my next commit.

We might as well also append "... with the help of a helper AutoRefresher. See also ...".

I'm providing more explanation about the AutoRefresher in the last comment below.


import msal

def assertion_callback():
Copy link
Member

@bgavrilMS bgavrilMS Sep 20, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add claims as input to this callback? This will help implementers of FIC perform token revocation, where the claims need to be take into account by the assertion provider as well.

May be separate PR, but want to make sure adding parametewrs to this is not a breaking change.

Copy link
Collaborator Author

@rayluo rayluo Sep 20, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An adjustable callback can be created as a class, so that it can maintain its state. The crux is that the MSAL code path calling this callback does not know what proper values to feed in. So, the app developer - or a higher-level API - will need to expect an assertion error from eSTS, and then adjust that callback instance accordingly.

class AdjustsableCallback:
    extra_claims = None

    def callback(self):
        return "A new assertion based on extra claims: " + self.extra_claims

ac = AdjustableCallback()
cca = ConfidentialClientApplication(..., client_credential={"client_assertion": ac.callback})

result = cca.acquire_token_for_client(...)
if result.get("error"):
    ac.extra_claims = get_new_claims_somehow()
    result = cca.acquire_token_for_client(...)

Do you want me to also document this advance scenario into the docs this time?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The claims have to flow from MSAL's acquire_token_for_client call into the callback. An example would be good.

Here's the equivalent .NET code

// singleton
var cca = ConfidentialCLientApp(...)
                .WithClientAssertion(
                        (input) => {
                              string claims = input.Claims; 
                              GetAssertionFromMsiOrWhatever(claims);
                  });


// later in the code
var authResult = cca.acquireTokenForClient();  // ok, callback invoked, claims is empty

// call downstream API
// 401 + WWW-AUthenticate returned
// parse header and extract claims

// get new token with claims challenge
authResult = cca.acquireTokenForClient(claims); // ok, callback is invoked, claims is not empty


smart_callback = msal.AutoRefresher(
assertion_callback,
expires_in=3600, # Whatever value that you see fit
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure what this auto refresher is. We should mention FIC scenarios in the docs instead.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure what this auto refresher is.

Naming is hard, and new name suggestions are welcome (perhaps CallbackCache instead?). But the functionality behind the "auto refresher" was needed since day 1, because even when in the legit authenticated-by-a-cert scenario, MSAL Python internally has (1) a helper function to use that cert to create a short-lived assertion, and (2) a helper class AutoRefresher to cache the callback outcome and only call the callback again after a predefined expiry time elapsed. Such a cache behavior is desirable for the #2 scenario described in this comment.

In this PR, we repurpose the #1 callback behavior to accept any callback; and expose the #2 cache behavior so that app developers don't need to reinvent the wheel.

We should mention FIC scenarios in the docs instead.

I suppose, the #1 callback can be used in any scenario including FIC, but the #2 cache concept shall always be applicable.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sample/confidential_client_sample.py Dismissed Show dismissed Hide dismissed
sample/confidential_client_sample.py Dismissed Show dismissed Hide dismissed
sample/confidential_client_sample.py Dismissed Show dismissed Hide dismissed
sample/device_flow_sample.py Dismissed Show dismissed Hide dismissed
sample/device_flow_sample.py Dismissed Show dismissed Hide dismissed
sample/username_password_sample.py Dismissed Show dismissed Hide dismissed
sample/username_password_sample.py Dismissed Show dismissed Hide dismissed
tests/test_e2e.py Dismissed Show dismissed Hide dismissed
secret_name = parse_qs(secret_url.query)["Secret"][0]
else: # Ciam6 era has a URL path that ends with the secret name
secret_name = secret_url.path.split("/")[-1]
logger.info('Detected secret name "%s" from "%s"', secret_name, raw_url)

Check failure

Code scanning / CodeQL

Clear-text logging of sensitive information High test

This expression logs
sensitive data (secret)
as clear text.
This expression logs
sensitive data (secret)
as clear text.
This expression logs
sensitive data (secret)
as clear text.

Copilot Autofix AI 2 months ago

To fix the problem, we should avoid logging the sensitive secret_name directly. Instead, we can log a generic message that indicates the presence of a secret without revealing its actual value. This approach maintains the usefulness of the log for debugging purposes while protecting sensitive information.

  • Replace the logging statement that logs secret_name with a more generic message.
  • Ensure that the new log message does not contain any sensitive information.
Suggested changeset 1
tests/test_e2e.py

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/tests/test_e2e.py b/tests/test_e2e.py
--- a/tests/test_e2e.py
+++ b/tests/test_e2e.py
@@ -1050,3 +1050,3 @@
             secret_name = secret_url.path.split("/")[-1]
-        logger.info('Detected secret name "%s" from "%s"', secret_name, raw_url)
+        logger.info('Detected a secret from the provided URL.')
         self._test_acquire_token_for_client(
EOF
@@ -1050,3 +1050,3 @@
secret_name = secret_url.path.split("/")[-1]
logger.info('Detected secret name "%s" from "%s"', secret_name, raw_url)
logger.info('Detected a secret from the provided URL.')
self._test_acquire_token_for_client(
Copilot is powered by AI and may make mistakes. Always verify output.
Positive Feedback
Negative Feedback

Provide additional feedback

Please help us improve GitHub Copilot by sharing more details about this comment.

Please select one or more of the options
secret_name = parse_qs(secret_url.query)["Secret"][0]
else: # Ciam6 era has a URL path that ends with the secret name
secret_name = secret_url.path.split("/")[-1]
logger.info('Detected secret name "%s" from "%s"', secret_name, raw_url)

Check failure

Code scanning / CodeQL

Clear-text logging of sensitive information High test

This expression logs
sensitive data (secret)
as clear text.

Copilot Autofix AI 2 months ago

To fix the problem, we should avoid logging the sensitive raw_url directly. Instead, we can log a sanitized version of the URL that does not include the sensitive parts. One approach is to log only the non-sensitive components of the URL, such as the scheme, netloc, and path, while omitting the query parameters and fragment.

  • Modify the logging statement on line 1051 to exclude sensitive information.
  • Introduce a helper function to sanitize the URL before logging.
Suggested changeset 1
tests/test_e2e.py

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/tests/test_e2e.py b/tests/test_e2e.py
--- a/tests/test_e2e.py
+++ b/tests/test_e2e.py
@@ -1050,3 +1050,4 @@
             secret_name = secret_url.path.split("/")[-1]
-        logger.info('Detected secret name "%s" from "%s"', secret_name, raw_url)
+        sanitized_url = f"{secret_url.scheme}://{secret_url.netloc}{secret_url.path}"
+        logger.info('Detected secret name "%s" from sanitized URL "%s"', secret_name, sanitized_url)
         self._test_acquire_token_for_client(
EOF
@@ -1050,3 +1050,4 @@
secret_name = secret_url.path.split("/")[-1]
logger.info('Detected secret name "%s" from "%s"', secret_name, raw_url)
sanitized_url = f"{secret_url.scheme}://{secret_url.netloc}{secret_url.path}"
logger.info('Detected secret name "%s" from sanitized URL "%s"', secret_name, sanitized_url)
self._test_acquire_token_for_client(
Copilot is powered by AI and may make mistakes. Always verify output.
Positive Feedback
Negative Feedback

Provide additional feedback

Please help us improve GitHub Copilot by sharing more details about this comment.

Please select one or more of the options
@@ -0,0 +1,51 @@
-----BEGIN ENCRYPTED PRIVATE KEY-----

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does msal Python utilize CI pipeline? Can you get these test certs from the lab instead of committing them here?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Those certs are not certs for any real services. We just created some dummy certs for testing the cert loading code path. Thanks for your review, though. :)

@rayluo
Copy link
Collaborator Author

rayluo commented Oct 31, 2024

Note: As an aftermath of all commits being rewritten, this PR currently contains +16,009 −1,044 lines of changes from 250 commits happened in last 2 years, plus a merge conflict. This is un-review-able. Please do not review until I fix it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet