Skip to content

OIDC troubleshooting

Use this guide when token exchange fails, claim matching does not work, or API calls return permission errors after a successful exchange. For background on how the pieces fit together, see OIDC concepts.

Quick diagnosis

    flowchart TD
    A[Token exchange fails] --> B{HTTP error from /oidc/token?}
    B -->|4xx from exchange| C[Check ID token validation]
    B -->|Exchange succeeds| D[API call fails]
    C --> C1[Decode JWT and compare claims]
    C --> C2[Check issuer URL and JWKS]
    C --> C3[Check audience matches]
    C --> C4[Check federated identity expiry]
    D --> D1[Check API scopes on federated identity]
    D --> D2[Check service account IAM roles]
    D --> D3[Check project context header]
  

Token exchange fails

The POST https://api.thalassa.cloud/oidc/token request returns an error, or access_token is empty in the response.

1. Decode and inspect the ID token

Always start by decoding the ID token your CI platform issued:

echo "${OIDC_TOKEN}" | cut -d. -f2 | base64 -d 2>/dev/null | jq

On macOS, use base64 -D if base64 -d is not available.

Verify:

  • The token is a valid JWT (three dot-separated segments).
  • exp is in the future (token not expired).
  • iss, sub, and aud match your configuration (see below).

Add a temporary debug step in CI if needed:

# GitHub Actions
- name: Debug token claims
  run: echo "${{ steps.oidc.outputs.token }}" | cut -d. -f2 | base64 -d 2>/dev/null | jq
# GitLab CI
debug:
  script:
    - apk add --no-cache jq
    - echo "${THALASSA_ID_TOKEN}" | cut -d. -f2 | base64 -d 2>/dev/null | jq

2. Subject claim does not match

Symptom: Exchange fails or returns an error indicating no matching federated identity.

Cause: The sub claim in the ID token does not match the Subject claim on the federated identity.

Fix:

  1. Copy the exact sub value from the decoded token.
  2. Update the federated identity subject claim to match, or use a wildcard pattern that covers it.
  3. Common GitHub sub formats:
    • Branch push: repo:OWNER/REPO:ref:refs/heads/BRANCH
    • Environment: repo:OWNER/REPO:environment:ENV_NAME
    • Pull request: repo:OWNER/REPO:pull_request
  4. Common GitLab sub formats:
    • Branch: project_path:GROUP/PROJECT:ref_type:branch:ref:BRANCH
    • Tag: project_path:GROUP/PROJECT:ref_type:tag:ref:TAG

Avoid overly broad wildcards like * unless you understand the security impact.

3. Audience (aud) mismatch

Symptom: Exchange fails even when sub looks correct.

Cause: The aud claim in the ID token does not match Trusted Audiences on the federated identity.

