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' - 'main'
jobs: jobs:
run: unit:
name: 'test' 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: permissions:
id-token: write id-token: write
contents: read contents: read
runs-on: '${{ matrix.operating-system }}' runs-on: '${{ matrix.operating-system }}'
strategy: strategy:
fail-fast: false
matrix: matrix:
operating-system: operating-system:
- 'ubuntu-latest' - 'ubuntu-latest'
@ -28,19 +49,40 @@ jobs:
with: with:
node-version: '12.x' node-version: '12.x'
- id: 'integration' - id: 'access-token'
name: 'integration' name: 'integration'
uses: './' uses: './'
with: with:
token_format: 'access_token'
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' id_token:
run: 'npm install' 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' - uses: 'actions/setup-node@master'
run: 'npm run lint' with:
node-version: '12.x'
- name: 'npm test' - id: 'id-token'
run: 'npm run test' 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' name: 'Authenticate to Google Cloud'
uses: 'github.com/sethvargo/oidc-auth-google-cloud' uses: 'github.com/sethvargo/oidc-auth-google-cloud'
with: with:
token_format: 'access_token'
workload_identity_provider: 'projects/123456789/locations/global/workloadIdentityPools/my-pool/providers/my-provider' workload_identity_provider: 'projects/123456789/locations/global/workloadIdentityPools/my-pool/providers/my-provider'
service_account: 'my-service-account@my-project.iam.gserviceaccount.com' 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 `"sigstore"`, but this variable exists in case custom values are permitted
in the future. The default value is `"sigstore"`. 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 - `delegates`: (Optional) List of additional service account emails or unique
identities to use for impersonation in the chain. By default there are no identities to use for impersonation in the chain. By default there are no
delegates. delegates.
- `lifetime`: (Optional) Desired lifetime duration of the access token, in - `access_token_lifetime`: (Optional) Desired lifetime duration of the access
seconds. This must be specified as the number of seconds with a trailing "s" token, in seconds. This must be specified as the number of seconds with a
(e.g. 30s). The default value is 1 hour (3600s). 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_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 ## Outputs
- `access_token`: The authenticated Google Cloud access token for calling - `access_token`: The authenticated Google Cloud access token for calling
other Google Cloud APIs. other Google Cloud APIs.
- `expiration`: The RFC3339 UTC "Zulu" format timestamp when the token - `access_token_expiration`: The RFC3339 UTC "Zulu" format timestamp when the
expires. token expires.
- `id_token`: The authenticated Google Cloud ID token. This token is only - `id_token`: The authenticated Google Cloud ID token. This token is only
generated when `id_token_audience` input parameter is provided. generated when `id_token_audience` input parameter is provided.

View File

@ -15,8 +15,8 @@
name: 'OIDC Authenticate to Google Cloud' name: 'OIDC Authenticate to Google Cloud'
author: 'sethvargo' author: 'sethvargo'
description: |- description: |-
Authenticate to Google Cloud from GitHub Actions using an OIDC token and Generate credentials to authenticate to Google Cloud from GitHub Actions using
Workload Identity Federation. an OIDC token and Workload Identity Federation.
inputs: inputs:
workload_identity_provider: workload_identity_provider:
@ -38,35 +38,62 @@ inputs:
exists in case custom values are permitted in the future. exists in case custom values are permitted in the future.
default: 'sigstore' default: 'sigstore'
required: false 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: delegates:
description: |- description: |-
List of additional service account emails or unique identities to use for List of additional service account emails or unique identities to use for
impersonation in the chain. impersonation in the chain.
default: '' default: ''
required: false required: false
lifetime:
# access token params
access_token_lifetime:
description: |- description: |-
Desired lifetime duration of the access token, in seconds. This must be 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' default: '3600s'
required: false 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: id_token_audience:
description: |- 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: '' default: ''
required: false 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: outputs:
access_token: access_token:
description: |- description: |-
The Google Cloud access token for calling other Google Cloud APIs. The Google Cloud access token for calling other Google Cloud APIs. This
expiration: is only available when "token_format" is "access_token".
access_token_expiration:
description: |- 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: id_token:
description: |- description: |-
The Google Cloud ID token. This token is only generated when The Google Cloud ID token. This is only available when "token_format" is
`id_token_audience` input parameter was provided. "id_token".
branding: branding:
icon: 'lock' icon: 'lock'

