chore: refactor WIF (#33)
* define common interfaces * common base client * refactor WIF to use interfaces and base client * refactor main * add build in CI * add name for build step * address comments * fix import * interface for credfile return * regen dist
This commit is contained in:
parent
3fe2a3779a
commit
d5a354ef10
15
.github/workflows/test.yaml
vendored
15
.github/workflows/test.yaml
vendored
@ -50,6 +50,11 @@ jobs:
|
||||
with:
|
||||
node-version: '12.x'
|
||||
|
||||
- name: 'build'
|
||||
run: |-
|
||||
npm ci
|
||||
npm run build
|
||||
|
||||
- uses: 'google-github-actions/setup-gcloud@master'
|
||||
with:
|
||||
project_id: 'actions-oidc-test'
|
||||
@ -84,6 +89,11 @@ jobs:
|
||||
with:
|
||||
node-version: '12.x'
|
||||
|
||||
- name: 'build'
|
||||
run: |-
|
||||
npm ci
|
||||
npm run build
|
||||
|
||||
- id: 'auth'
|
||||
name: 'auth'
|
||||
uses: './'
|
||||
@ -108,6 +118,11 @@ jobs:
|
||||
with:
|
||||
node-version: '12.x'
|
||||
|
||||
- name: 'build'
|
||||
run: |-
|
||||
npm ci
|
||||
npm run build
|
||||
|
||||
- id: 'auth'
|
||||
name: 'auth'
|
||||
uses: './'
|
||||
|
684
dist/index.js
vendored
684
dist/index.js
vendored
@ -194,27 +194,8 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const core = __importStar(__webpack_require__(470));
|
||||
const client_1 = __webpack_require__(976);
|
||||
const url_1 = __webpack_require__(835);
|
||||
/**
|
||||
* Converts a multi-line or comma-separated collection of strings into an array
|
||||
* of trimmed strings.
|
||||
*/
|
||||
function explodeStrings(input) {
|
||||
if (input == null || input.length === 0) {
|
||||
return [];
|
||||
}
|
||||
const list = new Array();
|
||||
for (const line of input.split(`\n`)) {
|
||||
for (const piece of line.split(',')) {
|
||||
const entry = piece.trim();
|
||||
if (entry !== '') {
|
||||
list.push(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
return list;
|
||||
}
|
||||
const workload_identity_1 = __webpack_require__(313);
|
||||
const utils_1 = __webpack_require__(163);
|
||||
/**
|
||||
* Executes the main action, documented inline.
|
||||
*/
|
||||
@ -226,11 +207,17 @@ function run() {
|
||||
required: true,
|
||||
});
|
||||
const serviceAccount = core.getInput('service_account', { required: true });
|
||||
const audience = core.getInput('audience') || `https://iam.googleapis.com/${workloadIdentityProvider}`;
|
||||
// audience will default to the WIF provider ID when used with WIF
|
||||
const audience = core.getInput('audience');
|
||||
const createCredentialsFile = core.getBooleanInput('create_credentials_file');
|
||||
const activateCredentialsFile = core.getBooleanInput('activate_credentials_file');
|
||||
const tokenFormat = core.getInput('token_format');
|
||||
const delegates = explodeStrings(core.getInput('delegates'));
|
||||
const delegates = (0, utils_1.explodeStrings)(core.getInput('delegates'));
|
||||
const client = new workload_identity_1.WIFClient({
|
||||
providerID: workloadIdentityProvider,
|
||||
serviceAccount: serviceAccount,
|
||||
audience: audience,
|
||||
});
|
||||
// Always write the credentials file first, before trying to generate
|
||||
// tokens. This will ensure the file is written even if token generation
|
||||
// fails, which means continue-on-error actions will still have the file
|
||||
@ -240,50 +227,16 @@ function run() {
|
||||
if (!runnerTempDir) {
|
||||
throw new Error('$RUNNER_TEMP is not set');
|
||||
}
|
||||
// Extract the request token and request URL from the environment. These
|
||||
// are only set when an id-token is requested and the submitter has
|
||||
// collaborator permissions.
|
||||
const requestToken = process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN;
|
||||
const requestURLRaw = process.env.ACTIONS_ID_TOKEN_REQUEST_URL;
|
||||
if (!requestToken || !requestURLRaw) {
|
||||
throw new Error('GitHub Actions did not inject $ACTIONS_ID_TOKEN_REQUEST_TOKEN or ' +
|
||||
'$ACTIONS_ID_TOKEN_REQUEST_URL into this job. This most likely ' +
|
||||
'means the GitHub Actions workflow permissions are incorrect, or ' +
|
||||
'this job is being run from a fork. For more information, please ' +
|
||||
'see the GitHub documentation at https://docs.github.com/en/actions/security-guides/automatic-token-authentication#permissions-for-the-github_token');
|
||||
}
|
||||
const requestURL = new url_1.URL(requestURLRaw);
|
||||
// Append the audience value to the request.
|
||||
const params = requestURL.searchParams;
|
||||
params.set('audience', audience);
|
||||
requestURL.search = params.toString();
|
||||
// Create the credentials file.
|
||||
const outputPath = yield client_1.Client.createCredentialsFile({
|
||||
providerID: workloadIdentityProvider,
|
||||
serviceAccount: serviceAccount,
|
||||
requestToken: requestToken,
|
||||
requestURL: requestURL.toString(),
|
||||
outputDir: runnerTempDir,
|
||||
});
|
||||
core.setOutput('credentials_file_path', outputPath);
|
||||
const { credentialsPath, envVars } = yield client.createCredentialsFile(runnerTempDir);
|
||||
core.setOutput('credentials_file_path', credentialsPath);
|
||||
// Also set the magic environment variable for gcloud and SDKs if
|
||||
// requested.
|
||||
if (activateCredentialsFile) {
|
||||
core.exportVariable('GOOGLE_APPLICATION_CREDENTIALS', outputPath);
|
||||
if (activateCredentialsFile && envVars) {
|
||||
for (const [k, v] of envVars) {
|
||||
core.exportVariable(k, v);
|
||||
}
|
||||
}
|
||||
}
|
||||
// getFederatedToken is a closure that gets the federated token.
|
||||
const getFederatedToken = () => __awaiter(this, void 0, void 0, function* () {
|
||||
// Get the GitHub OIDC token.
|
||||
const githubOIDCToken = yield core.getIDToken(audience);
|
||||
// Exchange the GitHub OIDC token for a Google Federated Token.
|
||||
const googleFederatedToken = yield client_1.Client.googleFederatedToken({
|
||||
providerID: workloadIdentityProvider,
|
||||
token: githubOIDCToken,
|
||||
});
|
||||
core.setSecret(googleFederatedToken);
|
||||
return googleFederatedToken;
|
||||
});
|
||||
switch (tokenFormat) {
|
||||
case '': {
|
||||
break;
|
||||
@ -293,14 +246,12 @@ function run() {
|
||||
}
|
||||
case 'access_token': {
|
||||
const accessTokenLifetime = core.getInput('access_token_lifetime');
|
||||
const accessTokenScopes = explodeStrings(core.getInput('access_token_scopes'));
|
||||
const googleFederatedToken = yield getFederatedToken();
|
||||
const { accessToken, expiration } = yield client_1.Client.googleAccessToken({
|
||||
token: googleFederatedToken,
|
||||
serviceAccount: serviceAccount,
|
||||
delegates: delegates,
|
||||
lifetime: accessTokenLifetime,
|
||||
const accessTokenScopes = (0, utils_1.explodeStrings)(core.getInput('access_token_scopes'));
|
||||
const { accessToken, expiration } = yield client.getAccessToken({
|
||||
serviceAccount,
|
||||
delegates,
|
||||
scopes: accessTokenScopes,
|
||||
lifetime: accessTokenLifetime,
|
||||
});
|
||||
core.setSecret(accessToken);
|
||||
core.setOutput('access_token', accessToken);
|
||||
@ -310,12 +261,10 @@ function run() {
|
||||
case 'id_token': {
|
||||
const idTokenAudience = core.getInput('id_token_audience', { required: true });
|
||||
const idTokenIncludeEmail = core.getBooleanInput('id_token_include_email');
|
||||
const googleFederatedToken = yield getFederatedToken();
|
||||
const { token } = yield client_1.Client.googleIDToken({
|
||||
token: googleFederatedToken,
|
||||
serviceAccount: serviceAccount,
|
||||
delegates: delegates,
|
||||
const { token } = yield client.getIDToken({
|
||||
serviceAccount,
|
||||
audience: idTokenAudience,
|
||||
delegates,
|
||||
includeEmail: idTokenIncludeEmail,
|
||||
});
|
||||
core.setSecret(token);
|
||||
@ -607,6 +556,75 @@ if (process.env.NODE_DEBUG && /\btunnel\b/.test(process.env.NODE_DEBUG)) {
|
||||
exports.debug = debug; // for test
|
||||
|
||||
|
||||
/***/ }),
|
||||
|
||||
/***/ 163:
|
||||
/***/ (function(__unusedmodule, exports, __webpack_require__) {
|
||||
|
||||
"use strict";
|
||||
|
||||
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
||||
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
||||
return new (P || (P = Promise))(function (resolve, reject) {
|
||||
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
||||
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
||||
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
||||
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
||||
});
|
||||
};
|
||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.explodeStrings = exports.writeCredFile = void 0;
|
||||
const fs_1 = __webpack_require__(747);
|
||||
const crypto_1 = __importDefault(__webpack_require__(417));
|
||||
const path_1 = __importDefault(__webpack_require__(622));
|
||||
/**
|
||||
* writeCredFile writes a file to disk in a given directory with a
|
||||
* random name.
|
||||
*
|
||||
* @param outputDir Directory to create random file in.
|
||||
* @param data Data to write to file.
|
||||
* @returns Path to written file.
|
||||
*/
|
||||
function writeCredFile(outputDir, data) {
|
||||
return __awaiter(this, void 0, void 0, function* () {
|
||||
// Generate a random filename to store the credential. 12 bytes is 24
|
||||
// characters in hex. It's not the ideal entropy, but we have to be under
|
||||
// the 255 character limit for Windows filenames (which includes their
|
||||
// entire leading path).
|
||||
const uniqueName = crypto_1.default.randomBytes(12).toString('hex');
|
||||
const pth = path_1.default.join(outputDir, uniqueName);
|
||||
// Write the file as 0640 so the owner has RW, group as R, and the file is
|
||||
// otherwise unreadable. Also write with EXCL to prevent a symlink attack.
|
||||
yield fs_1.promises.writeFile(pth, data, { mode: 0o640, flag: 'wx' });
|
||||
return pth;
|
||||
});
|
||||
}
|
||||
exports.writeCredFile = writeCredFile;
|
||||
/**
|
||||
* Converts a multi-line or comma-separated collection of strings into an array
|
||||
* of trimmed strings.
|
||||
*/
|
||||
function explodeStrings(input) {
|
||||
if (input == null || input.length === 0) {
|
||||
return [];
|
||||
}
|
||||
const list = new Array();
|
||||
for (const line of input.split(`\n`)) {
|
||||
for (const piece of line.split(',')) {
|
||||
const entry = piece.trim();
|
||||
if (entry !== '') {
|
||||
list.push(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
return list;
|
||||
}
|
||||
exports.explodeStrings = explodeStrings;
|
||||
|
||||
|
||||
/***/ }),
|
||||
|
||||
/***/ 211:
|
||||
@ -680,6 +698,174 @@ class PersonalAccessTokenCredentialHandler {
|
||||
exports.PersonalAccessTokenCredentialHandler = PersonalAccessTokenCredentialHandler;
|
||||
|
||||
|
||||
/***/ }),
|
||||
|
||||
/***/ 313:
|
||||
/***/ (function(__unusedmodule, exports, __webpack_require__) {
|
||||
|
||||
"use strict";
|
||||
|
||||
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
|
||||
}) : (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
o[k2] = m[k];
|
||||
}));
|
||||
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
||||
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
||||
}) : function(o, v) {
|
||||
o["default"] = v;
|
||||
});
|
||||
var __importStar = (this && this.__importStar) || function (mod) {
|
||||
if (mod && mod.__esModule) return mod;
|
||||
var result = {};
|
||||
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
||||
__setModuleDefault(result, mod);
|
||||
return result;
|
||||
};
|
||||
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
||||
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
||||
return new (P || (P = Promise))(function (resolve, reject) {
|
||||
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
||||
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
||||
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
||||
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
||||
});
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.WIFClient = void 0;
|
||||
const url_1 = __webpack_require__(835);
|
||||
const core = __importStar(__webpack_require__(470));
|
||||
const utils_1 = __webpack_require__(163);
|
||||
const base_1 = __webpack_require__(843);
|
||||
class WIFClient {
|
||||
constructor(opts) {
|
||||
this.providerID = opts.providerID;
|
||||
this.serviceAccount = opts.serviceAccount;
|
||||
this.audience = opts.audience ? opts.audience : `https://iam.googleapis.com/${this.providerID}`;
|
||||
}
|
||||
/**
|
||||
* googleFederatedToken generates a Google Cloud federated token using the
|
||||
* provided OIDC token and Workload Identity Provider.
|
||||
*/
|
||||
static googleFederatedToken({ providerID, token, }) {
|
||||
return __awaiter(this, void 0, void 0, function* () {
|
||||
const stsURL = new url_1.URL('https://sts.googleapis.com/v1/token');
|
||||
const data = {
|
||||
audience: '//iam.googleapis.com/' + providerID,
|
||||
grantType: 'urn:ietf:params:oauth:grant-type:token-exchange',
|
||||
requestedTokenType: 'urn:ietf:params:oauth:token-type:access_token',
|
||||
scope: 'https://www.googleapis.com/auth/cloud-platform',
|
||||
subjectTokenType: 'urn:ietf:params:oauth:token-type:jwt',
|
||||
subjectToken: token,
|
||||
};
|
||||
const opts = {
|
||||
hostname: stsURL.hostname,
|
||||
port: stsURL.port,
|
||||
path: stsURL.pathname + stsURL.search,
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
};
|
||||
try {
|
||||
const resp = yield base_1.BaseClient.request(opts, JSON.stringify(data));
|
||||
const parsed = JSON.parse(resp);
|
||||
return parsed['access_token'];
|
||||
}
|
||||
catch (err) {
|
||||
throw new Error(`failed to generate Google Cloud federated token for ${providerID}: ${err}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
/**
|
||||
* getFederatedToken generates a Google Cloud federated token using the
|
||||
* GitHub OIDC token.
|
||||
*/
|
||||
getFederatedToken() {
|
||||
return __awaiter(this, void 0, void 0, function* () {
|
||||
// Get the GitHub OIDC token.
|
||||
const githubOIDCToken = yield core.getIDToken(this.audience);
|
||||
// Exchange the GitHub OIDC token for a Google Federated Token.
|
||||
const googleFederatedToken = yield WIFClient.googleFederatedToken({
|
||||
providerID: this.providerID,
|
||||
token: githubOIDCToken,
|
||||
});
|
||||
core.setSecret(googleFederatedToken);
|
||||
return googleFederatedToken;
|
||||
});
|
||||
}
|
||||
/**
|
||||
* getAccessToken generates a Google Cloud access token for the provided
|
||||
* service account email or unique id.
|
||||
*/
|
||||
getAccessToken(opts) {
|
||||
return __awaiter(this, void 0, void 0, function* () {
|
||||
const googleFederatedToken = yield this.getFederatedToken();
|
||||
return yield base_1.BaseClient.googleAccessToken(googleFederatedToken, opts);
|
||||
});
|
||||
}
|
||||
/**
|
||||
* getIDToken generates a Google Cloud ID token for the provided
|
||||
* service account email or unique id.
|
||||
*/
|
||||
getIDToken(tokenParams) {
|
||||
return __awaiter(this, void 0, void 0, function* () {
|
||||
const googleFederatedToken = yield this.getFederatedToken();
|
||||
return yield base_1.BaseClient.googleIDToken(googleFederatedToken, tokenParams);
|
||||
});
|
||||
}
|
||||
/**
|
||||
* createCredentialsFile creates a Google Cloud credentials file that can be
|
||||
* set as GOOGLE_APPLICATION_CREDENTIALS for gcloud and client libraries.
|
||||
*/
|
||||
createCredentialsFile(outputDir) {
|
||||
return __awaiter(this, void 0, void 0, function* () {
|
||||
// Extract the request token and request URL from the environment. These
|
||||
// are only set when an id-token is requested and the submitter has
|
||||
// collaborator permissions.
|
||||
const requestToken = process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN;
|
||||
const requestURLRaw = process.env.ACTIONS_ID_TOKEN_REQUEST_URL;
|
||||
if (!requestToken || !requestURLRaw) {
|
||||
throw new Error('GitHub Actions did not inject $ACTIONS_ID_TOKEN_REQUEST_TOKEN or ' +
|
||||
'$ACTIONS_ID_TOKEN_REQUEST_URL into this job. This most likely ' +
|
||||
'means the GitHub Actions workflow permissions are incorrect, or ' +
|
||||
'this job is being run from a fork. For more information, please ' +
|
||||
'see the GitHub documentation at https://docs.github.com/en/actions/security-guides/automatic-token-authentication#permissions-for-the-github_token');
|
||||
}
|
||||
const requestURL = new url_1.URL(requestURLRaw);
|
||||
// Append the audience value to the request.
|
||||
const params = requestURL.searchParams;
|
||||
params.set('audience', this.audience);
|
||||
requestURL.search = params.toString();
|
||||
const data = {
|
||||
type: 'external_account',
|
||||
audience: `//iam.googleapis.com/${this.providerID}`,
|
||||
subject_token_type: 'urn:ietf:params:oauth:token-type:jwt',
|
||||
token_url: 'https://sts.googleapis.com/v1/token',
|
||||
service_account_impersonation_url: `https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/${this.serviceAccount}:generateAccessToken`,
|
||||
credential_source: {
|
||||
url: requestURL,
|
||||
headers: {
|
||||
Authorization: `Bearer ${requestToken}`,
|
||||
},
|
||||
format: {
|
||||
type: 'json',
|
||||
subject_token_field_name: 'value',
|
||||
},
|
||||
},
|
||||
};
|
||||
const credentialsPath = yield (0, utils_1.writeCredFile)(outputDir, JSON.stringify(data));
|
||||
const envVars = new Map([['GOOGLE_APPLICATION_CREDENTIALS', credentialsPath]]);
|
||||
return { credentialsPath, envVars };
|
||||
});
|
||||
}
|
||||
}
|
||||
exports.WIFClient = WIFClient;
|
||||
|
||||
|
||||
/***/ }),
|
||||
|
||||
/***/ 357:
|
||||
@ -1798,6 +1984,144 @@ module.exports = require("fs");
|
||||
|
||||
module.exports = require("url");
|
||||
|
||||
/***/ }),
|
||||
|
||||
/***/ 843:
|
||||
/***/ (function(__unusedmodule, exports, __webpack_require__) {
|
||||
|
||||
"use strict";
|
||||
|
||||
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
||||
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
||||
return new (P || (P = Promise))(function (resolve, reject) {
|
||||
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
||||
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
||||
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
||||
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
||||
});
|
||||
};
|
||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.BaseClient = void 0;
|
||||
const https_1 = __importDefault(__webpack_require__(211));
|
||||
const url_1 = __webpack_require__(835);
|
||||
class BaseClient {
|
||||
/**
|
||||
* request is a high-level helper that returns a promise from the executed
|
||||
* request.
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types
|
||||
static request(opts, data) {
|
||||
if (!opts.headers) {
|
||||
opts.headers = {};
|
||||
}
|
||||
if (!opts.headers['User-Agent']) {
|
||||
opts.headers['User-Agent'] = 'google-github-actions:auth/0.3.1';
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
const req = https_1.default.request(opts, (res) => {
|
||||
res.setEncoding('utf8');
|
||||
let body = '';
|
||||
res.on('data', (data) => {
|
||||
body += data;
|
||||
});
|
||||
res.on('end', () => {
|
||||
if (res.statusCode && res.statusCode >= 400) {
|
||||
reject(body);
|
||||
}
|
||||
else {
|
||||
resolve(body);
|
||||
}
|
||||
});
|
||||
});
|
||||
req.on('error', (err) => {
|
||||
reject(err);
|
||||
});
|
||||
if (data != null) {
|
||||
req.write(data);
|
||||
}
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
/**
|
||||
* googleIDToken generates a Google Cloud ID token for the provided
|
||||
* service account email or unique id.
|
||||
*/
|
||||
static googleIDToken(token, { serviceAccount, audience, delegates, includeEmail }) {
|
||||
return __awaiter(this, void 0, void 0, function* () {
|
||||
const serviceAccountID = `projects/-/serviceAccounts/${serviceAccount}`;
|
||||
const tokenURL = new url_1.URL(`https://iamcredentials.googleapis.com/v1/${serviceAccountID}:generateIdToken`);
|
||||
const data = {
|
||||
delegates: delegates,
|
||||
audience: audience,
|
||||
includeEmail: includeEmail,
|
||||
};
|
||||
const opts = {
|
||||
hostname: tokenURL.hostname,
|
||||
port: tokenURL.port,
|
||||
path: tokenURL.pathname + tokenURL.search,
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
};
|
||||
try {
|
||||
const resp = yield BaseClient.request(opts, JSON.stringify(data));
|
||||
const parsed = JSON.parse(resp);
|
||||
return {
|
||||
token: parsed['token'],
|
||||
};
|
||||
}
|
||||
catch (err) {
|
||||
throw new Error(`failed to generate Google Cloud ID token for ${serviceAccount}: ${err}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
/**
|
||||
* googleAccessToken generates a Google Cloud access token for the provided
|
||||
* service account email or unique id.
|
||||
*/
|
||||
static googleAccessToken(token, { serviceAccount, delegates, scopes, lifetime }) {
|
||||
return __awaiter(this, void 0, void 0, function* () {
|
||||
const serviceAccountID = `projects/-/serviceAccounts/${serviceAccount}`;
|
||||
const tokenURL = new url_1.URL(`https://iamcredentials.googleapis.com/v1/${serviceAccountID}:generateAccessToken`);
|
||||
const data = {
|
||||
delegates: delegates,
|
||||
lifetime: lifetime,
|
||||
scope: scopes,
|
||||
};
|
||||
const opts = {
|
||||
hostname: tokenURL.hostname,
|
||||
port: tokenURL.port,
|
||||
path: tokenURL.pathname + tokenURL.search,
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
};
|
||||
try {
|
||||
const resp = yield BaseClient.request(opts, JSON.stringify(data));
|
||||
const parsed = JSON.parse(resp);
|
||||
return {
|
||||
accessToken: parsed['accessToken'],
|
||||
expiration: parsed['expireTime'],
|
||||
};
|
||||
}
|
||||
catch (err) {
|
||||
throw new Error(`failed to generate Google Cloud access token for ${serviceAccount}: ${err}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
exports.BaseClient = BaseClient;
|
||||
|
||||
|
||||
/***/ }),
|
||||
|
||||
/***/ 950:
|
||||
@ -1863,216 +2187,6 @@ function checkBypass(reqUrl) {
|
||||
exports.checkBypass = checkBypass;
|
||||
|
||||
|
||||
/***/ }),
|
||||
|
||||
/***/ 976:
|
||||
/***/ (function(__unusedmodule, exports, __webpack_require__) {
|
||||
|
||||
"use strict";
|
||||
|
||||
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
||||
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
||||
return new (P || (P = Promise))(function (resolve, reject) {
|
||||
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
||||
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
||||
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
||||
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
||||
});
|
||||
};
|
||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.Client = void 0;
|
||||
const https_1 = __importDefault(__webpack_require__(211));
|
||||
const fs_1 = __webpack_require__(747);
|
||||
const crypto_1 = __importDefault(__webpack_require__(417));
|
||||
const path_1 = __importDefault(__webpack_require__(622));
|
||||
const url_1 = __webpack_require__(835);
|
||||
class Client {
|
||||
/**
|
||||
* request is a high-level helper that returns a promise from the executed
|
||||
* request.
|
||||
*/
|
||||
static request(opts, data) {
|
||||
if (!opts.headers) {
|
||||
opts.headers = {};
|
||||
}
|
||||
if (!opts.headers['User-Agent']) {
|
||||
opts.headers['User-Agent'] = 'google-github-actions:auth/0.3.0';
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
const req = https_1.default.request(opts, (res) => {
|
||||
res.setEncoding('utf8');
|
||||
let body = '';
|
||||
res.on('data', (data) => {
|
||||
body += data;
|
||||
});
|
||||
res.on('end', () => {
|
||||
if (res.statusCode && res.statusCode >= 400) {
|
||||
reject(body);
|
||||
}
|
||||
else {
|
||||
resolve(body);
|
||||
}
|
||||
});
|
||||
});
|
||||
req.on('error', (err) => {
|
||||
reject(err);
|
||||
});
|
||||
if (data != null) {
|
||||
req.write(data);
|
||||
}
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
/**
|
||||
* googleFederatedToken generates a Google Cloud federated token using the
|
||||
* provided OIDC token and Workload Identity Provider.
|
||||
*/
|
||||
static googleFederatedToken({ providerID, token, }) {
|
||||
return __awaiter(this, void 0, void 0, function* () {
|
||||
const stsURL = new url_1.URL('https://sts.googleapis.com/v1/token');
|
||||
const data = {
|
||||
audience: '//iam.googleapis.com/' + providerID,
|
||||
grantType: 'urn:ietf:params:oauth:grant-type:token-exchange',
|
||||
requestedTokenType: 'urn:ietf:params:oauth:token-type:access_token',
|
||||
scope: 'https://www.googleapis.com/auth/cloud-platform',
|
||||
subjectTokenType: 'urn:ietf:params:oauth:token-type:jwt',
|
||||
subjectToken: token,
|
||||
};
|
||||
const opts = {
|
||||
hostname: stsURL.hostname,
|
||||
port: stsURL.port,
|
||||
path: stsURL.pathname + stsURL.search,
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
};
|
||||
try {
|
||||
const resp = yield Client.request(opts, JSON.stringify(data));
|
||||
const parsed = JSON.parse(resp);
|
||||
return parsed['access_token'];
|
||||
}
|
||||
catch (err) {
|
||||
throw new Error(`failed to generate Google Cloud federated token for ${providerID}: ${err}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
/**
|
||||
* googleAccessToken generates a Google Cloud access token for the provided
|
||||
* service account email or unique id.
|
||||
*/
|
||||
static googleAccessToken({ token, serviceAccount, delegates, scopes, lifetime, }) {
|
||||
return __awaiter(this, void 0, void 0, function* () {
|
||||
const serviceAccountID = `projects/-/serviceAccounts/${serviceAccount}`;
|
||||
const tokenURL = new url_1.URL(`https://iamcredentials.googleapis.com/v1/${serviceAccountID}:generateAccessToken`);
|
||||
const data = {
|
||||
delegates: delegates,
|
||||
lifetime: lifetime,
|
||||
scope: scopes,
|
||||
};
|
||||
const opts = {
|
||||
hostname: tokenURL.hostname,
|
||||
port: tokenURL.port,
|
||||
path: tokenURL.pathname + tokenURL.search,
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
};
|
||||
try {
|
||||
const resp = yield Client.request(opts, JSON.stringify(data));
|
||||
const parsed = JSON.parse(resp);
|
||||
return {
|
||||
accessToken: parsed['accessToken'],
|
||||
expiration: parsed['expireTime'],
|
||||
};
|
||||
}
|
||||
catch (err) {
|
||||
throw new Error(`failed to generate Google Cloud access token for ${serviceAccount}: ${err}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
/**
|
||||
* googleIDToken generates a Google Cloud ID token for the provided
|
||||
* service account email or unique id.
|
||||
*/
|
||||
static googleIDToken({ token, serviceAccount, audience, delegates, includeEmail, }) {
|
||||
return __awaiter(this, void 0, void 0, function* () {
|
||||
const serviceAccountID = `projects/-/serviceAccounts/${serviceAccount}`;
|
||||
const tokenURL = new url_1.URL(`https://iamcredentials.googleapis.com/v1/${serviceAccountID}:generateIdToken`);
|
||||
const data = {
|
||||
delegates: delegates,
|
||||
audience: audience,
|
||||
includeEmail: includeEmail,
|
||||
};
|
||||
const opts = {
|
||||
hostname: tokenURL.hostname,
|
||||
port: tokenURL.port,
|
||||
path: tokenURL.pathname + tokenURL.search,
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
};
|
||||
try {
|
||||
const resp = yield Client.request(opts, JSON.stringify(data));
|
||||
const parsed = JSON.parse(resp);
|
||||
return {
|
||||
token: parsed['token'],
|
||||
};
|
||||
}
|
||||
catch (err) {
|
||||
throw new Error(`failed to generate Google Cloud ID token for ${serviceAccount}: ${err}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
/**
|
||||
* createCredentialsFile creates a Google Cloud credentials file that can be
|
||||
* set as GOOGLE_APPLICATION_CREDENTIALS for gcloud and client libraries.
|
||||
*/
|
||||
static createCredentialsFile({ providerID, serviceAccount, requestToken, requestURL, outputDir, }) {
|
||||
return __awaiter(this, void 0, void 0, function* () {
|
||||
const data = {
|
||||
type: 'external_account',
|
||||
audience: `//iam.googleapis.com/${providerID}`,
|
||||
subject_token_type: 'urn:ietf:params:oauth:token-type:jwt',
|
||||
token_url: 'https://sts.googleapis.com/v1/token',
|
||||
service_account_impersonation_url: `https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/${serviceAccount}:generateAccessToken`,
|
||||
credential_source: {
|
||||
url: requestURL,
|
||||
headers: {
|
||||
Authorization: `Bearer ${requestToken}`,
|
||||
},
|
||||
format: {
|
||||
type: 'json',
|
||||
subject_token_field_name: 'value',
|
||||
},
|
||||
},
|
||||
};
|
||||
// Generate a random filename to store the credential. 12 bytes is 24
|
||||
// characters in hex. It's not the ideal entropy, but we have to be under
|
||||
// the 255 character limit for Windows filenames (which includes their
|
||||
// entire leading path).
|
||||
const uniqueName = crypto_1.default.randomBytes(12).toString('hex');
|
||||
const pth = path_1.default.join(outputDir, uniqueName);
|
||||
// Write the file as 0640 so the owner has RW, group as R, and the file is
|
||||
// otherwise unreadable. Also write with EXCL to prevent a symlink attack.
|
||||
yield fs_1.promises.writeFile(pth, JSON.stringify(data), { mode: 0o640, flag: 'wx' });
|
||||
return pth;
|
||||
});
|
||||
}
|
||||
}
|
||||
exports.Client = Client;
|
||||
|
||||
|
||||
/***/ })
|
||||
|
||||
/******/ });
|
79
src/actionauth.ts
Normal file
79
src/actionauth.ts
Normal file
@ -0,0 +1,79 @@
|
||||
/**
|
||||
* Defines the main interface for all clients that generate credentials.
|
||||
*/
|
||||
export interface ActionAuth {
|
||||
getAccessToken(opts: GoogleAccessTokenParameters): Promise<GoogleAccessTokenResponse>;
|
||||
getIDToken(opts: GoogleIDTokenParameters): Promise<GoogleIDTokenResponse>;
|
||||
createCredentialsFile(outputDir: string): Promise<CreateCredentialsFileResponse>;
|
||||
}
|
||||
|
||||
/**
|
||||
* GoogleAccessTokenParameters are the parameters to generate a Google Cloud
|
||||
* access token as described in:
|
||||
*
|
||||
* https://cloud.google.com/iam/docs/reference/credentials/rest/v1/projects.serviceAccounts/generateAccessToken
|
||||
*
|
||||
* @param serviceAccount Optional email address or unique identifier of the service
|
||||
* account.
|
||||
* @param delegates Optional sequence of service accounts in the delegation
|
||||
* chain.
|
||||
* @param lifetime Optional validity period as a duration.
|
||||
*/
|
||||
export interface GoogleAccessTokenParameters {
|
||||
serviceAccount?: string;
|
||||
delegates?: Array<string>;
|
||||
scopes?: Array<string>;
|
||||
lifetime?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* GoogleAccessTokenResponse is the response from generating an access token.
|
||||
*
|
||||
* @param accessToken OAuth 2.0 access token.
|
||||
* @param expiration A timestamp in RFC3339 UTC "Zulu" format when the token
|
||||
* expires.
|
||||
*/
|
||||
export interface GoogleAccessTokenResponse {
|
||||
accessToken: string;
|
||||
expiration: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* GoogleIDTokenParameters are the parameters to generate a Google Cloud
|
||||
* ID token as described in:
|
||||
*
|
||||
* https://cloud.google.com/iam/docs/reference/credentials/rest/v1/projects.serviceAccounts/generateIdToken
|
||||
*
|
||||
* @param serviceAccount Email address or unique identifier of the service
|
||||
* account.
|
||||
* @param audience The audience for the token.
|
||||
* @param delegates Optional sequence of service accounts in the delegation
|
||||
* chain.
|
||||
*/
|
||||
export interface GoogleIDTokenParameters {
|
||||
serviceAccount?: string;
|
||||
audience: string;
|
||||
delegates?: Array<string>;
|
||||
includeEmail?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* GoogleIDTokenResponse is the response from generating an ID token.
|
||||
*
|
||||
* @param token ID token.
|
||||
* expires.
|
||||
*/
|
||||
export interface GoogleIDTokenResponse {
|
||||
token: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* CreateCredentialsFileResponse is the response from creating a credential file.
|
||||
*
|
||||
* @param credentialsPath Path to the created credentials file.
|
||||
* @param envVars Optional key value pairs that can be exported as env variables.
|
||||
*/
|
||||
export interface CreateCredentialsFileResponse {
|
||||
credentialsPath: string;
|
||||
envVars?: Map<string, string>;
|
||||
}
|
139
src/base.ts
Normal file
139
src/base.ts
Normal file
@ -0,0 +1,139 @@
|
||||
import https, { RequestOptions } from 'https';
|
||||
import { URL } from 'url';
|
||||
import {
|
||||
GoogleAccessTokenParameters,
|
||||
GoogleAccessTokenResponse,
|
||||
GoogleIDTokenParameters,
|
||||
GoogleIDTokenResponse,
|
||||
} from './actionauth';
|
||||
|
||||
export class BaseClient {
|
||||
/**
|
||||
* request is a high-level helper that returns a promise from the executed
|
||||
* request.
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types
|
||||
static request(opts: RequestOptions, data?: any): Promise<string> {
|
||||
if (!opts.headers) {
|
||||
opts.headers = {};
|
||||
}
|
||||
|
||||
if (!opts.headers['User-Agent']) {
|
||||
opts.headers['User-Agent'] = 'google-github-actions:auth/0.3.1';
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const req = https.request(opts, (res) => {
|
||||
res.setEncoding('utf8');
|
||||
|
||||
let body = '';
|
||||
res.on('data', (data) => {
|
||||
body += data;
|
||||
});
|
||||
|
||||
res.on('end', () => {
|
||||
if (res.statusCode && res.statusCode >= 400) {
|
||||
reject(body);
|
||||
} else {
|
||||
resolve(body);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
req.on('error', (err) => {
|
||||
reject(err);
|
||||
});
|
||||
|
||||
if (data != null) {
|
||||
req.write(data);
|
||||
}
|
||||
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* googleIDToken generates a Google Cloud ID token for the provided
|
||||
* service account email or unique id.
|
||||
*/
|
||||
static async googleIDToken(
|
||||
token: string,
|
||||
{ serviceAccount, audience, delegates, includeEmail }: GoogleIDTokenParameters,
|
||||
): Promise<GoogleIDTokenResponse> {
|
||||
const serviceAccountID = `projects/-/serviceAccounts/${serviceAccount}`;
|
||||
const tokenURL = new URL(
|
||||
`https://iamcredentials.googleapis.com/v1/${serviceAccountID}:generateIdToken`,
|
||||
);
|
||||
|
||||
const data = {
|
||||
delegates: delegates,
|
||||
audience: audience,
|
||||
includeEmail: includeEmail,
|
||||
};
|
||||
|
||||
const opts = {
|
||||
hostname: tokenURL.hostname,
|
||||
port: tokenURL.port,
|
||||
path: tokenURL.pathname + tokenURL.search,
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
};
|
||||
|
||||
try {
|
||||
const resp = await BaseClient.request(opts, JSON.stringify(data));
|
||||
const parsed = JSON.parse(resp);
|
||||
return {
|
||||
token: parsed['token'],
|
||||
};
|
||||
} catch (err) {
|
||||
throw new Error(`failed to generate Google Cloud ID token for ${serviceAccount}: ${err}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* googleAccessToken generates a Google Cloud access token for the provided
|
||||
* service account email or unique id.
|
||||
*/
|
||||
static async googleAccessToken(
|
||||
token: string,
|
||||
{ serviceAccount, delegates, scopes, lifetime }: GoogleAccessTokenParameters,
|
||||
): Promise<GoogleAccessTokenResponse> {
|
||||
const serviceAccountID = `projects/-/serviceAccounts/${serviceAccount}`;
|
||||
const tokenURL = new URL(
|
||||
`https://iamcredentials.googleapis.com/v1/${serviceAccountID}:generateAccessToken`,
|
||||
);
|
||||
|
||||
const data = {
|
||||
delegates: delegates,
|
||||
lifetime: lifetime,
|
||||
scope: scopes,
|
||||
};
|
||||
|
||||
const opts = {
|
||||
hostname: tokenURL.hostname,
|
||||
port: tokenURL.port,
|
||||
path: tokenURL.pathname + tokenURL.search,
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
};
|
||||
|
||||
try {
|
||||
const resp = await BaseClient.request(opts, JSON.stringify(data));
|
||||
const parsed = JSON.parse(resp);
|
||||
return {
|
||||
accessToken: parsed['accessToken'],
|
||||
expiration: parsed['expireTime'],
|
||||
};
|
||||
} catch (err) {
|
||||
throw new Error(`failed to generate Google Cloud access token for ${serviceAccount}: ${err}`);
|
||||
}
|
||||
}
|
||||
}
|
328
src/client.ts
328
src/client.ts
@ -1,328 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
import https, { RequestOptions } from 'https';
|
||||
import { promises as fs } from 'fs';
|
||||
import crypto from 'crypto';
|
||||
import path from 'path';
|
||||
import { URL } from 'url';
|
||||
|
||||
/**
|
||||
* GoogleFederatedTokenParameters are the parameters to generate a Federated
|
||||
* Identity Token as described in:
|
||||
*
|
||||
* https://cloud.google.com/iam/docs/access-resources-oidc#exchange-token
|
||||
*
|
||||
* @param providerID Full path (including project, location, etc) to the Google
|
||||
* Cloud Workload Identity Provider.
|
||||
* @param token OIDC token to exchange for a Google Cloud federated token.
|
||||
*/
|
||||
interface GoogleFederatedTokenParameters {
|
||||
providerID: string;
|
||||
token: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* GoogleAccessTokenParameters are the parameters to generate a Google Cloud
|
||||
* access token as described in:
|
||||
*
|
||||
* https://cloud.google.com/iam/docs/reference/credentials/rest/v1/projects.serviceAccounts/generateAccessToken
|
||||
*
|
||||
* @param token OAuth token or Federated access token with permissions to call
|
||||
* the API.
|
||||
* @param serviceAccount Email address or unique identifier of the service
|
||||
* account.
|
||||
* @param delegates Optional sequence of service accounts in the delegation
|
||||
* chain.
|
||||
* @param lifetime Optional validity period as a duration.
|
||||
*/
|
||||
interface GoogleAccessTokenParameters {
|
||||
token: string;
|
||||
serviceAccount: string;
|
||||
delegates?: Array<string>;
|
||||
scopes?: Array<string>;
|
||||
lifetime?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* GoogleAccessTokenResponse is the response from generating an access token.
|
||||
*
|
||||
* @param accessToken OAuth 2.0 access token.
|
||||
* @param expiration A timestamp in RFC3339 UTC "Zulu" format when the token
|
||||
* expires.
|
||||
*/
|
||||
interface GoogleAccessTokenResponse {
|
||||
accessToken: string;
|
||||
expiration: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* GoogleIDTokenParameters are the parameters to generate a Google Cloud
|
||||
* ID token as described in:
|
||||
*
|
||||
* https://cloud.google.com/iam/docs/reference/credentials/rest/v1/projects.serviceAccounts/generateIdToken
|
||||
*
|
||||
* @param token OAuth token or Federated access token with permissions to call
|
||||
* the API.
|
||||
* @param serviceAccount Email address or unique identifier of the service
|
||||
* account.
|
||||
* @param audience The audience for the token.
|
||||
* @param delegates Optional sequence of service accounts in the delegation
|
||||
* chain.
|
||||
*/
|
||||
interface GoogleIDTokenParameters {
|
||||
token: string;
|
||||
serviceAccount: string;
|
||||
audience: string;
|
||||
delegates?: Array<string>;
|
||||
includeEmail?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* GoogleIDTokenResponse is the response from generating an ID token.
|
||||
*
|
||||
* @param token ID token.
|
||||
* expires.
|
||||
*/
|
||||
interface GoogleIDTokenResponse {
|
||||
token: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* CreateCredentialsFileParameters are the parameters to generate a Google Cloud
|
||||
* credentials file for use with gcloud and other SDKs.
|
||||
*
|
||||
* @param providerID Full path (including project, location, etc) to the Google
|
||||
* Cloud Workload Identity Provider.
|
||||
* @param serviceAccount Email address or unique identifier of the service
|
||||
* account to impersonate
|
||||
* @param requestToken Local environment token to use as authentication to
|
||||
* acquire the real OIDC token.
|
||||
* @param requestURL URL endpoint to use to request the token.
|
||||
* @param outputDir Path to a directory on disk where the file should be
|
||||
* written. The function will determine the file name and write to it securely.
|
||||
*/
|
||||
interface CreateCredentialsFileParameters {
|
||||
providerID: string;
|
||||
serviceAccount: string;
|
||||
requestToken: string;
|
||||
requestURL: string;
|
||||
outputDir: string;
|
||||
}
|
||||
|
||||
export class Client {
|
||||
/**
|
||||
* request is a high-level helper that returns a promise from the executed
|
||||
* request.
|
||||
*/
|
||||
static request(opts: RequestOptions, data?: any): Promise<string> {
|
||||
if (!opts.headers) {
|
||||
opts.headers = {};
|
||||
}
|
||||
|
||||
if (!opts.headers['User-Agent']) {
|
||||
opts.headers['User-Agent'] = 'google-github-actions:auth/0.3.0';
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const req = https.request(opts, (res) => {
|
||||
res.setEncoding('utf8');
|
||||
|
||||
let body = '';
|
||||
res.on('data', (data) => {
|
||||
body += data;
|
||||
});
|
||||
|
||||
res.on('end', () => {
|
||||
if (res.statusCode && res.statusCode >= 400) {
|
||||
reject(body);
|
||||
} else {
|
||||
resolve(body);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
req.on('error', (err) => {
|
||||
reject(err);
|
||||
});
|
||||
|
||||
if (data != null) {
|
||||
req.write(data);
|
||||
}
|
||||
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* googleFederatedToken generates a Google Cloud federated token using the
|
||||
* provided OIDC token and Workload Identity Provider.
|
||||
*/
|
||||
static async googleFederatedToken({
|
||||
providerID,
|
||||
token,
|
||||
}: GoogleFederatedTokenParameters): Promise<string> {
|
||||
const stsURL = new URL('https://sts.googleapis.com/v1/token');
|
||||
|
||||
const data = {
|
||||
audience: '//iam.googleapis.com/' + providerID,
|
||||
grantType: 'urn:ietf:params:oauth:grant-type:token-exchange',
|
||||
requestedTokenType: 'urn:ietf:params:oauth:token-type:access_token',
|
||||
scope: 'https://www.googleapis.com/auth/cloud-platform',
|
||||
subjectTokenType: 'urn:ietf:params:oauth:token-type:jwt',
|
||||
subjectToken: token,
|
||||
};
|
||||
|
||||
const opts = {
|
||||
hostname: stsURL.hostname,
|
||||
port: stsURL.port,
|
||||
path: stsURL.pathname + stsURL.search,
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
};
|
||||
|
||||
try {
|
||||
const resp = await Client.request(opts, JSON.stringify(data));
|
||||
const parsed = JSON.parse(resp);
|
||||
return parsed['access_token'];
|
||||
} catch (err) {
|
||||
throw new Error(`failed to generate Google Cloud federated token for ${providerID}: ${err}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* googleAccessToken generates a Google Cloud access token for the provided
|
||||
* service account email or unique id.
|
||||
*/
|
||||
static async googleAccessToken({
|
||||
token,
|
||||
serviceAccount,
|
||||
delegates,
|
||||
scopes,
|
||||
lifetime,
|
||||
}: GoogleAccessTokenParameters): Promise<GoogleAccessTokenResponse> {
|
||||
const serviceAccountID = `projects/-/serviceAccounts/${serviceAccount}`;
|
||||
const tokenURL = new URL(
|
||||
`https://iamcredentials.googleapis.com/v1/${serviceAccountID}:generateAccessToken`,
|
||||
);
|
||||
|
||||
const data = {
|
||||
delegates: delegates,
|
||||
lifetime: lifetime,
|
||||
scope: scopes,
|
||||
};
|
||||
|
||||
const opts = {
|
||||
hostname: tokenURL.hostname,
|
||||
port: tokenURL.port,
|
||||
path: tokenURL.pathname + tokenURL.search,
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
};
|
||||
|
||||
try {
|
||||
const resp = await Client.request(opts, JSON.stringify(data));
|
||||
const parsed = JSON.parse(resp);
|
||||
return {
|
||||
accessToken: parsed['accessToken'],
|
||||
expiration: parsed['expireTime'],
|
||||
};
|
||||
} catch (err) {
|
||||
throw new Error(`failed to generate Google Cloud access token for ${serviceAccount}: ${err}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* googleIDToken generates a Google Cloud ID token for the provided
|
||||
* service account email or unique id.
|
||||
*/
|
||||
static async googleIDToken({
|
||||
token,
|
||||
serviceAccount,
|
||||
audience,
|
||||
delegates,
|
||||
includeEmail,
|
||||
}: GoogleIDTokenParameters): Promise<GoogleIDTokenResponse> {
|
||||
const serviceAccountID = `projects/-/serviceAccounts/${serviceAccount}`;
|
||||
const tokenURL = new URL(
|
||||
`https://iamcredentials.googleapis.com/v1/${serviceAccountID}:generateIdToken`,
|
||||
);
|
||||
|
||||
const data = {
|
||||
delegates: delegates,
|
||||
audience: audience,
|
||||
includeEmail: includeEmail,
|
||||
};
|
||||
|
||||
const opts = {
|
||||
hostname: tokenURL.hostname,
|
||||
port: tokenURL.port,
|
||||
path: tokenURL.pathname + tokenURL.search,
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
};
|
||||
|
||||
try {
|
||||
const resp = await Client.request(opts, JSON.stringify(data));
|
||||
const parsed = JSON.parse(resp);
|
||||
return {
|
||||
token: parsed['token'],
|
||||
};
|
||||
} catch (err) {
|
||||
throw new Error(`failed to generate Google Cloud ID token for ${serviceAccount}: ${err}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* createCredentialsFile creates a Google Cloud credentials file that can be
|
||||
* set as GOOGLE_APPLICATION_CREDENTIALS for gcloud and client libraries.
|
||||
*/
|
||||
static async createCredentialsFile({
|
||||
providerID,
|
||||
serviceAccount,
|
||||
requestToken,
|
||||
requestURL,
|
||||
outputDir,
|
||||
}: CreateCredentialsFileParameters): Promise<string> {
|
||||
const data = {
|
||||
type: 'external_account',
|
||||
audience: `//iam.googleapis.com/${providerID}`,
|
||||
subject_token_type: 'urn:ietf:params:oauth:token-type:jwt',
|
||||
token_url: 'https://sts.googleapis.com/v1/token',
|
||||
service_account_impersonation_url: `https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/${serviceAccount}:generateAccessToken`,
|
||||
credential_source: {
|
||||
url: requestURL,
|
||||
headers: {
|
||||
Authorization: `Bearer ${requestToken}`,
|
||||
},
|
||||
format: {
|
||||
type: 'json',
|
||||
subject_token_field_name: 'value',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// Generate a random filename to store the credential. 12 bytes is 24
|
||||
// characters in hex. It's not the ideal entropy, but we have to be under
|
||||
// the 255 character limit for Windows filenames (which includes their
|
||||
// entire leading path).
|
||||
const uniqueName = crypto.randomBytes(12).toString('hex');
|
||||
const pth = path.join(outputDir, uniqueName);
|
||||
|
||||
// Write the file as 0640 so the owner has RW, group as R, and the file is
|
||||
// otherwise unreadable. Also write with EXCL to prevent a symlink attack.
|
||||
await fs.writeFile(pth, JSON.stringify(data), { mode: 0o640, flag: 'wx' });
|
||||
|
||||
return pth;
|
||||
}
|
||||
}
|
109
src/main.ts
109
src/main.ts
@ -1,29 +1,9 @@
|
||||
'use strict';
|
||||
|
||||
import * as core from '@actions/core';
|
||||
import { Client } from './client';
|
||||
import { URL } from 'url';
|
||||
|
||||
/**
|
||||
* Converts a multi-line or comma-separated collection of strings into an array
|
||||
* of trimmed strings.
|
||||
*/
|
||||
function explodeStrings(input: string): Array<string> {
|
||||
if (input == null || input.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const list = new Array<string>();
|
||||
for (const line of input.split(`\n`)) {
|
||||
for (const piece of line.split(',')) {
|
||||
const entry = piece.trim();
|
||||
if (entry !== '') {
|
||||
list.push(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
return list;
|
||||
}
|
||||
import { WIFClient } from './workload_identity';
|
||||
import { ActionAuth } from './actionauth';
|
||||
import { explodeStrings } from './utils';
|
||||
|
||||
/**
|
||||
* Executes the main action, documented inline.
|
||||
@ -35,13 +15,19 @@ async function run(): Promise<void> {
|
||||
required: true,
|
||||
});
|
||||
const serviceAccount = core.getInput('service_account', { required: true });
|
||||
const audience =
|
||||
core.getInput('audience') || `https://iam.googleapis.com/${workloadIdentityProvider}`;
|
||||
// audience will default to the WIF provider ID when used with WIF
|
||||
const audience = core.getInput('audience');
|
||||
const createCredentialsFile = core.getBooleanInput('create_credentials_file');
|
||||
const activateCredentialsFile = core.getBooleanInput('activate_credentials_file');
|
||||
const tokenFormat = core.getInput('token_format');
|
||||
const delegates = explodeStrings(core.getInput('delegates'));
|
||||
|
||||
const client: ActionAuth = new WIFClient({
|
||||
providerID: workloadIdentityProvider,
|
||||
serviceAccount: serviceAccount,
|
||||
audience: audience,
|
||||
});
|
||||
|
||||
// Always write the credentials file first, before trying to generate
|
||||
// tokens. This will ensure the file is written even if token generation
|
||||
// fails, which means continue-on-error actions will still have the file
|
||||
@ -52,59 +38,18 @@ async function run(): Promise<void> {
|
||||
throw new Error('$RUNNER_TEMP is not set');
|
||||
}
|
||||
|
||||
// Extract the request token and request URL from the environment. These
|
||||
// are only set when an id-token is requested and the submitter has
|
||||
// collaborator permissions.
|
||||
const requestToken = process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN;
|
||||
const requestURLRaw = process.env.ACTIONS_ID_TOKEN_REQUEST_URL;
|
||||
if (!requestToken || !requestURLRaw) {
|
||||
throw new Error(
|
||||
'GitHub Actions did not inject $ACTIONS_ID_TOKEN_REQUEST_TOKEN or ' +
|
||||
'$ACTIONS_ID_TOKEN_REQUEST_URL into this job. This most likely ' +
|
||||
'means the GitHub Actions workflow permissions are incorrect, or ' +
|
||||
'this job is being run from a fork. For more information, please ' +
|
||||
'see the GitHub documentation at https://docs.github.com/en/actions/security-guides/automatic-token-authentication#permissions-for-the-github_token',
|
||||
);
|
||||
}
|
||||
|
||||
const requestURL = new URL(requestURLRaw);
|
||||
|
||||
// Append the audience value to the request.
|
||||
const params = requestURL.searchParams;
|
||||
params.set('audience', audience);
|
||||
requestURL.search = params.toString();
|
||||
|
||||
// Create the credentials file.
|
||||
const outputPath = await Client.createCredentialsFile({
|
||||
providerID: workloadIdentityProvider,
|
||||
serviceAccount: serviceAccount,
|
||||
requestToken: requestToken,
|
||||
requestURL: requestURL.toString(),
|
||||
outputDir: runnerTempDir,
|
||||
});
|
||||
core.setOutput('credentials_file_path', outputPath);
|
||||
const { credentialsPath, envVars } = await client.createCredentialsFile(runnerTempDir);
|
||||
core.setOutput('credentials_file_path', credentialsPath);
|
||||
|
||||
// Also set the magic environment variable for gcloud and SDKs if
|
||||
// requested.
|
||||
if (activateCredentialsFile) {
|
||||
core.exportVariable('GOOGLE_APPLICATION_CREDENTIALS', outputPath);
|
||||
if (activateCredentialsFile && envVars) {
|
||||
for (const [k, v] of envVars) {
|
||||
core.exportVariable(k, v);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// getFederatedToken is a closure that gets the federated token.
|
||||
const getFederatedToken = async (): Promise<string> => {
|
||||
// Get the GitHub OIDC token.
|
||||
const githubOIDCToken = await core.getIDToken(audience);
|
||||
|
||||
// Exchange the GitHub OIDC token for a Google Federated Token.
|
||||
const googleFederatedToken = await Client.googleFederatedToken({
|
||||
providerID: workloadIdentityProvider,
|
||||
token: githubOIDCToken,
|
||||
});
|
||||
core.setSecret(googleFederatedToken);
|
||||
return googleFederatedToken;
|
||||
};
|
||||
|
||||
switch (tokenFormat) {
|
||||
case '': {
|
||||
break;
|
||||
@ -116,14 +61,13 @@ async function run(): Promise<void> {
|
||||
const accessTokenLifetime = core.getInput('access_token_lifetime');
|
||||
const accessTokenScopes = explodeStrings(core.getInput('access_token_scopes'));
|
||||
|
||||
const googleFederatedToken = await getFederatedToken();
|
||||
const { accessToken, expiration } = await Client.googleAccessToken({
|
||||
token: googleFederatedToken,
|
||||
serviceAccount: serviceAccount,
|
||||
delegates: delegates,
|
||||
lifetime: accessTokenLifetime,
|
||||
const { accessToken, expiration } = await client.getAccessToken({
|
||||
serviceAccount,
|
||||
delegates,
|
||||
scopes: accessTokenScopes,
|
||||
lifetime: accessTokenLifetime,
|
||||
});
|
||||
|
||||
core.setSecret(accessToken);
|
||||
core.setOutput('access_token', accessToken);
|
||||
core.setOutput('access_token_expiration', expiration);
|
||||
@ -132,13 +76,10 @@ async function run(): Promise<void> {
|
||||
case 'id_token': {
|
||||
const idTokenAudience = core.getInput('id_token_audience', { required: true });
|
||||
const idTokenIncludeEmail = core.getBooleanInput('id_token_include_email');
|
||||
|
||||
const googleFederatedToken = await getFederatedToken();
|
||||
const { token } = await Client.googleIDToken({
|
||||
token: googleFederatedToken,
|
||||
serviceAccount: serviceAccount,
|
||||
delegates: delegates,
|
||||
const { token } = await client.getIDToken({
|
||||
serviceAccount,
|
||||
audience: idTokenAudience,
|
||||
delegates,
|
||||
includeEmail: idTokenIncludeEmail,
|
||||
});
|
||||
core.setSecret(token);
|
||||
|
47
src/utils.ts
Normal file
47
src/utils.ts
Normal file
@ -0,0 +1,47 @@
|
||||
import { promises as fs } from 'fs';
|
||||
import crypto from 'crypto';
|
||||
import path from 'path';
|
||||
|
||||
/**
|
||||
* writeCredFile writes a file to disk in a given directory with a
|
||||
* random name.
|
||||
*
|
||||
* @param outputDir Directory to create random file in.
|
||||
* @param data Data to write to file.
|
||||
* @returns Path to written file.
|
||||
*/
|
||||
export async function writeCredFile(outputDir: string, data: string): Promise<string> {
|
||||
// Generate a random filename to store the credential. 12 bytes is 24
|
||||
// characters in hex. It's not the ideal entropy, but we have to be under
|
||||
// the 255 character limit for Windows filenames (which includes their
|
||||
// entire leading path).
|
||||
const uniqueName = crypto.randomBytes(12).toString('hex');
|
||||
const pth = path.join(outputDir, uniqueName);
|
||||
|
||||
// Write the file as 0640 so the owner has RW, group as R, and the file is
|
||||
// otherwise unreadable. Also write with EXCL to prevent a symlink attack.
|
||||
await fs.writeFile(pth, data, { mode: 0o640, flag: 'wx' });
|
||||
|
||||
return pth;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a multi-line or comma-separated collection of strings into an array
|
||||
* of trimmed strings.
|
||||
*/
|
||||
export function explodeStrings(input: string): Array<string> {
|
||||
if (input == null || input.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const list = new Array<string>();
|
||||
for (const line of input.split(`\n`)) {
|
||||
for (const piece of line.split(',')) {
|
||||
const entry = piece.trim();
|
||||
if (entry !== '') {
|
||||
list.push(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
return list;
|
||||
}
|
178
src/workload_identity.ts
Normal file
178
src/workload_identity.ts
Normal file
@ -0,0 +1,178 @@
|
||||
'use strict';
|
||||
|
||||
import { URL } from 'url';
|
||||
import * as core from '@actions/core';
|
||||
import {
|
||||
ActionAuth,
|
||||
CreateCredentialsFileResponse,
|
||||
GoogleAccessTokenParameters,
|
||||
GoogleAccessTokenResponse,
|
||||
GoogleIDTokenParameters,
|
||||
GoogleIDTokenResponse,
|
||||
} from './actionauth';
|
||||
import { writeCredFile } from './utils';
|
||||
import { BaseClient } from './base';
|
||||
|
||||
/**
|
||||
* GoogleFederatedTokenParameters are the parameters to generate a Federated
|
||||
* Identity Token as described in:
|
||||
*
|
||||
* https://cloud.google.com/iam/docs/access-resources-oidc#exchange-token
|
||||
*
|
||||
* @param providerID Full path (including project, location, etc) to the Google
|
||||
* Cloud Workload Identity Provider.
|
||||
* @param token OIDC token to exchange for a Google Cloud federated token.
|
||||
*/
|
||||
interface GoogleFederatedTokenParameters {
|
||||
providerID: string;
|
||||
token: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Available options to create the WIF client.
|
||||
*
|
||||
* @param providerID Full path (including project, location, etc) to the Google
|
||||
* Cloud Workload Identity Provider.
|
||||
* @param serviceAccount Email address or unique identifier of the service
|
||||
* account to impersonate
|
||||
* @param audience The value for the audience parameter in the generated GitHub Actions OIDC token,
|
||||
* defaults to the value of workload_identity_provider
|
||||
*/
|
||||
interface WIFClientOptions {
|
||||
providerID: string;
|
||||
serviceAccount: string;
|
||||
audience?: string;
|
||||
}
|
||||
|
||||
export class WIFClient implements ActionAuth {
|
||||
readonly providerID: string;
|
||||
readonly serviceAccount: string;
|
||||
readonly audience: string;
|
||||
|
||||
constructor(opts: WIFClientOptions) {
|
||||
this.providerID = opts.providerID;
|
||||
this.serviceAccount = opts.serviceAccount;
|
||||
this.audience = opts.audience ? opts.audience : `https://iam.googleapis.com/${this.providerID}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* googleFederatedToken generates a Google Cloud federated token using the
|
||||
* provided OIDC token and Workload Identity Provider.
|
||||
*/
|
||||
static async googleFederatedToken({
|
||||
providerID,
|
||||
token,
|
||||
}: GoogleFederatedTokenParameters): Promise<string> {
|
||||
const stsURL = new URL('https://sts.googleapis.com/v1/token');
|
||||
|
||||
const data = {
|
||||
audience: '//iam.googleapis.com/' + providerID,
|
||||
grantType: 'urn:ietf:params:oauth:grant-type:token-exchange',
|
||||
requestedTokenType: 'urn:ietf:params:oauth:token-type:access_token',
|
||||
scope: 'https://www.googleapis.com/auth/cloud-platform',
|
||||
subjectTokenType: 'urn:ietf:params:oauth:token-type:jwt',
|
||||
subjectToken: token,
|
||||
};
|
||||
|
||||
const opts = {
|
||||
hostname: stsURL.hostname,
|
||||
port: stsURL.port,
|
||||
path: stsURL.pathname + stsURL.search,
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
};
|
||||
|
||||
try {
|
||||
const resp = await BaseClient.request(opts, JSON.stringify(data));
|
||||
const parsed = JSON.parse(resp);
|
||||
return parsed['access_token'];
|
||||
} catch (err) {
|
||||
throw new Error(`failed to generate Google Cloud federated token for ${providerID}: ${err}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* getFederatedToken generates a Google Cloud federated token using the
|
||||
* GitHub OIDC token.
|
||||
*/
|
||||
private async getFederatedToken(): Promise<string> {
|
||||
// Get the GitHub OIDC token.
|
||||
const githubOIDCToken = await core.getIDToken(this.audience);
|
||||
// Exchange the GitHub OIDC token for a Google Federated Token.
|
||||
const googleFederatedToken = await WIFClient.googleFederatedToken({
|
||||
providerID: this.providerID,
|
||||
token: githubOIDCToken,
|
||||
});
|
||||
core.setSecret(googleFederatedToken);
|
||||
return googleFederatedToken;
|
||||
}
|
||||
|
||||
/**
|
||||
* getAccessToken generates a Google Cloud access token for the provided
|
||||
* service account email or unique id.
|
||||
*/
|
||||
async getAccessToken(opts: GoogleAccessTokenParameters): Promise<GoogleAccessTokenResponse> {
|
||||
const googleFederatedToken = await this.getFederatedToken();
|
||||
return await BaseClient.googleAccessToken(googleFederatedToken, opts);
|
||||
}
|
||||
|
||||
/**
|
||||
* getIDToken generates a Google Cloud ID token for the provided
|
||||
* service account email or unique id.
|
||||
*/
|
||||
async getIDToken(tokenParams: GoogleIDTokenParameters): Promise<GoogleIDTokenResponse> {
|
||||
const googleFederatedToken = await this.getFederatedToken();
|
||||
return await BaseClient.googleIDToken(googleFederatedToken, tokenParams);
|
||||
}
|
||||
|
||||
/**
|
||||
* createCredentialsFile creates a Google Cloud credentials file that can be
|
||||
* set as GOOGLE_APPLICATION_CREDENTIALS for gcloud and client libraries.
|
||||
*/
|
||||
async createCredentialsFile(outputDir: string): Promise<CreateCredentialsFileResponse> {
|
||||
// Extract the request token and request URL from the environment. These
|
||||
// are only set when an id-token is requested and the submitter has
|
||||
// collaborator permissions.
|
||||
const requestToken = process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN;
|
||||
const requestURLRaw = process.env.ACTIONS_ID_TOKEN_REQUEST_URL;
|
||||
if (!requestToken || !requestURLRaw) {
|
||||
throw new Error(
|
||||
'GitHub Actions did not inject $ACTIONS_ID_TOKEN_REQUEST_TOKEN or ' +
|
||||
'$ACTIONS_ID_TOKEN_REQUEST_URL into this job. This most likely ' +
|
||||
'means the GitHub Actions workflow permissions are incorrect, or ' +
|
||||
'this job is being run from a fork. For more information, please ' +
|
||||
'see the GitHub documentation at https://docs.github.com/en/actions/security-guides/automatic-token-authentication#permissions-for-the-github_token',
|
||||
);
|
||||
}
|
||||
const requestURL = new URL(requestURLRaw);
|
||||
|
||||
// Append the audience value to the request.
|
||||
const params = requestURL.searchParams;
|
||||
params.set('audience', this.audience);
|
||||
requestURL.search = params.toString();
|
||||
const data = {
|
||||
type: 'external_account',
|
||||
audience: `//iam.googleapis.com/${this.providerID}`,
|
||||
subject_token_type: 'urn:ietf:params:oauth:token-type:jwt',
|
||||
token_url: 'https://sts.googleapis.com/v1/token',
|
||||
service_account_impersonation_url: `https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/${this.serviceAccount}:generateAccessToken`,
|
||||
credential_source: {
|
||||
url: requestURL,
|
||||
headers: {
|
||||
Authorization: `Bearer ${requestToken}`,
|
||||
},
|
||||
format: {
|
||||
type: 'json',
|
||||
subject_token_field_name: 'value',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const credentialsPath = await writeCredFile(outputDir, JSON.stringify(data));
|
||||
const envVars = new Map<string, string>([['GOOGLE_APPLICATION_CREDENTIALS', credentialsPath]]);
|
||||
return { credentialsPath, envVars };
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user