Share via

Title: AzureCLI@2 with addSpnToEnvironment: true caches $idToken for its full 24h lifetime instead of issuing a fresh one per pipeline run

Liu He 5 Reputation points Microsoft Employee
2026-04-21T13:45:24.22+00:00

Affected Service: Azure DevOps Pipelines — AzureCLI@2 task with Workload Identity Federation (OIDC)

Severity: High — breaks scheduled pipelines reliably


Summary

When using AzureCLI@2 with addSpnToEnvironment: true and a Workload Identity Federation (OIDC) service connection, the $idToken is obtained once and then cached for its full 24h+5m lifetime. All subsequent pipeline runs — whether scheduled or manual — receive the same cached token until it expires. The task itself authenticates to Azure successfully each time (using a fresh internal token), but the $idToken exposed to the user script is always the cached one from ~24 hours ago. For a pipeline, which is triggered every 24 hours, it means a soon-to-expire $idToken is passed to later steps.


Repro Steps

  1. Create a service connection using Workload Identity Federation (OIDC)
  2. Create a pipeline with AzureCLI@2 + addSpnToEnvironment: true
  3. Run the pipeline — succeeds, $idToken is fresh (e.g., nbf=01:04 UTC)
  4. Run the pipeline again any number of times during the next 24 hours
  5. Decode $idToken in every subsequent run — it always has the same nbf/exp from step 3

Pipeline snippet to reproduce:

- task: AzureCLI@2
  inputs:
    azureSubscription: 'my-oidc-service-connection'
    addSpnToEnvironment: true
    scriptType: bash
    scriptLocation: inlineScript
    inlineScript: |
      echo "OIDC token obtained at: $(date -u +%Y-%m-%dT%H:%M:%SZ)"
      echo "Token length: ${#idToken}"
      # Decode JWT payload to inspect nbf/exp claims
      echo "$idToken" | cut -d. -f2 | tr '_-' '/+' | base64 -d 2>/dev/null | python3 -c "
      import sys, json, datetime
      t = json.load(sys.stdin)
      nbf = datetime.datetime.utcfromtimestamp(t.get('nbf',0)).isoformat() + 'Z'
      exp = datetime.datetime.utcfromtimestamp(t.get('exp',0)).isoformat() + 'Z'
      iss = t.get('iss','')
      print(f'Token nbf={nbf} exp={exp} iss={iss}')
      " 2>/dev/null || echo "Could not decode token claims"
      echo "##vso[task.setvariable variable=OIDC_TOKEN;issecret=true]$idToken"

The token was first obtained during a nightly scheduled run at ~01:04 UTC. All subsequent runs — including manual runs hours later — receive the same cached token:

OIDC token obtained at: 2026-04-21T10:58:23Z Token length: 1527 Token nbf=2026-04-21T01:04:56Z exp=2026-04-22T01:09:56Z iss=***

The AzureCLI@2 task succeeds in all runs (it authenticates to Azure correctly using its own internal mechanism), but $idToken is always the same cached value from the first run. When used as client_assertion for downstream Entra ID token requests after the token expires, it fails with (nightly run from 2026-04-21, used the cached value from 2026-04-20):

2026-04-21T01:15:07.727+00:00 ERROR Token error Unauthorized: error=invalid_client desc=AADSTS700024: Client assertion is not within its valid time range. Current time: 2026-04-21T01:15:07.6888766Z, assertion valid from 2026-04-20T01:04:59.0000000Z, expiry time of assertion 2026-04-21T01:09:59.0000000Z. Review the documentation at https://learn.microsoft.com/entra/identity-platform/certificate-credentials . Trace ID: 4b12cc4e-c3e1-46db-8824-01039e9c1a00 Correlation ID: 76f0970f-e2e9-4ac4-85b1-ffb0424c000c Timestamp: 2026-04-21 01:15:07Z cid=76f0970f-e2e9-4ac4-85b1-ffb0424c000c

Azure DevOps

1 answer

Sort by: Most helpful
  1. Alex Burlachenko 22,120 Reputation points MVP Volunteer Moderator
    2026-05-25T09:30:20.7266667+00:00

    Liu He hi & thanks for join me here at Q&A portal,

    AzureCLI@2 task bug with the exposed $idToken, not with Azure login itself. The task login can succeed while the environment variable keeps returning a stale OIDC token. Do not reuse $idToken from addSpnToEnvironment for downstream token exchange until Microsoft fixes the task behavior.

    AzureCLI@2 docs say addSpnToEnvironment: true exposes servicePrincipalId, tenantId, and either servicePrincipalKey or idToken to the script environment. https://learn.microsoft.com/en-us/azure/devops/pipelines/tasks/reference/azure-cli-v2?view=azure-pipelines

    Your repro is strong because the task authenticates successfully, but the decoded $idToken keeps the old nbf and exp. That means Azure DevOps is likely caching the token exported to the script, while the task internally gets a fresh token for its own az login flow.

    dont pass $idToken to later steps as client_assertion. Request a fresh OIDC token inside the step that needs it, or keep the downstream token exchange inside the same AzureCLI@2 step immediately after login. If u are using Terraform, use the newer OIDC refresh variables flow instead of manually passing a cached assertion. Microsoft describes the refresh approach https://devblogs.microsoft.com/devops/introducing-azure-devops-id-token-refresh-and-terraform-task-version-5/

    Open a Developer Community issue or Azure DevOps support ticket with the exact repro, task version, agent OS, hosted vs self-hosted agent, service connection ID, and decoded token nbf/exp values from multiple runs. https://developercommunity.visualstudio.com/t/Azure-DevOps-Pipelines-%E2%80%94-AzureCLI2-OIDC/11079123

    This is not Entra rejecting a valid token randomly. Entra is correct: the assertion is expired. The bug is that the pipeline keeps handing u the old assertion.

    rgds,

    Alex

    &

    If my answer was helpful pls mark it and additional thx if u follow me at Q&A portal
    

    Was this answer helpful?

    1 person found this answer helpful.
    0 comments No comments

Your answer

Answers can be marked as 'Accepted' by the question author and 'Recommended' by moderators, which helps users know the answer solved the author's problem.