Skip to content

Add use_paw_oauth to TM1Service for one-line PAW sign-in - #1495

Draft
bdunleavy22 wants to merge 8 commits into
cubewise-code:masterfrom
bdunleavy22:adding_defaults
Draft

bdunleavy22 wants to merge 8 commits into
cubewise-code:masterfrom
bdunleavy22:adding_defaults

Conversation

@bdunleavy22

@bdunleavy22 bdunleavy22 commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

Depends on #1494 (and through it #1488). Until those merge, this diff also shows their commits;
only the last commit is new here.

Open question: with use_paw_oauth, TM1Service can open a browser. It's strictly opt-in, and nothing
changes for existing connections, but it's a first for TM1py, so I'd value your view on whether that's acceptable
or whether you'd rather keep only the explicit PawOAuthSession route from #1494.

Summary

Sign in to PAW as a user, through the browser, with one extra kwarg:

with TM1Service(
    base_url="https://<paw-host>/api/v1/tm1/<database>",  # TM1 12: https://<paw-host>/api/<tenant>/v0/tm1/<database>
    use_paw_oauth=True,  # or "keyring" to sign in once across runs, or a TokenCache
    paw_client_id="<client id>",
    paw_client_secret=os.environ["PAW_CLIENT_SECRET"],
) as tm1:
    ...
 RestService.__init__
+  if use_paw_oauth
+    oauth = PawOAuthSession.from_base_url(base_url, paw_client_id, ...)   # /oauth2/* on base_url's host
+    oauth.login()                                                         # browser, or silent refresh
+    access_token = oauth.access_token                                     # callable from #1488
+    keep_authorization_header defaults to True                            # flag from #1488
   determine auth mode  -> ACCESS_TOKEN
   connect
TM1py/Services/RestService.py        # use_paw_oauth, paw_client_id, paw_client_secret, paw_redirect_uri
TM1py/Utils/PawOAuth.py              # PawOAuthSession.from_base_url, default redirect_uri
Tests/RestService_paw_oauth_test.py  # offline: endpoint derivation, wiring, cache choices, invalid combinations
docs/getting-started.md              # leads with use_paw_oauth; PawOAuthSession as the explicit alternative

paw_client_id is deliberately separate from application_client_id, which is a TM1 12 engine application client
for headless service-to-service sign-in. paw_redirect_uri defaults to http://127.0.0.1:8000/oauth/paw/callback.
use_paw_oauth accepts True/False and their config.ini spellings, "in_memory", "keyring", or a
TokenCache.

Evidence

Offline unit tests (Tests/RestService_paw_oauth_test.py, 10 tests with subtests); the browser sign-in is stubbed
and connect() is patched out:

base_url https://<env>.planning-analytics.cloud.ibm.com/api/v1/tm1/CONS        -> /oauth2/authorize, /oauth2/token on that host
base_url https://<region>.planninganalytics.saas.ibm.com/api/<t>/v0/tm1/<db>    -> /oauth2/authorize, /oauth2/token on that host
use_paw_oauth=True                    -> login() once, ACCESS_TOKEN mode, keep_authorization_header on, Bearer sent at session start
use_paw_oauth="keyring" / TokenCache  -> that cache is used
use_paw_oauth False/None/"False"/""   -> no sign-in, other auth modes unchanged
"memory", no paw_client_id, no base_url, unparseable base_url, also access_token -> ValueError, no sign-in

Against real instances with my own user (manual smoke test, not committed), using only
TM1Service(base_url=..., use_paw_oauth="keyring", paw_client_id=..., paw_client_secret=..., **connection_options),
with no access_token, OAuth URLs or keep_authorization_header passed:

