Conversation
OAuth access tokens only lived in memory and no refresh token was issued, so an authorization_code client like claude.ai fell back to "needs authentication" on every restart and at least once every 24 h, forcing a manual passkey/TOTP sign-in each time. - /oauth/token returns a refresh_token on the authorization_code grant (never on client_credentials, which keeps its TOTP-on-every-exchange rule) - new refresh_token grant: same client_secret_post check, rotates the refresh token and issues a new access token; replaying a rotated-out token revokes the whole family (OAuth 2.1 / RFC 9700 reuse detection) - OAuth access and refresh tokens are persisted in tokens.db as SHA-256 hashes (new oauth_tokens / refresh_tokens tables, created in place), reloaded at startup, expired rows purged; dashboard session bearers stay memory-only - server.refresh_token_ttl (default 30 days, 0 = never expires), env BEACONMCP_REFRESH_TOKEN_TTL, wizard field and config summary - refresh_token added to grant_types_supported (main and DCR metadata) and to the DCR registration response - deleting a connector drops its OAuth tokens; tokens of clients missing from clients.json are pruned at startup; security_end_session also drops the refresh family - audit: auth.token.refresh, auth.token.refresh.reuse, and auth.token.issue now also emitted for authorization_code - docs: configuration, security, clients, yaml example Tests: 498 passed.
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.
Fixes the two reasons an
authorization_codeconnector (claude.ai in particular) keeps dropping to "needs authentication":/oauth/tokencalledtoken_store.issue(client_id)without a name, so nothing was persisted and everysystemctl restart beaconmcp(including a self-update) turned the connector's bearer into a 401./oauth/tokennever returned arefresh_token, so the operator had to sign in again with passkey/TOTP at least once a day.What changed
Refresh tokens (
authorization_codegrant only)/oauth/tokennow returnsrefresh_tokennext to the access token.grant_type=refresh_token: sameclient_id+client_secret_postcheck as the other grants, issues a new access token and a new refresh token, and rotates the old one out./oauth/authorizeagain.client_credentialsstill gets no refresh token: it keeps its fresh-TOTP-on-every-exchange rule.refresh_tokenis advertised ingrant_types_supported(main and DCR metadata) and in the DCR registration response.Persistence
tokens.db, in two new tables (oauth_tokens,refresh_tokens) created in place on first start. They are stored as SHA-256 hashes and kept in memory keyed by that hash, so the file alone does not hand out working credentials.Revocation
/app/connectorsdrops its OAuth tokens.clients.jsonare pruned (coversbeaconmcp auth revokerun while the server is up). Skipped if no client loaded at all, so an unreadableclients.jsoncannot wipe the db.security_end_sessionkeeps its grace window for the bearer, and also drops the refresh family right away, so the client can't quietly mint a replacement.Config
server.refresh_token_ttl(seconds, default 30 days,0= never expires), envBEACONMCP_REFRESH_TOKEN_TTL, wizard field,validate-configsummary. Each refresh restarts the clock, so a client in regular use never signs in again.Audit
auth.token.refreshon success,auth.token.refresh.reusewhen a family gets revoked,auth.token.failwithreason=refresh_invalid|refresh_expiredotherwise.auth.token.issueis now also emitted forauthorization_code(it was only emitted forclient_credentials).Docs: configuration, security, clients (ChatGPT / Le Chat no longer re-prompt every 24 h), yaml example.
Notes
named_tokens(the dashboard revokes them by prefix). Hashing them too is doable but out of scope here.resourceis not tracked by the token endpoint today, so there is no audience to carry over on refresh./oauth/authorizeconfirmation screen still shows "Access expires" at +24 h, which is now the access token only; the client renews it on its own. Could use a copy tweak in a follow-up.Upgrade
No migration step: the new tables are created on first start and existing named tokens are kept. Access tokens issued by the previous version were never persisted, so clients connected before the upgrade have to sign in once after it; from then on restarts and the 24 h expiry are transparent.
Tests
tests/test_oauth_refresh.py(26 tests): refresh OK, rotation, replay revokes the family (also across a restart), other families untouched, wrong client refused, unknown/expired refresh refused, configurable TTL and0, persistence across a newTokenStoreon the same db, hashed storage, expired rows purged, in-place upgrade of an oldtokens.db, client revoke, end-session revoke, startup pruning, metadata, YAML parsing and wizard round-trip.ruff checkclean on touched files.