Skip to main content

When an MCP server needs to act on a user’s behalf — read their repositories, file their tickets — that user completes an OAuth flow once. Rolter records the result in two places: a grant, which is the consent itself, and one or more sessions, which are the tokens minted under that consent. The dashboard exposes both under MCP Gateway → OAuth Grants and MCP Gateway → Auth Sessions.

No token material leaves the control plane#

Access and refresh tokens are sealed with the deployment key encryption key and stored alongside the session row. Only credential-bearing internal code can open them. The control plane projects the newest live, scope-valid access token per user/server through its separately token-guarded internal snapshot channel; no public API response, and therefore no dashboard screen, ever carries a token value.

What you see instead is metadata: when the session was created, when it expires, when it was last used, and a has_refresh_token flag rendered as a Renewable badge. That flag is enough to answer “will this keep working tomorrow?” without the token ever being serialised out of the control plane.

Warning

If a screen or an integration ever appears to show you an MCP access token, that is a bug worth reporting — the API surface has no field to carry one.

A grant ties one user to one MCP server, with the scopes they agreed to. Sessions hang off the grant: a session can expire, be refreshed, or be revoked on its own without disturbing the consent behind it.

That asymmetry is deliberate, and it decides which action you want:

Revoke a session

Kills one set of tokens. The consent stands, so a fresh session can be minted for the same user and server without asking them again. Use this to boot a leaked or stale token.

Revoke a grant

Withdraws the consent and revokes every session under it, in a single database transaction. The user must go through the OAuth flow again before that server can act for them. Use this when access should end, not merely restart.

Because the cascade is the point, the dashboard names it before you commit: the confirmation on a grant tells you how many live sessions will be revoked along with it. Withdrawn consent can never leave a live token behind — the grant and its sessions are revoked together or not at all.

Revoked grants are not deleted. The row stays with its revoked_at and revoked_by fields set, so the audit trail of who consented, and who withdrew it, survives.

Filtering and revoking in bulk#

Both screens filter by server and by owner, and a Live only toggle hides revoked grants and expired or revoked sessions. When a leaver’s access has to end, pick their address in the owner filter and use Revoke all shown. The action appears once a server or an owner is picked, so one click cannot take the whole org. Its confirmation names the owner (or server) and the count, and on a grant it states how many live sessions go with them. Rows are revoked one by one: if some fail, the rest still go, and the dialog says how many did not.

Who sees which rows#

Both listings are scoped by ownership, and the dashboard says so on the page rather than implying the table is the whole org:

Org admins

See every grant and every session in the org, and may revoke any of them.

Members and viewers

See only the grants and sessions they own, and may revoke only those.

Everyone else

Gets a 403. Every listing is joined through the org, so a caller without a membership cannot read another tenant’s rows even by guessing an id.

Both screens surface that 403 as a plain message rather than an empty table, so “nothing here” is never confused with “not allowed”.

Before a user can consent, an org admin registers the OAuth client Rolter presents to the server’s authorization server. The dashboard does this on MCP Gateway → MCP Catalog: open Configure on the server and fill in the OAuth client section.

Register the client

The client ID is the only field that is always required. Most MCP servers publish where their authorization server lives, and Rolter discovers the authorization and token URLs from that; enter them yourself only for a server that publishes nothing. When you do, both travel together — one of them is not a fallback — and both must be https, with http accepted only on loopback so a local stub still works.

Endpoint discovery picks the mode. Discover automatically treats the two URLs as an optional fallback; Configure manually never probes the server and makes both URLs required. Re-saving the form keeps whichever mode the server already has.

Pin the issuer (optional)

Issuer is the authorization server’s issuer identifier. Pinning it makes RFC 9207 validation strict: a callback whose iss does not match is refused. It matters most for a hand-configured server — with no discovery metadata and no pinned issuer, an authorization response that carries iss has nothing authentic to be checked against and is refused.

Check what discovery found

Under automatic discovery, Discovered endpoints shows the issuer, authorization URL and token URL the last successful discovery resolved, when it ran, and whether the server sends iss in its callbacks. It is read-only and empty until someone first connects, because only an interactive Connect probes the server. Changing the server URL, the issuer or the mode clears it, and the form warns you before you save.

Copy the redirect URI

The Redirect URI field is filled in for you from the deployment’s own public URL. Register that exact callback with the authorization server; Rolter never takes it from the request, so a spoofed Host header cannot send an authorization code somewhere else.

