Validate CI OIDC
Validate GitHub Actions or GitLab CI OIDC tokens with provider-specific claim assertions.
Validates an OIDC token issued by a CI provider against the matching built-in issuer profile and asserts provider-specific claims (repository, ref, project path, ref protection). Replaces the bespoke per-provider verification logic most teams write.
This endpoint applies a server-side issuer profile keyed off provider:
github_actions→ issuerhttps://token.actions.githubusercontent.com, RS256gitlab→ issuerhttps://gitlab.com, RS256
You do not pass policy or issuer_profile_id here.
Request
| Field | Type | Required | Description |
|---|---|---|---|
| token | string | yes | The OIDC ID token from the CI runner. |
| provider | "github_actions" | "gitlab" | yes | Selects the issuer profile and the set of provider-specific claim checks. |
| expected_repository | string | no | GitHub Actions only. Asserts the token's repository claim equals this value (e.g. "acme/api"). |
| expected_ref | string | no | GitHub Actions only. Asserts the token's ref claim equals this value (e.g. "refs/heads/main"). |
| expected_project_path | string | no | GitLab only. Asserts the token's project_path claim equals this value (e.g. "acme/api"). |
| expected_ref_protected | string | no | GitLab only. Asserts the token's ref_protected claim equals this string. Use "true" to require protected refs. |
Response — 200
Same shape as /v1/validate/jwt: valid, statuses, findings[], summary. Provider-specific claim failures appear as findings alongside standard validation failures.
{
"valid": false,
"statuses": {
"signature": "pass",
"issuer": "pass",
"audience": "pass",
"algorithm": "pass",
"time": "pass",
"required_claims": "fail"
},
"findings": [
{
"code": "GITHUB_REPO_MISMATCH",
"severity": "error",
"message": "Token repository claim does not match expected_repository.",
"evidence": {
"token_repository": "fork/api",
"expected_repository": "acme/api"
}
}
],
"summary": "Token is NOT valid: repository mismatch."
} Errors
| Channel | Code | Cause |
|---|---|---|
| 400 | MALFORMED_TOKEN | Token is not a parseable JWT. |
| 200, in findings | SIGNATURE_INVALID | Token signature failed against the provider JWKS. |
| 200, in findings | GITHUB_REPO_MISMATCH | repository claim ≠ expected_repository. |
| 200, in findings | GITHUB_REF_MISMATCH | ref claim ≠ expected_ref. |
| 200, in findings | GITLAB_PROJECT_MISMATCH | project_path claim ≠ expected_project_path. |
| 200, in findings | GITLAB_REF_PROTECTION_MISMATCH | ref_protected claim ≠ expected_ref_protected. |
| 422 | CI_PROVIDER_UNKNOWN | provider is not one of the supported values. |
Examples
GitHub Actions workflow that retrieves the token, validates it, and fails the build on rejection. Full version (with token masking and failure handling) in Examples and the CI · OIDC guide.
- name: Retrieve runner OIDC token
id: token
run: |
JWT=$(curl -sSf \
-H "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \
"$ACTIONS_ID_TOKEN_REQUEST_URL&audience=api://jwtshield" \
| python3 -c 'import json,sys; print(json.load(sys.stdin)["value"])')
echo "::add-mask::$JWT"
echo "jwt=$JWT" >> "$GITHUB_OUTPUT"
- name: Validate via JWTShield
run: |
curl -sSf -H "Authorization: Bearer ${{ secrets.JWTSHIELD_KEY }}" \
-H "Content-Type: application/json" \
-d '{
"token": "${{ steps.token.outputs.jwt }}",
"provider": "github_actions",
"expected_repository": "${{ github.repository }}",
"expected_ref": "${{ github.ref }}"
}' \
"$JWTSHIELD_URL/v1/validate/ci-oidc"
GitLab CI:
validate-oidc:
stage: validate
image: python:3.12-slim
id_tokens:
JWTSHIELD_OIDC_TOKEN:
aud: "api://jwtshield"
script:
- |
curl -sSf -H "Authorization: Bearer $JWTSHIELD_KEY" \
-H "Content-Type: application/json" \
-d "{\"token\":\"$JWTSHIELD_OIDC_TOKEN\",\"provider\":\"gitlab\",\"expected_project_path\":\"$CI_PROJECT_PATH\",\"expected_ref_protected\":\"true\"}" \
"$JWTSHIELD_URL/v1/validate/ci-oidc"
See the CI · OIDC guide for the complete workflow including failure handling and protected-branch enforcement.