mirror of
https://github.com/google-github-actions/auth.git
synced 2026-06-05 19:53:29 +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:
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:
|
with:
|
||||||
workload_identity_provider: 'projects/469401941463/locations/global/workloadIdentityPools/github-actions/providers/github-oidc-auth-google-cloud'
|
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'
|
service_account: 'github-secret-accessor@actions-oidc-test.iam.gserviceaccount.com'
|
||||||
|
id_token_audience: 'foo'
|
||||||
|
|
||||||
- name: 'npm install'
|
- name: 'npm install'
|
||||||
run: 'npm install'
|
run: 'npm install'
|
||||||
|
|
|
||||||
|
|
@ -82,6 +82,8 @@ jobs:
|
||||||
seconds. This must be specified as the number of seconds with a trailing "s"
|
seconds. This must be specified as the number of seconds with a trailing "s"
|
||||||
(e.g. 30s). The default value is 1 hour (3600s).
|
(e.g. 30s). The default value is 1 hour (3600s).
|
||||||
|
|
||||||
|
- `id_token_audience`: (Optional) The audience for the generated ID Token.
|
||||||
|
|
||||||
## Outputs
|
## Outputs
|
||||||
|
|
||||||
- `access_token`: The authenticated Google Cloud access token for calling
|
- `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
|
- `expiration`: The RFC3339 UTC "Zulu" format timestamp when the token
|
||||||
expires.
|
expires.
|
||||||
|
|
||||||
|
- `id_token`: The authenticated Google Cloud ID token. This token is only
|
||||||
|
generated when `id_token_audience` input parameter is provided.
|
||||||
|
|
||||||
## Setup
|
## Setup
|
||||||
|
|
||||||
To exchange a GitHub Actions OIDC token for a Google Cloud access token, you
|
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).
|
specified as the number of seconds with a trailing "s" (e.g. 30s).
|
||||||
default: '3600s'
|
default: '3600s'
|
||||||
required: false
|
required: false
|
||||||
|
id_token_audience:
|
||||||
|
description: |-
|
||||||
|
The audience for the generated Google Cloud ID Token.
|
||||||
|
default: ''
|
||||||
|
required: false
|
||||||
|
|
||||||
outputs:
|
outputs:
|
||||||
access_token:
|
access_token:
|
||||||
|
|
@ -58,6 +63,10 @@ outputs:
|
||||||
expiration:
|
expiration:
|
||||||
description: |-
|
description: |-
|
||||||
The expiration timestamp for the access token.
|
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:
|
branding:
|
||||||
icon: 'lock'
|
icon: 'lock'
|
||||||
|
|
|
||||||
48
dist/index.js
vendored
48
dist/index.js
vendored
|
|
@ -220,6 +220,7 @@ function run() {
|
||||||
const audience = core.getInput('audience');
|
const audience = core.getInput('audience');
|
||||||
const delegates = explodeStrings(core.getInput('delegates'));
|
const delegates = explodeStrings(core.getInput('delegates'));
|
||||||
const lifetime = core.getInput('lifetime');
|
const lifetime = core.getInput('lifetime');
|
||||||
|
const idTokenAudience = core.getInput('id_token_audience');
|
||||||
// Extract the GitHub Actions OIDC token.
|
// Extract the GitHub Actions OIDC token.
|
||||||
const requestToken = process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN;
|
const requestToken = process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN;
|
||||||
if (!requestToken) {
|
if (!requestToken) {
|
||||||
|
|
@ -251,6 +252,17 @@ function run() {
|
||||||
core.setSecret(accessToken);
|
core.setSecret(accessToken);
|
||||||
core.setOutput('access_token', accessToken);
|
core.setOutput('access_token', accessToken);
|
||||||
core.setOutput('expiration', expiration);
|
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) {
|
catch (err) {
|
||||||
core.setFailed(`Action failed with error: ${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;
|
exports.Client = Client;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -64,6 +64,37 @@ interface GoogleAccessTokenResponse {
|
||||||
expiration: 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>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GoogleIDTokenResponse is the response from generating an ID token.
|
||||||
|
*
|
||||||
|
* @param token ID token.
|
||||||
|
* expires.
|
||||||
|
*/
|
||||||
|
interface GoogleIDTokenResponse {
|
||||||
|
token: string;
|
||||||
|
}
|
||||||
|
|
||||||
export class Client {
|
export class Client {
|
||||||
/**
|
/**
|
||||||
* request is a high-level helper that returns a promise from the executed
|
* 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}`);
|
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 audience = core.getInput('audience');
|
||||||
const delegates = explodeStrings(core.getInput('delegates'));
|
const delegates = explodeStrings(core.getInput('delegates'));
|
||||||
const lifetime = core.getInput('lifetime');
|
const lifetime = core.getInput('lifetime');
|
||||||
|
const idTokenAudience = core.getInput('id_token_audience');
|
||||||
|
|
||||||
// Extract the GitHub Actions OIDC token.
|
// Extract the GitHub Actions OIDC token.
|
||||||
const requestToken = process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN;
|
const requestToken = process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN;
|
||||||
|
|
@ -71,6 +72,18 @@ async function run(): Promise<void> {
|
||||||
core.setSecret(accessToken);
|
core.setSecret(accessToken);
|
||||||
core.setOutput('access_token', accessToken);
|
core.setOutput('access_token', accessToken);
|
||||||
core.setOutput('expiration', expiration);
|
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) {
|
} catch (err) {
|
||||||
core.setFailed(`Action failed with error: ${err}`);
|
core.setFailed(`Action failed with error: ${err}`);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue