1
0
Fork 0
mirror of synced 2026-06-05 15:35:14 +00:00

Support ID Token generation (#1)

* Support id token generation

* Fix id_token_audience validation

* Add id_token_audience to test workflow

* Generate dist/index.js for id token support
This commit is contained in:
Yuki Furuyama 2021-09-19 00:34:46 +09:00 committed by GitHub
commit e13dfdd573
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 151 additions and 0 deletions

View file

@ -34,6 +34,7 @@ jobs:
with:
workload_identity_provider: 'projects/469401941463/locations/global/workloadIdentityPools/github-actions/providers/github-oidc-auth-google-cloud'
service_account: 'github-secret-accessor@actions-oidc-test.iam.gserviceaccount.com'
id_token_audience: 'foo'
- name: 'npm install'
run: 'npm install'

View file

@ -82,6 +82,8 @@ jobs:
seconds. This must be specified as the number of seconds with a trailing "s"
(e.g. 30s). The default value is 1 hour (3600s).
- `id_token_audience`: (Optional) The audience for the generated ID Token.
## Outputs
- `access_token`: The authenticated Google Cloud access token for calling
@ -90,6 +92,9 @@ jobs:
- `expiration`: The RFC3339 UTC "Zulu" format timestamp when the token
expires.
- `id_token`: The authenticated Google Cloud ID token. This token is only
generated when `id_token_audience` input parameter is provided.
## Setup
To exchange a GitHub Actions OIDC token for a Google Cloud access token, you

View file

@ -50,6 +50,11 @@ inputs:
specified as the number of seconds with a trailing "s" (e.g. 30s).
default: '3600s'
required: false
id_token_audience:
description: |-
The audience for the generated Google Cloud ID Token.
default: ''
required: false
outputs:
access_token:
@ -58,6 +63,10 @@ outputs:
expiration:
description: |-
The expiration timestamp for the access token.
id_token:
description: |-
The Google Cloud ID token. This token is only generated when
`id_token_audience` input parameter was provided.
branding:
icon: 'lock'

48
dist/index.js vendored
View file

@ -220,6 +220,7 @@ function run() {
const audience = core.getInput('audience');
const delegates = explodeStrings(core.getInput('delegates'));
const lifetime = core.getInput('lifetime');
const idTokenAudience = core.getInput('id_token_audience');
// Extract the GitHub Actions OIDC token.
const requestToken = process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN;
if (!requestToken) {
@ -251,6 +252,17 @@ function run() {
core.setSecret(accessToken);
core.setOutput('access_token', accessToken);
core.setOutput('expiration', expiration);
// Exchange the Google Federated Token for an ID token.
if (idTokenAudience != '') {
const { token } = yield client_1.Client.googleIDToken({
token: googleFederatedToken,
serviceAccount: serviceAccount,
delegates: delegates,
audience: idTokenAudience,
});
core.setSecret(token);
core.setOutput('id_token', token);
}
}
catch (err) {
core.setFailed(`Action failed with error: ${err}`);
@ -859,6 +871,42 @@ class Client {
}
});
}
/**
* googleIDToken generates a Google Cloud ID token for the provided
* service account email or unique id.
*/
static googleIDToken({ token, serviceAccount, audience, delegates, }) {
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: true,
};
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}`);
}
});
}
}
exports.Client = Client;

View file

@ -64,6 +64,37 @@ interface GoogleAccessTokenResponse {
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>;
}
/**
* GoogleIDTokenResponse is the response from generating an ID token.
*
* @param token ID token.
* expires.
*/
interface GoogleIDTokenResponse {
token: string;
}
export class Client {
/**
* request is a high-level helper that returns a promise from the executed
@ -218,4 +249,48 @@ export class Client {
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,
}: 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: true,
};
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}`);
}
}
}

View file

@ -37,6 +37,7 @@ async function run(): Promise<void> {
const audience = core.getInput('audience');
const delegates = explodeStrings(core.getInput('delegates'));
const lifetime = core.getInput('lifetime');
const idTokenAudience = core.getInput('id_token_audience');
// Extract the GitHub Actions OIDC token.
const requestToken = process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN;
@ -71,6 +72,18 @@ async function run(): Promise<void> {
core.setSecret(accessToken);
core.setOutput('access_token', accessToken);
core.setOutput('expiration', expiration);
// Exchange the Google Federated Token for an ID token.
if (idTokenAudience != '') {
const { token } = await Client.googleIDToken({
token: googleFederatedToken,
serviceAccount: serviceAccount,
delegates: delegates,
audience: idTokenAudience,
});
core.setSecret(token);
core.setOutput('id_token', token);
}
} catch (err) {
core.setFailed(`Action failed with error: ${err}`);
}