Workload Identity Federation (let’s just call this WIF) allows app principals not residing within Azure to request short lived access tokens. This removes the need of storing client secrets or certificates within GitHub as Action secrets. One drawback ist that currently only the Azure modules support the usage of WIF.

How it works

WIF relies on trust. This trust is directly configured on the Azure AD app registration and scoped to an individual GitHub repository and optionally fine grained by limiting the usage to single git refs, specifically branches and tags. By trusting GitHub as external identity provider (IdP), a GitHub Action can request an identity token from the GitHub IdP and exchange this one against an access token within Azure AD.

Entra Workload Identity Federation

A GitHub IdP issued token (decoded) contains the following information:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
{  
    "jti": "1a9fe224-c936-46f6-a38b-3300e76251b0",  
    "sub": "repo:nicolonsky/musical-octo-telegram:ref:refs/heads/main",  
    "aud": "api://AzureADTokenExchange",  
    "ref": "refs/heads/main",  
    "sha": "9f112c0b82547e35b10250d7f3165789bf97862f",  
    "repository": "nicolonsky/musical-octo-telegram",  
    "repository_owner": "nicolonsky",  
    "repository_owner_id": "32899754",  
    "run_id": "3986560796",  
    "run_number": "2",  
    "run_attempt": "2",  
    "repository_visibility": "private",  
    "repository_id": "592303002",  
    "actor_id": "32899754",  
    "actor": "nicolonsky",  
    "workflow": ".github/workflows/wif.yaml",  
    "head_ref": "",  
    "base_ref": "",  
    "event_name": "push",  
    "ref_type": "branch",  
    "workflow_ref": "nicolonsky/musical-octo-telegram/.github/workflows/wif.yaml@refs/heads/main",  
    "workflow_sha": "9f112c0b82547e35b10250d7f3165789bf97862f",  
    "job_workflow_ref": "nicolonsky/musical-octo-telegram/.github/workflows/wif.yaml@refs/heads/main",  
    "job_workflow_sha": "9f112c0b82547e35b10250d7f3165789bf97862f",  
    "iss": "https://token.actions.githubusercontent.com",  
    "nbf": 1674486850,  
    "exp": 1674487750,  
    "iat": 1674487450  
}

Did you recognize the matching sub attribute within the GitHub issued token and the Azure AD configuration? For a successful authentication the:

  • Token issuer: iss
  • Subject: sub
  • Audience: aud

must match (besides some standard OIDC claims like the validity epoch-time).

By sending the GitHub ID token via HTTP POST to the Azure AD token endpoint with some additional metadata we can exchange the GitHub ID token with an Azure AD access token:

1
2
3
4
5
6
7
8
9
POST: https://login.microsoftonline.com/{TENANT_ID}/oauth2/v2.0/token  
'Content-Type': 'application/x-www-form-urlencoded'  
{  
  scope: 'https://graph.microsoft.com/.default',  
  client_id: {CLIENT_ID},  
  client_assertion_type: 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer',  
  grant_type: 'client_credentials',  
  client_assertion: {GITHUB_ID_TOKEN}  
}

From an Open ID Connect perspective this is just another specialization of the client_credentials flow.

As a response we receive the requested Azure AD access token, in this case I requested the token for the Microsoft Graph API:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
{  
    "aud": "https://graph.microsoft.com",  
    "iss": "https://sts.windows.net/30204efd-acb7-41a7-9fe9-233cec0556c1/",  
    "iat": 1674487150,  
    "nbf": 1674487150,  
    "exp": 1674491050,  
    "aio": "E2ZgYPhp8niXNfv9G2K3W2LUy15nAAA=",  
    "app_displayname": "GitHub Workflow Identity Federation Example",  
    "appid": "504fd139-6825-4e25-ad7a-627f100ab466",  
    "appidacr": "2",  
    "idp": "https://sts.windows.net/30204efd-acb7-41a7-9fe9-233cec0556c1/",  
    "idtyp": "app",  
    "oid": "a7538a92-ba98-4678-9374-f4a79b85c28f",  
    "rh": "0.AU8A_U4gMLesp0Gf6SM87AVWwQMAAAAAAAAAwAAAAAAAAABPAAA.",  
    "roles": [  
        "Organization.Read.All"  
    ],  
    "sub": "a7538a92-ba98-4678-9374-f4a79b85c28f",  
    "tenant_region_scope": "EU",  
    "tid": "30204efd-acb7-41a7-9fe9-233cec0556c1",  
    "uti": "X2K54jgbIUKog5IhWrZNAA",  
    "ver": "1.0",  
    "wids": [  
        "0997a1d0-0d1d-4acb-b408-d5ca73121e90"  
    ],  
    "xms_tcdt": 1645079402,  
    "xms_tdbr": "EU"  
}

Wiring up my first GitHub Action step

Because the azure/login@v1 GitHub Action is mainly related to Azure I wanted to author my own GitHub Action step that is customisable and can be reused across multiple pipelines and works with Microsoft Graph.

You can find the Action directly in the GitHub Marketplace:

Azure AD Workload Identity Federation · Actions · GitHub Marketplace

Writing the Action was quite fun but also simple, because GitHub provides a nice toolkit to write custom Actions in JavaScript: actions/toolkit: The GitHub ToolKit for developing GitHub Actions.

Here a simple example about how to use the action with the Microsoft Graph PowerShell SDK:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
on: [push]  
  
permissions:  
  id-token: write  
  contents: read  
  
jobs:  
  hello_world_job:  
    runs-on: ubuntu-latest  
    name: WIF Example  
    steps:  
      - name: Azure AD Workload Identity Federation  
        uses: nicolonsky/[email protected]  
        with:  
          tenant_id: '30204efd-acb7-41a7-9fe9-233cec0556c1'  
          client_id: '504fd139-6825-4e25-ad7a-627f100ab466'  
      - name: Do some Stuff  
        run: |  
          Install-Module -Name Microsoft.Graph.Authentication  
          Connect-MgGraph -AccessToken $env:ACCESS_TOKEN  
          Invoke-MgGraphRequest -Uri '/beta/organization' | Select-Object -ExpandProperty value  
        shell: pwsh

Pretty cool and another step towards pass-, ooh, I mean credential-less world! 😉

Additional information