TM1 11.8 on IBM Cloud TM1 12.5.9 PAaaS
OAuth endpoints derived from base_url match the client's real ones ✅ ✅
Default redirect URI (http://127.0.0.1:8000/oauth/paw/callback) ✅ ✅
Signed in silently from the keyring-cached refresh token, no browser ✅ ✅
whoami after session start (header kept automatically) ✅ ✅
Logout ✅ ✅

The interactive path is the same PawOAuthSession.login() that #1494's smoke tests exercised with a real browser
sign-in on both instances.

Merge Danger

Door: two-way for behaviour, but adds public kwargs

Off by default and reverted cleanly. Once released, the use_paw_oauth / paw_* names are public API, so they're
worth settling in review.

Blast Radius: opt-in

Only TM1Service/RestService constructions with use_paw_oauth set are affected; the helper module isn't even
imported otherwise. With it on, TM1Service(...) blocks until the browser sign-in completes (up to 180 s), unless a
cached refresh token signs in silently. Token requests don't yet use TM1Service's verify or proxies settings.

🤖 Generated with Claude Code

bdunleavy22 and others added 8 commits October 1, 2026 12:03
Add option to keep Authorization header on requests
…oken

- Parse keep_authorization_header with translate_to_boolean, so a config.ini
  value of "False" no longer switches it on.
- Document keep_authorization_header as an option for token-based gateways
  (e.g. PAW's OAuth route), and document access_token.
- Accept a zero-argument callable for access_token. It is resolved on every
  connect and reconnect, so a refreshing OAuth client supplies a fresh token
  after a session timeout instead of the stale one.
- Add offline unit tests for the header being kept/removed, the "False"
  string, and the callable being resolved on each session start.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
TM1py.Utils.PawOAuth runs the OAuth 2.0 authorization-code flow with PKCE
against Planning Analytics Workspace, then refreshes the access token on
demand. Its access_token method is passed to TM1Service as a callable
access_token, together with keep_authorization_header=True.

It stays outside RestService because the flow opens a browser; the caller
decides when an interactive sign-in is acceptable, and TM1py itself stays
headless. Standard library plus requests only.

- The loopback listener is bound before the browser opens, so an instant
  redirect is not refused.
- Only 127.0.0.1 / [::1] redirect URIs are accepted (RFC 8252 8.3), since
  "localhost" may resolve to the other address family than the listener.
- A busy port raises OAuthError; address reuse is off on Windows, where it
  would let the listener share a port with another process.
- Requests have a read timeout, so a browser's idle speculative connection
  cannot block the single-threaded listener.
- Offline unit tests for the listener and token handling, and a section in
  the getting-started docs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Both were confirmed against real instances (TM1 11.8 on IBM Cloud, TM1 12.5.9
PAaaS): the OAuth token is accepted on PAW's API route, not on TM1 11's
/tm1/api/<db> endpoint, which only accepts CAM authentication.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Keeps the refresh token in the OS credential store via keyring, so later
processes refresh silently instead of opening the browser. keyring is an
optional extra (pip install TM1py[keyring]) and imported only when the cache
is created, with an ImportError naming the extra if it is missing.

Windows Credential Manager rejects values over 1280 UTF-16 characters
("The stub received bad data"), which refresh tokens can exceed, so the token
is stored in 1000-character chunks plus a count entry written last. Checked
against the real WinVaultKeyring backend with tokens up to 4000 characters.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
cache now accepts "in_memory" (the default, also None), "keyring", or a
TokenCache instance. "keyring" builds a KeyringTokenCache keyed on the token
endpoint host, client ID and scope, with characters outside [A-Za-z0-9._-]
replaced and a short hash of the exact values appended, so sign-ins that only
differ in replaced characters stay separate. The client secret is left out of
the key: key names are visible in the credential store, and a rotated secret
should keep the stored token.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
use_paw_oauth=True (or "in_memory", "keyring", or a TokenCache, which also
picks where the refresh token is kept) signs in to PAW through the browser
before connecting. The OAuth endpoints are derived from base_url's host,
paw_client_id and paw_client_secret identify the PAW OAuth client, and
paw_redirect_uri defaults to http://127.0.0.1:8000/oauth/paw/callback. The
session's access_token callable is supplied as access_token, and
keep_authorization_header defaults to True, so the existing ACCESS_TOKEN mode
and reconnect handling apply unchanged.

paw_client_id is deliberately separate from application_client_id, which is
a TM1 12 engine application client for headless service-to-service sign-in.

Also adds PawOAuthSession.from_base_url and a default redirect_uri.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
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.

1 participant