usermanagement: let OAuth accounts exchange a refresh token - #78
Merged
tekrajchhetri merged 1 commit intoAug 11, 2026
Merged
Conversation
/api/auth/exchange looked the credential row up with the active-only
jwt_user_repo.get_by_email, so it 401'd "Account inactive" for every
Globus/ORCID/GitHub user. OAuth onboarding creates that row as a SHELL with
is_active=False on purpose — an OAuth user has no usable password, and the shell
exists only to supply a stable user_id claim (see the get_by_email_any_status
docstring, which already says OAuth flows must use it). The result: an OAuth user
could log in and mint a refresh token that nothing would ever accept.
That broke two flows on the same line. The MCP/skill paste-code login
(cli/start -> cli/exchange -> exchange) dead-ended at the last hop, so
brainkb_whoami read authenticated:false right after a successful login and a PAT
could never be minted — minting needs a session token, and the only way to one
from a refresh token is this endpoint. The UI's silent renew goes through the
same call (oauth.py mints web_refresh precisely "exchanged by the UI at
/api/auth/exchange"), so web sessions died at TTL instead of renewing.
Not a bare swap to get_by_email_any_status, because is_active is overloaded:
POST /api/admin/users/deactivate flips the same column, so dropping the check
would make deactivation a no-op here. The refresh token records how it was
issued — auth_source="password" from /auth/login, the provider name from OAuth —
so the check now applies only to password credentials, where is_active really is
the deactivation switch. OAuth accounts are removed by banning, which the
is_banned -> 403 check below already enforces.
Also distinguishes a missing row ("Unknown account") from a switched-off one
("Account inactive"), which were previously the same message.
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.
Fix OAuth refresh-token exchange for shell users
/api/auth/exchangeusedjwt_user_repo.get_by_email, which only returns active users. This caused every Globus/ORCID/GitHub OAuth user to fail token exchange with401 Account inactive.OAuth onboarding intentionally creates a SHELL user with
is_active=False: OAuth users do not have a usable password, and the row exists only to provide a stableuser_idclaim. As documented byget_by_email_any_status, OAuth flows must therefore be able to resolve these users regardless ofis_active.This broke both:
cli/start -> cli/exchange -> /api/auth/exchangesucceeded initially but failed at the final token exchange, leavingbrainkb_whoamiasauthenticated:falseand preventing PAT creation.web_refreshtokens are exchanged through the same endpoint, so OAuth sessions expired at TTL instead of renewing silently.The fix is not simply replacing the lookup with
get_by_email_any_status.is_activeis also used by/api/admin/users/deactivate, so ignoring it globally would make password-account deactivation ineffective.Instead,
/api/auth/exchangenow:get_by_email_any_status;is_activeonly whenauth_source == "password";is_bannedfor OAuth account revocation;"Unknown account"from"Account inactive".This restores OAuth refresh, MCP/skill authentication, and PAT minting without weakening password-account deactivation.