Exchange for a federated token

The OIDC token has no power in GCP on its own. The runner posts it to Google's Security Token Service, which verifies it against Forgejo's public keys and returns a short-lived federated access token: GCP now recognizes the identity, but this is not the service account's token yet.

Call sequence

runner forgejo ci forgejo forgejo.authlab.app STS sts.googleapis.com iam credentials iamcredentials.googleapis.com cloud storage storage.googleapis.com step 1 GET /idtoken 200 · oidc jwt step 2 1 POST /token 2 GET discovery 3 GET JWKS 4 200 · federated
  • the runner trades its OIDC token for a federated token
  • audience must equal the token's aud claim
  • the JWT payload (purple) is shortened here for brevity; the header and signature are complete
  • STS reads the issuer's discovery document to find its keys (jwks_uri)
  • the key whose kid matches the token header verifies the signature
  • signature, aud, iss and exp check out: STS answers the POST
  • the access token is opaque, not a JWT (redacted: it is a live bearer credential)
request POST
curl -X POST https://sts.googleapis.com/v1/token \
    -H 'Content-Type: application/json' \
    -d '{
      "audience": "//iam.googleapis.com/projects/1234567890/locations/global/workloadIdentityPools/lab-pool/providers/ci-provider",
      "grantType": "urn:ietf:params:oauth:grant-type:token-exchange",
      "requestedTokenType": "urn:ietf:params:oauth:token-type:access_token",
      "scope": "https://www.googleapis.com/auth/cloud-platform",
      "subjectTokenType": "urn:ietf:params:oauth:token-type:jwt",
      "subjectToken": "eyJhbGciOiJSUzI1NiIsImtpZCI6IjlZUDA2bjJvMTd6c3NZMWVBRkFoOUhoYjdTUnhqbGdhWmNmLUdiN2xmaU0iLCJ0eXAiOiJKV1QifQ.eyJhY3RvciI6ImF1dGhsYWIiLCJhdWQiOiIvL2lhbS5nb29nbGVhcGlzLmNvbS9wcm9qZWN0cy8xMjM0NTY3ODkwL2xvY2F0aW9ucy9nbG9iYWwvd29ya2xvYWRJZGVudGl0eVBvb2xzL2xhYi1wb29sL3Byb3ZpZGVycy9jaS1wcm92aWRlciIsImJhc2VfcmVmIjoiIiwiZXZlbnRfbmFtZSI6….anonymized-signature-not-valid-for-cryptographic-use"
    }'
request GET
curl https://forgejo.authlab.app/api/actions/.well-known/openid-configuration
response 200 OK
{
  "issuer": "https://forgejo.authlab.app/api/actions",
  "jwks_uri": "https://forgejo.authlab.app/api/actions/.well-known/keys",
  "subject_types_supported": ["public"],
  "response_types_supported": ["id_token"],
  "id_token_signing_alg_values_supported": ["RS256"],
  "scopes_supported": ["openid"],
  "claims_supported": ["sub", "aud", "exp", "iat", "iss", "nbf", "actor", "base_ref", "event_name", "head_ref", "ref", "ref_protected", "ref_type", "repository", "repository_owner", "run_attempt", "run_id", "run_number", "sha", "workflow", "workflow_ref"]
}
request GET
curl https://forgejo.authlab.app/api/actions/.well-known/keys
response 200 OK
{
  "keys": [
    {
      "alg": "RS256",
      "e": "AQAB",
      "kid": "9YP06n2o17zssY1eAFAh9Hhb7SRxjlgaZcf-Gb7lfiM",
      "kty": "RSA",
      "n": "vv9BnIrR-ZzrN7rQZwM6bIeGf_6jV-B8B_PDlTPofZXHrfWbfpoEJcTVEX5aR8YZrfEbDDXxYwbhecJl1x1CUeCtP_8f8S65XpFcj6yJBQ0AQoWjoZp_IB-XLuvT5i6MMxyCZVd07os51ovsKd1yt_uL6YBunifBzBEIE_lNg45bBd2shBEAyCTJAk2GBqDoIm4vDmSGv3Bq1IZ4acz1Yqxlu0gy29FPazy1HcLzVKPYsHc8KyxdsusFSmCesPW8pLOjzYchKhKSKSpxAZZdls2GHIP7Wn_ZKMM7UbmJoeIDakhEVJ8sJtqsKQr6L6uRiqrAh9SXd28MnwPx3_JEUw",
      "use": "sig"
    }
  ]
}
response 200 OK
{
  "access_token": "ya29.c.REDACTED-federated-access-token-not-valid-for-use",
  "issued_token_type": "urn:ietf:params:oauth:token-type:access_token",
  "token_type": "Bearer",
  "expires_in": 3599
}

What STS validates

STS checks the OIDC token before it mints anything. Hover a claim for why.

audience
request.audience ≡ token.aud
signature
token.sig ✓ issuer JWKS