OAuth for assistants
API tokens authenticate a program you run. OAuth answers a different question: how does an assistant you did not write, running on someone else’s infrastructure, get permission to read your inventory, and how do you take that permission back?
The answer is an OAuth 2.1 authorization server in the NextPKI console. You sign in, you see who is asking, you decide, and you can revoke it later from Connected applications.
What an OAuth grant can do
Section titled “What an OAuth grant can do”Read only. certs:read, and nothing else. This is not a default you can widen
with a parameter: the authorization server rejects any other scope, and the database
enforces it as a constraint on the grant row. An assistant cannot open a renewal,
order a certificate, change settings or spend money.
If you want automation that renews, that is an API token with
certs:renew, held by software you operate. The two paths are deliberately
separate: one is a person delegating reading to a third party, the other is your own
system acting for you.
Endpoints
Section titled “Endpoints”| Endpoint | Norm | Purpose |
|---|---|---|
GET /.well-known/oauth-authorization-server |
RFC 8414 | Discovery. Start here; do not hardcode the rest |
GET /oauth/authorize |
OAuth 2.1 | Authorization request and the consent screen |
POST /oauth/token |
OAuth 2.1 | Code to token, refresh, token exchange |
POST /oauth/introspect |
RFC 7662 | Token state, for resource servers only |
The MCP server publishes GET /.well-known/oauth-protected-resource (RFC 9728),
which is how a client discovers which authorization server to talk to.
What clients must send
Section titled “What clients must send”Four things are mandatory, and requests without them are rejected rather than silently accepted with a weaker guarantee.
PKCE with S256. code_challenge_method=plain is not offered, because it does
not protect anything. The metadata advertises code_challenge_methods_supported: ["S256"] so a client can verify support before starting.
A resource parameter (RFC 8707) naming the resource the token is for. Without
it the server would not know what to bind the token to, so it refuses to guess. The
token is then valid only for that resource, which is the point.
A client_id that is an HTTPS URL pointing to a Client ID Metadata Document.
The server fetches your metadata there and reads client_name and redirect_uris
from it. There is no registration endpoint and no registration form.
An exact redirect_uri from that document. Matching is exact, never by prefix,
because prefix matching is the classic open redirect: https://good.example.com
would also accept https://good.example.com.attacker.test. Schemes are limited to
https, or http for localhost during development.
Dynamic Client Registration is not offered
Section titled “Dynamic Client Registration is not offered”RFC 7591 is deprecated in the MCP authorization specification and retained there only for backwards compatibility. Client ID Metadata Documents are the supported path: no registration database, no mass-registration abuse to defend against, and your client identity is a URL you already control.
The flow
Section titled “The flow”- Your client discovers the authorization server from the protected resource metadata.
- It sends the user to
/oauth/authorizewithclient_id,redirect_uri,resource,scope=certs:read,state, and the PKCE challenge. - Not signed in? The console signs the user in first, then returns to exactly that request.
- The consent screen names the application, shows the host it will send the user back to, explains what the access allows, and asks which organisation’s data is being shared when the user belongs to more than one.
- On approval the user returns to your
redirect_uriwithcode,stateandiss. Theissparameter (RFC 9207) lets you detect a mix-up attack; check it. - Your client exchanges the code at
/oauth/tokenwith the PKCE verifier and gets an access token and a refresh token.
The consent screen shows the redirect host on purpose. A familiar application name pointing at an unfamiliar host is what a phishing attempt looks like, and only the person approving can tell the difference.
Lifetimes
Section titled “Lifetimes”| Artefact | Lifetime | Notes |
|---|---|---|
| Authorization code | 2 minutes | Single use |
| Access token | 15 minutes | Short by design; a leak is worth little |
| Refresh token | 30 days | Rotated on every use |
| Exchanged service token | 5 minutes | Never leaves the MCP server |
Reuse revokes everything
Section titled “Reuse revokes everything”Presenting an authorization code twice, or a refresh token that has already been rotated, is treated as evidence that someone else has it. The response is not just an error for that one request: the entire grant falls, along with every token issued under it.
This follows RFC 9700 and it has a consequence worth designing around. A client that
crashes after rotating and then retries with the old refresh token will lose its
access and the user will have to approve again. Store the rotated token before you
use it, and treat invalid_grant on refresh as “start a new authorization”, not as
“retry”.
Why your access token does not work against the API
Section titled “Why your access token does not work against the API”An access token issued for the MCP server carries that server as its audience. If
you present it to api.nextpki.com, you get 401, and that is correct: the token
was not issued for the API.
Internally the MCP server exchanges it (RFC 8693) for a short-lived token whose
audience is the API, authenticating itself as a confidential client to do so. That
exchange is recorded in the audit log as
oauth.token.exchange, so “which assistant read what, on whose behalf, and when” is
an answerable question.
The alternative, passing your token straight through, is forbidden by the MCP specification for exactly this reason: the API could no longer tell whether it was the intended recipient.
You do not implement any of this. It matters only because it explains why a token
that works fine against the MCP server returns 401 from the API.
Revoking access
Section titled “Revoking access”In the console, Connected applications:
- Konto → Verbundene Apps shows the access you granted. Any role can reach it, because any member can grant it.
- Settings → Verbundene Anwendungen shows every grant in the organisation, for owners and admins.
Each entry names the application, its client identity, the resource, the rights, when it was granted, when it was last used and how many tokens are currently valid. That last-used column is the one that tells you whether an access is still needed.
Revocation is immediate and takes the tokens with it. An assistant does not keep
reading until its access token happens to expire. Every revocation is written to the
audit log as oauth.grant.revoke.
Client checklist
Section titled “Client checklist”- Discover, do not hardcode: read the protected resource metadata, then the authorization server metadata.
- Generate a fresh PKCE verifier per authorization request.
- Send
resourcein both the authorization request and the token request. - Verify
isson the authorization response. - Verify
state. - Ask for
certs:read. Asking for more fails rather than being trimmed, so you find out immediately instead of wondering why a call returns403. - Persist the rotated refresh token before using it.
- Treat
invalid_granton refresh as “the grant is gone”, not “retry”.