66
dist/index.js vendored
View File

@ -225,37 +225,51 @@ function run() {
}); });
const serviceAccount = core.getInput('service_account', { required: true }); const serviceAccount = core.getInput('service_account', { required: true });
const audience = core.getInput('audience'); const audience = core.getInput('audience');
const tokenFormat = core.getInput('token_format', { required: true });
const delegates = explodeStrings(core.getInput('delegates')); 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 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); const githubOIDCToken = yield core.getIDToken(audience);
// Exchange the GitHub OIDC token for a Google Federated Token. // Exchange the GitHub OIDC token for a Google Federated Token.
const googleFederatedToken = yield client_1.Client.googleFederatedToken({ const googleFederatedToken = yield client_1.Client.googleFederatedToken({
providerID: workloadIdentityProvider, providerID: workloadIdentityProvider,
token: githubOIDCToken, token: githubOIDCToken,
}); });
core.setSecret(googleFederatedToken); core.setSecret(googleFederatedToken);
// Exchange the Google Federated Token for an access token. switch (tokenFormat) {
const { accessToken, expiration } = yield client_1.Client.googleAccessToken({ case 'access_token': {
token: googleFederatedToken, // Exchange the Google Federated Token for an access token.
serviceAccount: serviceAccount, const { accessToken, expiration } = yield client_1.Client.googleAccessToken({
delegates: delegates, token: googleFederatedToken,
lifetime: lifetime, serviceAccount: serviceAccount,
}); delegates: delegates,
core.setSecret(accessToken); lifetime: accessTokenLifetime,
core.setOutput('access_token', accessToken); scopes: accessTokenScopes,
core.setOutput('expiration', expiration); });
// Exchange the Google Federated Token for an ID token. core.setSecret(accessToken);
if (idTokenAudience != '') { core.setOutput('access_token', accessToken);
const { token } = yield client_1.Client.googleIDToken({ core.setOutput('access_token_expiration', expiration);
token: googleFederatedToken, break;
serviceAccount: serviceAccount, }
delegates: delegates, case 'id_token': {
audience: idTokenAudience, // Exchange the Google Federated Token for an id token.
}); const { token } = yield client_1.Client.googleIDToken({
core.setSecret(token); token: googleFederatedToken,
core.setOutput('id_token', token); 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) { catch (err) {
@ -1880,14 +1894,14 @@ class Client {
* googleAccessToken generates a Google Cloud access token for the provided * googleAccessToken generates a Google Cloud access token for the provided
* service account email or unique id. * 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* () { return __awaiter(this, void 0, void 0, function* () {
const serviceAccountID = `projects/-/serviceAccounts/${serviceAccount}`; const serviceAccountID = `projects/-/serviceAccounts/${serviceAccount}`;
const tokenURL = new url_1.URL(`https://iamcredentials.googleapis.com/v1/${serviceAccountID}:generateAccessToken`); const tokenURL = new url_1.URL(`https://iamcredentials.googleapis.com/v1/${serviceAccountID}:generateAccessToken`);
const data = { const data = {
delegates: delegates, delegates: delegates,
scope: 'https://www.googleapis.com/auth/cloud-platform',
lifetime: lifetime, lifetime: lifetime,
scope: scopes,
}; };
const opts = { const opts = {
hostname: tokenURL.hostname, hostname: tokenURL.hostname,
@ -1917,14 +1931,14 @@ class Client {
* googleIDToken generates a Google Cloud ID token for the provided * googleIDToken generates a Google Cloud ID token for the provided
* service account email or unique id. * 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* () { return __awaiter(this, void 0, void 0, function* () {
const serviceAccountID = `projects/-/serviceAccounts/${serviceAccount}`; const serviceAccountID = `projects/-/serviceAccounts/${serviceAccount}`;
const tokenURL = new url_1.URL(`https://iamcredentials.googleapis.com/v1/${serviceAccountID}:generateIdToken`); const tokenURL = new url_1.URL(`https://iamcredentials.googleapis.com/v1/${serviceAccountID}:generateIdToken`);
const data = { const data = {
delegates: delegates, delegates: delegates,
audience: audience, audience: audience,
includeEmail: true, includeEmail: includeEmail,
}; };
const opts = { const opts = {
hostname: tokenURL.hostname, hostname: tokenURL.hostname,

36
package-lock.json generated
View File

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

View File

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

View File

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