A Forgejo Runner requests a job-scoped OIDC token from the Forgejo Actions runtime. The token is signed by Forgejo's private key — the runner never manages a long-lived credential. This signed assertion is what it will trade for GCP access in the next step.
what actually happens
- The runner asks Forgejo for an OIDC token, naming the GCP
provider as the
audience. - Forgejo signs the token — its claims (
repository,ref,repository_owner) describe exactly which workflow run it came from.
origin · what mints this
- trigger
- push authlab/artifacts-visualizer refs/heads/main
- request
- GET /api/actions/_apis/pipelines/workflows/40/idtoken ?audience=
//iam.googleapis.com/…/forgejo-provider
# .forgejo/workflows/capture.yml
on: push
permissions:
id-token: write # lets the job request an OIDC token
jobs:
capture:
steps:
- name: Mint OIDC token
with:
audience: //iam.googleapis.com/…/forgejo-provider
the audience you request becomes the
token's aud
Header
algRS256kid9YP06n2o17zssY1eAFAh9Hhb7SRxjlgaZcf-Gb7lfiMtypJWT
Payload
| Claim | Value |
|---|---|
actor | erick |
aud | //iam.googleapis.com/projects/1234567890/locations/global/workloadIdentityPools/authlab-pool/providers/forgejo-provider |
base_ref | |
event_name | push |
exp | 1778622390 |
head_ref | |
iat | 1778618790 |
iss | https://forgejo.authlab.app/api/actions |
nbf | 1778618790 |
ref | refs/heads/main |
ref_protected | false |
ref_type | branch |
repository | authlab/artifacts-visualizer |
repository_owner | authlab |
run_attempt | 1 |
run_id | 40 |
run_number | 16 |
sha | 14c23450c8322e4c8e315e68d4241d184826be06 |
sub | repo:authlab/artifacts-visualizer:ref:refs/heads/main |
workflow | capture.yml |
workflow_ref | authlab/artifacts-visualizer/.forgejo/workflows/capture.yml@refs/heads/main |
Compact Serialization
eyJhbGciOiJSUzI1NiIsImtpZCI6IjlZUDA2bjJvMTd6c3NZMWVBRkFoOUhoYjdTUnhqbGdhWmNmLUdiN2xmaU0iLCJ0eXAiOiJKV1QifQeyJhY3RvciI6ImVyaWNrIiwiYXVkIjoiLy9pYW0uZ29vZ2xlYXBpcy5jb20vcHJvamVjdHMvMTIzNDU2Nzg5MC9sb2NhdGlvbnMvZ2xvYmFsL3dvcmtsb2FkSWRlbnRpdHlQb29scy9hdXRobGFiLXBvb2wvcHJvdmlkZXJzL2Zvcmdlam8tcHJvdmlkZXIiLCJiYXNlX3JlZiI6IiIsImV2ZW50X25hbWUiOiJwdXNoIiwiZXhwIjoxNzc4NjIyMzkwLCJoZWFkX3JlZiI6IiIsImlhdCI6MTc3ODYxODc5MCwiaXNzIjoiaHR0cHM6Ly9mb3JnZWpvLmF1dGhsYWIuYXBwL2FwaS9hY3Rpb25zIiwibmJmIjoxNzc4NjE4NzkwLCJyZWYiOiJyZWZzL2hlYWRzL21haW4iLCJyZWZfcHJvdGVjdGVkIjoiZmFsc2UiLCJyZWZfdHlwZSI6ImJyYW5jaCIsInJlcG9zaXRvcnkiOiJhdXRobGFiL2FydGlmYWN0cy12aXN1YWxpemVyIiwicmVwb3NpdG9yeV9vd25lciI6ImF1dGhsYWIiLCJydW5fYXR0ZW1wdCI6IjEiLCJydW5faWQiOiI0MCIsInJ1bl9udW1iZXIiOiIxNiIsInNoYSI6IjE0YzIzNDUwYzgzMjJlNGM4ZTMxNWU2OGQ0MjQxZDE4NDgyNmJlMDYiLCJzdWIiOiJyZXBvOmF1dGhsYWIvYXJ0aWZhY3RzLXZpc3VhbGl6ZXI6cmVmOnJlZnMvaGVhZHMvbWFpbiIsIndvcmtmbG93IjoiY2FwdHVyZS55bWwiLCJ3b3JrZmxvd19yZWYiOiJhdXRobGFiL2FydGlmYWN0cy12aXN1YWxpemVyLy5mb3JnZWpvL3dvcmtmbG93cy9jYXB0dXJlLnltbEByZWZzL2hlYWRzL21haW4ifQanonymized-signature-not-valid-for-cryptographic-use
What must match
STS checks these before anything else. Hover a claim for why.
aud token.aud ≡ WIF provider URI
iss token.iss ≡ provider issuer URI
IAM binding
The principalSet authorizes only tokens
whose claims match. Hover a claim for why.
repository token.repository ≡ attribute.repository
IAM binding — principalSet
principalSet://iam.googleapis.com/ projects/1234567890/ locations/global/ workloadIdentityPools/authlab-pool/ attribute.repository/authlab/artifacts-visualizer
matches JWT
repository claim — also encoded in sub Only tokens from this repo, this pool match. Step 3 shows how to loosen or tighten the scope.