From 891f0676d44efaa2665b5ad53f388bde0210afc0 Mon Sep 17 00:00:00 2001 From: Kai Amundsen Date: Thu, 28 May 2026 15:34:42 -0500 Subject: [PATCH 1/3] Add PKCE support --- config/settings/base.py | 1 + 1 file changed, 1 insertion(+) diff --git a/config/settings/base.py b/config/settings/base.py index deaf164f8..7faad72f9 100644 --- a/config/settings/base.py +++ b/config/settings/base.py @@ -360,6 +360,7 @@ def safe_key() -> str: SOCIALACCOUNT_PROVIDERS = { "salesforce": { "SCOPE": ["web", "full", "refresh_token"], + "OAUTH_PKCE_ENABLED": True, "APP": { "client_id": SFDX_CLIENT_ID, "secret": SFDX_CLIENT_SECRET, From aadd138fce6327f91defe19e9b8c1216299cef7c Mon Sep 17 00:00:00 2001 From: Kai Amundsen Date: Fri, 29 May 2026 09:24:29 -0500 Subject: [PATCH 2/3] Save/update refresh token for RTR --- metadeploy/api/jobs.py | 39 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/metadeploy/api/jobs.py b/metadeploy/api/jobs.py index c96a0362f..90c702ee7 100644 --- a/metadeploy/api/jobs.py +++ b/metadeploy/api/jobs.py @@ -30,6 +30,7 @@ from django_rq import job as django_rq_job from rq.exceptions import ShutDownImminentException from rq.worker import StopRequested +from sfdo_template_helpers.crypto import fernet_decrypt, fernet_encrypt from .cci_configs import MetaDeployCCI, extract_user_and_repo from .cleanup import cleanup_user_data @@ -177,6 +178,34 @@ def prepend_python_path(path): sys.path = prev_path +def _save_updated_user_tokens(user, org_config): + """Write any rotated OAuth tokens back to the user's SocialToken record. + + Salesforce uses refresh token rotation: each time the refresh token is used, + a new one is returned that must be stored for the next refresh. Without this, + the second job run for a user will fail with invalid_grant / expired token. + """ + social_token = user.social_account.socialtoken_set.first() + if not social_token: + return + + changed = False + new_access_token = org_config.access_token + new_refresh_token = org_config.refresh_token + + if new_access_token and new_access_token != fernet_decrypt(social_token.token): + social_token.token = fernet_encrypt(new_access_token) + changed = True + if new_refresh_token and new_refresh_token != fernet_decrypt( + social_token.token_secret + ): + social_token.token_secret = fernet_encrypt(new_refresh_token) + changed = True + + if changed: + social_token.save() + + def run_flows( *, plan: Plan, @@ -275,7 +304,15 @@ def run_flows( ] org = ctx.keychain.get_org(current_org) if not settings.METADEPLOY_FAST_FORWARD: - result.run(ctx, plan, steps, org) + try: + # Refresh the access token right before running so long-polling + # tasks (e.g. SetOrgWideDefaults) start with the freshest token. + if result.user: + org_config.refresh_oauth_token(ctx.keychain) + result.run(ctx, plan, steps, org) + finally: + if result.user: + _save_updated_user_tokens(result.user, org) run_flows_job = job(run_flows) From 3c7939f4e7e7b66ef863df87d718e4247c034099 Mon Sep 17 00:00:00 2001 From: Kai Amundsen Date: Mon, 1 Jun 2026 09:14:35 -0500 Subject: [PATCH 3/3] Bump CCI and Simple-Salesforce to support new requirements --- metadeploy/api/salesforce.py | 1 + requirements/prod.txt | 50 +++++++++++++++++++++++++----------- 2 files changed, 36 insertions(+), 15 deletions(-) diff --git a/metadeploy/api/salesforce.py b/metadeploy/api/salesforce.py index 89872dde9..8df23615d 100644 --- a/metadeploy/api/salesforce.py +++ b/metadeploy/api/salesforce.py @@ -109,6 +109,7 @@ def refresh_access_token( try: org_config = OrgConfig(config, org_name, keychain=keychain) org_config.refresh_oauth_token(keychain, is_sandbox=sbx_login) + config.update(org_config.config) return org_config except HTTPError as err: _handle_sf_error(err, scratch_org=scratch_org) diff --git a/requirements/prod.txt b/requirements/prod.txt index ae74f3bf2..ae54e7dc8 100644 --- a/requirements/prod.txt +++ b/requirements/prod.txt @@ -6,10 +6,10 @@ # aioredis==1.3.1 # via channels-redis +annotated-types==0.7.0 + # via pydantic ansi2html==1.9.1 # via -r requirements/prod.in -appdirs==1.4.4 - # via fs asgiref==3.7.2 # via # channels @@ -23,8 +23,7 @@ attrs==23.2.0 # automat # service-identity # twisted -authlib==1.2.1 - # via simple-salesforce + # zeep autobahn==23.6.2 # via daphne automat==22.10.0 @@ -53,7 +52,7 @@ channels-redis==3.4.1 # via -r requirements/prod.in charset-normalizer==3.2.0 # via requests -click==8.1.6 +click==8.3.3 # via # cumulusci # django-rq-scheduler @@ -69,14 +68,13 @@ crontab==1.0.1 # via rq-scheduler cryptography==41.0.7 # via - # authlib # autobahn # cumulusci # pyjwt # pyopenssl # service-identity # sfdo-template-helpers -cumulusci==4.0.1 +cumulusci==4.10.0 # via -r requirements/prod.in daphne==3.0.2 # via channels @@ -150,8 +148,6 @@ faker-nonprofit==1.0.0 # via snowfakery freezegun==1.4.0 # via rq-scheduler -fs==2.4.16 - # via cumulusci github3-py==4.0.1 # via # -r requirements/prod.in @@ -179,6 +175,8 @@ importlib-metadata==6.8.0 # via keyring incremental==22.10.0 # via twisted +isodate==0.7.2 + # via zeep jinja2==3.1.2 # via # cumulusci @@ -194,7 +192,9 @@ logfmt==0.4 # -r requirements/prod.in # sfdo-template-helpers lxml==4.9.3 - # via cumulusci + # via + # cumulusci + # zeep markdown==3.5.2 # via sfdo-template-helpers markdown-it-py==2.2.0 @@ -206,6 +206,8 @@ markupsafe==2.1.3 # werkzeug mdurl==0.1.2 # via markdown-it-py +more-itertools==11.0.2 + # via simple-salesforce msgpack==1.0.7 # via channels-redis natsort==8.4.0 @@ -213,11 +215,15 @@ natsort==8.4.0 oauthlib==3.2.2 # via requests-oauthlib packaging==23.2 - # via django-js-reverse + # via + # cumulusci + # django-js-reverse pillow==10.2.0 # via # -r requirements/prod.in # django-colorfield +platformdirs==4.9.6 + # via zeep psutil==5.9.6 # via cumulusci psycopg2-binary==2.9.9 @@ -230,10 +236,12 @@ pyasn1-modules==0.3.0 # via service-identity pycparser==2.21 # via cffi -pydantic==1.10.12 +pydantic==2.9.2 # via # cumulusci # snowfakery +pydantic-core==2.23.4 + # via pydantic pygments==2.17.2 # via rich pyjwt[crypto]==2.8.0 @@ -241,6 +249,7 @@ pyjwt[crypto]==2.8.0 # cumulusci # django-allauth # github3-py + # simple-salesforce pyopenssl==23.3.0 # via twisted python-baseconv==1.2.2 @@ -261,6 +270,7 @@ pytz==2023.3.post1 # via # cumulusci # djangorestframework + # zeep pyyaml==6.0.1 # via # cumulusci @@ -275,16 +285,23 @@ requests==2.29.0 # cumulusci # django-allauth # github3-py + # requests-file # requests-futures # requests-oauthlib + # requests-toolbelt # robotframework-requests # salesforce-bulk # simple-salesforce # snowfakery + # zeep +requests-file==3.0.1 + # via zeep requests-futures==1.0.1 # via cumulusci requests-oauthlib==1.3.1 # via django-allauth +requests-toolbelt==1.0.0 + # via zeep rich==13.9.4 # via cumulusci robotframework==6.1.1 @@ -332,7 +349,7 @@ service-identity==24.1.0 # twisted sfdo-template-helpers @ https://github.com/SFDO-Tooling/sfdo-template-helpers/archive/v0.23.0.tar.gz # via -r requirements/prod.in -simple-salesforce==1.11.4 +simple-salesforce==1.12.9 # via # cumulusci # salesforce-bulk @@ -340,10 +357,9 @@ six==1.16.0 # via # automat # bleach - # fs # python-dateutil # salesforce-bulk -snowfakery==4.0.0 +snowfakery==4.2.1 # via cumulusci sqlalchemy==1.4.49 # via @@ -358,6 +374,8 @@ txaio==23.1.1 typing-extensions==4.7.1 # via # pydantic + # pydantic-core + # simple-salesforce # twisted unicodecsv==0.14.1 # via salesforce-bulk @@ -377,6 +395,8 @@ whitenoise==6.6.0 # via -r requirements/prod.in xmltodict==0.13.0 # via cumulusci +zeep==4.3.2 + # via simple-salesforce zipp==3.17.0 # via importlib-metadata zope-interface==6.1