Skip to main content

Troubleshooting 'OIDC Token Validation Failed' in GitHub Actions

 Few things halt a deployment pipeline faster than an opaque IAM error. If you are migrating from long-lived Service Account JSON keys to Keyless Authentication (Workload Identity Federation) on Google Cloud, you have likely encountered the infamous OpenID Connect token validation failed or Issuer URI mismatch error.

These errors are notoriously difficult to debug because they occur at the intersection of two massive systems: GitHub's OIDC Provider and Google Cloud's Security Token Service (STS). The error messages often obscure the actual root cause: a mismatch between the claims in the JWT (JSON Web Token) generated by GitHub and the attribute mappings configured in your cloud provider.

This guide details the root cause of OIDC validation failures, provides a method to inspect the raw token on the fly, and offers the Terraform and CLI configurations to resolve the issue permanently.

The Anatomy of a Federation Failure

To fix the error, you must understand the handshake failure. When your GitHub Action requests authentication, the following sequence occurs:

  1. Token Request: The runner requests an OIDC token from GitHub's internal provider.
  2. Token Generation: GitHub issues a JWT signed by its private key. This token contains standard claims (issaud) and custom claims (subrepositoryref).
  3. Exchange: The google-github-actions/auth action sends this JWT to the Google Cloud STS.
  4. Validation (The Failure Point): Google verifies the signature and checks the claims against your Workload Identity Pool Provider configuration.

If the Issuer URL (iss) or the Subject (sub) defined in your GCP Trust Policy does not byte-for-byte match the claims in the incoming token, the handshake is rejected.

The Most Common Culprits

  • Missing Permissions: The workflow lacks id-token: write.
  • Subject Mismatch: The mapped google.subject in GCP does not match the GitHub sub claim (often due to case sensitivity or branch specificities).
  • Attribute Mapping Drift: You are filtering by attribute.repository in GCP, but haven't mapped the repository claim in your provider settings.

Immediate Fix: Debugging the Raw JWT

Stop guessing which claim is failing. The most effective way to troubleshoot is to inspect the exact token GitHub is generating during the workflow run.

Add the following step to your failing GitHub Actions workflow. This script extracts the OIDC token, decodes the payload, and prints the claims.

Note: We use jq to parse the token safely. This step should only be used for debugging and removed in production.

jobs:
  debug-oidc:
    runs-on: ubuntu-latest
    permissions:
      id-token: write # Required for OIDC
      contents: read
    steps:
      - name: Debug OIDC Token
        run: |
          # 1. Request the OIDC token from GitHub's internal provider
          # ACTIONS_ID_TOKEN_REQUEST_URL and ACTIONS_ID_TOKEN_REQUEST_TOKEN are injected by the runner
          ID_TOKEN=$(curl -sLS "${ACTIONS_ID_TOKEN_REQUEST_URL}&audience=google-dep" \
            -H "Authorization: bearer ${ACTIONS_ID_TOKEN_REQUEST_TOKEN}" | jq -r '.value')

          # 2. Decode the JWT header and payload (middle part of the token)
          echo "=== DECODED OIDC CLAIMS ==="
          echo "$ID_TOKEN" | cut -d '.' -f 2 | base64 -d 2>/dev/null | jq .

Analyzing the Output

Run the workflow. The output will look like this:

{
  "aud": "google-dep",
  "exp": 1630000000,
  "iat": 1629999100,
  "iss": "https://token.actions.githubusercontent.com",
  "repository": "my-org/backend-service",
  "repository_owner": "my-org",
  "sub": "repo:my-org/backend-service:ref:refs/heads/main"
}

Compare these values strictly against your Google Cloud configuration. The sub field is particularly sensitive; it includes the repo, the resource type, and the ref (branch/tag).

Solution 1: Correcting Attribute Mappings (Terraform)

The most robust fix is ensuring your Workload Identity Pool Provider explicitly maps the claims you need. A common error is relying on defaults that don't capture the repository or ref correctly.

If you are using Terraform, update your google_iam_workload_identity_pool_provider resource.

