From 58b7fc7482af766955f8d037e2ede7c15979b208 Mon Sep 17 00:00:00 2001 From: Thomas Boop <52323235+thboop@users.noreply.github.com> Date: Tue, 28 Sep 2021 12:55:21 -0400 Subject: [PATCH] Full release of actions/core 1.6.0 with oidc behavior (#919) * OIDC Client for actions/core Co-authored-by: Sourav Chanduka Co-authored-by: Sourav Chanduka Co-authored-by: Tingluo Huang --- README.md | 48 ++++++++++++++++++++++++ RELEASES.md | 4 ++ __tests__/core.test.ts | 18 +++++++++ package-lock.json | 52 +++++++++++++++++++++++++- package.json | 5 ++- src/core.ts | 6 +++ src/oidc-utils.ts | 84 ++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 214 insertions(+), 3 deletions(-) create mode 100644 src/oidc-utils.ts diff --git a/README.md b/README.md index abac1d4..8f227a8 100644 --- a/README.md +++ b/README.md @@ -262,3 +262,51 @@ var pid = core.getState("pidToKill"); process.kill(pid); ``` + +#### OIDC Token + +You can use these methods to interact with the GitHub OIDC provider and get a JWT ID token which would help to get access token from third party cloud providers. + +**Method Name**: getIDToken() + +**Inputs** + +audience : optional + +**Outputs** + +A [JWT](https://jwt.io/) ID Token + +In action's `main.ts`: +```js +const core = require('@actions/core'); +async function getIDTokenAction(): Promise { + + const audience = core.getInput('audience', {required: false}) + + const id_token1 = await core.getIDToken() // ID Token with default audience + const id_token2 = await core.getIDToken(audience) // ID token with custom audience + + // this id_token can be used to get access token from third party cloud providers +} +getIDTokenAction() +``` + +In action's `actions.yml`: + +```yaml +name: 'GetIDToken' +description: 'Get ID token from Github OIDC provider' +inputs: + audience: + description: 'Audience for which the ID token is intended for' + required: false +outputs: + id_token1: + description: 'ID token obtained from OIDC provider' + id_token2: + description: 'ID token obtained from OIDC provider' +runs: + using: 'node12' + main: 'dist/index.js' +``` \ No newline at end of file diff --git a/RELEASES.md b/RELEASES.md index 2445ae2..df556fe 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -1,5 +1,9 @@ # @actions/core Releases +### 1.6.0 +- [Added OIDC Client function `getIDToken`](https://github.com/actions/toolkit/pull/919) +- [Added `file` parameter to `AnnotationProperties`](https://github.com/actions/toolkit/pull/896) + ### 1.5.0 - [Added support for notice annotations and more annotation fields](https://github.com/actions/toolkit/pull/855) diff --git a/__tests__/core.test.ts b/__tests__/core.test.ts index 34cfab9..7ffcc6c 100644 --- a/__tests__/core.test.ts +++ b/__tests__/core.test.ts @@ -2,6 +2,7 @@ import * as fs from 'fs' import * as os from 'os' import * as path from 'path' import * as core from '../src/core' +import {HttpClient} from '@actions/http-client' import {toCommandProperties} from '../src/utils' /* eslint-disable @typescript-eslint/unbound-method */ @@ -469,3 +470,20 @@ function verifyFileCommand(command: string, expectedContents: string): void { fs.unlinkSync(filePath) } } + +function getTokenEndPoint(): string { + return 'https://vstoken.actions.githubusercontent.com/.well-known/openid-configuration' +} + +describe('oidc-client-tests', () => { + it('Get Http Client', async () => { + const http = new HttpClient('actions/oidc-client') + expect(http).toBeDefined() + }) + + it('HTTP get request to get token endpoint', async () => { + const http = new HttpClient('actions/oidc-client') + const res = await http.get(getTokenEndPoint()) + expect(res.message.statusCode).toBe(200) + }) +}) diff --git a/package-lock.json b/package-lock.json index a3f78bb..7eb2b6a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,14 +1,62 @@ { "name": "@actions/core", - "version": "1.4.0", - "lockfileVersion": 1, + "version": "1.6.0", + "lockfileVersion": 2, "requires": true, + "packages": { + "": { + "name": "@actions/core", + "version": "1.6.0", + "license": "MIT", + "dependencies": { + "@actions/http-client": "^1.0.11" + }, + "devDependencies": { + "@types/node": "^12.0.2" + } + }, + "node_modules/@actions/http-client": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-1.0.11.tgz", + "integrity": "sha512-VRYHGQV1rqnROJqdMvGUbY/Kn8vriQe/F9HR2AlYHzmKuM/p3kjNuXhmdBfcVgsvRWTz5C5XW5xvndZrVBuAYg==", + "dependencies": { + "tunnel": "0.0.6" + } + }, + "node_modules/@types/node": { + "version": "12.0.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-12.0.2.tgz", + "integrity": "sha512-5tabW/i+9mhrfEOUcLDu2xBPsHJ+X5Orqy9FKpale3SjDA17j5AEpYq5vfy3oAeAHGcvANRCO3NV3d2D6q3NiA==", + "dev": true + }, + "node_modules/tunnel": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", + "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==", + "engines": { + "node": ">=0.6.11 <=0.7.0 || >=0.7.3" + } + } + }, "dependencies": { + "@actions/http-client": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-1.0.11.tgz", + "integrity": "sha512-VRYHGQV1rqnROJqdMvGUbY/Kn8vriQe/F9HR2AlYHzmKuM/p3kjNuXhmdBfcVgsvRWTz5C5XW5xvndZrVBuAYg==", + "requires": { + "tunnel": "0.0.6" + } + }, "@types/node": { "version": "12.0.2", "resolved": "https://registry.npmjs.org/@types/node/-/node-12.0.2.tgz", "integrity": "sha512-5tabW/i+9mhrfEOUcLDu2xBPsHJ+X5Orqy9FKpale3SjDA17j5AEpYq5vfy3oAeAHGcvANRCO3NV3d2D6q3NiA==", "dev": true + }, + "tunnel": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", + "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==" } } } diff --git a/package.json b/package.json index d0b42a5..8d7a399 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@actions/core", - "version": "1.5.0", + "version": "1.6.0", "description": "Actions core lib", "keywords": [ "github", @@ -35,6 +35,9 @@ "bugs": { "url": "https://github.com/actions/toolkit/issues" }, + "dependencies": { + "@actions/http-client": "^1.0.11" + }, "devDependencies": { "@types/node": "^12.0.2" } diff --git a/src/core.ts b/src/core.ts index 9ef6aca..fafe3e6 100644 --- a/src/core.ts +++ b/src/core.ts @@ -5,6 +5,8 @@ import {toCommandProperties, toCommandValue} from './utils' import * as os from 'os' import * as path from 'path' +import {OidcClient} from './oidc-utils' + /** * Interface for getInput options */ @@ -353,3 +355,7 @@ export function saveState(name: string, value: any): void { export function getState(name: string): string { return process.env[`STATE_${name}`] || '' } + +export async function getIDToken(aud?: string): Promise { + return await OidcClient.getIDToken(aud) +} diff --git a/src/oidc-utils.ts b/src/oidc-utils.ts new file mode 100644 index 0000000..e33da5f --- /dev/null +++ b/src/oidc-utils.ts @@ -0,0 +1,84 @@ +/* eslint-disable @typescript-eslint/no-extraneous-class */ +import * as actions_http_client from '@actions/http-client' +import {IRequestOptions} from '@actions/http-client/interfaces' +import {HttpClient} from '@actions/http-client' +import {BearerCredentialHandler} from '@actions/http-client/auth' +import {debug, setSecret} from './core' +interface TokenResponse { + value?: string +} + +export class OidcClient { + private static createHttpClient( + allowRetry = true, + maxRetry = 10 + ): actions_http_client.HttpClient { + const requestOptions: IRequestOptions = { + allowRetries: allowRetry, + maxRetries: maxRetry + } + + return new HttpClient( + 'actions/oidc-client', + [new BearerCredentialHandler(OidcClient.getRequestToken())], + requestOptions + ) + } + + private static getRequestToken(): string { + const token = process.env['ACTIONS_ID_TOKEN_REQUEST_TOKEN'] + if (!token) { + throw new Error( + 'Unable to get ACTIONS_ID_TOKEN_REQUEST_TOKEN env variable' + ) + } + return token + } + + private static getIDTokenUrl(): string { + const runtimeUrl = process.env['ACTIONS_ID_TOKEN_REQUEST_URL'] + if (!runtimeUrl) { + throw new Error('Unable to get ACTIONS_ID_TOKEN_REQUEST_URL env variable') + } + return runtimeUrl + } + + private static async getCall(id_token_url: string): Promise { + const httpclient = OidcClient.createHttpClient() + + const res = await httpclient + .getJson(id_token_url) + .catch(error => { + throw new Error( + `Failed to get ID Token. \n + Error Code : ${error.statusCode}\n + Error Message: ${error.result.message}` + ) + }) + + const id_token = res.result?.value + if (!id_token) { + throw new Error('Response json body do not have ID Token field') + } + return id_token + } + + static async getIDToken(audience?: string): Promise { + try { + // New ID Token is requested from action service + let id_token_url: string = OidcClient.getIDTokenUrl() + if (audience) { + const encodedAudience = encodeURIComponent(audience) + id_token_url = `${id_token_url}&audience=${encodedAudience}` + } + + debug(`ID token url is ${id_token_url}`) + + const id_token = await OidcClient.getCall(id_token_url) + setSecret(id_token) + return id_token + } catch (error) { + throw new Error(`Error message: ${error.message}`) + } + } +}