AADSTS70021 no matching federated identity record found, and its two siblings
AADSTS70021 no matching federated identity record found is Microsoft Entra ID saying it received a token from GitHub and could find no federated credential on the application whose issuer, subject and audience all matched it. The same sentence is carried by two other codes that do name which of the three was wrong, so the digits are the diagnosis and the words are shared.

What this error means
A cloud login step fails after the OIDC token has been minted, which means the GitHub half worked. The message begins with a code and then a sentence about a presented assertion. Take the code apart before reading the sentence. The five-digit AADSTS70021 arrives with no detail about what failed to match. The six-digit AADSTS700212 continues ... presented assertion audience and then the audience it saw. The six-digit AADSTS700213 continues ... presented assertion subject and then the subject in quotes, usually with a follow-on sentence telling you to check the credential's subject, audience and issuer against it. The longer codes are far more useful, and a report quoting one of them as the other loses the only distinguishing information in the message.
AADSTS700213: No matching federated identity record found for presented assertion subject 'repo:JeffreyCA/azd-dev-prod:environment:dev'. Check your federated identity credential Subject, Audience and Issuer against the presented assertion.One sentence, three codes
Entra ID is a closed service, so what follows is drawn from Microsoft's published error-code reference and from the text people have recorded in their own runs rather than from any source we can read. Across those reports the pattern is consistent: the words No matching federated identity record found for presented assertion are shared, and the code decides whether anything follows them.
That matters when you go looking for help. A search for the sentence returns results for all three codes mixed together, and advice written for a subject mismatch is useless for an audience one. Strip the sentence and search the code.
| Code | What follows the shared sentence | What it tells you |
|---|---|---|
AADSTS70021 | Nothing beyond a full stop | Something did not match; the message does not say what |
AADSTS700212 | audience and the audience it received | The audience is the mismatch |
AADSTS700213 | subject and the subject in quotes | The subject is the mismatch, and it is quoted for you |
Common causes
The subject changed because the trigger or the environment changed
The largest cause, and usually the result of an edit that looked unrelated. Adding an environment: to a job, deploying from a tag as well as a branch, or moving a job into a reusable workflow all change the generated subject, while the credential still matches the old one. The code that names the subject quotes the new value straight back at you.
No credential exists for this repository at all
The plain case: the application was created, the workflow was written, and the federated credential step was skipped or applied to a different application. In our experience this is what the bare five-digit code most often turns out to be, since it offers no detail to narrow it with.
The issuer was stored with a trailing slash or a typo
A hand-copied URL that looks correct. Microsoft's own CLI documentation carried a sample with a trailing slash, which produced this error for anyone who followed it, and the correction was raised as a pull request against the docs rather than against any code.
The audience is wrong for this cloud
The global value is not universal. A sovereign tenant expects its own audience, and a workflow using the default sends something that tenant will never match. The code that names the audience prints the value it received, which distinguishes this from a subject problem immediately.
How to fix it
Read the code, not the sentence
- Take the digits after
AADSTSand stop reading the shared sentence. - On a code naming the subject, copy the quoted subject verbatim; that is the exact string a credential must carry.
- On a code naming the audience, compare the printed audience against what your tenant expects.
- On the bare five-digit code, you have no detail, so check all three fields of the credential in turn.
Register a credential for the subject the run actually presented
Create the federated credential with the subject taken out of the error message, the fixed GitHub issuer, and the audience your cloud uses. Pasting the subject rather than retyping it removes the whole class of near-miss mistakes.
{
"name": "gha-environment-dev",
"issuer": "https://token.actions.githubusercontent.com",
"subject": "repo:my-org/my-repo:environment:dev",
"audiences": ["api://AzureADTokenExchange"]
}Add one credential per entity you deploy from
A credential matches a single subject, so a branch, a tag and an environment each need their own. Enumerating them once, when the deployment paths are designed, is cheaper than discovering each one at the moment a release fails.
repo:my-org/my-repo:ref:refs/heads/main
repo:my-org/my-repo:ref:refs/tags/v1.0.0
repo:my-org/my-repo:environment:production
repo:my-org/my-repo:pull_requestStop adding permissions to the workflow
- Confirm that the log contains an
AADSTScode; if it does, the token was minted and presented. - That rules out
id-token: writeand every other GitHub-side permission as the cause. - Take the change to the Entra application registration instead.
The subject is generated, and it changes with the trigger
GitHub builds the subject claim from the run: a repository, then a qualifier describing how the workflow was reached. A push to a branch, a tag, a pull request and a run against a deployment environment all produce different subjects, and a job that calls a reusable workflow can add a further claim naming that workflow.
A federated credential matches one subject exactly. So a repository that deploys from main and from a tag needs two credentials, and one that adds an environment: to a job has changed the subject of every run of that job. That last case is the one that catches people, because adding an environment reads as a protection change rather than as an identity change, and the quoted failure above is exactly it: a subject scoped to an environment, against a credential that was not.
| How the workflow was reached | Shape of the subject claim |
|---|---|
| A push to a branch | repo:<owner>/<repo>:ref:refs/heads/<branch> |
| A tag | repo:<owner>/<repo>:ref:refs/tags/<tag> |
| A job naming an environment | repo:<owner>/<repo>:environment:<name> |
| A pull request | repo:<owner>/<repo>:pull_request |
The issuer has to match character for character
The issuer is the least variable of the three and still accounts for a recurring class of this failure, because it is copied by hand and a URL can gain a trailing slash without looking any different. A pull request against Microsoft's own Azure CLI documentation was opened for precisely that: a sample that created a federated credential with a trailing slash on the issuer, which made the credential unusable from a GitHub Actions workflow and produced this error.
There is nothing clever to do about it. The value is fixed, it has no trailing slash, and the fix is to compare the stored credential against it rather than against what you remember typing.
https://token.actions.githubusercontent.comThe audience is not the same everywhere
The audience for Entra ID in the global cloud is api://AzureADTokenExchange, and that is what the login actions send by default. A sovereign cloud does not use the same value: one public report records a script failing in Azure China against exactly this sentence, under the code that names the audience, because the tenant there expects api://AzureADTokenExchangeChina.
So an audience mismatch has two quite different origins. Either the workflow is sending a value the credential does not carry, or the tenant is one where the default value is not the right one. The code that names the audience prints what it received, which settles which of those you have.
What this failure proves about the GitHub side
It proves the GitHub half succeeded. A token was requested, minted and presented, which means the job had the id-token: write permission, the runner had the request variables, and the token endpoint answered. Everything before the exchange worked.
That is worth saying because the two failures are often triaged together. If the OIDC token never existed you would be reading a client-side message from @actions/core instead, with no AADSTS code in it at all: GitHub Actions OIDC failed to get ID token covers that side. Adding permissions does not move this error.
Why there is no recorded run on this page
Reproducing this would mean standing up an Entra application, registering a credential we knew to be wrong, and recording the rejection, which would teach a reader our tenant identifiers and nothing about theirs. It would also be a recording of a closed service that we could describe but not explain: we can see what Entra returned, never why. The three codes and their continuations are quoted from reports by people who hit them on their own tenants, and the claims about what Entra checks are scoped to Microsoft's own documentation.
How to prevent it
- Treat a change to a job's environment, trigger or reusable-workflow structure as a change to the identity it presents.
- Register credentials for every deployment path at the time the paths are created.
- Paste the issuer from documentation and check for a trailing slash before saving.
- Record which audience your tenant expects alongside the client and tenant identifiers, so a sovereign cloud is not a surprise.
Frequently asked questions
What is the difference between AADSTS70021, AADSTS700212 and AADSTS700213?
audience and subject respectively and print the value that was presented, which makes them far easier to act on.Does this error mean my workflow is missing id-token: write?
@actions/core and no AADSTS code in it at all.Why did this start after I added an environment to the job?
Is the audience always api://AzureADTokenExchange?
api://AzureADTokenExchangeChina. The code that names the audience prints what was received, which tells you which side to change.Related guides
References
- Microsoft Entra: identity platform error code reference
- Microsoft Entra: configure a federated identity credential to trust an external provider
- GitHub Docs: OpenID Connect concepts, including how the subject claim is formed
- Azure/azure-dev#5473: the subject-naming code quoted here, with an environment-scoped subject
- Azure/azure-cli#25178: a documented issuer with a trailing slash producing the bare code
- GitHub Actions documentation