resource "google_iam_workload_identity_pool_provider" "github_provider" {
  project                            = var.project_id
  workload_identity_pool_id          = google_iam_workload_identity_pool.main.workload_identity_pool_id
  workload_identity_pool_provider_id = "github-provider"
  display_name                       = "GitHub Actions Provider"
  
  # CRITICAL: Ensure the issuer URI is exact. No trailing slash is standard, 
  # but check your specific error message if it complains about slashes.
  oidc {
    issuer_uri = "https://token.actions.githubusercontent.com"
  }

  # CRITICAL: Map GitHub claims to Google attributes.
  # google.subject is the primary identifier used for IAM binding.
  attribute_mapping = {
    "google.subject"       = "assertion.sub"
    "attribute.actor"      = "assertion.actor"
    "attribute.repository" = "assertion.repository"
    "attribute.ref"        = "assertion.ref"
  }

  # Optional: Restrict access at the Provider level
  # This prevents any repo in GitHub from assuming this identity, 
  # even if they know the pool ID.
  attribute_condition = "attribute.repository == 'my-org/backend-service'"
}

Applying the IAM Binding

Once the mapping is correct, ensure the IAM binding on the Service Account uses the mapped google.subject or specific attributes.

resource "google_service_account_iam_member" "workload_identity_user" {
  service_account_id = google_service_account.deployment_sa.name
  role               = "roles/iam.workloadIdentityUser"

  # Option A: Allow specific branch (High Security)
  member = "principal://iam.googleapis.com/projects/${var.project_number}/locations/global/workloadIdentityPools/${var.pool_id}/subject/repo:my-org/backend-service:ref:refs/heads/main"

  # Option B: Allow entire repository (Easier for PRs/Feature branches)
  # Requires "attribute.repository" to be mapped in the provider above
  # member = "principalSet://iam.googleapis.com/projects/${var.project_number}/locations/global/workloadIdentityPools/${var.pool_id}/attribute.repository/my-org/backend-service"
}

Solution 2: Fixing "Issuer URI Mismatch" via CLI

If you see Issuer URI mismatch, it often means the provider was created with a slight variation in the URL (e.g., https://token.actions.githubusercontent.com/).

You can patch this immediately using the gcloud CLI without tearing down infrastructure:

# 1. Get the current configuration to spot the typo
gcloud iam workload-identity-pools providers describe "github-provider" \
  --project="my-project-id" \
  --location="global" \
  --workload-identity-pool="my-pool"

# 2. Update the issuer-uri to the canonical GitHub URL
gcloud iam workload-identity-pools providers update-oidc "github-provider" \
  --project="my-project-id" \
  --location="global" \
  --workload-identity-pool="my-pool" \
  --issuer-uri="https://token.actions.githubusercontent.com"

Deep Dive: The "sub" Claim Pitfall

The sub (Subject) claim is the most misunderstood component of OIDC in GitHub Actions. It is not static. It changes based on the trigger event.

  1. Push to main: repo:org/repo:ref:refs/heads/main
  2. Pull Request: repo:org/repo:pull_request (Note: This is generic and dangerous if not handled correctly).
  3. Tag: repo:org/repo:ref:refs/tags/v1.0.0
  4. Environment: repo:org/repo:environment:production

If your GCP IAM policy binds to refs/heads/main, and your workflow is triggered by a tag, validation will fail.

Best Practice: Map attribute.repository separately (as shown in the Terraform example above) and use principalSet bindings on the repository attribute. This allows any branch or tag within a trusted repository to authenticate, assuming your internal code review processes are secure.

Edge Case: The id-token: write Requirement

If the error message is generic ("Unable to get OIDC token"), the issue is likely within the GitHub Actions runner itself, not GCP.

Since defaults changed in 2021, workflows do not have permission to request OIDC tokens by default. You must explicitly grant this in your YAML.

permissions:
  contents: read
  id-token: write # MANDATORY for google-github-actions/auth

If you are using reusable workflows, this permission must be present in the caller workflow, or passed down correctly.

Conclusion

OIDC Token Validation failures are rarely about the credentials themselves; they are almost always about metadata alignment. The mismatch lies between what GitHub asserts (the JWT claims) and what Google Cloud expects (the Attribute Conditions).

By isolating the raw token using jq and aligning your Attribute Mappings via Terraform, you eliminate the guesswork. Prefer mapping specific attributes like repository over relying solely on the complex sub string, as this provides a more resilient and readable security posture for your CI/CD pipelines.