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:
parent
271e0346a0
commit
e13dfdd573
6 changed files with 151 additions and 0 deletions
1
.github/workflows/test.yaml
vendored
1
.github/workflows/test.yaml
vendored
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
48
dist/index.js
vendored
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
13
src/main.ts
13
src/main.ts
|
|
@ -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}`);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue