From fc01a7e976d7500cf57fae6734d007e7df6d58a1 Mon Sep 17 00:00:00 2001 From: Alexandre Bourret Date: Fri, 31 May 2024 13:44:28 +0200 Subject: [PATCH 1/9] decoding jwt tokens and finding external ip for diagnostic --- python-lib/common.py | 53 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/python-lib/common.py b/python-lib/common.py index ad7040c..fa6df8c 100644 --- a/python-lib/common.py +++ b/python-lib/common.py @@ -130,6 +130,59 @@ def update_dict_in_kwargs(kwargs, key_to_update, update): return kwargs +def run_oauth_diagnosis(jwt_token): + censored_token = diagnose_jwt(jwt_token) + kernel_external_ip = get_kernel_external_ip() + ip_in_jwt = censored_token.get("ipaddr", "") + if ip_in_jwt != kernel_external_ip: + logger.error("The plugin external IP address ({}) does not match the IP allowed in the SSO token ({})".format( + ip_in_jwt, + kernel_external_ip + )) + + +def diagnose_jwt(jwt_token): + keys_to_report = ["aud", "exp", "app_displayname", "appid", "ipaddr", "name", "scp", "unique_name", "upn"] + decoded_token = decode_jwt(jwt_token) + censored_token = {} + for key_to_report in keys_to_report: + censored_token[key_to_report] = decoded_token.get(key_to_report) + logger.info("Decoded token: {}".format(censored_token)) + return censored_token + + +def decode_jwt(jwt_token): + try: + import base64 + import json + sub_tokens = jwt_token.split('.') + if len(sub_tokens)<2: + logger.error("JWT format is wrong") + return {} + token_of_interest = sub_tokens[1] + padded_token = token_of_interest + "="*divmod(len(token_of_interest),4)[1] + decoded_token = base64.urlsafe_b64decode(padded_token.encode('utf-8')) + json_token = json.loads(decoded_token) + return json_token + except Exception as error: + logger.error("Could not decode JWT token ({})".format(error)) + return {} + + +def get_kernel_external_ip(): + try: + import requests + response = requests.get("https://api64.ipify.org?format=json") + if response.status_code >= 400: + logger.error("Error {} trying to check kernel's external ip:{}".format(response.status_code, response.content)) + json_response = response.json() + kernel_external_ip = json_response.get("ip", "") + return kernel_external_ip + except Exception as error: + logger.error("Could not fetch kernel's remote ip ({})".format(error)) + return "" + + class ItemsLimit(): def __init__(self, records_limit=-1): self.has_no_limit = (records_limit == -1) From 93803a66a4e4d192dc1aa0c10f0270ede32c5d38 Mon Sep 17 00:00:00 2001 From: Alexandre Bourret Date: Fri, 31 May 2024 14:11:26 +0200 Subject: [PATCH 2/9] confirm ips match --- python-lib/common.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/python-lib/common.py b/python-lib/common.py index fa6df8c..2bb47b5 100644 --- a/python-lib/common.py +++ b/python-lib/common.py @@ -139,6 +139,8 @@ def run_oauth_diagnosis(jwt_token): ip_in_jwt, kernel_external_ip )) + else: + logger.info("IP addresses in the OAuath token and the plugin kernel match") def diagnose_jwt(jwt_token): From 995202088ba55e6a676352b3e67963667dc74031 Mon Sep 17 00:00:00 2001 From: Alexandre Bourret Date: Fri, 31 May 2024 14:12:02 +0200 Subject: [PATCH 3/9] running diag on jwt on 403 errors with oauth settings --- python-lib/sharepoint_client.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/python-lib/sharepoint_client.py b/python-lib/sharepoint_client.py index 3c92c50..80d744f 100644 --- a/python-lib/sharepoint_client.py +++ b/python-lib/sharepoint_client.py @@ -17,7 +17,7 @@ from common import ( is_email_address, get_value_from_path, parse_url, get_value_from_paths, is_request_performed, ItemsLimit, - is_empty_path, merge_paths + is_empty_path, merge_paths, run_oauth_diagnosis ) from safe_logger import SafeLogger @@ -56,6 +56,8 @@ def __init__(self, config): self.apply_paths_overwrite(config) self.setup_sharepoint_online_url(login_details) self.sharepoint_access_token = login_details['sharepoint_oauth'] + self.auth_token_for_diag =self.sharepoint_access_token + self.jwt_diag_done = False self.session.update_settings(session=SharePointSession( None, None, @@ -740,6 +742,7 @@ def assert_response_ok(self, response, no_json=False, calling_method=""): logger.error("403 error. Checking for federated namespace.") self.assert_non_federated_namespace() logger.error("User does not belong to federated namespace.") + self.assert_valid_jwt_token() raise SharePointClientError("403 Forbidden. Please check your account credentials. ({})".format(calling_method)) raise SharePointClientError("Error {} ({})".format(status_code, calling_method)) if not no_json: @@ -769,6 +772,11 @@ def assert_non_federated_namespace(self): + "Please contact your administrator to configure a Single Sign On or an App token access." ) + def assert_valid_jwt_token(self): + if self.auth_token_for_diag and not self.jwt_diag_done: + self.jwt_diag_done = True + run_oauth_diagnosis(self.auth_token_for_diag) + @staticmethod def get_enriched_error_message(response): try: From 395ae3dede4e0449da95ae479ea3cdfa8cb85d51 Mon Sep 17 00:00:00 2001 From: Alexandre Bourret Date: Mon, 3 Jun 2024 10:39:46 +0200 Subject: [PATCH 4/9] typos --- python-lib/common.py | 4 ++-- python-lib/sharepoint_client.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/python-lib/common.py b/python-lib/common.py index 2bb47b5..da4f77e 100644 --- a/python-lib/common.py +++ b/python-lib/common.py @@ -130,7 +130,7 @@ def update_dict_in_kwargs(kwargs, key_to_update, update): return kwargs -def run_oauth_diagnosis(jwt_token): +def run_oauth_diagnostic(jwt_token): censored_token = diagnose_jwt(jwt_token) kernel_external_ip = get_kernel_external_ip() ip_in_jwt = censored_token.get("ipaddr", "") @@ -140,7 +140,7 @@ def run_oauth_diagnosis(jwt_token): kernel_external_ip )) else: - logger.info("IP addresses in the OAuath token and the plugin kernel match") + logger.info("IP addresses in the OAuth token and the plugin kernel match") def diagnose_jwt(jwt_token): diff --git a/python-lib/sharepoint_client.py b/python-lib/sharepoint_client.py index 80d744f..8202d5f 100644 --- a/python-lib/sharepoint_client.py +++ b/python-lib/sharepoint_client.py @@ -17,7 +17,7 @@ from common import ( is_email_address, get_value_from_path, parse_url, get_value_from_paths, is_request_performed, ItemsLimit, - is_empty_path, merge_paths, run_oauth_diagnosis + is_empty_path, merge_paths, run_oauth_diagnostic ) from safe_logger import SafeLogger @@ -775,7 +775,7 @@ def assert_non_federated_namespace(self): def assert_valid_jwt_token(self): if self.auth_token_for_diag and not self.jwt_diag_done: self.jwt_diag_done = True - run_oauth_diagnosis(self.auth_token_for_diag) + run_oauth_diagnostic(self.auth_token_for_diag) @staticmethod def get_enriched_error_message(response): From 668107f176d11453a34475144715167aefa97c54 Mon Sep 17 00:00:00 2001 From: Alexandre Bourret Date: Mon, 10 Jun 2024 17:22:41 +0200 Subject: [PATCH 5/9] looking for actual ip address only if ip is in the jwt token --- python-lib/common.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/python-lib/common.py b/python-lib/common.py index f183a4c..abec4cb 100644 --- a/python-lib/common.py +++ b/python-lib/common.py @@ -155,19 +155,22 @@ def update_dict_in_kwargs(kwargs, key_to_update, update): def run_oauth_diagnostic(jwt_token): censored_token = diagnose_jwt(jwt_token) - kernel_external_ip = get_kernel_external_ip() + ip_in_jwt = censored_token.get("ipaddr", "") - if ip_in_jwt != kernel_external_ip: - logger.error("The plugin external IP address ({}) does not match the IP allowed in the SSO token ({})".format( - ip_in_jwt, - kernel_external_ip - )) + if not ip_in_jwt: + logger.info("No IP address in the JWT token") else: - logger.info("IP addresses in the OAuth token and the plugin kernel match") + kernel_external_ip = get_kernel_external_ip() + if not kernel_external_ip: + return + if ip_in_jwt != kernel_external_ip: + logger.error("The plugin external IP address does not match the IP allowed in the JWT token") + else: + logger.info("IP addresses in the OAuth token and the plugin kernel match") def diagnose_jwt(jwt_token): - keys_to_report = ["aud", "exp", "app_displayname", "appid", "ipaddr", "name", "scp", "unique_name", "upn"] + keys_to_report = ["aud", "exp", "app_displayname", "appid", "ipaddr", "name", "scp", "unique_name", "upn", "roles"] decoded_token = decode_jwt(jwt_token) censored_token = {} for key_to_report in keys_to_report: From 7e24b8c70c2c00703146c66a3d0cbdbd6d5d2e2f Mon Sep 17 00:00:00 2001 From: Alexandre Bourret Date: Mon, 10 Jun 2024 17:23:21 +0200 Subject: [PATCH 6/9] v1.1.4 --- plugin.json | 2 +- python-lib/dss_constants.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/plugin.json b/plugin.json index ffead09..c357c1b 100644 --- a/plugin.json +++ b/plugin.json @@ -1,6 +1,6 @@ { "id": "sharepoint-online", - "version": "1.1.3", + "version": "1.1.4", "meta": { "label": "SharePoint Online", "description": "Read and write data from/to your SharePoint Online account", diff --git a/python-lib/dss_constants.py b/python-lib/dss_constants.py index d6dc8b8..89fedcc 100644 --- a/python-lib/dss_constants.py +++ b/python-lib/dss_constants.py @@ -37,7 +37,7 @@ class DSSConstants(object): "sharepoint_oauth": "The access token is missing" } PATH = 'path' - PLUGIN_VERSION = "1.1.3" + PLUGIN_VERSION = "1.1.4" SECRET_PARAMETERS_KEYS = ["Authorization", "sharepoint_username", "sharepoint_password", "client_secret", "client_certificate", "passphrase"] SITE_APP_DETAILS = { "sharepoint_tenant": "The tenant name is missing", From b3feba6b66e2d6fe7956468e266ad7f4456154e4 Mon Sep 17 00:00:00 2001 From: Alexandre Bourret Date: Mon, 10 Jun 2024 17:23:35 +0200 Subject: [PATCH 7/9] Changelog update --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 43853aa..53ca43a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## [Version 1.1.4](https://github.com/dataiku/dss-plugin-sharepoint-online/releases/tag/v1.1.4) - Feature release - 2024-06-10 + +- Add JWT token diag in case of authorization error + ## [Version 1.1.3](https://github.com/dataiku/dss-plugin-sharepoint-online/releases/tag/v1.1.3) - Feature release - 2024-06-04 - Add login with Azure AD app certificate From 10c3e7ef3f261c85cd09289e86300f1fcac5119e Mon Sep 17 00:00:00 2001 From: Alexandre Bourret Date: Mon, 24 Jun 2024 15:24:03 +0200 Subject: [PATCH 8/9] comments --- python-lib/common.py | 5 ++++- python-lib/sharepoint_client.py | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/python-lib/common.py b/python-lib/common.py index abec4cb..1fc2c00 100644 --- a/python-lib/common.py +++ b/python-lib/common.py @@ -155,11 +155,12 @@ def update_dict_in_kwargs(kwargs, key_to_update, update): def run_oauth_diagnostic(jwt_token): censored_token = diagnose_jwt(jwt_token) - ip_in_jwt = censored_token.get("ipaddr", "") if not ip_in_jwt: logger.info("No IP address in the JWT token") else: + # Retrieve the plugin's external IP to check it matches the one + # stored in the JWT token kernel_external_ip = get_kernel_external_ip() if not kernel_external_ip: return @@ -170,6 +171,8 @@ def run_oauth_diagnostic(jwt_token): def diagnose_jwt(jwt_token): + # Display ebough details about the JWT token to allow debugging + # without making it possible to reuse it keys_to_report = ["aud", "exp", "app_displayname", "appid", "ipaddr", "name", "scp", "unique_name", "upn", "roles"] decoded_token = decode_jwt(jwt_token) censored_token = {} diff --git a/python-lib/sharepoint_client.py b/python-lib/sharepoint_client.py index 0d8dbeb..81cb5ea 100644 --- a/python-lib/sharepoint_client.py +++ b/python-lib/sharepoint_client.py @@ -816,6 +816,7 @@ def assert_non_federated_namespace(self): ) def run_jwt_validity_test(self): + # Called following 403 error if self.auth_token_for_diag and not self.jwt_diag_done: self.jwt_diag_done = True run_oauth_diagnostic(self.auth_token_for_diag) From 65569d7749599efee5daadaff76fcfb99b4f407f Mon Sep 17 00:00:00 2001 From: Alexandre Bourret Date: Fri, 28 Jun 2024 14:44:07 +0200 Subject: [PATCH 9/9] moving logs to more relevant place --- python-lib/sharepoint_client.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/python-lib/sharepoint_client.py b/python-lib/sharepoint_client.py index 81cb5ea..cc497bb 100644 --- a/python-lib/sharepoint_client.py +++ b/python-lib/sharepoint_client.py @@ -782,9 +782,7 @@ def assert_response_ok(self, response, no_json=False, calling_method=""): if status_code == 404: raise SharePointClientError("Not found. Please check tenant, site type or site name. ({})".format(calling_method)) if status_code == 403: - logger.error("403 error. Checking for federated namespace.") self.assert_non_federated_namespace() - logger.error("User does not belong to federated namespace.") self.run_jwt_validity_test() raise SharePointClientError("403 Forbidden. Please check your account credentials. ({})".format(calling_method)) raise SharePointClientError("Error {} ({})".format(status_code, calling_method)) @@ -794,6 +792,7 @@ def assert_response_ok(self, response, no_json=False, calling_method=""): def assert_non_federated_namespace(self): # Called following 403 error if self.username_for_namespace_diag: + logger.error("403 error. Checking for federated namespace.") # username / password login was used to login # we check if the email used as username belongs to a federated namespace json_response = "" @@ -814,11 +813,13 @@ def assert_non_federated_namespace(self): + "Dataiku might not be able to use it to access SharePoint-Online. " + "Please contact your administrator to configure a Single Sign On or an App token access." ) + logger.error("User does not belong to federated namespace.") def run_jwt_validity_test(self): # Called following 403 error if self.auth_token_for_diag and not self.jwt_diag_done: self.jwt_diag_done = True + logger.info("403 Error. Running diag on auth token") run_oauth_diagnostic(self.auth_token_for_diag) @staticmethod