SG-43166 Integrate MEDM authentication into Toolkit bootstrap#1100
SG-43166 Integrate MEDM authentication into Toolkit bootstrap#1100stevelittlefish wants to merge 18 commits into
Conversation
Adds a Flow/MEDM authentication path that triggers proactively during
ToolkitManager bootstrap when the resolved project is "AM-ready" (project
field sg_flow_am_id is set). On the silent path (keyring or refresh
token), no UI surfaces; otherwise a browser PKCE flow opens before the
engine starts.
Sourced from the tk-framework-flowam PoC, which will be deleted in time.
* python/tank_vendor/adsk_auth/ vendored PKCE + keyring helper
(PyJWT + keyring as new third-party
deps, added to per-Python pkgs.zip
via requirements/<py>/requirements.txt)
* python/tank/authentication/flow_auth/
init_authentication(), get_access_token(), check_token_expiry()
FlowAuthSettings + resolve_flow_auth_settings() — defaults + env-var
overrides (TK_FLOW_AUTH_APPLICATION_ID/BASE_URL/CALLBACK_URL)
FlowAuthError / FlowAuthConfigurationError
AM_READY_PROJECT_FIELD (single point of truth — currently
sg_flow_am_id pending confirmation from Julien)
* python/tank/bootstrap/manager.py
_resolve_project_id() extracted from _get_configuration
_check_and_trigger_am_auth() new hook called right before
_get_updated_configuration returns. Configuration errors raise
TankBootstrapError; runtime auth failures are logged and swallowed
unless TK_FLOW_AUTH_REQUIRED=1.
Tests: 29 new (16 flow_auth unit + 13 bootstrap hook), all existing
bootstrap (103) and authentication (101) tests still pass.
FlowAuthenticationHandler from flowam is intentionally dropped — out
of scope for this ticket; tk-core only triggers auth here, it does not
own a GQL handler.
TODOs flagged in code for follow-up: confirm AM-ready field name and
real production APS values (application_id, base_url, callback_url) with
Julien Langlois before release. pkgs.zip regeneration is a release-time
step and not included in this commit.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…nto SG-43166-integrate-medm-auth
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
||
| """Settings resolver for Flow / MEDM authentication.""" | ||
|
|
||
| from __future__ import annotations |
There was a problem hiding this comment.
It will not import into python 3.10 without it, so we couldn't test inside Houdini.
- Add Sphinx-style type info to _resolve_project_id and _check_and_trigger_am_auth - Change _check_and_trigger_am_auth to accept entity instead of project_id, merging the ShotGrid AM-ready field fetch with project resolution to save one API round-trip (deep-field notation for the general entity case) - Drop the redundant _resolve_project_id call in _get_updated_configuration Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…sertion Regenerated pkgs.zip, frozen_requirements.txt, and certs for Python 3.7, 3.9, 3.10, 3.11, and 3.13. Also fixed update_python_packages.py to count .dist-info directories instead of package directories when asserting install completeness, since namespace packages (ruamel.yaml, jaraco.*) share parent directories and caused a false assertion failure. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## ticket/SG-43217/flow-data-sdk #1100 +/- ##
=================================================================
+ Coverage 79.51% 79.55% +0.03%
=================================================================
Files 198 204 +6
Lines 20842 20900 +58
=================================================================
+ Hits 16573 16627 +54
- Misses 4269 4273 +4
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
|
Haven't looked at the whole PR yet, but off the top of my head, I'm wondering if "flow_auth" would be a better name for the vendor package rather than "adsk_auth", because this is really specific to flow and not a general autodesk solution. |
| import json | ||
| import urllib.request | ||
| import urllib.error | ||
| import webbrowser |
There was a problem hiding this comment.
Why the empty line in the middle of imports?
And can we keep alphabetical order please?
There was a problem hiding this comment.
Julien, if you actually care about this, you should add isort to the pre-commit and also to the CI.
| ruamel.yaml==0.18.13 | ||
| ruamel.yaml.clib==0.2.8 | ||
| shotgun-api3==3.9.0 | ||
| typing-extensions==4.7.1 |
There was a problem hiding this comment.
I don't know it's a transitive dependency
| return FlowAuthSettings( | ||
| auth_application_id=os.environ.get( | ||
| "TK_FLOW_AUTH_APPLICATION_ID", DEFAULT_AUTH_APPLICATION_ID | ||
| ), | ||
| auth_base_url=os.environ.get("TK_FLOW_AUTH_BASE_URL", DEFAULT_AUTH_BASE_URL), | ||
| auth_callback_url=os.environ.get( | ||
| "TK_FLOW_AUTH_CALLBACK_URL", DEFAULT_AUTH_CALLBACK_URL | ||
| ), | ||
| ) |
There was a problem hiding this comment.
Is this new? I don't remember us having these settings in env vars before?
There was a problem hiding this comment.
They weren't in env vars before, they were set in the config.
We can change the way these are set later - this was just the simplest way to be able to override them to test auth against staging but have the prod values in the code.
It will be easier to fix this when we have a proper config set up and some code that uses all of this.
That's actually not true though! The reason authentication isn't in the flow data SDK is because it's not specific to Flow - it is the auth process for all Autodesk products! |
There was a problem hiding this comment.
Pull request overview
This PR adds a Flow/MEDM (AM) authentication pathway to tk-core, proactively triggered during ToolkitManager bootstrap when the resolved project is AM-ready (based on sg_flow_am_id). It also introduces a vendored adsk_auth PKCE + token persistence implementation and adds PyJWT as a dependency across supported Python versions.
Changes:
- Add
ToolkitManagerhelpers to resolve project id and trigger Flow/MEDM auth during bootstrap for AM-ready projects (with optional hard-fail viaTK_FLOW_AUTH_REQUIRED=1). - Introduce new
tank.authentication.flow_authmodule (settings resolution, token acquisition/refresh, Flow client factory, error types). - Vendor a minimal
tank_vendor.adsk_authPKCE + token file-store implementation and add PyJWT to per-Python requirements.
Reviewed changes
Copilot reviewed 28 out of 33 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/bootstrap_tests/test_manager_flow_auth.py | Adds bootstrap tests for AM-ready detection and auth triggering behavior. |
| tests/authentication_tests/test_flow_auth.py | Adds unit tests for flow_auth init/token helpers and Flow client factory. |
| tests/authentication_tests/test_adsk_auth_file_store.py | Adds tests for the vendored token JSON file store behavior and permissions. |
| requirements/update_python_packages.py | Adjusts dependency-count sanity check to use .dist-info directories. |
| requirements/3.7/requirements.txt | Adds PyJWT dependency for Python 3.7. |
| requirements/3.7/frozen_requirements.txt | Updates frozen dependency lock for Python 3.7 (incl. transitive deps). |
| requirements/3.9/requirements.txt | Adds PyJWT dependency for Python 3.9. |
| requirements/3.9/frozen_requirements.txt | Updates frozen dependency lock for Python 3.9. |
| requirements/3.10/requirements.txt | Adds PyJWT dependency for Python 3.10. |
| requirements/3.10/frozen_requirements.txt | Updates frozen dependency lock for Python 3.10. |
| requirements/3.11/requirements.txt | Adds PyJWT dependency for Python 3.11. |
| requirements/3.11/frozen_requirements.txt | Updates frozen dependency lock for Python 3.11. |
| requirements/3.13/requirements.txt | Adds PyJWT dependency for Python 3.13. |
| requirements/3.13/frozen_requirements.txt | Updates frozen dependency lock for Python 3.13. |
| python/tank/bootstrap/manager.py | Adds project id resolver helper and triggers MEDM auth during configuration update. |
| python/tank/authentication/flow_auth/errors.py | Introduces Flow auth error types. |
| python/tank/authentication/flow_auth/_settings.py | Adds env/default-based Flow auth settings resolver + dataclass. |
| python/tank/authentication/flow_auth/_constants.py | Adds AM-ready field constant + APS defaults/scopes. |
| python/tank/authentication/flow_auth/_client.py | Adds Flow GQL client factory + auth handler adapter. |
| python/tank/authentication/flow_auth/_authentication.py | Implements auth init, token acquisition, expiry checks, and lazy init. |
| python/tank/authentication/flow_auth/init.py | Exposes the Flow auth public API surface. |
| python/tank/authentication/init.py | Re-exports Flow auth helpers from the authentication package. |
| python/tank/init.py | Exposes Flow auth client/token helpers at top-level tank API. |
| python/tank_vendor/adsk_auth/init.py | Defines vendored adsk_auth package exports. |
| python/tank_vendor/adsk_auth/config.py | Adds PKCE configuration dataclass + base URL normalization. |
| python/tank_vendor/adsk_auth/file_store.py | Adds JSON file token persistence (no keyring). |
| python/tank_vendor/adsk_auth/pkce.py | Implements browser PKCE flow + local callback server. |
| python/tank_vendor/adsk_auth/token.py | Adds token retrieval logic (cache → file → refresh → browser PKCE). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| def server_bind(self) -> None: | ||
| self.socket = socket.socket(self.address_family, socket.SOCK_STREAM) | ||
| self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) | ||
| if hasattr(socket, "IPV6_V6ONLY"): | ||
| self.socket.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 0) | ||
| self.socket.bind(("::", self.server_address[1])) | ||
| self.server_address = self.socket.getsockname() | ||
|
|
||
|
|
||
| class _ThreadingCallbackServerIPv4(ThreadingMixIn, HTTPServer): | ||
| """Threaded HTTP server binding to 0.0.0.0 (IPv4 only). Fallback when IPv6 is unavailable.""" | ||
|
|
||
| def server_bind(self) -> None: | ||
| self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) | ||
| self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) | ||
| self.socket.bind(self.server_address) | ||
| self.server_address = self.socket.getsockname() |
| server_thread = threading.Thread( | ||
| target=run_callback_server, | ||
| args=(session_store, port), | ||
| kwargs={"ready_event": ready_event, "init_error": init_error}, | ||
| ) | ||
| server_thread.daemon = True | ||
| server_thread.start() | ||
| # Ensure server is bound and listening before opening browser (avoids race on Windows) | ||
| if not ready_event.wait(timeout=5): | ||
| _logger.warning("Callback server may not be ready yet") | ||
| if init_error: | ||
| e = init_error[0] | ||
| raise RuntimeError( | ||
| f"Port {port} is already in use (callback URL {config.callback_url}). " | ||
| "Another auth process may be running. Exit it or use a different callback URL." | ||
| ) from e | ||
| time.sleep(0.5) | ||
| try: | ||
| b = webbrowser.get(using=browser) | ||
| b.open_new(auth_url) | ||
| # With ThreadingMixIn the server thread never exits; poll with short joins | ||
| # so we return as soon as the callback handler has set session_store[state]. | ||
| deadline = time.monotonic() + time_out | ||
| while time.monotonic() < deadline: | ||
| if session_store.get(state) is not None: | ||
| break | ||
| server_thread.join(timeout=0.25) | ||
| except webbrowser.Error as e: | ||
| _logger.error("Browser error: %s", e) | ||
| server_thread.join(timeout=0.5) | ||
|
|
||
| if session_store.get(state) is None: | ||
| raise RuntimeError("Failed to obtain authorization code from browser") | ||
| code = session_store[state] | ||
| token = exchange_code(config, code, code_verifier) | ||
| if not token or "access_token" not in token: | ||
| raise RuntimeError("Token exchange did not return an access_token") | ||
| return token |
| _aps_configuration = AuthConfig( | ||
| application_id=auth_application_id, | ||
| base_url=auth_base_url, | ||
| callback_url=auth_callback_url, | ||
| description="PKCE Test App", | ||
| required_application_scopes=REQUIRED_SCOPES, | ||
| storage_dir=LocalFileStorageManager.get_global_root( | ||
| LocalFileStorageManager.CACHE | ||
| ), | ||
| ) |
| distro==1.9.0 | ||
| packaging==25.0 | ||
| pyjwt==2.10.1 | ||
| pyyaml==6.0.2 |
| # Count .dist-info directories rather than package directories because | ||
| # namespace packages (e.g. ruamel.yaml + ruamel.yaml.clib, jaraco.*) | ||
| # share a single parent directory, making package_names < nb_dependencies. | ||
| dist_info_count = len( | ||
| [name for name in os.listdir(temp_dir) if name.endswith(".dist-info")] | ||
| ) | ||
| assert dist_info_count >= nb_dependencies |
| log.warning( | ||
| "MEDM auth failed; bootstrap will continue without a " | ||
| "pre-fetched token. Error: %s", | ||
| e, |
| def web_authenticate( | ||
| config: AuthConfig, | ||
| *, | ||
| time_out: float = 30.0, | ||
| browser: Any = None, | ||
| ) -> Dict[str, Any]: | ||
| """Run PKCE in browser; return token dict (access_token, refresh_token, ...).""" | ||
| import time | ||
|
|
||
| code_challenge, code_verifier = create_code_pair() | ||
| auth_url, state = build_authorize_url(config, code_challenge) | ||
| session_store: Dict[str, Any] = {state: None} | ||
| port = _callback_server_port(config.callback_url) | ||
| if _is_port_in_use(port): | ||
| raise RuntimeError( | ||
| f"Port {port} is already in use (callback URL {config.callback_url}). " | ||
| "Another auth process may be running. Exit it or use a different callback URL." | ||
| ) | ||
| ready_event = threading.Event() |
Summary
ToolkitManagerbootstrap when the resolved project is "AM-ready" (sg_flow_am_idfield is set)adsk_authmodule provides PKCE + keyring helpers;PyJWTandkeyringadded as new third-party deps via per-Pythonrequirements.txtpython/tank/authentication/flow_auth/module exposesinit_authentication(),get_access_token(),FlowAuthSettings, and error typesTK_FLOW_AUTH_REQUIRED=1env var turns auth failures from log-and-swallow into hard errorsDependencies
This branch is built on top of SG-43217 (Flow Data SDK vendoring,
ticket/SG-43217/flow-data-sdk) and includes those commits. It should be merged after SG-43217 lands on master, at which point this branch can be rebased onto master (dropping the SG-43217 commits) before final merge.Notes
application_id,base_url,callback_url) are TODO pending confirmation from Julien Langlois before releasepkgs.zipregeneration (PyJWT + keyring) is a release-time step and is not included in this commitFlowAuthenticationHandlerfromtk-framework-flowamis intentionally out of scope — tk-core only triggers auth, it does not own a GQL handlertk-framework-flowamPoC, which will be deleted in timeTest plan
flow_authunit tests (tests/authentication_tests/test_flow_auth.py)tests/bootstrap_tests/test_manager_flow_auth.py)application_id/base_url/callback_urlwith Julien before releasepkgs.zip(PyJWT + keyring) at release time🤖 Generated with Claude Code