From 0d51df5933dd569e86e436ab15c607640f0d31c9 Mon Sep 17 00:00:00 2001 From: Alex Bourret Date: Wed, 8 Apr 2026 13:36:59 +0200 Subject: [PATCH 1/9] Adding access token refresh mechanism --- python-lib/sharepoint_client.py | 26 ++++++----- python-lib/sharepoint_fresh_token.py | 64 ++++++++++++++++++++++++++++ 2 files changed, 76 insertions(+), 14 deletions(-) create mode 100644 python-lib/sharepoint_fresh_token.py diff --git a/python-lib/sharepoint_client.py b/python-lib/sharepoint_client.py index 898dbf3..8df22b3 100644 --- a/python-lib/sharepoint_client.py +++ b/python-lib/sharepoint_client.py @@ -21,6 +21,7 @@ format_private_key, format_certificate_thumbprint, url_encode ) from safe_logger import SafeLogger +from sharepoint_fresh_token import FreshToken logger = SafeLogger("sharepoint-online plugin", DSSConstants.SECRET_PARAMETERS_KEYS) @@ -64,7 +65,7 @@ def __init__(self, config, root_name_overwrite_legacy_mode=False): None, self.sharepoint_url, self.sharepoint_site, - sharepoint_access_token=self.sharepoint_access_token + access_token_getter=FreshToken(self.sharepoint_access_token) ), max_retries=SharePointConstants.MAX_RETRIES, base_retry_timer_sec=SharePointConstants.WAIT_TIME_BEFORE_RETRY_SEC @@ -103,13 +104,12 @@ def __init__(self, config, root_name_overwrite_legacy_mode=False): self.tenant_id = login_details.get("tenant_id") self.client_secret = login_details.get("client_secret") self.client_id = login_details.get("client_id") - self.sharepoint_access_token = self.get_site_app_access_token() self.session.update_settings(session=SharePointSession( None, None, self.sharepoint_url, self.sharepoint_site, - sharepoint_access_token=self.sharepoint_access_token + access_token_getter=FreshToken(self.get_site_app_access_token()) ), max_retries=SharePointConstants.MAX_RETRIES, base_retry_timer_sec=SharePointConstants.WAIT_TIME_BEFORE_RETRY_SEC @@ -126,13 +126,12 @@ def __init__(self, config, root_name_overwrite_legacy_mode=False): self.client_certificate_thumbprint = format_certificate_thumbprint(login_details.get("client_certificate_thumbprint")) self.passphrase = login_details.get("passphrase") self.client_id = login_details.get("client_id") - self.sharepoint_access_token = self.get_certificate_app_access_token() self.session.update_settings(session=SharePointSession( None, None, self.sharepoint_url, self.sharepoint_site, - sharepoint_access_token=self.sharepoint_access_token + access_token_getter=FreshToken(self.get_certificate_app_access_token) ), max_retries=SharePointConstants.MAX_RETRIES, base_retry_timer_sec=SharePointConstants.WAIT_TIME_BEFORE_RETRY_SEC @@ -148,13 +147,12 @@ def __init__(self, config, root_name_overwrite_legacy_mode=False): self.sharepoint_tenant = login_details.get("sharepoint_tenant") username = login_details.get("username") password = login_details.get("password") - self.sharepoint_access_token = self.get_username_password_access_token(username, password) self.session.update_settings(session=SharePointSession( None, None, self.sharepoint_url, self.sharepoint_site, - sharepoint_access_token=self.sharepoint_access_token + access_token_getter=FreshToken(self.get_username_password_access_token(username, password)) ), max_retries=SharePointConstants.MAX_RETRIES, base_retry_timer_sec=SharePointConstants.WAIT_TIME_BEFORE_RETRY_SEC @@ -1098,12 +1096,12 @@ def is_column_displayable(self, column, display_metadata=False, metadata_to_retr class SharePointSession(): - def __init__(self, sharepoint_user_name, sharepoint_password, sharepoint_url, sharepoint_site, sharepoint_access_token=None, max_retry=10): + def __init__(self, sharepoint_user_name, sharepoint_password, sharepoint_url, sharepoint_site, access_token_getter=None, max_retry=10): self.sharepoint_url = sharepoint_url self.sharepoint_site = sharepoint_site - self.sharepoint_access_token = sharepoint_access_token + self.access_token_getter = access_token_getter requests.adapters.DEFAULT_RETRIES = max_retry - self.form_digest_value = get_form_digest_value(sharepoint_url, sharepoint_site, sharepoint_access_token=self.sharepoint_access_token) + self.form_digest_value = get_form_digest_value(sharepoint_url, sharepoint_site, access_token_getter=self.access_token_getter) def get(self, url, headers=None, params=None): retries_limit = ItemsLimit(SharePointConstants.MAX_RETRIES) @@ -1152,10 +1150,10 @@ def close(): logger.info("Closing SharePointSession.") def get_authorization_bearer(self): - return "Bearer {}".format(self.sharepoint_access_token) + return "Bearer {}".format(self.access_token_getter.access_token) -def get_form_digest_value(sharepoint_url, sharepoint_site, session=None, sharepoint_access_token=None): +def get_form_digest_value(sharepoint_url, sharepoint_site, session=None, access_token_getter=None): def get_contextinfo_url(): return "https://{}/{}/_api/contextinfo".format( sharepoint_url, sharepoint_site @@ -1170,8 +1168,8 @@ def get_contextinfo_url(): ) form_digest_value = None try: - if sharepoint_access_token: - headers = {**DSSConstants.JSON_HEADERS, **{"Authorization": "Bearer {}".format(sharepoint_access_token)}} + if access_token_getter: + headers = {**DSSConstants.JSON_HEADERS, **{"Authorization": "Bearer {}".format(access_token_getter.access_token)}} response = session.post( url=get_contextinfo_url(), headers=headers, diff --git a/python-lib/sharepoint_fresh_token.py b/python-lib/sharepoint_fresh_token.py new file mode 100644 index 0000000..f897b5b --- /dev/null +++ b/python-lib/sharepoint_fresh_token.py @@ -0,0 +1,64 @@ +from safe_logger import SafeLogger +from dss_constants import DSSConstants +import time + +logger = SafeLogger("sharepoint-online plugin FreshToken", DSSConstants.SECRET_PARAMETERS_KEYS) +TOKEN_VALIDITY_SAFETY_MARGIN_SECONDS = 60 + + +class FreshToken(): + def __init__(self, token_refresh_method): + logger.info("FreshToken init") + if isinstance(token_refresh_method, str): + logger.info("No refresh method available") + self.current_token = token_refresh_method + self.token_refresh_method = self._default_refresh_method + self.token_validity = None + else: + logger.info("Using refresh method") + self.token_refresh_method = token_refresh_method + self.refresh_token() + + def _default_refresh_method(self): + return self.current_token + + def is_token_still_valid(self): + if self.token_validity is None: + return True + epoch_time_now = int(time.time()) + if (epoch_time_now > self.token_validity): + return False + return True + + def refresh_token(self): + self.current_token = self.token_refresh_method() + decoded_jwt = decode_jwt(self.current_token) + self.token_validity = decoded_jwt.get("exp", None) + if isinstance(self.token_validity, int): + self.token_validity = self.token_validity - TOKEN_VALIDITY_SAFETY_MARGIN_SECONDS + logger.info("The token is valid until {}".format(self.token_validity)) + + @property + def access_token(self): + if not self.is_token_still_valid(): + logger.info("Token reaching its time limit, refreshing it...") + self.refresh_token() + return self.current_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 {} From 5b10f73102308dc3426e9b9536db21e3ebf7c5ce Mon Sep 17 00:00:00 2001 From: Alex Bourret Date: Wed, 8 Apr 2026 15:07:59 +0200 Subject: [PATCH 2/9] use updatable access token for SSO with DSS 14.5+ --- python-lib/sharepoint_client.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/python-lib/sharepoint_client.py b/python-lib/sharepoint_client.py index 8df22b3..8eb5e27 100644 --- a/python-lib/sharepoint_client.py +++ b/python-lib/sharepoint_client.py @@ -60,12 +60,19 @@ def __init__(self, config, root_name_overwrite_legacy_mode=False): self.apply_paths_overwrite(config) self.setup_sharepoint_online_url(login_details) self.sharepoint_access_token = login_details['sharepoint_oauth'] + if "__credentials" in login_details: + logger.info("Refreshable access token") + from dataiku.core import plugin + access_token = plugin.OAuthCredentials(login_details.get("__credentials", {}).get("sharepoint_oauth")) + else: + logger.info("One time access token") + access_token = FreshToken(self.sharepoint_access_token) self.session.update_settings(session=SharePointSession( None, None, self.sharepoint_url, self.sharepoint_site, - access_token_getter=FreshToken(self.sharepoint_access_token) + access_token_getter=access_token ), max_retries=SharePointConstants.MAX_RETRIES, base_retry_timer_sec=SharePointConstants.WAIT_TIME_BEFORE_RETRY_SEC From 21eb731e10c22620068f55ecfa5e3f943e76edb2 Mon Sep 17 00:00:00 2001 From: Alex Bourret Date: Wed, 8 Apr 2026 17:02:15 +0200 Subject: [PATCH 3/9] v1.4.0 --- 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 f41d293..c39a192 100644 --- a/plugin.json +++ b/plugin.json @@ -1,6 +1,6 @@ { "id": "sharepoint-online", - "version": "1.3.1", + "version": "1.4.0", "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 0b60de9..f981fd0 100644 --- a/python-lib/dss_constants.py +++ b/python-lib/dss_constants.py @@ -38,7 +38,7 @@ class DSSConstants(object): "sharepoint_oauth": "The access token is missing" } PATH = 'path' - PLUGIN_VERSION = "1.3.1" + PLUGIN_VERSION = "1.4.0" SECRET_PARAMETERS_KEYS = ["Authorization", "sharepoint_username", "sharepoint_password", "client_secret", "client_certificate", "passphrase"] SITE_APP_DETAILS = { "sharepoint_tenant": "The tenant name is missing", From acbc12ad4671a69f21fafef77d3c79713c9bcd55 Mon Sep 17 00:00:00 2001 From: Alex Bourret Date: Wed, 8 Apr 2026 17:02:22 +0200 Subject: [PATCH 4/9] update changelog --- CHANGELOG.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f41864a..4022fc2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,10 @@ # Changelog -## [Version 1.3.1](https://github.com/dataiku/dss-plugin-sharepoint-online/releases/tag/v1.3.1) - Security release - 2026-04-08 +## [Version 1.4.0](https://github.com/dataiku/dss-plugin-sharepoint-online/releases/tag/v1.4.0) - Security and feature release - 2026-04-08 - Increase the version of the package cryptography to 46.0.7 in response to CVE-2026-34073 and CVE-2026-39892 +- Add access token refreshing for *Certificates* and *App username password* +- Add access token refreshing for *Azure Single Sign On* preset on DSS 14.5+ ## [Version 1.3.0](https://github.com/dataiku/dss-plugin-sharepoint-online/releases/tag/v1.3.0) - Security release - 2026-02-26 From dad8d8a92093759a08c291f640a964e7fddf8455 Mon Sep 17 00:00:00 2001 From: Alex Bourret Date: Wed, 6 May 2026 18:15:07 +0200 Subject: [PATCH 5/9] renaming --- python-lib/sharepoint_client.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/python-lib/sharepoint_client.py b/python-lib/sharepoint_client.py index 8eb5e27..a176609 100644 --- a/python-lib/sharepoint_client.py +++ b/python-lib/sharepoint_client.py @@ -63,16 +63,16 @@ def __init__(self, config, root_name_overwrite_legacy_mode=False): if "__credentials" in login_details: logger.info("Refreshable access token") from dataiku.core import plugin - access_token = plugin.OAuthCredentials(login_details.get("__credentials", {}).get("sharepoint_oauth")) + access_token_getter = plugin.OAuthCredentials(login_details.get("__credentials", {}).get("sharepoint_oauth")) else: logger.info("One time access token") - access_token = FreshToken(self.sharepoint_access_token) + access_token_getter = FreshToken(self.sharepoint_access_token) self.session.update_settings(session=SharePointSession( None, None, self.sharepoint_url, self.sharepoint_site, - access_token_getter=access_token + access_token_getter=access_token_getter ), max_retries=SharePointConstants.MAX_RETRIES, base_retry_timer_sec=SharePointConstants.WAIT_TIME_BEFORE_RETRY_SEC From 092cdbcccd525e5d938e2480f2aa3c3535ea26c9 Mon Sep 17 00:00:00 2001 From: Alex Bourret Date: Wed, 6 May 2026 18:16:17 +0200 Subject: [PATCH 6/9] renaming + bug fix --- python-lib/sharepoint_fresh_token.py | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/python-lib/sharepoint_fresh_token.py b/python-lib/sharepoint_fresh_token.py index f897b5b..acdc846 100644 --- a/python-lib/sharepoint_fresh_token.py +++ b/python-lib/sharepoint_fresh_token.py @@ -13,7 +13,7 @@ def __init__(self, token_refresh_method): logger.info("No refresh method available") self.current_token = token_refresh_method self.token_refresh_method = self._default_refresh_method - self.token_validity = None + self.token_renewal_time = None else: logger.info("Using refresh method") self.token_refresh_method = token_refresh_method @@ -22,25 +22,23 @@ def __init__(self, token_refresh_method): def _default_refresh_method(self): return self.current_token - def is_token_still_valid(self): - if self.token_validity is None: + def token_needs_renewal(self): + if self.token_renewal_time is None: return True epoch_time_now = int(time.time()) - if (epoch_time_now > self.token_validity): - return False - return True + return self.token_renewal_time <= epoch_time_now def refresh_token(self): self.current_token = self.token_refresh_method() decoded_jwt = decode_jwt(self.current_token) - self.token_validity = decoded_jwt.get("exp", None) - if isinstance(self.token_validity, int): - self.token_validity = self.token_validity - TOKEN_VALIDITY_SAFETY_MARGIN_SECONDS - logger.info("The token is valid until {}".format(self.token_validity)) + self.token_renewal_time = decoded_jwt.get("exp", None) + if isinstance(self.token_renewal_time, int): + self.token_renewal_time = self.token_renewal_time - TOKEN_VALIDITY_SAFETY_MARGIN_SECONDS + logger.info("The token is valid until {}".format(self.token_renewal_time)) @property def access_token(self): - if not self.is_token_still_valid(): + if not self.token_needs_renewal(): logger.info("Token reaching its time limit, refreshing it...") self.refresh_token() return self.current_token @@ -54,8 +52,8 @@ def decode_jwt(jwt_token): 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] + token_payload = sub_tokens[1] + padded_token = token_payload + "=" * (-len(token_payload) % 4) decoded_token = base64.urlsafe_b64decode(padded_token.encode('utf-8')) json_token = json.loads(decoded_token) return json_token From fc65a2ccc764b3043019db300912f4aa44f36d35 Mon Sep 17 00:00:00 2001 From: Alex Bourret Date: Wed, 6 May 2026 18:16:32 +0200 Subject: [PATCH 7/9] unit test for jwt decoding --- tests/python/unit/test_common.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/python/unit/test_common.py b/tests/python/unit/test_common.py index aa6d87a..2a1c091 100644 --- a/tests/python/unit/test_common.py +++ b/tests/python/unit/test_common.py @@ -1,4 +1,5 @@ from common import get_value_from_path, is_request_performed, decode_retry_after_header +from sharepoint_fresh_token import decode_jwt from sharepoint_constants import SharePointConstants import pytest @@ -32,6 +33,7 @@ def setup_class(self): self.mock_response_http_429_date_in_past = MockResponse(429, {"Retry-After": "Wed, 21 Oct 2015 07:28:00 GMT"}) self.mock_response_http_429_date_in_future = MockResponse(429, {"Retry-After": "Wed, 21 Oct 9999 07:28:00 GMT"}) self.mock_response_http_429_garbage = MockResponse(429, {"Retry-After": "blablablabla"}) + self.mock_jwt_token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.KMUFsIDTnFmyG3nMiGM6H9FNFUROf3wh7SmqJp-QV30" def test_get_value_from_path_long_path(self): key = get_value_from_path(self.dictionary_to_search, self.ok_path_1) @@ -85,3 +87,7 @@ def test_decode_retry_after_header_garbage(self): def test_decode_retry_after_header_no_header(self): seconds_before_retry = decode_retry_after_header(self.mock_response_http_429_no_header) assert seconds_before_retry == SharePointConstants.DEFAULT_WAIT_BEFORE_RETRY + + def test_decode_jwt(self): + decoded_jwt_token = decode_jwt(self.mock_jwt_token) + assert decoded_jwt_token == {'sub': '1234567890', 'name': 'John Doe', 'admin': True, 'iat': 1516239022} From 780e2d43bc2c677be3c1c6ea23ac3bea0a78b55d Mon Sep 17 00:00:00 2001 From: Alex Bourret Date: Thu, 7 May 2026 11:48:35 +0200 Subject: [PATCH 8/9] two parameters for FreshToken --- python-lib/sharepoint_client.py | 8 ++++---- python-lib/sharepoint_fresh_token.py | 10 +++++----- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/python-lib/sharepoint_client.py b/python-lib/sharepoint_client.py index a176609..0bb10e5 100644 --- a/python-lib/sharepoint_client.py +++ b/python-lib/sharepoint_client.py @@ -66,7 +66,7 @@ def __init__(self, config, root_name_overwrite_legacy_mode=False): access_token_getter = plugin.OAuthCredentials(login_details.get("__credentials", {}).get("sharepoint_oauth")) else: logger.info("One time access token") - access_token_getter = FreshToken(self.sharepoint_access_token) + access_token_getter = FreshToken(access_token=self.sharepoint_access_token) self.session.update_settings(session=SharePointSession( None, None, @@ -116,7 +116,7 @@ def __init__(self, config, root_name_overwrite_legacy_mode=False): None, self.sharepoint_url, self.sharepoint_site, - access_token_getter=FreshToken(self.get_site_app_access_token()) + access_token_getter=FreshToken(access_token=self.get_site_app_access_token()) ), max_retries=SharePointConstants.MAX_RETRIES, base_retry_timer_sec=SharePointConstants.WAIT_TIME_BEFORE_RETRY_SEC @@ -138,7 +138,7 @@ def __init__(self, config, root_name_overwrite_legacy_mode=False): None, self.sharepoint_url, self.sharepoint_site, - access_token_getter=FreshToken(self.get_certificate_app_access_token) + access_token_getter=FreshToken(token_refresh_method=self.get_certificate_app_access_token) ), max_retries=SharePointConstants.MAX_RETRIES, base_retry_timer_sec=SharePointConstants.WAIT_TIME_BEFORE_RETRY_SEC @@ -159,7 +159,7 @@ def __init__(self, config, root_name_overwrite_legacy_mode=False): None, self.sharepoint_url, self.sharepoint_site, - access_token_getter=FreshToken(self.get_username_password_access_token(username, password)) + access_token_getter=FreshToken(access_token=self.get_username_password_access_token(username, password)) ), max_retries=SharePointConstants.MAX_RETRIES, base_retry_timer_sec=SharePointConstants.WAIT_TIME_BEFORE_RETRY_SEC diff --git a/python-lib/sharepoint_fresh_token.py b/python-lib/sharepoint_fresh_token.py index acdc846..1905049 100644 --- a/python-lib/sharepoint_fresh_token.py +++ b/python-lib/sharepoint_fresh_token.py @@ -7,14 +7,14 @@ class FreshToken(): - def __init__(self, token_refresh_method): + def __init__(self, token_refresh_method=None, access_token=None): logger.info("FreshToken init") - if isinstance(token_refresh_method, str): - logger.info("No refresh method available") - self.current_token = token_refresh_method + if access_token: + logger.info("Permanent access token provided") + self.current_token = access_token self.token_refresh_method = self._default_refresh_method self.token_renewal_time = None - else: + if token_refresh_method is not None: logger.info("Using refresh method") self.token_refresh_method = token_refresh_method self.refresh_token() From 7ab7fd11b47138b33f53212d20db847828e5ad56 Mon Sep 17 00:00:00 2001 From: Alex Bourret Date: Tue, 9 Jun 2026 10:13:21 +0200 Subject: [PATCH 9/9] small refacto --- python-lib/sharepoint_client.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/python-lib/sharepoint_client.py b/python-lib/sharepoint_client.py index 0bb10e5..2b797b0 100644 --- a/python-lib/sharepoint_client.py +++ b/python-lib/sharepoint_client.py @@ -59,14 +59,14 @@ def __init__(self, config, root_name_overwrite_legacy_mode=False): self.setup_login_details(login_details) self.apply_paths_overwrite(config) self.setup_sharepoint_online_url(login_details) - self.sharepoint_access_token = login_details['sharepoint_oauth'] + sharepoint_access_token = login_details['sharepoint_oauth'] if "__credentials" in login_details: logger.info("Refreshable access token") from dataiku.core import plugin access_token_getter = plugin.OAuthCredentials(login_details.get("__credentials", {}).get("sharepoint_oauth")) else: logger.info("One time access token") - access_token_getter = FreshToken(access_token=self.sharepoint_access_token) + access_token_getter = FreshToken(access_token=sharepoint_access_token) self.session.update_settings(session=SharePointSession( None, None,