Set the client secret

The secret is write-only. A badge says whether one is stored, the field is never pre-filled, and leaving it blank keeps the stored secret unchanged — so saving the form for an unrelated reason cannot wipe a credential you were not rotating. Clear stored secret removes it and downgrades the client to a public one.

Connect

Connect on the server card starts consent and opens the authorization server in a new tab. Finish the approval there. That tab then lands on Auth Sessions, which says whether the connection worked, and the card you started from shows Connected until the session’s expiry without a reload. Only servers set to OAuth consent offer the action. The action stays disabled until a client ID is registered — and, for a server set to manual discovery, until both endpoints are too — because until then there is nowhere to send the user.

Note

Consent belongs to a person. Connect signs in as you, so the grant is owned by the account you are signed in with. A user may connect a server in an organization they hold a role in, and a signed-in superadmin may connect any server as themselves, even one in an organization they hold no role in. The grant is theirs: they see it, and revoke it, like any owner. The admin token and open mode (no ROLTER_ADMIN_TOKEN) are not a person, so a consent started with either is refused with a 400 that says so. The grant would belong to nobody, and no one could see or revoke it. Sign in as an account to connect.

When the authorization server sends you back, the tab lands on Auth Sessions. On success it names the server and highlights the new session. On a failure it says what went wrong in the dashboard’s own words and links back to the MCP Catalog. The authorization server’s own error text is never shown: a response whose issuer did not check out may not be displayed at all, and the rest is summarised rather than echoed onto the page.

ReasonWhat happenedWhat to do
access_deniedThe authorization server says consent was declinedConnect again and approve the requested access
authorization_failedIt answered with some other errorCheck the server’s OAuth client, then connect again
state_invalidThe approval link expired, was already used, or arrived incompleteConnect again to get a new link
issuer_mismatchThe response did not come from the authorization server Rolter sent you toPin or correct the issuer, then connect again
token_exchange_failedConsent was given, but the token endpoint refused the code or could not be reachedCheck the client ID and secret
not_configuredThe control plane has no ROLTER_KEK, or no OAuth client resolves for the serverAsk an operator to fix the deployment
internal_errorThe control plane failed while storing the consentTry again; the control plane log names the cause

The control plane logs every failure with the full detail and the server it concerns, so the reason on screen is a starting point rather than all there is.

An OAuth consent server’s card states where you stand: Not connected, Connected until a time, or Session expired (the action then reads Reconnect). That is a live session of yours, meaning an active grant of yours with a session under it that is neither revoked nor expired. An org admin sees their own sessions only, even though Auth Sessions lists everyone’s. A card says nothing when your role may not read the session listings. A server set to OAuth consent with no client registered shows an oauth, no client badge and the reason Connect is unavailable.

The same registration over the API, for a server whose authorization server is discoverable:

And for one that publishes no metadata, with the endpoints as the fallback:

discovery is auto by default, which tries discovery on every consent and falls back to whatever you configured. Setting it to manual skips the probe altogether, which is worth doing for a server you know publishes nothing.

Reading the same endpoint back returns what the last successful discovery resolved, so a misconfigured authorization server is visible without digging through logs:

FieldMeaning
discovered_issuerthe issuer the metadata named
discovered_authorize_url, discovered_token_urlthe endpoints it named
discovered_iss_supportedwhether it advertised authorization_response_iss_parameter_supported
discovered_atwhen the cache was last filled, or absent if never

discovered_iss_supported is the one worth knowing about: when it is true, a callback that arrives without an iss parameter is rejected rather than accepted, per RFC 9207 §2.4. If consent is failing on a server that used to work, this field tells you which rule is being applied.

Note

The server row and the server list spell these oauth_discovered_*; this endpoint drops the prefix. Same values, two names.

This endpoint is admin-only in both directions — even reading it, because the row names a third party the organization has chosen to trust. The secret is sealed with the deployment key on write and is never readable back; omitting client_secret keeps the stored one, and sending an empty secret downgrades the client to a public one.

Only a request that actually stores a secret needs ROLTER_KEK. Correcting a token_url, rotating the issuer, changing default_scopes or clearing the secret all work on a deployment without it, so losing the key never leaves a misconfigured client uncorrectable.

Discovery, audience binding and issuer checks#

Rolter’s MCP client follows the current MCP authorization specification (revision draft, tracked as of 2026-09-08). Three of its requirements change what you see when something goes wrong:

The authorization server is discovered, not typed

