Refactor to support access and id tokens (#3)

This commit is contained in:
Seth Vargo 2021-09-18 12:12:21 -04:00 committed by GitHub
parent afef6a5b6d
commit cb396c3f31
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 212 additions and 93 deletions

View File

@ -9,13 +9,34 @@ on:
- 'main'
jobs:
run:
name: 'test'
unit:
name: 'unit'
runs-on: 'ubuntu-latest'
steps:
- uses: 'actions/checkout@v2'
- uses: 'actions/setup-node@master'
with:
node-version: '12.x'
- name: 'npm install'
run: 'npm install'
- name: 'npm lint'
run: 'npm run lint'
- name: 'npm test'
run: 'npm run test'
access_token:
name: 'access_token'
permissions:
id-token: write
contents: read
runs-on: '${{ matrix.operating-system }}'
strategy:
fail-fast: false
matrix:
operating-system:
- 'ubuntu-latest'
@ -28,19 +49,40 @@ jobs:
with:
node-version: '12.x'
- id: 'integration'
- id: 'access-token'
name: 'integration'
uses: './'
with:
token_format: 'access_token'
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'
id_token:
name: 'id_token'
permissions:
id-token: write
contents: read
runs-on: '${{ matrix.operating-system }}'
strategy:
fail-fast: false
matrix:
operating-system:
- 'ubuntu-latest'
- 'windows-latest'
- 'macos-latest'
steps:
- uses: 'actions/checkout@v2'
- name: 'npm lint'
run: 'npm run lint'
- uses: 'actions/setup-node@master'
with:
node-version: '12.x'
- name: 'npm test'
run: 'npm run test'
- id: 'id-token'
name: 'integration'
uses: './'
with:
token_format: 'id_token'
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: 'my-aud'
id_token_include_email: true

View File

@ -41,6 +41,7 @@ jobs:
name: 'Authenticate to Google Cloud'
uses: 'github.com/sethvargo/oidc-auth-google-cloud'
with:
token_format: 'access_token'
workload_identity_provider: 'projects/123456789/locations/global/workloadIdentityPools/my-pool/providers/my-provider'
service_account: 'my-service-account@my-project.iam.gserviceaccount.com'
@ -74,23 +75,40 @@ jobs:
`"sigstore"`, but this variable exists in case custom values are permitted
in the future. The default value is `"sigstore"`.
- `token_format`: (Optional) Format of the generated token. For OAuth 2.0
access tokens, specify "access_token". For OIDC tokens, specify "id_token".
The default value is "access_token".
- `delegates`: (Optional) List of additional service account emails or unique
identities to use for impersonation in the chain. By default there are no
delegates.
- `lifetime`: (Optional) Desired lifetime duration of the access token, in
seconds. This must be specified as the number of seconds with a trailing "s"
(e.g. 30s). The default value is 1 hour (3600s).
- `access_token_lifetime`: (Optional) Desired lifetime duration of the access
token, in seconds. This must be specified as the number of seconds with a
trailing "s" (e.g. 30s). The default value is 1 hour (3600s).
- `access_token_scopes`: (Optional) List of OAuth 2.0 access scopes to be
included in the generated token. This is only valid when "token_format" is
"access_token". The default value is:
```text
https://www.googleapis.com/auth/cloud-platform
```
- `id_token_audience`: (Optional) The audience for the generated ID Token.
- `id_token_include_email`: (Optional) Optional parameter of whether to
include the service account email in the generated token. If true, the token
will contain "email" and "email_verified" claims. This is only valid when
"token_format" is "access_token". The default value is false.
## Outputs
- `access_token`: The authenticated Google Cloud access token for calling
other Google Cloud APIs.
- `expiration`: The RFC3339 UTC "Zulu" format timestamp when the token
expires.
- `access_token_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.

View File

@ -15,8 +15,8 @@
name: 'OIDC Authenticate to Google Cloud'
author: 'sethvargo'
description: |-
Authenticate to Google Cloud from GitHub Actions using an OIDC token and
Workload Identity Federation.
Generate credentials to authenticate to Google Cloud from GitHub Actions using
an OIDC token and Workload Identity Federation.
inputs:
workload_identity_provider:
@ -38,35 +38,62 @@ inputs:
exists in case custom values are permitted in the future.
default: 'sigstore'
required: false
token_format:
description: |-
Format for the generated token. For OAuth 2.0 access tokens, specify
"access_token". For OIDC tokens, specify "id_token".
default: 'access_token'
required: true
delegates:
description: |-
List of additional service account emails or unique identities to use for
impersonation in the chain.
default: ''
required: false
lifetime:
# access token params
access_token_lifetime:
description: |-
Desired lifetime duration of the access token, in seconds. This must be
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). This is
only valid when "token_format" is "access_token".
default: '3600s'
required: false
access_token_scopes:
description: |-
List of OAuth 2.0 access scopes to be included in the generated token.
This is only valid when "token_format" is "access_token".
default: 'https://www.googleapis.com/auth/cloud-platform'
# id token params
id_token_audience:
description: |-
The audience for the generated Google Cloud ID Token.
The audience (aud) for the generated Google Cloud ID Token. This is only
valid when "token_format" is "id_token".
default: ''
required: false
id_token_include_email:
description: |-
Optional parameter of whether to include the service account email in the
generated token. If true, the token will contain "email" and
"email_verified" claims. This is only valid when "token_format" is
"access_token".
default: false
required: false
outputs:
access_token:
description: |-
The Google Cloud access token for calling other Google Cloud APIs.
expiration:
The Google Cloud access token for calling other Google Cloud APIs. This
is only available when "token_format" is "access_token".
access_token_expiration:
description: |-
The expiration timestamp for the access token.
The expiration timestamp for the access token. This is only available
when "token_format" is "access_token".
id_token:
description: |-
The Google Cloud ID token. This token is only generated when
`id_token_audience` input parameter was provided.
The Google Cloud ID token. This is only available when "token_format" is
"id_token".
branding:
icon: 'lock'

66
dist/index.js vendored
View File

@ -225,37 +225,51 @@ function run() {
});
const serviceAccount = core.getInput('service_account', { required: true });
const audience = core.getInput('audience');
const tokenFormat = core.getInput('token_format', { required: true });
const delegates = explodeStrings(core.getInput('delegates'));
const lifetime = core.getInput('lifetime');
const accessTokenLifetime = core.getInput('access_token_lifetime');
const accessTokenScopes = explodeStrings(core.getInput('access_token_scopes'));
const idTokenAudience = core.getInput('id_token_audience');
const idTokenIncludeEmail = core.getBooleanInput('id_token_include_email');
// 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);
// Exchange the Google Federated Token for an access token.
const { accessToken, expiration } = yield client_1.Client.googleAccessToken({
token: googleFederatedToken,
serviceAccount: serviceAccount,
delegates: delegates,
lifetime: lifetime,
});
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);
switch (tokenFormat) {
case 'access_token': {
// Exchange the Google Federated Token for an access token.
const { accessToken, expiration } = yield client_1.Client.googleAccessToken({
token: googleFederatedToken,
serviceAccount: serviceAccount,
delegates: delegates,
lifetime: accessTokenLifetime,
scopes: accessTokenScopes,
});
core.setSecret(accessToken);
core.setOutput('access_token', accessToken);
core.setOutput('access_token_expiration', expiration);
break;
}
case 'id_token': {
// Exchange the Google Federated Token for an id token.
const { token } = yield client_1.Client.googleIDToken({
token: googleFederatedToken,
serviceAccount: serviceAccount,
delegates: delegates,
audience: idTokenAudience,
includeEmail: idTokenIncludeEmail,
});
core.setSecret(token);
core.setOutput('id_token', token);
break;
}
default: {
throw new Error(`unknown token format "${tokenFormat}"`);
}
}
}
catch (err) {
@ -1880,14 +1894,14 @@ class Client {
* googleAccessToken generates a Google Cloud access token for the provided
* service account email or unique id.
*/
static googleAccessToken({ token, serviceAccount, delegates, lifetime, }) {
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,
scope: 'https://www.googleapis.com/auth/cloud-platform',
lifetime: lifetime,
scope: scopes,
};
const opts = {
hostname: tokenURL.hostname,
@ -1917,14 +1931,14 @@ class Client {
* googleIDToken generates a Google Cloud ID token for the provided
* service account email or unique id.
*/
static googleIDToken({ token, serviceAccount, audience, delegates, }) {
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: true,
includeEmail: includeEmail,
};
const opts = {
hostname: tokenURL.hostname,

36
package-lock.json generated
View File

@ -55,9 +55,9 @@
}
},
"node_modules/@babel/helper-validator-identifier": {
"version": "7.14.9",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.9.tgz",
"integrity": "sha512-pQYxPY0UP6IHISRitNe8bsijHex4TWZXi2HwKVsjPiltzlhse2znVcm9Ace510VT1kxIHjGJCZZQBX2gJDbo0g==",
"version": "7.15.7",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.15.7.tgz",
"integrity": "sha512-K4JvCtQqad9OY2+yTU8w+E82ywk/fe+ELNlt1G8z3bVGlZfn/hOcQQsUhGhW/N+tb3fxK800wLtKOE/aM0m72w==",
"dev": true,
"engines": {
"node": ">=6.9.0"
@ -287,9 +287,9 @@
"dev": true
},
"node_modules/@types/node": {
"version": "16.9.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.9.1.tgz",
"integrity": "sha512-QpLcX9ZSsq3YYUUnD3nFDY8H7wctAhQj/TFKL8Ya8v5fMm3CFXxo8zStsLAl780ltoYoo1WvKUVGBQK+1ifr7g==",
"version": "16.9.2",
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.9.2.tgz",
"integrity": "sha512-ZHty/hKoOLZvSz6BtP1g7tc7nUeJhoCf3flLjh8ZEv1vFKBWHXcnMbJMyN/pftSljNyy0kNW/UqI3DccnBnZ8w==",
"dev": true
},
"node_modules/@typescript-eslint/eslint-plugin": {
@ -1975,9 +1975,9 @@
}
},
"node_modules/prettier": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.4.0.tgz",
"integrity": "sha512-DsEPLY1dE5HF3BxCRBmD4uYZ+5DCbvatnolqTqcxEgKVZnL2kUfyu7b8pPQ5+hTBkdhU9SLUmK0/pHb07RE4WQ==",
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.4.1.tgz",
"integrity": "sha512-9fbDAXSBcc6Bs1mZrDYb3XKzDLm4EXXL9sC1LqKP5rZkT6KRr/rf9amVUcODVXgguK/isJz0d0hP72WeaKWsvA==",
"dev": true,
"bin": {
"prettier": "bin-prettier.js"
@ -2714,9 +2714,9 @@
}
},
"@babel/helper-validator-identifier": {
"version": "7.14.9",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.9.tgz",
"integrity": "sha512-pQYxPY0UP6IHISRitNe8bsijHex4TWZXi2HwKVsjPiltzlhse2znVcm9Ace510VT1kxIHjGJCZZQBX2gJDbo0g==",
"version": "7.15.7",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.15.7.tgz",
"integrity": "sha512-K4JvCtQqad9OY2+yTU8w+E82ywk/fe+ELNlt1G8z3bVGlZfn/hOcQQsUhGhW/N+tb3fxK800wLtKOE/aM0m72w==",
"dev": true
},
"@babel/highlight": {
@ -2906,9 +2906,9 @@
"dev": true
},
"@types/node": {
"version": "16.9.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.9.1.tgz",
"integrity": "sha512-QpLcX9ZSsq3YYUUnD3nFDY8H7wctAhQj/TFKL8Ya8v5fMm3CFXxo8zStsLAl780ltoYoo1WvKUVGBQK+1ifr7g==",
"version": "16.9.2",
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.9.2.tgz",
"integrity": "sha512-ZHty/hKoOLZvSz6BtP1g7tc7nUeJhoCf3flLjh8ZEv1vFKBWHXcnMbJMyN/pftSljNyy0kNW/UqI3DccnBnZ8w==",
"dev": true
},
"@typescript-eslint/eslint-plugin": {
@ -4115,9 +4115,9 @@
"dev": true
},
"prettier": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.4.0.tgz",
"integrity": "sha512-DsEPLY1dE5HF3BxCRBmD4uYZ+5DCbvatnolqTqcxEgKVZnL2kUfyu7b8pPQ5+hTBkdhU9SLUmK0/pHb07RE4WQ==",
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.4.1.tgz",
"integrity": "sha512-9fbDAXSBcc6Bs1mZrDYb3XKzDLm4EXXL9sC1LqKP5rZkT6KRr/rf9amVUcODVXgguK/isJz0d0hP72WeaKWsvA==",
"dev": true
},
"prettier-linter-helpers": {

View File

@ -34,6 +34,7 @@ interface GoogleAccessTokenParameters {
token: string;
serviceAccount: string;
delegates?: Array<string>;
scopes?: Array<string>;
lifetime?: string;
}
@ -68,6 +69,7 @@ interface GoogleIDTokenParameters {
serviceAccount: string;
audience: string;
delegates?: Array<string>;
includeEmail?: boolean;
}
/**
@ -163,6 +165,7 @@ export class Client {
token,
serviceAccount,
delegates,
scopes,
lifetime,
}: GoogleAccessTokenParameters): Promise<GoogleAccessTokenResponse> {
const serviceAccountID = `projects/-/serviceAccounts/${serviceAccount}`;
@ -172,8 +175,8 @@ export class Client {
const data = {
delegates: delegates,
scope: 'https://www.googleapis.com/auth/cloud-platform',
lifetime: lifetime,
scope: scopes,
};
const opts = {
@ -209,6 +212,7 @@ export class Client {
serviceAccount,
audience,
delegates,
includeEmail,
}: GoogleIDTokenParameters): Promise<GoogleIDTokenResponse> {
const serviceAccountID = `projects/-/serviceAccounts/${serviceAccount}`;
const tokenURL = new URL(
@ -218,7 +222,7 @@ export class Client {
const data = {
delegates: delegates,
audience: audience,
includeEmail: true,
includeEmail: includeEmail,
};
const opts = {

View File

@ -35,10 +35,14 @@ async function run(): Promise<void> {
});
const serviceAccount = core.getInput('service_account', { required: true });
const audience = core.getInput('audience');
const tokenFormat = core.getInput('token_format', { required: true });
const delegates = explodeStrings(core.getInput('delegates'));
const lifetime = core.getInput('lifetime');
const accessTokenLifetime = core.getInput('access_token_lifetime');
const accessTokenScopes = explodeStrings(core.getInput('access_token_scopes'));
const idTokenAudience = core.getInput('id_token_audience');
const idTokenIncludeEmail = core.getBooleanInput('id_token_include_email');
// Get the GitHub OIDC token.
const githubOIDCToken = await core.getIDToken(audience);
// Exchange the GitHub OIDC token for a Google Federated Token.
@ -48,27 +52,37 @@ async function run(): Promise<void> {
});
core.setSecret(googleFederatedToken);
// Exchange the Google Federated Token for an access token.
const { accessToken, expiration } = await Client.googleAccessToken({
token: googleFederatedToken,
serviceAccount: serviceAccount,
delegates: delegates,
lifetime: lifetime,
});
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);
switch (tokenFormat) {
case 'access_token': {
// Exchange the Google Federated Token for an access token.
const { accessToken, expiration } = await Client.googleAccessToken({
token: googleFederatedToken,
serviceAccount: serviceAccount,
delegates: delegates,
lifetime: accessTokenLifetime,
scopes: accessTokenScopes,
});
core.setSecret(accessToken);
core.setOutput('access_token', accessToken);
core.setOutput('access_token_expiration', expiration);
break;
}
case 'id_token': {
// Exchange the Google Federated Token for an id token.
const { token } = await Client.googleIDToken({
token: googleFederatedToken,
serviceAccount: serviceAccount,
delegates: delegates,
audience: idTokenAudience,
includeEmail: idTokenIncludeEmail,
});
core.setSecret(token);
core.setOutput('id_token', token);
break;
}
default: {
throw new Error(`unknown token format "${tokenFormat}"`);
}
}
} catch (err) {
core.setFailed(`Action failed with error: ${err}`);