A deployment job needs both permission to run and an identity that a provider can trust. A GitHub environment can supply deployment gates and scoped configuration. OIDC can supply short-lived job identity so a provider can issue narrowly authorized credentials without a long-lived cloud key stored in the repository. These mechanisms complement one another; neither defines the entire deployment policy alone. This guide requests and inspects identity claims without exchanging them for cloud credentials or deploying anything. Its local exercise models claim matching only, not JWT verification.
Create an environment policy before the job uses it
Choose an environment name such as staging and configure its intended protections in the practice repository before running the demonstration. Depending on feature availability, the policy can include reviewers, branch or tag restrictions and environment configuration. Verify support for your plan and repository visibility. A name in workflow YAML is not evidence that the desired protections exist. Review the actual environment settings and observe a test run's gate behavior.
Decide which revision is eligible to deploy and who approves it. A workflow dispatched from an unexpected branch should not automatically gain production authority just because it names production as its environment. Keep the environment's branch policy, workflow permissions and provider trust conditions consistent. Record the artifact or revision being promoted so the reviewer knows what approval means. A useful gate approves a concrete deployment candidate, not an unexplained run that may later rebuild different content.
Separate identity issuance from provider authorization
id-token: write permits the job to request an OIDC identity token. It does not grant a cloud role or package-write capability by itself. The external provider must validate the token and accept its claims under a configured trust relationship. The provider then issues credentials whose permissions determine the operations allowed. Keep the requesting permission, identity claims, trust conditions and resulting role capabilities as four explicit parts of the design.
A production provider checks the issuer's signature and supported verification rules, token lifetime, audience and the subject or other accepted claims. Merely decoding a JWT payload does not perform those checks. The example decodes a token received from GitHub's runner endpoint only to show a few nonsecret claims; it is not a verifier you should copy into a cloud authentication service. Never print the bearer token or publish it as an artifact. Inspection output should reveal the policy inputs without revealing reusable credential material.
Match the actual subject contract
Inspect your repository's actual subject format rather than copying a legacy string unexamined. GitHub's current documentation describes immutable owner and repository identifiers in the default subject for newly created GitHub.com repositories, with older and customized configurations potentially using other formats. When the job references an environment, that environment context affects its subject. Match the precise issued value or supported claim conditions in your provider and maintain them deliberately when repository identity or policy changes.
An environment subject does not automatically encode every branch restriction you intended. Apply the corresponding environment policy or additional supported provider conditions. Avoid a broad wildcard that trusts every repository merely because all tokens share GitHub's issuer. Also choose the audience expected by the intended provider exchange; an identity token minted for one consumer should not be treated as interchangeable with another. The demonstration uses example.invalid as an intentional audience and never submits the token to that domain.
Review credential use after the exchange
Once a provider issues a short-lived role session, its permissions still matter. Give staging a bounded role appropriate to staging resources rather than reusing unrestricted production administration. Keep the trusted deployment job's code and inputs under review, and avoid executing contribution-controlled scripts with the resulting credentials. Short lifetime limits exposure duration; it does not make an overpowered role safe during that lifetime.
Log the nonsecret revision, environment and result needed to diagnose a deployment. Plan for failed exchanges and rejected gates with clear errors instead of falling back silently to a stored long-lived key. Provider-specific setup differs, so use its official GitHub OIDC guide for the real exchange and verification contract. Test both accepted and rejected contexts before production use. This includes an incorrect audience, another repository and an unauthorized environment or branch, not only the happy path.
Sources and prerequisites
Review Actions, security and teams. Consult GitHub's OIDC reference, environment model, environment management and secure-use guidance.
GitHub Environments and OIDC Deployment Identity Summary
- Configure environment gates and provider trust as complementary policies.
- Requesting identity is separate from receiving an authorized cloud role.
- Inspect the actual subject contract and never confuse decoding with verification.
GitHub Environments and OIDC Deployment Identity Examples
Each example shows the code, the output it produces and what each part does.
Inspect selected OIDC claims in an environment job
Create and configure staging first, then save this complete workflow as .github/workflows/oidc-inspect.yml. It requires a current GitHub.com hosted runner with Node.js18+ and uses the runner's documented OIDC request endpoint. For a repository using the illustrated immutable subject, the final lines have the following shape. Actual identifiers and customized subjects differ.
The workflow requests identity and prints only the selected claims. It does not verify a JWT locally, exchange credentials or deploy a service. The expected subject is illustrative; use the actual issued value and current provider documentation when configuring trust.
name: Inspect deployment identity
on: workflow_dispatch
permissions: {}
jobs:
inspect:
environment: staging
permissions:
id-token: write
runs-on: ubuntu-latest
steps:
- name: Inspect nonsecret identity claims
run: |
node --input-type=module <<'NODE'
const endpoint = process.env.ACTIONS_ID_TOKEN_REQUEST_URL;
const requestToken = process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN;
if (!endpoint || !requestToken) throw new Error('OIDC request unavailable');
const url = new URL(endpoint);
url.searchParams.set('audience', 'https://example.invalid/deploy');
const response = await fetch(url, {
headers: { Authorization: 'Bearer ' + requestToken },
signal: AbortSignal.timeout(10000)
});
if (!response.ok) throw new Error('OIDC request failed: ' + response.status);
const body = await response.json();
if (typeof body.value !== 'string') throw new Error('Missing identity token');
const parts = body.value.split('.');
if (parts.length !== 3) throw new Error('Unexpected token structure');
const claims = JSON.parse(Buffer.from(parts[1], 'base64url').toString('utf8'));
console.log('audience:', claims.aud);
console.log('subject:', claims.sub);
NODEaudience: https://example.invalid/deploy
subject: repo:demo-org@123456/demo-repo@456789:environment:stagingGitHub Environments and OIDC Deployment Identity in Real Projects
Check OIDC claims against a deployment policy
Run this complete Node.js exercise with fabricated claims. It models three explicit policy fields after a hypothetical verified-token step. The function does not validate signatures or lifetime and must not be used as a real authentication implementation.
const policy = {
iss: 'https://token.actions.githubusercontent.com',
aud: 'https://example.invalid/deploy',
sub: 'repo:demo-org@123456/demo-repo@456789:environment:staging'
};
const matches = claims => Object.entries(policy).every(([key, value]) => claims[key] === value);
console.log('expected context:', matches({ ...policy }));
console.log('wrong audience:', matches({ ...policy, aud: 'https://other.invalid' }));
console.log('other repository:', matches({ ...policy, sub: 'repo:other@12/project@34:environment:staging' }));
console.log('production context:', matches({ ...policy, sub: policy.sub.replace(':staging', ':production') }));expected context: true
wrong audience: false
other repository: false
production context: falseA shared issuer is insufficient because several repositories can receive GitHub identity tokens. Narrow matching rejects other subjects and audiences. A real provider must first establish token validity and then apply its supported policy language and role permissions.
GitHub Environments and OIDC Deployment Identity FAQ
Does id-token: write let a job deploy to my cloud account?
It lets the job request an OIDC identity token; it does not automatically grant a cloud role. The provider must trust the verified identity under its configured conditions and issue credentials with specific permissions. Review that role separately from the GitHub permission. A job can successfully receive an identity token and still be correctly denied by the provider because its audience, subject or environment context does not satisfy the policy.
Why does my subject differ from an older tutorial?
Subject formats can depend on repository age, immutable identity settings, environment context and customization. Current GitHub.com documentation includes owner and repository identifiers for applicable immutable subjects, while other repositories may retain an older format. Inspect the issued nonsecret claims and compare them with your provider's expected conditions. Do not broaden a policy indiscriminately to make a mismatch disappear; update the exact contract after understanding the identity change.
Is decoding the token payload enough to authenticate a job?
No. Decoding reveals text but does not verify its cryptographic signature, issuer, lifetime or intended audience. A production provider must perform the documented verification and then apply authorization conditions. The example inspects a token obtained from the runner endpoint for learning purposes only. The local policy exercise uses fabricated claims and deliberately assumes a separate verified-token step that it does not implement.
Why use environment protection if the provider checks the subject?
The provider's trust condition identifies an eligible job context, while environment protection controls when and from where that context can proceed inside GitHub. Keep branch restrictions, reviewer gates and provider conditions consistent. An environment name alone is not proof of reviewed deployment approval. Test the complete path with the intended candidate revision and rejected contexts, and keep resulting cloud permissions bounded to the resources the deployment actually needs.
GitHub Environments and OIDC Deployment Identity Exercises
Answer each one and check it instantly. If you get stuck, open the hint before the answer.
Why does the local policy model reject production context?
GitHub Environments and OIDC Deployment Identity Quiz
2 questions to check what you learned.
Which statement correctly describes decoding an OIDC JWT?
- It grants every cloud permission immediately
- It replaces the provider's trust policy
- It reveals claims but does not establish signature validity
What should a provider trust policy restrict besides the issuer?
- The intended audience and eligible job identity conditions
- Only the workflow's friendly display name
- Only the time the developer opened the editor