API keys are the most common way for code to authenticate with another service.
They're simple, but they come with some well-known tradeoffs.
Since late 2025, npm has revoked every classic token, Vercel has announced it will label Marketplace integrations that don't support OIDC "Not secure", and Anthropic and PlanetScale have both shipped ways to authenticate without one. They're all moving toward OIDC workload identity. We recently launched OIDC workload identity on Trigger.dev too.
OIDC (OpenID Connect) is an identity standard built on OAuth 2.0. Workload identity borrows its signed tokens and its way of publishing keys, for running code like a CI job or a background task.
With OIDC workload identity, the platform your code runs on vouches for it with a signed token that expires in minutes, so the services it calls don't need to give it an API key.
In this post, we'll cover how it works: who issues the token, what's in it, and how a service checks it.
Common limitations of API keys
OIDC solves some of the common limitations of API keys.
Many services let you set an expiry on a key, but in practice most keys are long-lived, and plenty never expire at all. They also tend to be broader than the job needs, because narrow keys take more setup and some keys can't be narrowed at all. Revoking a key is hard when lots of services use it, and sometimes there's no record of where it's used.
With AI agents, keys end up in even more places. An agent that runs commands can read any environment variable or file your code can. When it reads a .env file or runs env to debug something, the key goes into its context, gets sent to the model provider, and ends up in the session's logs.
Short-lived credentials make all of this much less of a problem. A token that's valid for minutes and meant for one service is worth far less than a key that lasts for months.
Example: authenticating an S3 read from CI
Let's imagine a CI job needs to read files from an S3 bucket. It can do that in two ways.
With an API key
The first uses an API key stored in the job's secrets. For AWS, that's an access key ID and secret:
AWS_ACCESS_KEY_ID=AKIA...AWS_SECRET_ACCESS_KEY=wJalr...
Those two lines work from any machine, for anyone who has them, until somebody deletes the key.
With OIDC workload identity
The second is a token the job asks for when it needs it:
{ "iss": "https://token.actions.githubusercontent.com", "sub": "repo:octo-org/octo-repo:ref:refs/heads/main", "aud": "sts.amazonaws.com", "iat": 1762531200, "exp": 1762531500}
It names one branch of one repository and the service it's meant for, and it expires after 5 minutes.
The job sends this token to AWS STS, the service that hands out temporary AWS credentials. AWS checks it against a trust rule you set up in AWS and returns credentials the job uses to read the bucket.
Who issues the token and who checks it
When code uses OIDC to call a service, three parties are involved:
- Issuer: the platform running your code (GitHub Actions, Kubernetes, Vercel, Trigger.dev). It signs the tokens.
- Job: the code that needs access, like a CI run, a Kubernetes pod or a Trigger.dev task. It asks the issuer for a token and sends it to the relying party.
- Relying party: the service you want to reach (AWS, npm, your own API).
They're roles, so one platform can be the issuer in one exchange and the relying party in another.
Trust is set up once, on the relying party's side. For example, you tell AWS that tokens from GitHub, for your repository, can use a particular IAM role.
The issuer signs each token with a private key that never leaves it, and publishes the matching public key.
The relying party checks the token's signature against the public key. The issuer and the relying party never share a secret.
The job and the issuer run on the same platform, which is how the issuer knows which job is asking. The relying party runs somewhere else and only ever sees the token.
This is how npm trusted publishing works. You tell npm to trust GitHub's tokens for your repository and release workflow, and that's the whole setup. npm has built-in support for GitHub's issuer, and there's no call between them about your job. Only GitHub can produce the signature, and anyone with GitHub's public key can check it.
What's in an OIDC token
The token the job gets from the issuer is a JSON Web Token (JWT): a header, a JSON payload of claims and a signature, joined with dots.
The claims that matter most are:
iss(issuer) is who made the token.sub(subject) is who the token is about, and it's the claim your rule usually matches.aud(audience) is who the token is for, so a token meant for AWS STS is rejected by a service expecting a different audience.
The header also carries a kid (key ID), which says which of the issuer's keys signed the token.
How a service checks a token
First, the relying party needs the issuer's public key. It takes the issuer you configured in your trust rule and fetches {issuer}/.well-known/openid-configuration, which says where that issuer publishes its public keys. The token's kid says which of those keys signed it. With that key, the relying party checks:
- That the token was signed with an algorithm you expect, like RS256.
- The signature, using that key.
- That
issmatches the issuer you configured, exactly. - That
audis you. - That the token hasn't expired.
- That
suband any other claims match your rule.
If your rule checks aud but not sub, it trusts every token from that issuer. For a shared issuer like GitHub's, that means any repository on GitHub, including one an attacker creates five minutes from now.
Most relying parties then hand back their own short-lived credential. AWS does it with AssumeRoleWithWebIdentity, which returns credentials that expire after an hour by default.
Issuing tokens vs accepting them
"OIDC support" can mean one of two things.
A platform that issues tokens gives you outbound workload identity: code running there can prove itself to other services. GitHub Actions, Kubernetes, Vercel Functions and Trigger.dev tasks work this way.
A platform that accepts tokens gives you inbound workload identity: it trusts code running somewhere else. AWS, npm, PyPI, PlanetScale and the Claude API work this way.
A Trigger.dev task can reach AWS without an API key because Trigger.dev issues tokens and AWS accepts them.
Using OIDC with Trigger.dev
Every Trigger.dev organization has its own issuer, https://oidc.trigger.dev/<your-org-slug>. To connect a service, add this issuer URL and your environment's subject to the service's settings. You'll find both on the OIDC page in the dashboard.
In AWS, for example, the issuer is added as an identity provider with sts.amazonaws.com as its audience, and the subject goes in the role's trust policy.
Then a task asks for a token when it needs one:
import { auth } from "@trigger.dev/sdk";const { token } = await auth.getOIDCToken({ audience: "sts.amazonaws.com" });
Each time a run executes, including each retry, is called an attempt. Each attempt gets its own run token, which auth.getOIDCToken uses for you.
Only the attempt that's currently running can get an OIDC token, and a run token from a finished attempt can't get one. No other key or login can get OIDC tokens for your tasks.
Run tokens are signed with a different key from OIDC tokens, and that key is never published, so a run token can't pass as an OIDC token. An OIDC token can't call Trigger.dev's own API either.
Tokens expire after 5 minutes. A long-running task can ask for a new one at any time, and the credentials you get back usually last longer. AWS credentials last an hour by default.
The subject names your organization, project and environment, like org:…:project:…:env:PRODUCTION:prod. Copy it exactly from the dashboard into the role's trust policy, so only tasks in that environment can assume the role.
Conclusion
Many platforms and services support OIDC workload identity now, and Trigger.dev tasks can use it to reach AWS, the Claude API and any other service that accepts OIDC tokens.
Workload identity is available now on Trigger.dev Cloud, with the SDK and CLI at v4.8.3 or later. To try it, follow the docs.





