diff --git a/action.yml b/action.yml index a0949ad..35aef7b 100644 --- a/action.yml +++ b/action.yml @@ -32,6 +32,6 @@ outputs: value: ${{ steps.octo-sts.outputs.token }} runs: - using: 'node20' + using: 'node24' main: 'index.js' post: 'post.js' diff --git a/index.js b/index.js index 4030c8d..3a743c8 100644 --- a/index.js +++ b/index.js @@ -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); } @@ -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); } @@ -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) { @@ -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); } })(); diff --git a/util.js b/util.js new file mode 100644 index 0000000..13b1918 --- /dev/null +++ b/util.js @@ -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) { + 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); +} \ No newline at end of file