Skip to content
Latchkey LogoLatchkey home

GitHub Actions id-token write permission not granted to the job

A GitHub Actions id-token write permission not granted problem is almost never a missing line; it is a line in a place that does not reach the job that needs it. The failure text is the same in every case, so the workflow file rather than the log is what distinguishes them.

Actions log, quoted from claudioorjunior/integra-escala#36
Failed to get OIDC token: Error message: Unable to get ACTIONS_ID_TOKEN_REQUEST_URL env variable
Error: Unexpected error

01Symptom

What this error means

An OIDC login fails and the workflow visibly contains id-token: write. Adding it again somewhere else changes nothing, and the job summary lists a permission set that does not include it. The message is identical to the one you get from having no permission at all, which is why the file gets re-read several times before anyone checks what the job was actually granted.

The message is the same as its sibling, deliberately

This page and the one on the missing request URL describe the same failure text, because there is only one. The toolkit reads an environment variable, finds nothing, and throws; it cannot tell you why the variable was absent. That sibling page covers the mechanism, the wrapper prefixes and the two variables. This one is about the thing that is actually different between repositories, which is where the permission was written.

If you have already established that the job simply never asked for the permission, the repair is one line and belongs on the sibling. Come here when the line demonstrably exists and the job still does not have it.

02Cause

Common causes

One id-token permission written at three levels and which jobs end up holding it
A job-level permissions block replaces the workflow-level one rather than merging with it, so a job that declares any scope must declare every scope it needs.

The job has its own permissions block that omits id-token

The common case, and it usually arrives through a well-intentioned change. Someone tightens a job by giving it contents: read, which silently discards everything the workflow level was providing, including the OIDC permission that another part of the job depends on.

The permission is inside a reusable workflow rather than at its caller

The called file is correct in isolation and cannot help itself. Every calling job has to declare the permission, so one shared workflow generates the same report from several repositories, none of which contains the fix.

The login happens in a different job from the one holding the permission

Splitting build and deploy into two jobs moves the OIDC login without moving the block, especially when the permission was written at a job that no longer performs the login. Nothing warns, because both jobs are individually valid.

An organization or enterprise policy withholds the scope

Rare but real, and the only case where the file is genuinely correct and unfixable locally. The job requests the permission and is granted less, which the run's permissions listing shows and the workflow file cannot.

03Fix

How to fix it

Compare what was requested with what was granted

  1. Open the failing job and read the permissions it was given, rather than the ones the file asks for.
  2. If id-token is absent there but present in the file, find which block actually applied to this job.
  3. If the job has its own permissions block, that block is the complete list and needs every scope the job uses.

Put the whole list on the job that logs in

When a job declares any permissions, declare all of them. This is the placement that does not depend on anything above it.

.github/workflows/deploy.yml (illustrative)
jobs:
  deploy:
    runs-on: ubuntu-latest
    permissions:
      id-token: write
      contents: read
    steps:
      - uses: actions/checkout@v5
      - uses: aws-actions/configure-aws-credentials@v5
        with:
          role-to-assume: arn:aws:iam::123456789012:role/deploy
          aws-region: eu-west-1

Declare it at the call site for a reusable workflow

The calling job is the only place that can grant it. Write it there even when the called workflow also declares it, since the called file can only narrow what it was given.

.github/workflows/release.yml (illustrative)
jobs:
  call-deploy:
    permissions:
      id-token: write
      contents: read
    uses: my-org/shared/.github/workflows/deploy.yml@v3

Keep the login in the job that holds the permission

When jobs are split, move the block with the step that mints the token rather than leaving it where it was. Credentials obtained by OIDC do not cross between jobs anyway, so each job that authenticates needs its own permission.

Where the block reaches and where it does not

Permissions are computed per job, and a job-level block is a replacement rather than an addition. That single rule explains most of these.

How the file is writtenWhat the job ends up holding
id-token: write at the workflow level only, job declares nothingThe workflow block, so the job holds it
id-token: write at the workflow level, job declares contents: readOnly contents: read, because the job block replaced the workflow one
id-token: write on the job itselfThe job block, which is the reliable placement
id-token: write inside a called reusable workflowAt most what the caller's job held, so usually nothing

The caller decides for a reusable workflow

A called workflow cannot grant itself more than the caller had, so writing the permission inside the reusable file achieves nothing when the calling job did not hold it. GitHub is explicit for workflows outside your own organization: set the permissions setting for id-token to write "explicitly at the caller workflow or job level", which "ensures the OIDC token is only available to intended caller workflows".

The practical consequence is that the file you have to edit is not the file that failed. A team that owns a shared deployment workflow will keep receiving reports about it that can only be fixed in the repositories calling it, which is worth documenting next to the workflow.

Why there is no recorded run on this page

What distinguishes this page from its sibling is not observable in a log at all: it is the shape of a workflow file, and every log for every placement is byte for byte the same. Recording a run would produce the same line the sibling already quotes, with nothing in it to indicate which of the four placements above was in effect. The evidence that does discriminate is the run's own permissions listing in your repository, and GitHub's published rule for callers, and neither is something a run of ours could stand in for.

04Prevent

How to prevent it

  • Treat a job-level permissions block as a complete list, and review it as one whenever a job gains a step.
  • Document at the top of a reusable workflow which permissions the caller must grant.
  • Put the permission on the job that performs the login, not on the workflow, so splitting jobs makes the loss obvious.
  • Read the granted permissions in the run summary before editing the file a second time.

Frequently asked questions

How is this different from the missing request URL page?
The failure text is the same, because there is only one message. That page explains the mechanism: which environment variable is read, in what order, and what the toolkit wraps around the throw. This page is for when the permission is visibly in the file and the job still does not hold it, which is a question about placement rather than about the message.
Does a job-level permissions block merge with the workflow-level one?
No, it replaces it. A job that declares any permissions holds exactly what it declared, so adding contents: read to a job silently removes an id-token: write that the workflow level was supplying. This is the single most common reason the line is present and the job does not have it.
Why does declaring it in the reusable workflow not work?
Because a called workflow cannot hold more than its caller. GitHub instructs you to set id-token to write explicitly at the caller workflow or job level, so that the OIDC token is only available to intended callers. The file that must change is the one doing the calling.
The file looks right and the job still lacks the permission. What now?
Read the run's permissions listing and compare it with the request. If the job asked for the scope and was granted less, no edit to the workflow will change it and the next question is an organization or enterprise policy, which someone with those settings has to answer.

References

Deploy jobs are the longest jobs you pay for. Latchkey runs them at $0.0025/min at 2 vCPU against $0.006. Start free → 30-day trial · No credit card