Fix:

  1. Check the aud value in the decoded token.
  2. Add that exact value to Trusted Audiences on the federated identity (e.g. https://api.thalassa.cloud).
  3. Configure your CI pipeline to request the same audience:
    • GitLab: set aud in id_tokens (see GitLab CI guide).
    • GitHub Actions: request the token with the intended audience when calling core.getIDToken('https://api.thalassa.cloud').

The audience in the pipeline, the token’s aud claim, and Trusted Audiences must all align.

4. Issuer URL mismatch

Symptom: Exchange fails with a signature or issuer validation error.

Cause: The iss claim does not match the OIDC Issuer URL on the identity provider.

Fix:

  • Use the exact issuer URL — a trailing slash can break matching.
  • GitHub Actions: https://token.actions.githubusercontent.com (no trailing slash)
  • GitLab.com: https://gitlab.com
  • Self-hosted GitLab: your instance base URL

Confirm by comparing the iss claim in the decoded token with the identity provider configuration.

5. JWKS / signature verification fails

Symptom: Exchange fails; logs or support indicate JWKS or signature problems.

Cause: Thalassa Cloud cannot fetch or verify signing keys.

Fix:

  1. Confirm automatic discovery works: {issuer}/.well-known/openid-configuration must be reachable from Thalassa Cloud.
  2. If discovery fails, set a Custom JWKS Endpoint manually.
  3. For self-hosted GitLab, ensure TLS certificates are valid and the JWKS endpoint is publicly reachable.
  4. If using Custom JWKS JSON, update it when the provider rotates keys.

6. Wrong organisation or service account ID

Symptom: Exchange fails with an error about the organisation or service account.

Fix:

  • Verify organisation_id and service_account_id in the exchange request.
  • Find organisation ID in the Console or via tcloud me organisations.
  • Find service account ID under IAMService Accounts.
  • Ensure the federated identity exists on that specific service account.

7. Federated identity expired

Symptom: Exchange worked previously but now fails.

Cause: The federated identity has an Expiry Date in the past.

Fix: Create a new federated identity or extend the expiry in the Console.

8. Additional claims mismatch

Symptom: Exchange fails when optional claim rules are configured.

Cause: Additional claims on the federated identity do not match the token (e.g. ref: refs/heads/main but the job runs on a feature branch).

Fix: Remove unnecessary additional claims, or update them to match the decoded token. Start with subject claim only, then add additional claims once the basic flow works.

Exchange succeeds but API calls fail

The bearer token is returned, but subsequent API calls return 401, 403, or resource-not-found errors.

1. API scopes too narrow

Symptom: 403 Forbidden on write operations; read works.

Cause: The federated identity API Scopes do not include the scope required for the action.

Fix:

  • Grant the required scopes on the federated identity (e.g. api:write for mutations, containerRegistry:pull for registry access).
  • Scopes can be narrowed further at exchange time if supported, but cannot exceed what the federated identity allows.

2. Service account lacks IAM permissions

Symptom: 403 Forbidden even with correct API scopes.

Cause: The service account does not have the IAM role or policy binding for the action.

Fix:

  1. Review roles on the service account under IAMService Accounts.
  2. Assign the required RBAC role or IAM policy binding.
  3. Remember: API scopes and IAM roles are both required — see three layers of access control.

3. Missing project context

Symptom: Resources exist but API returns not found or permission denied.

Cause: The API call targets project-scoped resources without an active project.

Fix:

  • Set X-Project-Identity to the project slug or identity.
  • Or pass ?project= on the request.
  • Or include a project claim if your federation setup supports it.

See Projects and IAM policy concepts.

4. Bearer token expired

Symptom: Calls work initially, then fail after some time.

Cause: The exchanged bearer token expired (default 1 hour).

Fix: Exchange a fresh token before long-running jobs, or request a longer lifetime via access_token_lifetime (minimum 5 minutes, maximum 24 hours).

CI platform-specific issues

GitHub Actions

IssueFix
No ID token issuedAdd permissions: id-token: write to the job
Wrong sub for environmentsUse environment: on the job; match repo:OWNER/REPO:environment:NAME
Audience not setPass audience to core.getIDToken('https://api.thalassa.cloud')
Forked PR workflowsTokens from forks may have different sub claims — do not match fork PRs unless intended

See GitHub Actions guide.

GitLab CI

IssueFix
No ID tokenAdd id_tokens block with matching aud (GitLab 15.7+)
Self-hosted GitLabIssuer URL must match instance URL; JWKS must be reachable
Wrong sub formatUse project_path:… format, not GitHub’s repo:… format

See GitLab CI guide.

Verify the exchange request

A minimal working exchange request:

curl -s -X POST https://api.thalassa.cloud/oidc/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=urn:ietf:params:oauth:grant-type:token-exchange" \
  -d "subject_token=${OIDC_TOKEN}" \
  -d "subject_token_type=urn:ietf:params:oauth:token-type:id_token" \
  -d "organisation_id=${THALASSA_ORGANISATION_ID}" \
  -d "service_account_id=${THALASSA_SERVICE_ACCOUNT_ID}" | jq

Use https://api.thalassa.cloud/oidc/token — not /v1/oidc/token.

Check the full JSON response for error details, not only access_token.

Using tcloud CLI

tcloud oidc token-exchange \
  --subject-token "${OIDC_TOKEN}" \
  --organisation-id "${THALASSA_ORGANISATION_ID}" \
  --service-account-id "${THALASSA_SERVICE_ACCOUNT_ID}"

If the CLI fails, run the curl command above to see the raw API error.

Related documentation