Required claims for GitHub flexible federated identity credentials

· 4 min read

Scenario

  • You have a large GitHub organisation with many repositories, all needing the same Azure access in some way, shape or form.
  • Traditional federated identity credentials match the subject claim as an exact string, and Entra allows only 20 per application.
  • Flexible federated identity credentials replace that exact string with an expression, so one credential can cover them all
claims['sub'] matches 'repo:contoso/contoso-repo:ref:refs/heads/*'

The example above is straight from the Microsoft documentation. It’s also rejected (fun) when tried, so I had to do some light digging.

What the docs claim

The concept page states, for the GitHub issuer:

Claim sub supports operators eq and matches Claim job_workflow_ref supports operators eq and matches

and gives three examples, all built from those two claims. Creation goes through Microsoft Graph beta or the portal only. Azure CLI, Azure PowerShell and the Terraform providers all error on flexible credentials.

Every one of those examples returns the same error (fun, fun, fun):

The FederatedIdentityCredential.ClaimsMatchingExpression.Value is invalid.
Rule exception: Expression configured for issuer 'https://token.actions.githubusercontent.com'
either lacks all required claims or contains unallowed claims.

Down the rabbit hole

The message names neither the missing claim nor the rule, so that led to me (and my friend Claude) probing 92 combinations across eight dimensions (side note: I thought there were only three dimensions, but I guess Claude knows better…).

This was done one variable at a time, with a plain-subject control in every run. That control is what let us rule out other issues such as permissions, the app object, the beta endpoint and pure skill issues on our behalf.

We made some headway by using a deliberately invalid test. claims['actor'] eq 'x' returns a different message, naming the claim, which meant the claim table was observable by brute force. Enumerating 24 GitHub OIDC claims against both operators turned up exactly six permitted pairs, two of which appear nowhere on the flexible-credentials page:

  • repository_id
  • repository_owner_id
  • Both with eq only.

And then, of course, hindsight is 20/20 once you know what to look for. Microsoft does document this, in precisely one place (that I could find), a how-to guide about something else entirely, Migrate GitHub Actions federated credentials to immutable subjects.

For GitHub, a flexible federated identity credential must match the sub claim and one or both of the following additional claims: repository_idrepository_owner_id … These additional claims are required regardless of whether sub uses a name-based, customized, or immutable format.

The feature’s own page neither says this nor links to it. Neither does the other how-to guide, Set up a Flexible Federated identity credential, whose GitHub example is rejected for exactly the same reason:

claims['sub'] matches 'repo:contoso/contoso-repo:ref:refs/heads/*' and claims['job_workflow_ref'] matches 'contoso/contoso-prod/.github/workflows/*.yml@refs/heads/main'

So of the three Learn pages covering this feature that I found, one documents the rule and two publish examples that plainly don’t work.

What works

The expression must contain sub, and at least one of repository_id or repository_owner_id compared with eq. Neither half alone is enough, and job_workflow_ref cannot substitute for either.

ClaimeqmatchesOn the feature’s docs page
subyesyesyes
job_workflow_refyesyesyes
repository_idyesnono
repository_owner_idyesnono

Everything else is rejected for both operators, including the name-based repository and repository_owner. The id-based ones being eq-only makes a lot of sense when you think about it.

sub is the authorisation predicate, and the immutable numeric id is an anti-spoofing anchor of sorts. A name wildcard like repo:contoso/contoso-* is claimable by anyone who can create a matching repository name. It also stops matching after an organisation rename, while repository_owner_id supposedly remains stable across renames and is not reused.

Practical example

Get the organisation id:

gh api orgs/contoso --jq .id

Then create the credential. Note the issuer has no trailing slash, and audiences must be a real JSON array:

$appObjectId below is the Microsoft Entra application object ID, not its application (client) ID.

$body = [ordered]@{
    name      = 'contoso-all-repos'
    issuer    = 'https://token.actions.githubusercontent.com'
    audiences = [string[]]@('api://AzureADTokenExchange')
    claimsMatchingExpression = [ordered]@{
        # The wildcard right after the org name absorbs the optional '@<owner_id>' that GitHub's
        # immutable subjects add, so this matches both the old and new subject formats.
        value = "claims['repository_owner_id'] eq '13371337'" +
                " and claims['sub'] matches 'repo:contoso*/contoso-*:ref:refs/heads/*'" +
                " and claims['job_workflow_ref'] matches 'contoso*/*/.github/workflows/deploy.yml@refs/heads/*'"
        languageVersion = 1
    }
}
$f = Join-Path $env:TEMP 'fic.json'
$body | ConvertTo-Json -Depth 5 | Set-Content $f -Encoding utf8NoBOM
az rest --method POST `
    --url "https://graph.microsoft.com/beta/applications/$appObjectId/federatedIdentityCredentials" `
    --body "@$f"
Remove-Item $f

So now you can use one credential for every repository in the organisation covered by all three rules in the expression. There are three independent constraints in this example:

  1. The org pinned by an id that cannot be forged or renamed
  2. The repository name prefixed
  3. Only jobs using deploy.yml as a reusable workflow able to mint a token

That last clause is optional. job_workflow_ref identifies the reusable workflow used by the job, not the calling workflow.

For GitHub.com, since 15 July 2026 GitHub emits an immutable subject (repo:contoso@13371337/repo@456:ref:…) for repositories that are created, renamed or transferred. This does not apply to GitHub Enterprise Server. Check the configured prefix for a repository with gh api repos/contoso/repo/actions/oidc/customization/sub --jq .sub_claim_prefix. In my testing, the adjacent use_immutable_subject flag read false on repositories whose prefix already carried the IDs.

Sources