Rolter asks the MCP server where its authorization server is — from the WWW-Authenticate challenge on an unauthenticated request, or from the /.well-known/oauth-protected-resource document — and validates that the document is really about that server before using it. What it resolves is remembered, so renewals never re-probe.

Changing the server’s url, its pinned issuer, or its discovery mode forgets it again, since the remembered endpoints belong to the authorization server the old value named. The next consent re-discovers; a renewal in between fails rather than reaching for the previous server. An edit that touches none of those three fields — rotating the client secret, say — leaves what was already discovered in place.

Every token is bound to one server

Both the authorization request and the token request carry a resource parameter naming the MCP server the token is for, so the token cannot be replayed at a different server. The value is the server’s URL in canonical form: no fragment, no trailing slash. It is shown back to you on the OAuth client response as resource.

An authorization server that does not understand the parameter ignores it; one that does will bind the token. Rolter sends it either way, as the specification requires.

The response has to come back from where it was sent

If the authorization server returns an iss parameter, Rolter compares it to the issuer it recorded when it sent you there — as an exact string, with no normalisation. A mismatch fails the consent and the authorization code is discarded rather than exchanged, and the server’s own error text is not shown, because a response that failed this check is not one to act on.

A hand-configured server with no issuer set has nothing to compare against. If its authorization server returns an iss, consent fails with a message asking you to pin issuer — that is deliberate, since accepting an unverifiable issuer would be a check in name only.

Connect calls POST /api/v1/mcp-servers/{id}/oauth/authorize, which answers with the authorization URL rather than a redirect — the caller is a fetch from the dashboard, which cannot usefully follow a cross-origin 302. After the user approves, the browser returns to /auth/mcp/callback, Rolter writes the grant and its first session together, and the browser is sent on to Auth Sessions (see Where consent ends). A replayed callback finds nothing to consume and fails.

Sessions renew themselves#

Rolter renews a session about five minutes before it expires, so a user consents once rather than every hour. Refresh-token rotation is handled: if the authorization server hands back a new refresh token, that is what gets stored.

Auth Sessions also offers a renew action on each row for when you do not want to wait for the sweeper — after widening scopes upstream, say. Only a session that stored a refresh token can be renewed, so the action is disabled on the rest rather than failing.

If the authorization server refuses the refresh with invalid_grant or invalid_scope, Rolter revokes the session instead of retrying — that answer means consent is gone, and the user simply consents again. A renew that fails in the dashboard usually means exactly this: the session is already gone, and the fix is to Connect again rather than to retry. Network blips and 5xx responses are left for the next sweep.

On your own rows, a session that is expired, revoked or has no refresh token shows a Reconnect action. It starts consent for that server exactly as Connect on the MCP Catalog does, in a new tab, so you do not have to leave Auth Sessions to find the way back. Other users’ rows keep revoke only: a consent is only ever given by the person it belongs to.

Which status a failed renew answers tells you whose problem it is. POST /api/v1/mcp/sessions/{id}/refresh (and the exchange below) answer 400 when the authorization server refused, 502 with code authorization_server_unavailable when it could not be reached or did not answer, and 500 when Rolter’s own storage failed. The 502 and the 500 carry one fixed sentence: the token endpoint’s address and the underlying error are in the control plane’s log, and the session is left alone to be retried.

A service that needs to call a downstream system on the user’s behalf can exchange a live session for a narrower one with POST /api/v1/mcp/sessions/{id}/exchange. The new session descends from the same grant, can be revoked on its own, and can never carry more scopes than the consent it came from.

Calling a server through Rolter#

Use your normal user-owned virtual key against the gateway path. The server slug is scoped to the key’s organization:

Before contacting the server, Rolter verifies that the key has a user owner, the server belongs to the same organization, the owner has a live consent and session, and the session covers every scope configured on the server. The virtual key is replaced with the downstream OAuth bearer token. Streamable HTTP and SSE responses stay streamed; stdio and WebSocket transports are not served by this HTTP path.

Org admins configure the baseline with PATCH /api/v1/mcp-servers/{id} and a {"required_scopes": [...]} body. Tightening the list immediately invalidates sessions that do not cover it; no grant or session needs to be deleted merely to change policy.

Audit#

Every revocation is written to the audit log as mcp_oauth_grant.revoke or mcp_oauth_session.revoke, with the acting principal, the org, and the affected user. See Audit Logs for how audit entries are scoped and queried.