Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,6 @@ outputs:
value: ${{ steps.octo-sts.outputs.token }}

runs:
using: 'node20'
using: 'node24'
main: 'index.js'
post: 'post.js'
40 changes: 24 additions & 16 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
const actionsToken = process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN;
const actionsUrl = process.env.ACTIONS_ID_TOKEN_REQUEST_URL;

const crypto = require('node:crypto');
const fs = require('node:fs');
const util = require('node:util');
const { error, debug, addMask, parseOIDC } = require('./util');
const appendFile = util.promisify(fs.appendFile);

if (!actionsToken || !actionsUrl) {
console.log(`::error::Missing required environment variables; have you set 'id-token: write' in your workflow permissions?`);
error(`Missing required environment variables; have you set 'id-token: write' in your workflow permissions?`);
process.exit(1);
}

Expand All @@ -11,7 +17,7 @@ const identity = process.env.INPUT_IDENTITY;
const domain = process.env.INPUT_DOMAIN;

if (!scope || !identity) {
console.log(`::error::Missing required inputs 'scope' and 'identity'`);
error(`Missing required inputs 'scope' and 'identity'`);
process.exit(1);
}

Expand All @@ -21,7 +27,7 @@ async function fetchWithRetry(url, options = {}, retries = 3, initialDelay = 100
try {
const response = await fetch(url, options);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
throw new Error(`HTTP error! status: ${response.status}, response: ${await response.text()}`);
}
return response;
} catch (error) {
Expand All @@ -39,27 +45,29 @@ async function fetchWithRetry(url, options = {}, retries = 3, initialDelay = 100
(async function main() {
// You can use await inside this function block
try {
debug('Fetching ID token from GitHub Actions');
const res = await fetchWithRetry(`${actionsUrl}&audience=${domain}`, { headers: { 'Authorization': `Bearer ${actionsToken}` } }, 5);
const json = await res.json();
// New scopes array
const scopes = [scope];
// Pass scopes as a comma-separated string in the URL
const scopesParam = scopes.join(',');
const res2 = await fetchWithRetry(`https://${domain}/sts/exchange?scope=${scope}&scopes=${scopesParam}&identity=${identity}`, { headers: { 'Authorization': `Bearer ${json.value}` } });

debug('Fetching GitHub Token from STS');

const parsedToken = parseOIDC(json.value);
console.log(`Got OIDC sub: ${parsedToken.sub}`);

const res2 = await fetchWithRetry(`https://${domain}/sts/exchange?scope=${scope}&scopes=${scope}&identity=${identity}`, { headers: { 'Authorization': `Bearer ${json.value}` } });
const json2 = await res2.json();

if (!json2.token) { console.log(`::error::${json2.message}`); process.exit(1); }
if (!json2.token) { error(json2.message); process.exit(1); }
const tok = json2.token;

const crypto = require('crypto');
const tokHash = crypto.createHash('sha256').update(tok).digest('hex');
console.log(`Token hash: ${tokHash}`);
debug(`Token hash: ${tokHash}`);

console.log(`::add-mask::${tok}`);
const fs = require('fs');
fs.appendFile(process.env.GITHUB_OUTPUT, `token=${tok}`, function (err) { if (err) throw err; }); // Write the output.
fs.appendFile(process.env.GITHUB_STATE, `token=${tok}`, function (err) { if (err) throw err; }); // Write the state, so the post job can delete the token.
addMask(tok);
await appendFile(process.env.GITHUB_OUTPUT, `token=${tok}`); // Write the output.
await appendFile(process.env.GITHUB_STATE, `token=${tok}`); // Write the state, so the post job can delete the token.
} catch (err) {
console.log(`::error::${err.stack}`); process.exit(1);
error(err.stack);
process.exit(1);
}
})();
27 changes: 27 additions & 0 deletions util.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
function encode(msg) {
return msg.replace(/%/g, '%25')
.replace(/\r/g, '%0D')
.replace(/\n/g, '%0A')
}

export function debug(msg) {
console.log(`::debug::${encode(msg)}`);
}

export function error(msg) {
console.log(`::error::${encode(msg)}`);
}

export function addMask(msg) {
console.log(`::add-mask::${encode(msg)}`);
}

export function parseOIDC(token) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm slightly worried about parsing the OIDC here, and not verifying it. I also don't hugely want to add verification to this code, and instead just forward it to the service which will validate it.

Was this only added so that we could log the OIDC subject? I wonder if there's a better way to do that. It's a very nice trait of this codebase that it's simple and "small enough to fit in your head".

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just wanted to output the subject for easier debugging :)

const parts = token.split('.');
if (parts.length !== 3) {
throw new Error('ID token is not in JWT format');
}
const payload = parts[1];
const decoded = Buffer.from(payload, 'base64').toString('utf8');
return JSON.parse(decoded);
}