ARTIFACTORY: Under the Hood: Debugging the OIDC Token Exchange Flow

ARTIFACTORY: Under the Hood: Debugging the OIDC Token Exchange Flow

Products
Frog_Artifactory
Content Type
User_Guide
AuthorFullName__c
Jordan Tangy
articleNumber
000006833
FirstPublishedDate
2026-01-22T10:23:32Z
lastModifiedDate
2026-01-22
This guide breaks down exactly how a GitHub Workflow requests an identity and exchanges it for a JFrog Access Token. We will trace the data from the initial request to the final handshake.

Phase 1: Requesting the Identity (GitHub Side)


The Goal: The GitHub Runner asks its own internal Identity Provider (IdP) for a "Subject Token"—a digital ID card that proves exactly which workflow is running.
curl -sLS -H "User-Agent: actions/oidc-client" \
     -H "Authorization: Bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \
     "${ACTIONS_ID_TOKEN_REQUEST_URL}&audience=jfrog-github" \
     | jq -r .value
 

Component

Value/Code

Why is it here?

User-Agent

-H "User-Agent: actions/oidc-client"

Identification. GitHub's internal IdP is picky. It requires this specific User-Agent to know the request is coming from a supported OIDC client logic, not a random curl or browser.

Auth Header

-H "Authorization: Bearer $...

Permission. This is the "password" for the request. The variable $ACTIONS_ID_TOKEN_REQUEST_TOKEN is a temporary secret injected by the runner (only if you set permissions: id-token: write). It proves this specific job is allowed to ask for a token.

The Audience

&audience=jfrog-github

Intention. This query parameter tells GitHub: "I intend to use this token to talk to 'jfrog-github'." GitHub burns this string into the aud field of the token. If this doesn't match your JFrog configuration later, the key won't work.

The Output

jq -r .value

Extraction. The raw response is a JSON object { "value": "eyJh..." }. We only want the string inside value.


Phase 2: Inspecting the "Subject Token"


The Goal: Understand what we actually received. This is the "Digital ID Card."


Important:

We often say we "decrypt" this token, but technically we are decoding it. It is a JWT (JSON Web Token) that is Signed (integrity proof), not Encrypted (hidden data). Anyone who holds the token can read its contents.


1. The Raw Token (Encoded)

This is what GitHub sent back. It is a long string of random-looking characters (Base64 Encoded).

Note: For security purposes, Github automatically encrypts the token. In the Github action logs, the token will be displayed as *** but using “echo -n "$ID_TOKEN" | base64 -w 0”, it will display this base64 string which encodes the JWT:
    ZXlKaGJHY2lPaUpTVXpJM... (truncated)
To decrypt this base64 string, copy it and run the following command:
    echo "ZXlKaGJHY2lPaUpTVXpJM..." | cut -d "." -f 2 | base64 -d
This will return the decoded JWT:
eyJhbGciOiJSUzI1NiIsIm... (truncated)

2. The Decoding Process
To see the data, we split the token (at the dots .) and decode the middle part (the Payload).
echo "eyJhbGciOiJSUzI1NiIsIm...." | cut -d "." -f 2 | base64 -d

3. The Decoded Payload (What we will later send to JFrog)
This is the JSON data hidden inside the string. This is an example:
{
  "sub": "repo:<GITHUB_USERNAME>/jfrog-github-oidc:ref:refs/heads/main",
  "repository": "<GITHUB_USERNAME>/jfrog-github-oidc",
  "iss": "https://token.actions.githubusercontent.com",
  "aud": "jfrog-github",
  "actor": "<GITHUB_USERNAME>",
  "workflow": "oidc-poc",
  "ref": "refs/heads/main"
}

Key Fields Explained:
  • iss (Issuer): "I am GitHub." (JFrog checks this against the Provider URL).
  • aud (Audience): "This is for jfrog-github." (JFrog checks this against your OIDC Audience setting).
  • sub (Subject): "I am the main branch of repo jfrog-github-oidc." (This is the primary identity string).
  • repository: (Custom Claim): "I belong to the repo <GITHUB_USERNAME/jfrog-github-oidc." (You likely use this in your JFrog Identity Mapping to assign the "Admin" role).

Phase 3: The Exchange (JFrog Side)


The Goal: The last step consists of sending the subject token generated by the IDP to JFrog to trade it for a JFrog "Access Token" that can actually perform actions (like uploading files).

The Command:
curl -X POST "https://<JFROG_URL>/access/api/v1/oidc/token" \
  -H "Content-Type: application/json" \
  -d '{
    "grant_type": "urn:ietf:params:oauth:grant-type:token-exchange",
    "subject_token_type": "urn:ietf:params:oauth:token-type:id_token",
    "subject_token": "eyJhbGci...", 
    "provider_name": "test"
  }'


Critical Details
  1. subject_token: We send the Raw Encoded String (from Phase 2, Step 1). We do NOT send the decoded JSON. JFrog needs the raw string so it can verify the cryptographic signature itself.
  2. provider_name: Must match the exact name of the integration you created in JFrog (e.g., "test"). This tells JFrog which "Identity Mappings" to look at.

How JFrog Validates (The "Handshake")

When JFrog receives this request, it performs two distinct checks:
  1. Security Validation:
    • Check: Is the signature valid using GitHub's public keys?
    • Check: Does the aud claim ("jfrog-github") match the config?
    • Result: If these fail, you get 401 Unauthorized.
  2. Identity Mapping:
    • Check: Does the payload contains repository: <GITHUB_USERNAME>/jfrog-github-oidc? (Or whatever you configured).
    • Result: If it matches, JFrog generates an Access Token with the permissions (Scope) defined in that mapping (e.g., Admin).

The Final Result

If both checks pass, JFrog responds with:
{
  "access_token": "fypV...",
  "scope": "applied-permissions/admin",
  "token_type": "Bearer",
  "expires_in": 3600
}

 

The Github Action can now use this access_token to perform Artifactory operation based on the scope of the token.