Skip to content

OAuth vs API keys for AI agents: revocation

OAuth vs API keys for AI agents is really a revocation question: a grant is re-read on every call, a static key stays valid until somebody rotates it.

Roopendra Talekar 8 min read
A delegated OAuth grant checked on every call set beside a static API key that stays valid until an operator rotates it

OAuth vs API keys for AI agents: the two shapes

An assistant that acts in your company's SaaS accounts holds one of two things, and the choice decides how fast you can stop it. OAuth vs API keys for AI agents is a revocation question before it is an authentication question. An OAuth grant is a record at the issuer, checked when the agent calls. A static API key is a string, and holding it is the authorization.

MCP (Model Context Protocol) is the standard way an AI assistant calls tools in other apps

The difference shows up the day somebody leaves, or the day a tool call writes to the wrong account. A grant has a lifecycle you control from outside the client. Mark it revoked and the next call fails. A key has no such lifecycle. The issuer learns you wanted it gone at the moment you rotate it, and not before.

Most companies run both shapes at once and cannot say which is where. Assistants sign in. Scripts carry keys.

What revoking an OAuth grant looks like

In Elaichi, grant revocation is effective on the next call. A grant is the OAuth record that lets one client, signed in as one member, call the organization's MCP endpoint. The revoked_at field is re-read from the organization store on every single call, with no cache in front of it. There is nothing to expire and nothing to wait for.

Membership changes ride the same path. removing or suspending a member revokes every live grant in the same transaction as the membership change. One write, and every client that member had pointed at the endpoint fails on its next request.

Not everything moves at that speed, and writing otherwise is the common error. Role membership and restrictions resolve through a 60 second cache plus edge propagation. A restriction is a rule about which connectors and which individual tools a target may reach. Change one and it takes effect within about two minutes, on MCP, the console and REST alike. Cut the person, and the next call fails. Narrow what the person may touch, and give it two minutes.

The cost of this shape is a read on the hot path. Every call pays for the freshness.

What rotating a static API key looks like

Nothing happens until somebody rotates the key. There is no per-call check to fail, because the key is the check. A key issued in March still works in October unless an operator went and replaced it.

Rotation is a distribution problem rather than an access-control one. The value sits in an environment file, a CI secret store, a laptop, a vendor's config screen and often a chat thread. Until the last copy is replaced, rotation is an outage waiting for whichever caller you forgot.

Scope is the second trade. A key carries whatever rights the issuer chose to package into it. Some vendors issue narrow, per-scope keys. Many issue one key with the account's full rights, so the agent gets more than its task needs.

Attribution is the third. A key has no person inside it. At the far end, the third party's log shows the key, and whoever holds it looks identical to whoever was meant to.

Where a long-lived key is still the right answer

Where there is no browser and no human to consent. A nightly sync, a CI step, a script on a box, code you wrote against an endpoint: none of them can complete an interactive sign-in on a schedule. An OAuth grant needs somebody present the first time. A headless caller does not have that somebody at 3am.

The second case is the third party itself. Plenty of vendors issue nothing but keys. When that is what exists, the credential service stores the value encrypted, and the configuration read-back returns the public values plus secret_paths. That is the list of dot-paths that were encrypted, carrying none of their values, and it is the only thing that decides whether a variable is a secret.

Take the trade knowingly. You gain a caller that runs without a person. You accept secure storage, a named owner and a rotation schedule.

Zapier MCP documents the same fork in a different product. For most MCP clients the member adds Zapier as a connector and signs in inside the client, and Zapier creates and configures the server during that sign-in. Clients not on Zapier's list, and code you write, use a connection token instead. That token is long-lived, tied to one server, and it "grants whoever holds it the ability to run the server's tools", which Zapier says to treat like a password and never distribute (https://docs.zapier.com/mcp/overview/how-connections-work, checked September 2026). Interactive sign-in where there is a human, a long-lived secret where there is not. Whether each member also needs their own server address is a separate question.

Why the model never sees the connector secret

Handing a key to a model puts it in the context window, where it can be echoed into output or into a shared transcript. Elaichi does not put it there. Connector credentials are not in Elaichi at all. A separate credential service holds per-account secrets, AES-256-GCM at rest, and owns refresh. A failed refresh marks the connection needs_reauth rather than failing silently, so a stale account is visible instead of mysterious.

What the client holds is the grant. Elaichi serves one organization-wide MCP endpoint, POST /mcp, behind OAuth. There are no per-toolbox URLs and no embedded tokens. Nothing a user pastes into Claude, ChatGPT or Cursor is a connector secret.

A connect URL is a one-time session that carries no token, which is why it is safe to return over MCP. Treating a setup link as a credential is a common mix-up. Editing an encrypted value through the read-back path is refused, and the refusal text is identical whichever side produced it, so a caller cannot probe which side said no.

An organization can supply its own OAuth app per connector. The accepted body is client_id, client_secret and scopes, and nothing endpoint-shaped is representable. It is gated on connector:manage, not connection:manage, so everybody who can delete a connection does not quietly gain the ability to repoint the organization's OAuth app. Across 450+, the storage shape is the same whichever credential the third party hands out.

One limit, stated plainly. Keeping the secret out of the prompt is not protection against prompt injection, and an MCP server cannot offer that, because it never sees a user prompt. What does hold on the endpoint is a permission check per operation, the forbidden classification, output redaction, OAuth scope limits and full audit logging.

What a grant decides, and what it does not

A grant proves who is calling. It does not decide what they may reach. Four OAuth scopes exist: mcp:read, mcp:write, mcp:destructive and mcp:tools. A tool classified forbidden is reachable under no scope. mcp:tools does not replace the ladder, so a connected tool whose method is a delete still needs mcp:destructive. Ahead of all of it, the tool:execute permission gates the whole endpoint. Without it, tools/list comes back empty and a call returns an error naming the permission.

Reach is governed separately, by restrictions. Targets are a role or a user. restriction targets are role or user only; the organization default is the absence of a rule, which means allow everything, because the organization default is the absence of any rule. A user rule replaces role rules rather than layering on them. Blocks beat allows. An allow rule that names nothing denies everything, which is the strictest thing you can express and the trap people hit first.

Blocks match the tool name or the pinned operation. Allows match the pinned operation only, and the reasoning behind that asymmetry is worth reading before you write your first rule.

Who gets a grant in the first place

A grant is only as good as the sign-in that produced it. Elaichi supports Google, GitHub and Microsoft sign-in, email codes, TOTP MFA with single-use recovery codes, and passkeys. Enterprise SAML and OIDC SSO are built in-house, with SCIM v2 for users and groups and group-to-role mapping. Domains are verified with a DNS TXT record, which lets auto-join hand a new member a configurable default role.

Elaichi's own API tokens get the discipline any long-lived secret needs: hashed at rest and shown once, so the value on screen is the only copy. actor_kind is a recorded field rather than an inference, with values including user, system, scim, api_token and ai_assistant. Token traffic stays separable from human traffic without guessing from a user agent.

What the audit trail shows after you cut access

one entry per tool-call attempt, succeeded or failed, and both name the account actually reached, taken from the execution rather than from the intent. After an unexpected change, the first question is which of two connected workspaces the agent wrote to. The record answers it.

Recorded per call: the operation and tool, the connection, the classification, whether it was approved, the outcome, and an error code only. Argument names and counts are logged. Argument values never are.

Two error strings exist for a failed call. The one returned to the caller is derived from the third party's response body, and it is never written anywhere else. The audit record's error is never derived from the request or the response, because audit rows are org-visible, readable by the in-product assistant, and forwarded to whatever destination you configure. Datadog export is implemented. Splunk HEC and Microsoft Sentinel are accepted but not yet delivering.

The trail is append-only and eventually consistent, so a row may take a moment to appear. The read-only Auditor seat is free, so the person reviewing this does not consume a license.

How do you cut an agent's access today

Suspend or remove the member first. That revokes every live grant in the same transaction and is effective on the next call. Then handle the static secrets, which no membership change can touch.

  1. Suspend or remove the member in Elaichi. Every live grant goes with the membership change, so the next call from any client fails.
  2. If the person stays but the reach was wrong, write a restriction against the role or against the user. Allow about two minutes for it to take effect.
  3. Rotate every long-lived key at the issuer, then replace each copy in environment files, CI stores and vendor screens. Nothing in a control plane shortens this step.
  4. Filter the audit trail by actor and time window to see which account each attempt actually reached.

Offboarding runs a preflight that refuses removal while a personal connection is still referenced by a toolbox entry. Transfer it to the organization, a team or another member, or delete it. A private connection is not transferable at all, because a credential only its owner could ever use does not become somebody else's. Unreferenced personal connections are cleaned up. The same sequence against a leaving contractor is written out in the contractor offboarding walkthrough.

When you do not need a control plane for this

One person, one assistant, one personal account, one client. Sign in with OAuth inside the client, and revoke it in the vendor's own settings page when you are done. There is no organization to answer to and no second account to confuse. A single developer's key in a local environment file is the same story.

The shape changes when a second person needs the same access, or when a key has to live somewhere a person is not. The case for waiting is argued in full in when you don't need an MCP gateway yet. If the open question is who should run the server that holds the credential, the cost comparison for self-hosting covers the operational side. The connector catalog lists what each account can be connected as, and the security overview covers residency, customer-managed keys and log tenancy.

FAQ

Frequently asked questions

What is the difference between an OAuth grant and an API key for an AI agent?

An OAuth grant is a record held by the issuer saying that a named client, acting for a named person, may take certain actions. It can be marked revoked, and the agent's next call then fails. An API key is a string whose possession is the authorization. It stays valid until somebody rotates it at the issuer and replaces every copy, and it carries no identity of the person behind the call.

How quickly does revoking an AI agent's access take effect in Elaichi?

Grant revocation, member removal and member suspension are effective on the next call, because Elaichi re-reads the revocation field from the organization store on every single call with no cache. Role membership and restriction changes work differently. They resolve through a 60 second cache plus edge propagation, so they take effect within about two minutes across MCP, the console and REST.

When should an AI agent still use a long-lived API key?

When there is no browser and no human present to complete a sign-in. Nightly jobs, CI steps and code written against an endpoint cannot produce an interactive OAuth consent on a schedule, so a long-lived key is the honest answer. It is also the only option when the third party issues nothing else. Give the key the narrowest scope the issuer offers, plus a named owner responsible for rotating it.

Is a connect URL a credential?

No. A connect URL is a one-time session that carries no token, which is why Elaichi can safely return one over MCP. Visiting it starts the flow that establishes a connection; it does not by itself grant access to anything, and it is not the thing to guard. The credential produced by that flow is held by a separate credential service, encrypted at rest, and never handed to the client.

What does the audit log record about which account an agent used?

Elaichi writes one entry per tool-call attempt, succeeded or failed, and both name the account actually reached, taken from the execution rather than from the intent. Each record holds the operation and tool, the connection, the classification, whether the call was approved, the outcome and an error code only. The actor kind is a stored field with values including user, api_token and ai_assistant. Argument names and counts are logged; argument values never are.

Put agents to work on your own systems

14 days on Gold, no credit card. Start with one app and one team.

Works with
Claude ChatGPT Cursor and any other MCP client, or the Elaichi Agent.
When the trial ends
Nothing is deleted. Connections, roles and the audit log stay where they are, so subscribing picks up exactly where you left off.