Repository navigation
feat(auth): sign in through the gateway in the browser when no admin key is set - #5
Open
mateo-berri wants to merge 9 commits into
Open
mateo-berri wants to merge 9 commits into
mateo-berri wants to merge 9 commits into
Conversation
…key is set A local stdio run without LITELLM_API_KEY now uses the gateway's native-client sign-in (/.well-known/litellm-cli-auth, dynamic registration, PKCE with a loopback redirect) on the first tool call, saves the credential in an owner-only file keyed by gateway, renews it before it expires, and announces the tools once the sign-in finishes. A startup AdminError now prints its own message.
…oke sign-ins on logout Announce the admin tools over subscriptions/listen so current clients refetch them after a slow sign-in, return the sign-in link from the sign_in tool, revoke and remove a rejected or non-admin sign-in, add litellm-admin-mcp logout, keep one sign-in file per gateway, and name the address a foreign discovery document points at.
Author
Author
|
bugbot run |
…nd renew a rejected sign-in before removing it
Author
Author
|
bugbot run |
…an use them in the same turn
Author
Author
|
bugbot run |
…in within a minute and accept a pasted callback address Only 401 or a 400 invalid_grant, invalid_client or unauthorized_client from /token removes a sign-in. An expired sign-in the gateway cannot renew right now is kept and reported instead of starting a new browser sign-in. The sign-in link wait now counts the browser launcher and lasts 40 seconds, and loading the admin tools stops waiting 55 seconds after sign_in started, so one-minute tool timeouts get an answer. sign_in takes an optional callback_url so a browser on another computer can finish the sign-in. The sign-in store directory is set to 0700 when it already exists, and sign_in is no longer marked read-only.
Author
Author
|
bugbot run |
…keep the write hint when renewal fails
…ore list_changed still reach them
…AdminError NotAdmin took no constructor arguments, so copy.copy and pickle, which rebuild an exception by calling its class with its message, raised TypeError where the plain AdminError it replaced worked.
Author
|
bugbot run |
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.
Today a local stdio run with no
LITELLM_API_KEYexits at startup. This PR signs in through the gateway's browser flow instead, the onelite login --pkceusesWith no saved sign-in, the server lists
sign_inplus the admin tools from its own catalog and opens nothing. The agent's first admin call opens the gateway's/authorizepage with S256 PKCE, or returns the link when no browser opens. That call never runs the tool, and the agent calls it again in the same turn. Other sessions reuse the saved sign-in, andlitellm-admin-mcp logoutrevokes it.LITELLM_API_KEYand hosted HTTP mode are unchangedUser Flow
Before
The flow fails at startup, because the connector exits with no admin key and offers no sign-in
litellm-adminto Claude Code with onlyLITELLM_BASE_URLinenv/mcp. The panel shows✘ litellm-adminwithStatus: ✘ failedPOST https://litellm-domain/key/generatewith the master keyAfter
The flow succeeds, because the first admin call sends the admin through the gateway's sign-in page and the agent creates the key in the same turn
LITELLM_BASE_URLinenv/mcpshowslitellm-adminwith 68 tools, and no browser opensGET https://litellm-domain/authorize?...&redirect_uri=http://127.0.0.1:<port>/callbackhttp://127.0.0.1:<port>/callback, which says "Signed in to LiteLLM."sk-...key with a $5 max budgetsign_inhttp://127.0.0.1:<port>/callback?code=...address into the chat, and the agent creates the keylitellm-admin-mcp logoutrevokes the sign-in and says when its last access token expires. The next start lists 68 tools againScreenshots / Proof of Fix
Last updated:
41f2c0b. QA ran on441cc77, and SSO onac85d2c. Changes since QA:facae88removes an unused helper, and41f2c0bletscopyandpicklerebuild the non-admin error. Proof remains valid because MCP clients see the same tools, text and statuses, and SSO happens on the gateway's pages with the connector's/authorizerequest and code exchange unchanged sinceac85d2c. Verification: 145 tests and CI pass at41f2c0bEach case drove a real TUI under tmux with no admin key, Claude Code 2.1.299 (Opus 5.5) or Codex 0.165.0 (GPT-6.1-Sol), against a 2-worker BerriAI/litellm#48080 gateway with Postgres, behind TLS for the first and third rows
3d4cd40)/mcp✘ failedlogoutac85d2c)Before
After (
441cc77)The first admin call,
describe_admin_tool, opened the default browser throughxdg-openon the https/authorizepage and answered after 43 seconds that the tool had not run. The agent called it again and created the key in the same turn, with no permission promptThe first call's answer in the detailed transcript, and the gateway's record of the key
Codex (
441cc77)A fresh
CODEX_HOMEand config directory, with aBROWSERscript that returns at once likexdg-openand starts the person 20 seconds later./mcplisted 68 tools and nothing opened. The model's first MCP call wasdescribe_admin_tool, which ran the sign-in and answered after 31.6 seconds, shown by Codex as Failed. The model then describedcreate_keyandget_key, Codex asked approval forcreate_key(Allow), and the key was created and checked in the same turn. At the end/mcp verboselisted 67 tools, withoutsign_inBefore this commit, at
43fcb86, Codex received the tool-change notice aftersign_inbut never gave the model the new tools, in that turn, the next one, or after/mcp, until a restartBrowser on another computer (
441cc77)The agent's side had no browser and no display. The person's browser ran in another network namespace, where
curl http://127.0.0.1:36001/callbackcould not connect, so its 127.0.0.1 was not the agent's. The first admin call returned the link in 0.9 seconds with the hint for this case. After Approve the browser showedERR_CONNECTION_REFUSED, and the person pasted that page's address. The agent calledsign_inwithcallback_url(16.9 seconds) and created the key in that turn/key/infofor that key returnedmax_budget: 5.0,spend: 0.0,created_by: "default_user_id",status: "active". Every QA key was deleted afterwardsNo browser, another open session, restart, and logout (
441cc77)Four Claude Code sessions shared one config directory, with
BROWSERandDISPLAYunset. Sessions 1 and 2 both listed 68 tools before the sign-in. In session 1 the first admin call returned the link and said to call the tool again, and after "I approved it in the browser, go ahead" the agent created the keySession 2, open since before the sign-in, then created its key in one turn with no link and no browser
A fresh session 3 listed 67 tools without
sign_in. Afterlogout, session 4 listed 68 tools withsign_inagain, and no browser opened in eitherSSO admin on two teams (
ac85d2c)The gateway used LiteLLM's generic OIDC SSO (
GENERIC_CLIENT_ID,GENERIC_AUTHORIZATION_ENDPOINT,GENERIC_TOKEN_ENDPOINT,GENERIC_USERINFO_ENDPOINT,GENERIC_USER_ROLE_ATTRIBUTE=role) againstghcr.io/navikt/mock-oauth2-server:6.1.4. The person signed in asqa7-sso-adminwith"role": "proxy_admin", a member of two teams, and picked the second team on the consent page. The gateway's hops were/authorize303,/sso/key/generate303, the identity provider,/sso/callback303,/authorize200,POST /authorize/complete303, andPOST /token200. The agent created the key with the team-bound sign-in, andGET /key/info?key=<hash>with the master key returnedteam_id: null,max_budget: 5.0,created_by: "qa7-sso-admin"Commands and config
The headline case and the browser on another computer used Envoy 1.35.3 terminating TLS at
https://172.17.0.1:29605in front of the gateway, withPROXY_BASE_URLset to that address, and the connector trusted the QA CA throughSSL_CERT_FILE. Claude Code ran in its default auto mode. A Playwright person used the real login and consent pages. The headline case made a.desktopentry for Chromium the default withxdg-settings set default-web-browserunder the connector'sXDG_CONFIG_HOMEandXDG_DATA_HOME. The other-computer case ran the person's headless Chromium withnsenterin another container's network namespace. The other cases used the plain-HTTP gateway athttp://127.0.0.1:26863{ "mcpServers": { "litellm-admin": { "command": "<checkout>/.venv/bin/litellm-admin-mcp", "args": [], "env": { "LITELLM_BASE_URL": "https://172.17.0.1:29605", "SSL_CERT_FILE": "<qa dir>/tls/ca.pem", "DISPLAY": ":162 (headline case only)", "XDG_CONFIG_HOME": "<qa dir>/<leg>/xdg-config", "XDG_DATA_HOME": "<qa dir>/legA/xdg-data (headline case only)" } } } }Codex's
config.tomlBefore,
/mcpdetail and Claude Code's MCP logThings the run saw
/key/infocalled with nokeyanswers 500This PR causes the first on purpose (see Decisions) and leaves the rest alone
Caveats (if any)
Medium
LITELLM_API_KEYLow
sign_inpast 55 secondslitellm-admin-mcp logoutfrom a shelllite logoutstops renewalDecisions
Design decisions and the alternatives considered
Before sign-in,
tools/listanswers at once withsign_inplus every admin tool the local config allows (allowed_tools,read_only), built from the connector's reviewed catalog with no gateway request. They carry the signed-in names, descriptions and annotations, with the discovery-mode input schema (body,queryandpathpointing atdescribe_admin_tool, and the realresponseschema), even underschema_mode=full, since the full schema comes from the gateway's OpenAPI document, which needs a credential. After sign-in the list is the gateway's own and dropssign_in. The earlier version listed onlysign_inand sentnotifications/tools/list_changedafter sign-in. Codex 0.165.0 receives that notice and never gives the model the new tools until a restart, and Cursor documents no support for it, so the flow cannot depend on a client listing tools againThe sign-in starts only when the agent calls
sign_inor an admin tool, so starting a client or opening another window never opens a browser tab. The first version opened the browser on the client's first tool list, which opened a tab in every new session, even one where nobody asked for the connector. Holdingtools/listuntil sign-in finished left Claude Code with no tools after a 60-second sign-in, and MCP URL elicitation has little client support yetAn admin tool called before sign-in runs the same sign-in as
sign_inand never runs the operation in that call. It returns an error result that says the user is signed in and the tool has not run yet, or gives the link and says to call the tool again. A call that waited up to 40 seconds for a person and then loaded the gateway's schema could pass a client's 60-second tool timeout while a write is still running, and the agent would then see a write whose outcome it cannot know. The cost is one more call. The result is marked as an error so neither the client nor the model reads it as the tool's output, which is why Claude Code shows it in red and Codex as Failed. In QA both called the tool again in the same turnsign_in, and an admin tool's first call, answer within 55 seconds, so a client with a 60-second tool timeout, Codex's default, always gets an answer. They wait up to 40 seconds for the person, counting the time the browser launcher takes, and otherwise return the link, which works for 5 minutes. They return the link at once when no browser opened. After approval the connector loads the gateway's admin tool list until the 55 seconds are up and keeps loading it in the background, and the next tool list reuses that load. It then publishesnotifications/tools/list_changedon thesubscriptions/listenstream that 2026-07-28 clients open, and also sends it the older way, so a client that listens swaps the stub schemas for the gateway's own. One sign-in runs at a time and outlives the request that started itThe browser opens in the background through Python's
webbrowserin a short-lived child process with no stdio, so a console browser or a chatty launcher can never touch the MCP stream. When the browser runs on another computer, the page after approval points at that computer's 127.0.0.1 and cannot load.sign_intakes that page's address ascallback_urland checks it exactly like the loopback listener does: only the pending sign-in'sstateis accepted, and the code is redeemed only with that sign-in's PKCE verifier. A paste with no pending sign-in, or with a wrong state, is refused. The alternative, a listener on an address other computers can reach, would put the code on the network. The link and the waiting message carry the hint for this casesign_inis marked not read-only, since it saves a credential, and idempotent, since a second call reuses the saved one. In QA Claude Code's default auto mode ran it with no permission promptThe saved sign-in can do what the proxy-admin key
LITELLM_API_KEYtakes can do, and what alite loginsession can do, since the gateway has no narrower grant for an admin client. It is bound to the admin's team when they are on one, or to the team they pick. Its access token lasts at most 24 hours, and its refresh token stays renewable only while it is used, until 14 idle days orlogout. The alternative, asking the admin to mint and paste a key, is the flow this PR replacesEach gateway's sign-in is its own file under
~/.config/litellm-admin-mcp/sign-ins/(or under$XDG_CONFIG_HOME), named by the URL-quoted gateway origin so every spelling of one address shares it, mode 0600 in a 0700 directory, written atomically. The directory's mode is set on every save, so one created earlier with a looser mode is tightened. Windows ignores those modes, and there the file relies on the user profile's permissions. Every request reads the file, so a sign-in made in one session serves the others. The alternatives were the OS keychain, which would add thekeyringdependency and its headless-Linux failure modes, and reusinglite's own record, which would couple this connector to the CLI's storage format. The access token is renewed 6 minutes before it expires, and a token another process already rotated is picked upA 408, a 429, a 5xx or a network error from the token endpoint is temporary and keeps the sign-in. Any other 4xx counts as the endpoint rejecting it, so a sign-in it will never renew is replaced by a new browser sign-in instead of locking the user out. When renewal fails temporarily, an unexpired token keeps working, and an expired one is kept while the request fails with an error that says to try again shortly
When the gateway rejects a saved sign-in with a 401, the connector renews it once and repeats the request only when it reads: listing tools, the admin check, and catalog routes marked read-only. A write is never repeated, since a 401 from the check after a write means the write already ran, so the agent is told to check gateway state first, also when that renewal fails. A sign-in still rejected after renewal, or one that is not a proxy admin, is revoked and removed, and the client is told its tools changed, so it lists them as before sign-in. A 403 from a single admin route keeps the sign-in, since signing in again would not change it
litellm-admin-mcp logoutposts the refresh token to the discovery document'srevocation_endpoint(RFC 7009) and removes the saved file. The gateway's revocation ends only the refresh chain, since its access tokens are stateless, so the message says until when the last one stays valid. When the gateway answers 503 nothing is removed and the user is told to retry. Any other failure removes the file and says the gateway did not confirmDiscovery is trusted only when it is contract version 1, offers S256, and names the configured gateway's origin for the issuer, every endpoint (revocation included), and the resource. Anything else is refused before any request goes to it. An origin mismatch names the address the gateway reported and says to set
PROXY_BASE_URLon the gateway, and suggestsLITELLM_BASE_URLonly when the reported address is one the connector accepts. The loopback listener accepts one response, only with the pending sign-in'sstateSeparately, a startup
AdminError(for example a malformedLITELLM_API_KEY) now prints its own message instead of only the exception class. Those messages are ours and carry no secrets. Other startup errors keep the generic line, since transport errors can include response bodiesTests
What the new tests cover
tests/test_signin.pyruns every case through a real MCP session against a stub gateway that checks the PKCE verifier, the redirect, the client, and the resource the way the proxy does, and records revocations. It covers the tool list before sign-in (the enabled catalog tools and helpers with the signed-in names, descriptions and annotations, a read-only config hiding write tools, and no browser or gateway request), a client that never lists tools again creating a key on the call after the one that signed in,sign_inwaiting for approval and loading the admin tools before it answers or linking a slow sign-in, a launcher that blocks counted in the link wait,sign_inand an admin call answering in time while a slow gateway still loads the admin tools, an admin call returning the link until the signed-in tools are announced, a sign-in saved by another session, reuse, renewal and rotation, a rejected read renewed and repeated, a rejected write not repeated, temporary and permanent token endpoint errors during renewal, a sign-in still rejected or not an admin being revoked with the tools listed as before sign-in, finishing from a pasted callback address (refused before a sign-in starts and with a forged state), every spelling of one origin sharing a file, an existing store directory made owner-only, a declined, forged, late, or abandoned sign-in, each kind of foreign or unsupported discovery document and the setting its error names, a gateway without the contract, two gateways side by side, andlogoutagainst a gateway that revokes, answers 503, or fails. Two cases run the packaged stdio process, one signing in through aBROWSERlauncher that writes to stdout and stderr, and one where a 2026-07-28 client gets the admin tools once a slow sign-in finishes. Full suite is 145 passed. The 7 cases added at441cc77fail at43fcb86, and the copy and pickle case added at41f2c0bfails atfacae88