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.
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 is the unit of consent#
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:
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.
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”.
Granting consent#
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.
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.
Where consent ends#
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.
| Reason | What happened | What to do |
|---|---|---|
access_denied | The authorization server says consent was declined | Connect again and approve the requested access |
authorization_failed | It answered with some other error | Check the server’s OAuth client, then connect again |
state_invalid | The approval link expired, was already used, or arrived incomplete | Connect again to get a new link |
issuer_mismatch | The response did not come from the authorization server Rolter sent you to | Pin or correct the issuer, then connect again |
token_exchange_failed | Consent was given, but the token endpoint refused the code or could not be reached | Check the client ID and secret |
not_configured | The control plane has no ROLTER_KEK, or no OAuth client resolves for the server | Ask an operator to fix the deployment |
internal_error | The control plane failed while storing the consent | Try 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:
| Field | Meaning |
|---|---|
discovered_issuer | the issuer the metadata named |
discovered_authorize_url, discovered_token_url | the endpoints it named |
discovered_iss_supported | whether it advertised authorization_response_iss_parameter_supported |
discovered_at | when 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.
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:
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.