Carry the organizations a session must re-authenticate for - #213
Open
ericmj wants to merge 6 commits into
Open
Conversation
An organization can require its members to authenticate through its identity provider, and that authentication expires on a clock the organization sets. When it lapses, the token grant drops the organization's scopes and names them in sso_reauth_required rather than leaving the client to guess why a fetch started 403ing. The name is the whole point: a scope dropped because the member was removed is not named, because authenticating again would not give it back. Only the ones a browser visit would fix are. sso_authorization/2 asks for the URL that does the fixing. It is bound to the session asking, so opening it renews that session rather than starting a new one, and refresh_tokens/1 is how a build tool picks up the scopes afterwards without waiting out an access token that has not expired. The sso_reauth callback is optional and reports the flagged set after every grant, including as an empty list. Which of them the running command actually needs is the build tool's question, not this module's.
This was referenced Aug 4, 2026
ericmj
marked this pull request as ready for review
August 5, 2026 20:06
report_sso_reauth/2 is one clause taking the list; the refresh path normalizes the raw token response with hex_api_oauth:sso_reauth_required/1 (now an exported private helper) and the device path destructures the map device_auth_flow already normalized. with_token_refresh_lock/2 carries the global lock and stored-token fetch for both resolution and forced refresh; a failed forced refresh still leaves the stored token in place for the caller to handle. has_refresh_token is derived from the device-auth result instead of hardcoded, the unreachable env source leaves the auth_context type, open_browser/1 is private again, and is_token_expired/1 is exported so build tools stop reimplementing the expiry buffer.
A repository request that triggered inline device auth retried with the API key set, and repository requests read repo_key, so the retry went out unauthenticated and 401ed again. It re-resolves repository auth after authenticating. with_repo also executed once and never acted on a 401 from a token it had already resolved, so a token the server rejects as expired was never refreshed or re-exchanged; it retries once with renewed credentials. A transport failure during a refresh was reported as an expired session and dropped the stored token, so a DNS blip printed that the session had expired and downgraded the rest of the run to anonymous. Only a refusal from the server clears the token now. with_repo documented auth_inline as defaulting to false and then took true on the 401 path, so a private package could open an interactive device flow against the caller's stated default. One transport error while polling ended a device authorization the user was minutes into. Polling continues until the device code expires. A malformed verification URI crashed the CLI, because the URL check threw where its caller expected a return value. The refresh token sentinel had two owners: an absent one was undefined in one place and absent in another, and undefined reached persistence, where Elixir stored it as a refresh token. The grant is normalized once. has_refresh_token was derived three ways in one module, and the source member of the auth context was written on eight paths and read nowhere.
Escape cmd.exe metacharacters before handing a server-supplied URL to the Windows browser opener. erts quotes an argument only when it contains a space or a quote, so a verification URI carrying a bare & reached cmd as a command separator. Classify a refresh response by what it says. Only 400 and 401 mean the refresh token is dead and clear the stored credentials; every other status, a transport failure, and a 200 whose body does not carry a usable access_token and expires_in leave the token in place. Renew on a token_expired challenge instead of resolving the same bearer again, and bound the renewals. The api path resolved with renewal disabled, so a token outside its expiry window came back unchanged and the request retried forever. The retry budget lives with the other retry state so it survives a reauthentication rather than resetting. Validate the device authorization, poll and refresh responses before destructuring them, so a malformed 200 returns an error the callers already handle instead of a badmatch, a badarith, or a badarg out of timer:sleep. Distinguish a missing sso_reauth_required from a malformed one. Missing still means nothing lapsed, for servers that do not send it; a value that is not a list of binaries no longer reads as an authoritative empty set that clears the stored organizations. Hold the device authentication lock over credential acquisition only. Taking a lock the same process already holds adds no reference, and releasing the inner one deleted the entry, so the outer body ran unlocked and a second process could start a concurrent device authentication.
`with_api/4` consulted `should_authenticate` only for `{error, no_auth}`. A
refused refresh resolves to `{error, {auth_error, token_refresh_failed}}`,
whose only clause required `optional`, which `with_session_api` does not pass,
so a caller that asked to be prompted up front got three "run mix hex.user
auth" messages in one `mix deps.get` and no prompt.
Only for a refused refresh, not `token_refresh_unavailable`: there the refresh
got no answer at all, so the token may still be good and the network is what is
wrong, and a device flow needing the same network is no help.
maennchen
approved these changes
Aug 30, 2026
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.
Carries the organizations a session must re-authenticate for from the token response through to the CLI.
hex_api_oauthparsessso_reauth_requiredout of a token response and threads it throughrefresh_token/3and the device-auth result.hex_cli_authreports it through a new optionalsso_reauthcallback, invoked after every token grant with the organizations the server says this session has to authenticate through their identity provider for. Clients that do not register the callback are unaffected.Needed by hexpm/hex#organization-sso-reauth, which registers the callback and prompts.