From 21bf1c44c11ffa87e7936e534aa79ae2930a866b Mon Sep 17 00:00:00 2001 From: Maarten Zuidhoorn Date: Thu, 21 May 2026 13:12:51 +0200 Subject: [PATCH 1/2] Add action to get short-lived access token using OIDC --- .github/actions/get-token/action.yml | 86 ++++++++++++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 .github/actions/get-token/action.yml diff --git a/.github/actions/get-token/action.yml b/.github/actions/get-token/action.yml new file mode 100644 index 00000000..f8fcffbc --- /dev/null +++ b/.github/actions/get-token/action.yml @@ -0,0 +1,86 @@ +name: Get Token +description: 'Get a short-lived access token to interact with the GitHub API. Note: Using this action requires the "id-token: write" permission to be set for the GitHub Actions workflow job.' + +inputs: + token-exchange-url: + description: 'The URL to exchange the OIDC token for an access token.' + required: true + permissions: + description: 'The permissions to request for the access token, in the format of "scope: permission", separated by newlines.' + required: true + +outputs: + token: + value: '${{ steps.access-token.outputs.token }}' + description: 'The short-lived access token obtained from the token exchange service.' + +runs: + using: composite + steps: + - name: Get OIDC token + id: oidc-token + uses: actions/github-script@v9 + with: + script: | + const token = await core.getIDToken(); + core.setSecret(token); + core.setOutput('token', token); + + - name: Exchange OIDC token + id: access-token + uses: actions/github-script@v9 + env: + OIDC_TOKEN: ${{ steps.oidc-token.outputs.token }} + TOKEN_EXCHANGE_URL: ${{ inputs.token-exchange-url }} + REQUESTED_PERMISSIONS: ${{ inputs.permissions }} + with: + script: | + const { + GITHUB_REPOSITORY, + OIDC_TOKEN, + TOKEN_EXCHANGE_URL, + REQUESTED_PERMISSIONS, + } = process.env; + + // This assumes `REQUESTED_PERMISSIONS` is a newline-separated string + // of "scope: permission" pairs, e.g.: + // ``` + // contents: read + // issues: write + // ``` + const requestedPermissions = REQUESTED_PERMISSIONS + .split('\n') + .filter(line => line.trim() !== '') + .map(line => { + const [scope, permission] = line.split(':').map(part => part.trim()); + return { scope, permission }; + }); + + const response = await fetch(`${TOKEN_EXCHANGE_URL}/api/exchange/token`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + oidcToken: OIDC_TOKEN, + targetRepo: GITHUB_REPOSITORY, + requested_permissions: requestedPermissions, + }), + }); + + if (!response.ok) { + const errorText = await response.text(); + return core.setFailed(`Token exchange failed: ${response.status} ${response.statusText} - ${errorText}`); + } + + const { token, permissions, expires_at, } = await response.json(); + if (!token) { + return core.setFailed('Token exchange response did not contain an access token.'); + } + + core.setSecret(token); + core.setOutput('token', token); + + core.info(`Access token obtained successfully. Token expires at: "${expires_at}".`); + + From be7b59c11d096c7aa5f153be838ae062115be1ca Mon Sep 17 00:00:00 2001 From: Maarten Zuidhoorn Date: Thu, 21 May 2026 13:13:22 +0200 Subject: [PATCH 2/2] Remove extra newline --- .github/actions/get-token/action.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/actions/get-token/action.yml b/.github/actions/get-token/action.yml index f8fcffbc..e306ae3c 100644 --- a/.github/actions/get-token/action.yml +++ b/.github/actions/get-token/action.yml @@ -82,5 +82,3 @@ runs: core.setOutput('token', token); core.info(`Access token obtained successfully. Token expires at: "${expires_at}".`); - -