From 10370ca55825931f54715d617bdd2da0b0148014 Mon Sep 17 00:00:00 2001 From: Ray Luo Date: Thu, 21 Mar 2024 16:02:23 -0700 Subject: [PATCH] Friendly hint covers most flows, tested. TODO: OBO --- msal/application.py | 21 ++++++++++++++----- msal/authority.py | 6 ++++++ .../confidential_client_certificate_sample.py | 6 +++++- sample/confidential_client_secret_sample.py | 6 +++++- sample/device_flow_sample.py | 6 +++++- sample/interactive_sample.py | 6 +++++- sample/username_password_sample.py | 9 ++++++-- 7 files changed, 49 insertions(+), 11 deletions(-) diff --git a/msal/application.py b/msal/application.py index 418448d4..ba7e8ce4 100644 --- a/msal/application.py +++ b/msal/application.py @@ -604,6 +604,16 @@ def _build_telemetry_context( self._telemetry_buffer, self._telemetry_lock, api_id, correlation_id=correlation_id, refresh_reason=refresh_reason) + def _adjust_response(self, response): # Adjust response inline + # Currently, this is used to provide better error message for CIAM CUD + error_description = response.get("error_description", "") + if ("AADSTS500207" in error_description # Observed in most auth grants + or "AADSTS900144" in error_description # Observed in ROPC + ) and self._oidc_authority and not self._oidc_authority.endswith("/v2.0"): + response["error_description"] = ( + 'Did you forget to append "/v2.0" to your oidc_authority? ' + + response["error_description"]) + def _get_regional_authority(self, central_authority): if not self._region_configured: # User did not opt-in to ESTS-R return None # Short circuit to completely bypass region detection @@ -974,11 +984,7 @@ def authorize(): # A controller in a web app **kwargs)) if "access_token" in response: response[self._TOKEN_SOURCE] = self._TOKEN_SOURCE_IDP - if ("AADSTS500207" in response.get("error_description", "") and - self._oidc_authority and not self._oidc_authority.endswith("/v2.0")): - response["error_description"] = ( - 'Did you forget to append "/v2.0" to your oidc_authority? ' - + response["error_description"]) + self._adjust_response(response) telemetry_context.update_telemetry(response) return response @@ -1706,6 +1712,7 @@ def acquire_token_by_username_password( **kwargs)) if "access_token" in response: response[self._TOKEN_SOURCE] = self._TOKEN_SOURCE_IDP + self._adjust_response(response) telemetry_context.update_telemetry(response) return response @@ -2008,6 +2015,8 @@ def acquire_token_interactive( **kwargs)) if "access_token" in response: response[self._TOKEN_SOURCE] = self._TOKEN_SOURCE_IDP + self._adjust_response(response) # Note: It won't improve + # the error rendered in browser, but still better than nothing telemetry_context.update_telemetry(response) return response @@ -2117,6 +2126,7 @@ def initiate_device_flow(self, scopes=None, **kwargs): headers={msal.telemetry.CLIENT_REQUEST_ID: correlation_id}, **kwargs) flow[self.DEVICE_FLOW_CORRELATION_ID] = correlation_id + self._adjust_response(flow) # AADSTS500207 would happen here, not at token endpoint return flow def acquire_token_by_device_flow(self, flow, claims_challenge=None, **kwargs): @@ -2214,6 +2224,7 @@ def _acquire_token_for_client( claims=_merge_claims_challenge_and_capabilities( self._client_capabilities, claims_challenge)), **kwargs) + self._adjust_response(response) telemetry_context.update_telemetry(response) return response diff --git a/msal/authority.py b/msal/authority.py index de19f963..d081d2cd 100644 --- a/msal/authority.py +++ b/msal/authority.py @@ -102,6 +102,7 @@ def __init__( def _initialize_oidc_authority(self, oidc_authority_url): authority, self.instance, tenant = canonicalize(oidc_authority_url) + self._is_oidc = True self.is_adfs = tenant.lower() == 'adfs' # As a convention self._is_b2c = True # Not exactly true, but # OIDC Authority was designed for CIAM which is the next gen of B2C. @@ -116,6 +117,7 @@ def _initialize_entra_authority( # instance discovery endpoint located at ``login.microsoftonline.com``. # You can customize the endpoint by providing a url as a string. # Or you can turn this behavior off by passing in a False here. + self._is_oidc = False if isinstance(authority_url, AuthorityBuilder): authority_url = str(authority_url) authority, self.instance, tenant = canonicalize(authority_url) @@ -162,6 +164,10 @@ def user_realm_discovery(self, username, correlation_id=None, response=None): # It will typically return a dict containing "ver", "account_type", # "federation_protocol", "cloud_audience_urn", # "federation_metadata_url", "federation_active_auth_url", etc. + if self._is_oidc: + # Conceptually, OIDC has no user real discovery. + # Besides, ROPC on CIAM CUD apparently works without the federation anyway + return {} # This can guide the caller to fall back normal ROPC flow if self.instance not in self.__class__._domains_without_user_realm_discovery: resp = response or self._http_client.get( "https://{netloc}/common/userrealm/{username}?api-version=1.0".format( diff --git a/sample/confidential_client_certificate_sample.py b/sample/confidential_client_certificate_sample.py index 2c4118a3..68f0c753 100644 --- a/sample/confidential_client_certificate_sample.py +++ b/sample/confidential_client_certificate_sample.py @@ -3,6 +3,9 @@ { "authority": "https://login.microsoftonline.com/Enter_the_Tenant_Name_Here", + // Usually you use this one + "oidc_authority": "https://contoso.com/Enter_the_Tenant_Name_Here", + // Alternatively, you use this one when your CIAM tenant has a custom domain "client_id": "your_client_id came from https://learn.microsoft.com/entra/identity-platform/quickstart-register-app", "scope": ["https://graph.microsoft.com/.default"], // Specific to Client Credentials Grant i.e. acquire_token_for_client(), @@ -50,7 +53,8 @@ # Create a preferably long-lived app instance, to avoid the overhead of app creation global_app = msal.ConfidentialClientApplication( - config["client_id"], authority=config["authority"], + config["client_id"], authority=config.get("authority"), + oidc_authority=config.get("oidc_authority"), # Use this for a CIAM custom domain client_credential={"thumbprint": config["thumbprint"], "private_key": open(config['private_key_file']).read()}, token_cache=global_token_cache, # Let this app (re)use an existing token cache. # If absent, ClientApplication will create its own empty token cache diff --git a/sample/confidential_client_secret_sample.py b/sample/confidential_client_secret_sample.py index 9ff6a81b..6dd8b836 100644 --- a/sample/confidential_client_secret_sample.py +++ b/sample/confidential_client_secret_sample.py @@ -3,6 +3,9 @@ { "authority": "https://login.microsoftonline.com/Enter_the_Tenant_Name_Here", + // Usually you use this one + "oidc_authority": "https://contoso.com/Enter_the_Tenant_Name_Here", + // Alternatively, you use this one when your CIAM tenant has a custom domain "client_id": "your_client_id came from https://learn.microsoft.com/entra/identity-platform/quickstart-register-app", "scope": ["https://graph.microsoft.com/.default"], // Specific to Client Credentials Grant i.e. acquire_token_for_client(), @@ -49,7 +52,8 @@ # Create a preferably long-lived app instance, to avoid the overhead of app creation global_app = msal.ConfidentialClientApplication( - config["client_id"], authority=config["authority"], + config["client_id"], authority=config.get("authority"), + oidc_authority=config.get("oidc_authority"), # Use this for a CIAM custom domain client_credential=config["secret"], token_cache=global_token_cache, # Let this app (re)use an existing token cache. # If absent, ClientApplication will create its own empty token cache diff --git a/sample/device_flow_sample.py b/sample/device_flow_sample.py index 7d998470..73cac7bb 100644 --- a/sample/device_flow_sample.py +++ b/sample/device_flow_sample.py @@ -3,6 +3,9 @@ { "authority": "https://login.microsoftonline.com/common", + // Usually you use this one + "oidc_authority": "https://contoso.com/Enter_the_Tenant_Name_Here", + // Alternatively, you use this one when your CIAM tenant has a custom domain "client_id": "your_client_id came from https://learn.microsoft.com/entra/identity-platform/quickstart-register-app", "scope": ["User.ReadBasic.All"], // You can find the other permission names from this document @@ -39,7 +42,8 @@ # Create a preferably long-lived app instance, to avoid the overhead of app creation global_app = msal.PublicClientApplication( - config["client_id"], authority=config["authority"], + config["client_id"], authority=config.get("authority"), + oidc_authority=config.get("oidc_authority"), # Use this for a CIAM custom domain token_cache=global_token_cache, # Let this app (re)use an existing token cache. # If absent, ClientApplication will create its own empty token cache ) diff --git a/sample/interactive_sample.py b/sample/interactive_sample.py index 58d8c9a9..f7e3b0a7 100644 --- a/sample/interactive_sample.py +++ b/sample/interactive_sample.py @@ -6,6 +6,9 @@ { "authority": "https://login.microsoftonline.com/organizations", + // Usually you use this one + "oidc_authority": "https://contoso.com/Enter_the_Tenant_Name_Here", + // Alternatively, you use this one when your CIAM tenant has a custom domain "client_id": "your_client_id came from https://learn.microsoft.com/entra/identity-platform/quickstart-register-app", "scope": ["User.ReadBasic.All"], // You can find the other permission names from this document @@ -36,7 +39,8 @@ # Create a preferably long-lived app instance, to avoid the overhead of app creation global_app = msal.PublicClientApplication( - config["client_id"], authority=config["authority"], + config["client_id"], authority=config.get("authority"), + oidc_authority=config.get("oidc_authority"), # Use this for a CIAM custom domain #enable_broker_on_windows=True, # Opted in. You will be guided to meet the prerequisites, if your app hasn't already # See also: https://docs.microsoft.com/en-us/azure/active-directory/develop/scenario-desktop-acquire-token-wam#wam-value-proposition token_cache=global_token_cache, # Let this app (re)use an existing token cache. diff --git a/sample/username_password_sample.py b/sample/username_password_sample.py index 3578e5ef..a51c5efd 100644 --- a/sample/username_password_sample.py +++ b/sample/username_password_sample.py @@ -3,6 +3,9 @@ { "authority": "https://login.microsoftonline.com/organizations", + // Usually you use this one + "oidc_authority": "https://contoso.com/Enter_the_Tenant_Name_Here", + // Alternatively, you use this one when your CIAM tenant has a custom domain "client_id": "your_client_id came from https://learn.microsoft.com/entra/identity-platform/quickstart-register-app", "username": "your_username@your_tenant.com", "scope": ["User.ReadBasic.All"], @@ -33,7 +36,8 @@ # logging.getLogger("msal").setLevel(logging.INFO) # Optionally disable MSAL DEBUG logs config = json.load(open(sys.argv[1])) -config["password"] = getpass.getpass() +config["password"] = getpass.getpass( + prompt="Enter password for {}: ".format(config["username"])) # If for whatever reason you plan to recreate same ClientApplication periodically, # you shall create one global token cache and reuse it by each ClientApplication @@ -44,7 +48,8 @@ global_app = msal.ClientApplication( config["client_id"], client_credential=config.get("client_secret"), - authority=config["authority"], + authority=config.get("authority"), + oidc_authority=config.get("oidc_authority"), # Use this for a CIAM custom domain token_cache=global_token_cache, # Let this app (re)use an existing token cache. # If absent, ClientApplication will create its own empty token cache )