API & Machine Access
Not every caller is a person at a browser. External developers call the product's API, customers connect their own systems, internal services call each other, and AI agents act on behalf of users. Each needs its own identity, credentials that can be scoped and revoked, and permissions no broader than the task requires.
Actors and Credentials
| Actor | Credential | Acts as |
|---|---|---|
| Script or quick integration | Personal API key | The user who created it, limited by scopes |
| Long-running customer integration | Service account key | A non-human member of the organization |
| Third-party or marketplace app | OAuth application | A user who authorized it, limited by consented scopes |
| Internal service | Workload identity: mTLS certificate, client credentials, or cloud-issued token | The service itself |
| AI agent | Registered agent identity plus a delegated token | The agent, on behalf of a user |
Architecture
Rendering diagram…
Diagram source
flowchart LR devs([Developers]) --> portal[Developer portal] portal --> registry[(Applications, keys, agents, scopes)] clients[API clients and agents] --> gateway[API gateway] gateway -->|verify key or token| registry gateway -->|rate limits per client| limits[(Quotas)] gateway --> api[Product API] issuer[Workload identity issuer] -.->|short-lived certificates or tokens| services[Internal services] api <-->|mTLS or signed token| services api -->|"check(actor, action, resource)"| engine[Policy engine] services --> engine
Delegated Access
When a machine acts for a person, as an OAuth app or an AI agent does, it holds a token that names both. The effective permission is the intersection of what the person may do and what the machine was granted. Neither side can expand the other.
Rendering diagram…
Diagram source
sequenceDiagram autonumber actor U as User participant G as Agent or app participant I as Authorization server participant T as Product API U->>I: Approve requested scopes G->>I: Token exchange: client credentials + user's grant, scopes billing:read refunds:request I-->>G: Delegated token (actor: agent, subject: alice, 15 minutes) G->>T: GET /invoices with delegated token T->>T: Allowed: alice can read, agent has billing:read G->>T: POST /refunds amount=900 T-->>G: Requires approval above agent limit T-->>U: Approval request
Internal services face the same problem. When a service calls another with only its own broad credentials, the downstream service cannot tell whose request it is serving, and the intermediary is tricked into using its access for someone who should not have it: the confused deputy.
Rendering diagram…
Diagram source
flowchart LR user([user:bob, viewer]) -->|"export project 7"| api[API service] api -->|"service credentials: full access"| storage[Storage service] storage -->|returns everything| api
OAuth 2.0 Token Exchange (RFC 8693) prevents this. The calling service exchanges the user's token for a new one that names the subject (Bob) and the actor (the API service), is limited to one audience and narrow scopes, and expires in minutes. The user's identity never travels as a plain header that any service could set.
| Situation | Pattern |
|---|---|
| Synchronous call between services | Exchanged token per hop, scoped to the next service. |
| Background job for a user | The job stores who requested it and re-checks that user's permissions when it runs, not only when it was queued. |
| Support staff in a customer account | Time-limited, approved elevation; every action is recorded as staff acting in the customer's organization, never as the customer. |
| AI agent or OAuth app for a user | Delegated token naming the machine and the user, as above. |
Service Policy
Calls between services are deny by default, and the policy is kept in the repository so every new dependency is visible in review:
caller callee operation decision
service:orders billing POST /charges allow
service:orders billing POST /refunds deny
service:support billing POST /refunds allow
any billing GET /health allowDesign Points
- Keys are shown once and stored hashed. Keys carry a recognizable prefix so leaked keys can be detected by secret scanners.
- Scopes map to permissions. A scope such as
invoices:readis a named bundle of product permissions, documented publicly. - Rotation without downtime. Two keys can be active at once during rotation, and every key records when it was last used.
- Rate and spend limits per client. Quotas apply per key, application, or agent, not per IP, so one integration cannot exhaust another's capacity and a looping agent is bounded.
- Agents are registered actors. Each agent has an owner, a description, and an allow-list of tools, so it can be listed, restricted, and disabled.
- Human approval for high-impact actions. Refunds, deletions, and external messages above a threshold wait for a person.
- Admin control. Organization admins can see and revoke every key, authorized app, and agent in their organization.
Across Ownership Scopes
| Scope | Who owns what |
|---|---|
| Managed | The broker issues API clients, machine identities, credentials, tokens, and scopes. |
| Hybrid | The application owns API clients, machine identities, credentials, token issuance, scopes, and policies. |
| Built | Machine authentication, credential management, token issuance, and authorization are owned end to end. |
Watch For
- Keys tied to departed users. Personal keys must stop working when their owner loses access. Integrations that should persist belong to service accounts.
- Over-broad scopes. Offer narrow scopes from the start; widening later is easy, narrowing breaks integrations.
- Shared secrets. A single API key used by every internal service gives no isolation. Each service needs its own identity.
- Network location as identity. "It came from inside the cluster" is not authorization.
- Credential lifetime. Long-lived machine credentials are the most valuable thing an attacker can steal; prefer certificates and tokens that expire in minutes or hours.
- Prompt injection as an authorization problem. Content an agent reads can instruct it to misuse its access. Permissions, not prompts, must be the boundary.
- Shared user sessions. Giving an agent the user's own session makes its actions indistinguishable from the user's and impossible to restrict.
- Delegation chains. Each hop should narrow scope, never widen it, and the chain should be visible in audit records.
- Implicit elevation. An API key must not carry more authority than the user who created it, and must lose it when the user does.