Skip to content

SG-43166 Integrate MEDM authentication into Toolkit bootstrap#1100

Open
stevelittlefish wants to merge 18 commits into
ticket/SG-43217/flow-data-sdkfrom
SG-43166-integrate-medm-auth
Open

SG-43166 Integrate MEDM authentication into Toolkit bootstrap#1100
stevelittlefish wants to merge 18 commits into
ticket/SG-43217/flow-data-sdkfrom
SG-43166-integrate-medm-auth

Conversation

@stevelittlefish
Copy link
Copy Markdown
Contributor

Summary

  • Adds a Flow/MEDM (AM) authentication path that triggers proactively during ToolkitManager bootstrap when the resolved project is "AM-ready" (sg_flow_am_id field is set)
  • Silent path (keyring or refresh token) surfaces no UI; otherwise a browser PKCE flow opens before the engine starts
  • Vendored adsk_auth module provides PKCE + keyring helpers; PyJWT and keyring added as new third-party deps via per-Python requirements.txt
  • New python/tank/authentication/flow_auth/ module exposes init_authentication(), get_access_token(), FlowAuthSettings, and error types
  • TK_FLOW_AUTH_REQUIRED=1 env var turns auth failures from log-and-swallow into hard errors

Dependencies

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

  • Production APS values (application_id, base_url, callback_url) are TODO pending confirmation from Julien Langlois before release
  • pkgs.zip regeneration (PyJWT + keyring) is a release-time step and is not included in this commit
  • FlowAuthenticationHandler from tk-framework-flowam is intentionally out of scope — tk-core only triggers auth, it does not own a GQL handler
  • Sourced from the tk-framework-flowam PoC, which will be deleted in time

Test plan

  • 16 new flow_auth unit tests (tests/authentication_tests/test_flow_auth.py)
  • 13 new bootstrap hook tests (tests/bootstrap_tests/test_manager_flow_auth.py)
  • All existing bootstrap (103) and authentication (101) tests still pass
  • Confirm production APS application_id / base_url / callback_url with Julien before release
  • Regenerate pkgs.zip (PyJWT + keyring) at release time

🤖 Generated with Claude Code

stevelittlefish and others added 5 commits May 20, 2026 15:40
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>
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>
@stevelittlefish stevelittlefish changed the base branch from master to ticket/SG-43217/flow-data-sdk May 21, 2026 14:45
Comment thread python/tank/bootstrap/manager.py Outdated

"""Settings resolver for Flow / MEDM authentication."""

from __future__ import annotations
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need this?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It will not import into python 3.10 without it, so we couldn't test inside Houdini.

Comment thread python/tank/bootstrap/manager.py Outdated
stevelittlefish and others added 3 commits May 22, 2026 11:57
- 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
Copy link
Copy Markdown

codecov Bot commented May 22, 2026

Codecov Report

❌ Patch coverage is 95.51282% with 7 lines in your changes missing coverage. Please review.
✅ Project coverage is 79.55%. Comparing base (8381203) to head (e601157).

Files with missing lines Patch % Lines
...n/tank/authentication/flow_auth/_authentication.py 94.59% 4 Missing ⚠️
python/tank/bootstrap/manager.py 94.87% 2 Missing ⚠️
python/tank/authentication/flow_auth/_settings.py 91.66% 1 Missing ⚠️
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     
Flag Coverage Δ
Linux 79.01% <95.51%> (+0.02%) ⬆️
Python-3.10 79.36% <95.51%> (+0.11%) ⬆️
Python-3.11 79.27% <95.51%> (+0.11%) ⬆️
Python-3.13 79.27% <95.51%> (+0.11%) ⬆️
Python-3.9 79.34% <95.51%> (+0.03%) ⬆️
Windows 79.05% <95.51%> (+0.12%) ⬆️
macOS 79.02% <95.51%> (+0.11%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@stevelittlefish stevelittlefish marked this pull request as ready for review May 26, 2026 15:48
@yungsiow
Copy link
Copy Markdown
Contributor

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.

Comment thread python/tank/authentication/flow_auth/_constants.py Outdated
import json
import urllib.request
import urllib.error
import webbrowser
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why the empty line in the middle of imports?
And can we keep alphabetical order please?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know it's a transitive dependency

Comment on lines +43 to +51
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
),
)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this new? I don't remember us having these settings in env vars before?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@stevelittlefish
Copy link
Copy Markdown
Contributor Author

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.

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!

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 ToolkitManager helpers to resolve project id and trigger Flow/MEDM auth during bootstrap for AM-ready projects (with optional hard-fail via TK_FLOW_AUTH_REQUIRED=1).
  • Introduce new tank.authentication.flow_auth module (settings resolution, token acquisition/refresh, Flow client factory, error types).
  • Vendor a minimal tank_vendor.adsk_auth PKCE + 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.

Comment on lines +177 to +193
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()
Comment on lines +248 to +285
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
Comment on lines +64 to +73
_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
),
)
Comment on lines 1 to 4
distro==1.9.0
packaging==25.0
pyjwt==2.10.1
pyyaml==6.0.2
Comment on lines +92 to +98
# 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,
Comment on lines +227 to +245
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()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants