
Your agent needs to call Salesforce on behalf of a user. You run the Authorization Code flow (RFC 6749), store the access_token and refresh_token against that user's connected account, and ship. The agent acts on their records. Everything works.
Six months later, the second enterprise customer onboards. Their Salesforce grant goes into the same store. A background job that runs hourly has no customer context in its execution scope. It resolves the first Salesforce token it finds. It updates records in Salesforce.
Not their records. The first customer's.
No exception. No 403. Salesforce accepted the call because the access_token was valid. The delegated grant was legitimate. The agent was authorized — to act for the wrong person, in the wrong org, on data it had no business touching.
The anomaly surfaces six weeks later, in a customer review.
One delegated grant per tool means one answer to every auth question.
get_token("salesforce") returns Alice's grant because there is only one. The OAuth callback has one possible owner. The refresh lock has one key space. Revocation of Alice's Salesforce grant correctly terminates all Salesforce access because Alice's grant was all the Salesforce access. Audit attribution is implicit.
These aren't oversights. They're correct simplifications. The physical reality of one user, one grant per tool, does the work that explicit enforcement would otherwise require.
What changes at the second customer isn't the complexity of the code. What changes is that "valid credential" is no longer the same as "authorized to act." Every layer of the auth stack that relied on those being identical now has to enforce the difference explicitly.
That's the shift. Not a schema migration. A different problem class.
Before naming what breaks at multi-tenant, it helps to be precise about what was always true about agent auth — and invisible at one tenant.
Five structural properties distinguish agent auth from application auth. At one tenant, none of them create visible problems. At N tenants, every one of them does.
An OAuth grant is delegated authority. Alice authorized your agent to act on her behalf in Salesforce. The grant is hers. She can revoke it from her phone at 11pm. Salesforce can invalidate it when she changes her password. Her employer can revoke it as part of offboarding. None of these events notify your agent before the next tool call fails.
In application auth, you issue the session. You control it. In agent auth, you hold a credential whose validity is controlled by three parties you don't own: the user, the OAuth provider, and sometimes the user's employer.
Your database has transactions. It has rollbacks. A wrong write can be compensated.
Salesforce doesn't. Slack doesn't. Linear doesn't. When your agent updates a Salesforce opportunity to "Closed Won" under the wrong user's grant, downstream commission calculations trigger. When it posts to the wrong Slack channel, the message is read. When it creates the wrong Linear ticket, it exists.
A misconfigured agent doesn't return bad data. It acts. In live external systems. Irreversibly. Without raising an exception. Because the access_token was valid and the downstream API accepted the call.
The blast radius of an auth failure in agent systems is not a data inconsistency. It is an action taken in an external system on behalf of someone who never authorized it.
In application auth, the identity chain is short: user authenticates, session is created, request is scoped to that session. Two hops, both visible.
In an agent system, the chain is longer:
Four hops. The human who authorized the action in step 1 is not present at step 4. The LLM that decided to act in step 2 has no direct relationship to the grant from step 1. For any action to be traceable back to its authorization, every hop must be linked — and that linkage must be maintained explicitly by the auth infrastructure, not inferred from ambient state.
This is what connection_id is for. Not tenant_id. Not user_id. An immutable identifier for the specific OAuth grant event that authorizes the entire chain. Every tool call in the audit log references it. Without it, you can prove a valid token was used. You cannot prove the specific authorization that covered the action hadn't been revoked between step 1 and step 4.
Standard auth infrastructure assumes a session: a user is present, they authenticated, requests are scoped to that session. The session ties identity to action.
Background agents have no session. The user who authorized the agent is not present when the agent acts. The job queue that fires at 3am has no ambient identity context. It has whatever was injected into the job payload at dispatch time — and if tenant_id and user_id weren't injected at dispatch, the job has no legitimate way to resolve them later.
This is the temporal gap. The grant was issued on Monday. The agent acts on Thursday at 3am. The auth infrastructure must explicitly maintain the connection between those two moments across potentially days of elapsed time, across job queue restarts, across process boundaries. An architecture that relies on session context to carry identity will silently fall back to the wrong grant — or the first one it finds — when the session is gone.
Whether the token is valid answers one question: can this agent authenticate to Salesforce?
It does not answer a different, more important question: should this agent be able to call salesforce_delete_record in this workflow context?
In application auth, access control governs what an authenticated user can read or write. The user interface constrains which operations are surfaced. The authorization model governs access to resources.
In agent systems, the LLM selects tools dynamically. If salesforce_delete_record is in the tool registry and the token has write scope, the LLM can call it. Whether it should — whether that capability is appropriate for this workflow, this tenant, this level of user consent — is not answered by token validity. It is a separate authorization layer that must be enforced before execution: which tools does this agent have the right to use, in this context, on behalf of this user, at this tenant?
This is not a token problem. It is a capability scoping problem. But it is an auth problem — and at single-tenant, it's invisible, because the agent's tool registry is yours and the blast radius of a wrong selection is local. At multi-tenant, a tool that is appropriate for one tenant's workflow may be catastrophically wrong for another's.
Every structural property above was true for one tenant. Multi-tenancy makes each one a live, concurrent failure mode.
Here is where each structural property lands in code:
These aren't the hard part. They're where the structural problem lands in code.
In single-tenant agent auth, two questions share one answer.
"Does this agent have a valid credential for Salesforce?" Yes. "Is this agent authorized to act on Salesforce?" Also yes — because the credential is Alice's, Alice is the only user, and the scope covers the operation.
At multi-tenant with dynamic tool selection, these diverge completely.
A valid credential answers the first question. It says nothing about the second. An agent can be fully authenticated — holding a live, scoped access_token — and still be operating outside the bounds of what was authorized, if the tool it's calling was never part of Alice's consent, or if Org 1's configuration restricts that capability, or if the workflow context doesn't justify that level of access.
"What the user can't do, the agent can't do" — but also: what the user didn't explicitly consent to delegate, the agent shouldn't be able to select.
The five identity layers that must be tracked explicitly in multi-tenant agent systems:
Collapsing any two of these produces a silent failure. The most common collapse: execution identity and tenant identity sharing a service account with no per-tenant binding. The agent holds a valid credential. The action goes to the wrong org. Salesforce accepts it.
Recommended Reading: Access Control for Multi-Tenant AI Agents covers the three privilege escalation patterns that emerge when these layers collapse.
Everything above is a production correctness problem. What follows is a commercial one.
The first enterprise customer's security team arrives with a questionnaire. Not a feature request. A forensic audit.
These questions require connection_id in every audit log entry from the first connected account. They require per-tenant credential isolation enforced at the schema level. They require revocation events propagated in real time, not discovered on the next failed tool call. They require token validity captured at execution time, not inferred from the fact that the call succeeded.
None of these can be retrofitted onto log entries written before the columns existed. Every tool call entry written without tenant_id and connection_id is a permanent gap. The audit gap is the period under review.
SOC 2 CC6.1 requires correlated evidence that credentials were valid at the time of each action, tied to specific principals. GDPR Article 6 requires every data access to be tied to a lawful basis traceable per action, per user, per tenant. The lawful basis for an agent action is the user's delegated grant.
The teams that built their own auth infrastructure spend the six weeks before enterprise close retrofitting properties the schema was never designed to carry. The teams that didn't spend those six weeks on the product.
The problem in production: you are maintaining live delegated authority relationships across N users' connected accounts, in M external systems with different token lifecycles, each capable of unilateral state changes, across K tenants, with compliance requirements arriving as a step function.
Authentication must be built into the infrastructure. That is what Scalekit is - your agent auth stack.
There is no single-tenant primitive. Every connected account is scoped to a specific user at a specific tenant for a specific provider from the moment of first authorization. get_token("salesforce") is not a valid operation. Resolution is always explicit:
The compound key is structural. Not a convention. Not a best practice to document and hope engineers follow.
Each provider's grant model is handled per its actual behavior:
When Slack changes their token rotation semantics — as they did in 2024 — the change is scoped to Scalekit's Slack connector. Agent code doesn't change. Silent failures don't cascade across tenants.
When Alice disconnects her Salesforce integration, or an IT admin runs an offboarding script, Scalekit receives the provider-side webhook and fires a structured event to your application:
The signal arrives when the authority changes. Not when the next 401 does. No polling. No stale state.
Every execute_tool call emits:
The SOC 2 auditor's question is answerable. So is the GDPR question. The chain from Alice's Monday authorization to Thursday's 3am tool call is intact in the log.
Token storage, refresh scheduling, concurrent locking, provider-specific lifecycle handling, revocation propagation, and audit emission run outside your application code. Credentials never touch the agent runtime.
The build vs. buy analysis covers what it costs to own what Scalekit owns. The short version: it's not one sprint, and the sprint estimate doesn't include Slack changing their token rotation policy six months post-ship.
In SaaS auth, a missing tenant_id filter returns the wrong data. Your application can catch it; your customer can report it; you can write a compensation. In agent auth, a missing tenant_id resolution causes your agent to act — under the wrong user's delegated authority, in a live external system, irreversibly. Salesforce accepted the write because the access_token was valid. Slack posted the message because the token was valid. There is no rollback. The authority backing the action belonged to someone who never authorized it, and the downstream system has no way to know the difference.
From the first background job that runs without a user session present. The moment your agent operates outside a synchronous request context — a scheduled workflow, an event-driven trigger, a queued task — the session that authenticated the user is gone. The only link between "Alice authorized this agent on Monday" and "this job running on Thursday at 3am" is what was explicitly injected into the job payload at dispatch time. If tenant_id, user_id, and a reference to the originating connection_id weren't included at dispatch, the job has no valid path to Alice's grant. Three tools doesn't change this. One background job does.
connection_id is an immutable identifier for a specific OAuth grant event — the record created when Alice completed the authorization flow and your system received her access_token + refresh_token. user_id identifies Alice. connection_id identifies the specific act of Alice granting access on a specific date, with specific scopes, from a specific OAuth app version. The compliance question isn't "did Alice's agent call Salesforce?" It's "was the grant covering that call still in force at execution time, and had Alice's consent not been superseded by a revocation?" Only connection_id links the tool call entry to the grant event that can answer that question. user_id alone cannot — Alice may have multiple Salesforce grants across re-authorization cycles.
Because the LLM selects tools dynamically based on context, not based on what the user explicitly consented to delegate. When Alice authorized your agent, she consented to a set of operations described in the OAuth consent screen. If the tool registry has salesforce_delete_record and the access_token has write scope, the LLM can call it — whether or not deleting records was part of what Alice understood she was authorizing. Tool availability per workflow context, per tenant, per user consent level is an authorization question: what capabilities should this agent be allowed to exercise here? Answering it with "whatever the token's scope covers" is not an authorization policy. It is an absence of one.
invalid_grant means the refresh_token was revoked, expired, or already consumed in single-use rotation. The user's delegated grant is gone. No retry will recover it. The agent must stop, emit a token.refresh_failed event, and surface a re-authorization prompt. A network error means the refresh HTTP request failed transiently; retrying with exponential backoff is correct. Treating invalid_grant as a transient error causes the agent to retry until the backoff budget is exhausted, consuming OAuth quota and leaving the workflow in a partially-executed state. Treating a network error as invalid_grant causes the agent to halt and prompt re-authorization for a grant that is still valid. The distinction requires explicit branching on the OAuth error response, not a generic exception handler.
OAuth Client Credentials (RFC 6749) for org-level automation in systems that support it: Salesforce service orgs, HubSpot, some Google Workspace APIs. It produces an org-scoped access_token with a standard expires_in and a defined refresh path, with no user authorization required. Service accounts (GCP IAM, AWS IAM) for Google Cloud APIs and AWS services. The critical constraint either way: the credential must be bound per tenant. A Client Credentials grant or service account shared across tenants means background jobs for Tenant B can execute under Tenant A's org-level authority with no error raised, because the token is valid and the downstream API accepts it.
The auditor's core question: "For this specific agent action at this specific time, which authorization grant was in force, and was it still valid?" That requires: connection_id linking to the original oauth.authorization_complete event, tenant_id, user_id, active oauth_scope at execution time, and a token_valid_at_execution assertion. The connection_id is what makes the chain traceable. Without it, you can prove a valid access_token was used. You cannot prove the specific grant backing it hadn't been revoked between issuance and use — which is exactly the question SOC 2 CC6.1 asks. Audit entries written without these fields cannot be retroactively attributed. The gap is permanent.
Related reading: