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 | jqOn macOS, use base64 -D if base64 -d is not available.
Verify:
- The token is a valid JWT (three dot-separated segments).
expis in the future (token not expired).iss,sub, andaudmatch 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 | jq2. 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:
- Copy the exact
subvalue from the decoded token. - Update the federated identity subject claim to match, or use a wildcard pattern that covers it.
- Common GitHub
subformats:- Branch push:
repo:OWNER/REPO:ref:refs/heads/BRANCH - Environment:
repo:OWNER/REPO:environment:ENV_NAME - Pull request:
repo:OWNER/REPO:pull_request
- Branch push:
- Common GitLab
subformats:- Branch:
project_path:GROUP/PROJECT:ref_type:branch:ref:BRANCH - Tag:
project_path:GROUP/PROJECT:ref_type:tag:ref:TAG
- Branch:
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:
- Check the
audvalue in the decoded token. - Add that exact value to Trusted Audiences on the federated identity (e.g.
https://api.thalassa.cloud). - Configure your CI pipeline to request the same audience:
- GitLab: set
audinid_tokens(see GitLab CI guide). - GitHub Actions: request the token with the intended audience when calling
core.getIDToken('https://api.thalassa.cloud').
- GitLab: set
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:
- Confirm automatic discovery works:
{issuer}/.well-known/openid-configurationmust be reachable from Thalassa Cloud. - If discovery fails, set a Custom JWKS Endpoint manually.
- For self-hosted GitLab, ensure TLS certificates are valid and the JWKS endpoint is publicly reachable.
- 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_idandservice_account_idin the exchange request. - Find organisation ID in the Console or via
tcloud me organisations. - Find service account ID under IAM → Service 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:writefor mutations,containerRegistry:pullfor 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:
- Review roles on the service account under IAM → Service Accounts.
- Assign the required RBAC role or IAM policy binding.
- 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-Identityto the project slug or identity. - Or pass
?project=on the request. - Or include a
projectclaim 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
| Issue | Fix |
|---|---|
| No ID token issued | Add permissions: id-token: write to the job |
Wrong sub for environments | Use environment: on the job; match repo:OWNER/REPO:environment:NAME |
| Audience not set | Pass audience to core.getIDToken('https://api.thalassa.cloud') |
| Forked PR workflows | Tokens from forks may have different sub claims — do not match fork PRs unless intended |
See GitHub Actions guide.
GitLab CI
| Issue | Fix |
|---|---|
| No ID token | Add id_tokens block with matching aud (GitLab 15.7+) |
| Self-hosted GitLab | Issuer URL must match instance URL; JWKS must be reachable |
Wrong sub format | Use 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}" | jqUse 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
- OIDC concepts — Mental model and glossary
- Create Federated Identity — Token matching and scopes
- Service Accounts — IAM roles and access credentials
- GitHub Actions — GitHub-specific configuration
- GitLab CI — GitLab-specific configuration