Token-only auth by default, with opt-in interactive_login() - #352
Draft
ggprior wants to merge 3 commits into
Draft
Token-only auth by default, with opt-in interactive_login()#352ggprior wants to merge 3 commits into
ggprior wants to merge 3 commits into
Conversation
The CLI's interactive login prompted for an email and password and posted them to /auth/login/ as an OAuth2 password grant. Since OIDC was introduced the credential lives at the identity provider, so that endpoint can only return 401 for any OIDC-provisioned user. Browser login was tried first but every failure fell through to that dead end, and its callback wait had no timeout, so a dropped callback hung the client until Ctrl+C. Authentication is now token-only. init() resolves a token from, in order: a token set via set_access_token(), the TABPFN_TOKEN environment variable, or a token cached by an earlier run. TABPFN_TOKEN is read from os.environ at resolution time rather than snapshotted at import, so setting it after `import tabpfn_client` takes effect. With no token, an interactive session prompts for a paste and a non-interactive one raises with instructions pointing at https://ux.priorlabs.ai/account/api-keys. A token that was supplied but rejected reports that, rather than "not found". Browser-based login and registration remain available, but only when asked for by name: from tabpfn_client import interactive_login interactive_login() It is never reached from init(), fit(), or predict(). The flow is adapted from the browser-auth implementation in the TabPFN package: the localhost callback server runs in a daemon thread while the main thread polls stdin, so a callback dropped by an identity provider no longer traps the user -- they can paste an API key instead. Headless sessions get the URL plus an OSC 52 clipboard copy, and the wait is bounded by a timeout. Removes browser_auth.py and the password, registration, and email verification code paths, along with the server endpoints they called and the password-strength dependency. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Contributor
Author
Follow-up: interactive signup flowFeedback on the signup steps (role list, redundant steps, email opt-in wording) is tracked separately in OPC-26 — Rework interactive signup flow in tabpfn-client. It is deliberately out of scope for this PR, which is about where authentication happens, not what the signup form asks. OPC-26 carries three open questions that need answers before anyone implements — notably whether a CLI-native signup should exist at all now that registration happens in the browser. |
init() still fell back to an interactive paste prompt when no token was found and stdin was a TTY. That is the wrong shape for a library: authentication must not block on input. Auth is now either fully explicit -- a token the caller supplies -- or fully interactive via interactive_login(), which the caller has to invoke by name. init() resolves a token from set_access_token(), then TABPFN_TOKEN, then the cache, and raises with instructions if none applies. It never reads stdin. Splits the token setter, which conflated "use this token" with "remember this token": set_token() -> in-process only persist_token() -> writes the cache, called only by interactive_login() This fixes an incidental credential leak: every init() with a TABPFN_TOKEN in the environment used to copy that token into site-packages/tabpfn_client/.tabpfn/config. A token supplied by the caller now stays in memory, and the cache holds only the result of an explicit login. Verified in a real pty that init() raises immediately without waiting on input, and that an env-supplied token leaves no file on disk. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The previous commits removed the client's email/password registration flow
along with password login. That went too far: CLI-native signup is still
needed, notably for course providers like DLAI where students create accounts
from inside a notebook and cannot be sent to a browser tab.
Signup returns, but only through interactive_login(), which now offers:
[1] Log in -> existing browser / paste-key path
[2] Create an account -> terminal-only signup
[q] Quit
CLI password *login* stays removed, since that is what OIDC broke. Returning
users log in via the browser, paste an API key, or set TABPFN_TOKEN.
The signup flow is reshaped per OPC-26, from six steps to three:
- the standalone Terms and Data Privacy steps are replaced by a notice under
the email field, shown before the prompt so it is on screen before anything
is submitted
- "Your Information" and "Help Us Serve You Better" merge into
"Complete your profile"
- the role list is replaced with the agreed nine
- "What do you want to use TabPFN for?" is dropped
The marketing opt-in is reworded as requested, but ships *unchecked* (y/N)
rather than pre-checked. The new wording is marketing rather than support, and
a pre-ticked box is not valid consent under GDPR Art. 4(11) -- see CJEU
Planet49 (C-673/17). Flipping it is a one-line change if Legal signs off;
tracked in OPC-26.
Restores the endpoints signup needs (register, validate_email,
password_policy, verify_email, send_verification_email) and the
password-strength dependency. login and send_reset_password_email stay out.
Verified end-to-end in a pty against a mocked server: the full three-step flow
completes, and the registration payload carries the new role, no use_case,
both consent flags, and contact_via_email=False on a bare Enter. The live
password policy endpoint was checked against production and parses correctly.
No real account was created.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
The CLI's interactive login collected an email and password and posted them to
/auth/login/as an OAuth2 password grant. Under OIDC the credential lives at the identity provider, so SSO users have no password to type and that path cannot succeed for them.Two things made it worse:
webbrowser.openreturns True on many headless boxes.Default path: token only, never interactive
This is a library, so authentication is not allowed to block on input.
init()resolves a token from, in order:set_access_token()TABPFN_TOKEN, read fromos.environat resolution timeinteractive_login()If none resolve it raises, pointing at https://ux.priorlabs.ai/account/api-keys and mentioning
interactive_login(). It never prompts, even on a TTY. A token that was supplied but rejected gets a distinct message rather than "not found".TABPFN_TOKENis now read fromos.environat resolution time rather than snapshotted at import, so setting it afterimport tabpfn_clienttakes effect.Incidental credential-leak fix
set_token()conflated "use this token" with "remember this token", and was called on everyinit(). So an env-suppliedTABPFN_TOKENwas silently copied intosite-packages/tabpfn_client/.tabpfn/config. It is now split:set_token()— in-process onlypersist_token()— writes the cache, called only byinteractive_login()A token the caller supplies stays in memory. The cache holds only the result of an explicit login.
Opt-in path:
interactive_login()Never reached from
init(),fit(), orpredict()— asserted bytest_init_never_triggers_interactive_login. It offers:CLI password login stays removed, since that is what OIDC broke.
Signup flow, reshaped per OPC-26
Six steps to three (OPC-26):
Also: the role list is replaced with the agreed nine (Data Scientist, ML Engineer, AI Engineer, Software Engineer, Product Manager, Researcher, Student, Executive, Other), and "What do you want to use TabPFN for?" is dropped.
The notice prints before the email prompt rather than after, so it is on screen before anything is submitted. It also carries the "do not upload personal, confidential or sensitive data" clause, because the server still requires
agreed_personally_identifiable_information— this way we are not asserting an agreement the user never saw (OPC-26 open question 3).One deliberate deviation
The email opt-in is reworded as requested, but ships unchecked
(y/N)rather than pre-checked.The new wording is marketing ("product news, offers and resources"), not support. Consent for that must be a clear affirmative act under GDPR Art. 4(11) / Recital 32, and CJEU Planet49 (C-673/17) held that a pre-ticked box is not valid consent. Prior Labs GmbH is established in Germany, so this applies directly. Flipping the default is a one-line change if whoever owns consent policy signs off — tracked in OPC-26.
Verification
Against live
api.priorlabs.ai/ux.priorlabs.ai:init()raises immediately with the api-keys message and does not wait for input. A non-TTY test cannot catch this regression, so it was checked through a pty.TABPFN_TOKEN=<bogus>leaves no file on disk.webbrowser.openstubbed): simulated GUI redirect delivered the token, callback won the race, paste prompt exited cleanly./redirect-success; a callback without a token deliberately does not set the event, so the paste prompt keeps running.use_case, both consent flags, andcontact_via_email=Falseon a bare Enter./auth/password_policy/returns["Length(8)", "Uppercase(1)", "Numbers(1)", "Special(1)"]and the restored client parses it correctly.171 unit tests pass; ruff and basedpyright clean.
Not verified: no real account was created against production, so the signup path is covered end-to-end against the mock server only. Say the word if you want a live run with a throwaway address. Separately, whether a real OIDC round-trip preserves the
callbackparam is unconfirmed — if it is dropped, the paste fallback covers it, which is why the race matters.🤖 Generated with Claude Code