Skip to content

Commit

Permalink
Updated dependencies and adding support to revoke the access token in…
Browse files Browse the repository at this point in the history
… the post action steps
  • Loading branch information
peter-murray authored Jan 29, 2024
1 parent b1ad34e commit 13fafb3
Show file tree
Hide file tree
Showing 18 changed files with 422 additions and 1,251 deletions.
2 changes: 1 addition & 1 deletion .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "Workspace",
"image": "ghcr.io/octodemo/container-nodejs-development:base-latest",
"image": "ghcr.io/octodemo/development-containers/javascript:base-202401-r1",
"extensions": [
"dbaeumer.vscode-eslint",
"redhat.vscode-yaml",
Expand Down
44 changes: 44 additions & 0 deletions .github/workflows/test_organization_installed_revocation.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Tests this action success by the application already being installed on an organization

name: Test Success - organization - installed with revocation

on:
push:
workflow_dispatch:
inputs:
branch:
description: The name of the branch to checkout for the action
required: true
default: main

jobs:
test:
runs-on: ubuntu-latest

steps:
- name: Checkout specified branch
if: github.event_name == 'workflow_dispatch'
uses: actions/checkout@v4
with:
ref: ${{ github.event.inputs.branch }}

- name: Checkout
if: github.event_name != 'workflow_dispatch'
uses: actions/checkout@v4

- name: Use action
id: use_action
uses: ./
with:
application_id: ${{ secrets.APPLICATION_ID }}
application_private_key: ${{ secrets.APPLICATION_PRIVATE_KEY }}
organization: octodemo
revoke_token: true

- name: Use token to read details
uses: actions/github-script@v7
with:
github-token: ${{ steps.use_action.outputs.token }}
script: |
const repo = await github.rest.repos.get({owner: 'octodemo', repo: 'demo-bootstrap'});
console.log(JSON.stringify(repo, null, 2));
8 changes: 4 additions & 4 deletions .github_application
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
{
"test": {
"applicationId": 123456,
"privateKey": "-----BEGIN RSA PRIVATE KEY-----\nk3y_g03s_her3\n-----END RSA PRIVATE KEY-----\n",
"applicationId": 74130,
"privateKey": "LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFb2dJQkFBS0NBUUVBbUhKOEJHdTFYZUZ4aENVQXBrNHNSTVI4RnRTdGtyMEx0OWtWTGNSUjRFWitiOWhHCmR0blJpOFhqV3d5MU5zMHliMkJMdHBpVHZKSFVKTUphWXluZ2ZkZnZhcWhocm1yYm5vV0pLQkxmeUxwTXFNS1EKQ3RialFxbnVrQURJUWVQd2ZGeTNpVHkxd1JkRC9zTUs1U0VtV0Fxb0pZQk50eTFZZzA2UzVkYVlPM2xjY3hrYQpQWjRjcm9McWF6Ny9tU3dDVTR5VWRSb3h4WVF4VG1MZXg5M2tqU09TTmdpK0FXc0lCbjV3UHI0VHNuVHFSeWpIClN2aEdMdk9YREpRYWZRdk56WjFSL1FYMzlOQk9xOEVKZW5pWXdaUm9uNVcvNVhMYW94MFFyUGhrY1BES3A5SVUKeHpJakUwWlNmMStUK1FFbTQ3TkFtSnhvZjFhdGRFVzZDTCtheHdJREFRQUJBb0lCQUQ3enI4REhsWnFSK1NWZgpmbGd1bWRzLzVCb3Rjd3ZRWXlGbFZIaVV4RmEvNVlCY0tDVDJKN0QzWTc1NmplNTJaK2hVTkkvUGk5cG53ZG40CkpBa2xCdDRRcUg0NzBES05UK216TFFOT1gvanM3YkVXdnhLcTBDZjhNbFptN0V0QlRGS2VtdS9pRVJBT2duYVcKcGs0ZUZVNXdBQ1dVU1FObWgxR1p4ZEdCZjFXM1VjUnQxcFRvOEtQTDluZm4vSGJiRFNsQkNVL3VIcWd2TSt2cApmTE03bzRIVDZ1K1ZzU00rWGZqeDhpeE5ZRHdoalNuKzQyZm13d1d3ZzJISHUrdUozZ1pUSWQwRUI1VW9hdUNjCjZUTlVtcEJscjU5UGFmVkZRWUY1S3VxaHJXKzVQaWpHcHBZcXg4Ynl6aFpOQzkwZnl5V0NXcXg2eGFZVm5OdzgKNkJmUXM2a0NnWUVBeVlyRVg1NU1RTzJnWDY2TGwxaGJDMzNzWk1OZzloVG1SK1doSTFjNksvbFZ1TFoyL0RPdwpsYTZ6eHdBU204Z0ZyVUFYbUljV2h2b3FwWGVzNWZzOVZKeDlNT0ZVYVBrckRPQllnY1laMUR6VVNVOHc3SSttCnlyV3hRUkRNajhvSGpRbHVpM0s2MzZucm5RajhxOGkvQ2dranVPcHJGZnliMzVEMFlDdjVXZzBDZ1lFQXdhT3cKRWFhN0l1MjFGa08vbmFjdVhjSnBhNkVlUTNqZFNlNlRQaXZ6bVVXU0haeGJuUy9XSnJaRjQwSExzUWxOZHl0ZgpNTTBKZFU0VmMyR0NVc1pMYjdQSmJwdVRqRERSSHJXV1pCMnhiemF0K3A3N2RzNWlOcXFRcTZ6M0syUVh4Y3ZTCis5am5VZXpDU2Y0N1R1OWNTTW96V3hTMW82b1BPSFdHVFRvdHR5TUNnWUFQdWc1Y3o4TnZoWnR3Ry9TMG1LWnkKSFI5bk5YL0pkQlFNSkRVUXh1dTVKcm16c2psU3NNM2t3RDh6RmlSZGw1d3B5c2lNbEc0RGxsM2hqNWNrVXhpVQpFNm9KT0d3WHpPbTVGWUNTajl6UUhQY0x5V3d0NlgvQWJiRXBQS0JaMEJBS3gyT2k2ZzcvQ1FsanRhSFIzZFphCmVDQWJlOTlqVmRUcit5bTJuM2ZUdVFLQmdBMm5TZ25rbEx0Z3dXMEJkK2hZMm1jWUJ6RGttbXF0Z2dUdGdvcFcKdFFWd3AxM1pJWWlTeituSTNtS295QUVDbytpc01Ua1NyQUVPY1dyQ1RGc2p5anZsRkdYdEtGa3hNLzJUVmpoVwo4NlRnMlNHYnhpVlpaZ2x1dTJhdmVub2Z3NkZadnRXdE5KcE5OR0hkUURkUG4xVXVsTEp1WW1SWTRGdmR4WXQ2CmQ3QzdBb0dBRUsvalFiZ0l3OXFLQUNOZ0JySnB1cU5Ham9JajFoQTRlb29DMXp1bFEyZUpnZ2J5OTBpSDg2VzEKM0xyOVZMVFkyc2JKTzlqekZVR0lOL01BOEhYQTE1a2grZHRibkRsdFRFZGNnenBCRzhCQUZRQ3hQWnBGWHhtZgpDUmhXN1l6RW1IeWJ4R0toR3NOK2M4NUhKTHZFSWwrRTh6eitXRk9xT240dkJXU0ZwSnc9Ci0tLS0tRU5EIFJTQSBQUklWQVRFIEtFWS0tLS0tCg==",
"repo": {
"owner": "your-org-name",
"repo": "your-repo-name"
"repo": "demo-bootstrap-old",
"owner": "octodemo"
},
"org": "octodemo"
},
Expand Down
17 changes: 13 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ generates for you or Base64 encode it in the secret.
* `organization`: An optional organization name if the GitHub Application is installed at the Organization level (instead of the repository).
* `github_api_base_url`: An optional URL to the GitHub API, this will be read and loaded from the runner environment by default, but you might be bridging access to a secondary GHES instance or from GHES to GHEC, you can utilize this to make sure the Octokit library is talking to the right GitHub instance.
* `https_proxy`: An optional proxy to use for connecting with the GitHub instance. If the runner has `HTTP_PROXY` or `HTTPS_PROXY` specified as environment variables it will attempt to use those if this parameter is not specified.
* `revoke_token`: An optional boolean `true` or `false` value to revoke the access token as part of the post job steps in the actions workflow. To preserve backwards compatibility on this action, it defaults to `false`.

#### Examples
Get a token with all the permissions of the GitHub Application:
Expand All @@ -90,7 +91,7 @@ jobs:
steps:
- name: Get Token
id: get_workflow_token
uses: peter-murray/workflow-application-token-action@v2
uses: peter-murray/workflow-application-token-action@v3
with:
application_id: ${{ secrets.APPLICATION_ID }}
application_private_key: ${{ secrets.APPLICATION_PRIVATE_KEY }}
Expand All @@ -113,7 +114,7 @@ jobs:
steps:
- name: Get Token
id: get_workflow_token
uses: peter-murray/workflow-application-token-action@v2
uses: peter-murray/workflow-application-token-action@v3
with:
application_id: ${{ secrets.APPLICATION_ID }}
application_private_key: ${{ secrets.APPLICATION_PRIVATE_KEY }}
Expand All @@ -137,7 +138,7 @@ jobs:
steps:
- name: Get Token
id: get_workflow_token
uses: peter-murray/workflow-application-token-action@v2
uses: peter-murray/workflow-application-token-action@v3
with:
application_id: ${{ secrets.APPLICATION_ID }}
application_private_key: ${{ secrets.APPLICATION_PRIVATE_KEY }}
Expand Down Expand Up @@ -166,7 +167,7 @@ jobs:
steps:
- name: Get Token
id: get_workflow_token
uses: peter-murray/workflow-application-token-action@v2
uses: peter-murray/workflow-application-token-action@v3
with:
application_id: ${{ secrets.APPLICATION_ID }}
application_private_key: ${{ secrets.APPLICATION_PRIVATE_KEY }}
Expand All @@ -185,5 +186,13 @@ then it will be parsed for hostname matches as to whether or not to use the prox

The format that is supported for `no_proxy` environment variable is a comma separated list of host names, e.g. `api.github.com,www.google.com` of when to not use the proxy server.


### Access Token revocation

To provide additional options for security around the access token and waiting on it to expire, you can leverage the `revoke_token` input set to `true` so that at the end of the
job run, a post actions step will revoke the access token, invalidating it so that is is immediately invalid and cannot be used.



### References
https://docs.github.com/en/developers/apps/authenticating-with-github-apps#authenticating-as-an-installation
10 changes: 8 additions & 2 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,19 @@ inputs:
description: Option proxy server for making the requests
required: false

revoke_token:
description: Flag to revoke the token after the job is complete, defaults to 'false', set to 'true' to revoke in the post job processing.
required: false
default: 'false'

outputs:
token:
description: A valid token representing the Application that can be used to access what the Application has been scoped to access.

runs:
using: node16
main: dist/index.js
using: node20
main: dist/main/index.js
post: dist/post/index.js

branding:
icon: lock
Expand Down
9 changes: 0 additions & 9 deletions dist/index.js

This file was deleted.

6 changes: 6 additions & 0 deletions dist/main/index.js

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions dist/main/index.js.map

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions dist/main/sourcemap-register.js

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions dist/post/index.js

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions dist/post/index.js.map

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions dist/post/sourcemap-register.js

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,11 @@ async function run() {
core.setOutput('token', accessToken.token);
core.info(JSON.stringify(accessToken));
core.info(`Successfully generated an access token for application.`)

if (core.getBooleanInput('revoke_token')) {
// Store the token for post state invalidation of it once the job is complete
core.saveState('token', accessToken.token);
}
} else {
fail('No installation of the specified GitHub application was able to be retrieved.');
}
Expand Down
52 changes: 35 additions & 17 deletions lib/github-application.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ const jwt = require('jsonwebtoken')
, github = require('@actions/github')
, core = require('@actions/core')
, PrivateKey = require('./private-key')
, HttpsProxyAgent = require('https-proxy-agent')
, url = require('url')
, HttpsProxyAgent = require('https-proxy-agent').HttpsProxyAgent
, URL = require('url')
;

module.exports.create = (privateKey, applicationId, baseApiUrl, timeout, proxy) => {
Expand All @@ -15,6 +15,21 @@ module.exports.create = (privateKey, applicationId, baseApiUrl, timeout, proxy)
});
}

module.exports.revokeAccessToken = (token, baseUrl, proxy) => {
// The token being provided is the one to be invalidated
const client = getOctokit(token, baseUrl, proxy);

return client.rest.apps.revokeInstallationAccessToken()
.catch(err => {
throw new Error(`Failed to revoke application token; ${err.message}`);
}).then(resp => {
if (resp.status === 204) {
return true;
}
throw new Error(`Unexpected status code ${resp.status}; ${resp.data}`);
});
}

class GitHubApplication {

constructor(privateKey, applicationId, baseApiUrl) {
Expand All @@ -40,20 +55,7 @@ class GitHubApplication {
};

const token = jwt.sign(payload, this.privateKey, { algorithm: 'RS256' });

// We need to get this here so we can potentially apply no_proxy rules
const baseUrl = getApiBaseUrl(this.githubApiBaseUrl);

const octokitOptions = {
baseUrl: baseUrl
};

const request = {
agent: getProxyAgent(proxy, baseUrl),
timeout: 5000
};
octokitOptions.request = request;
this._client = new github.getOctokit(token, octokitOptions);
this._client = getOctokit(token, this._githubApiUrl, proxy);

return this.client.request('GET /app', {
mediaType: {
Expand Down Expand Up @@ -170,6 +172,22 @@ class GitHubApplication {
}
}

function getOctokit(token, baseApiUrl, proxy) {
const baseUrl = getApiBaseUrl(baseApiUrl);

const octokitOptions = {
baseUrl: baseUrl
};
const request = {
agent: getProxyAgent(proxy, baseUrl),
timeout: 5000
};
octokitOptions.request = request;
const client = new github.getOctokit(token, octokitOptions);

return client;
}

function _validateVariableValue(variableName, value) {
if (!value) {
throw new Error(`A valid ${variableName} must be provided, was "${value}"`);
Expand Down Expand Up @@ -216,7 +234,7 @@ function getProxyAgent(proxy, baseUrl) {
function proxyExcluded(noProxy, baseUrl) {
if (noProxy) {
const noProxyHosts = noProxy.split(',').map(part => part.trim());
const baseUrlHost = url.parse(baseUrl).host;
const baseUrlHost = new URL(baseUrl).host;

core.debug(`noProxyHosts = ${JSON.stringify(noProxyHosts)}`);
core.debug(`baseUrlHost = ${baseUrlHost}`);
Expand Down
77 changes: 62 additions & 15 deletions lib/github-application.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ describe('GitHubApplication', () => {
}
});

describe('Installed Application - GHES', () => {
describe.skip('Installed Application - GHES', () => {

const TEST_APPLICATION_NAME = 'test-ghes';

Expand Down Expand Up @@ -195,7 +195,6 @@ describe('GitHubApplication', () => {
});
});


it('should be able to get access token for a repository installation', async () => {
const repoInstall = await app.getRepositoryInstallation(
testValues.getTestRepositoryOwner(TEST_APPLICATION_NAME),
Expand Down Expand Up @@ -248,20 +247,68 @@ describe('GitHubApplication', () => {
expect(accessToken).to.have.property('token');

// Use the token to access the repository
const client = new github.getOctokit(accessToken.token)
, repoName = testValues.getTestRepository(TEST_APPLICATION_NAME)
, ownerName = testValues.getTestRepositoryOwner(TEST_APPLICATION_NAME)
, repo = await client.rest.repos.get({
owner: ownerName,
repo: repoName,
});

expect(repo).to.have.property('status').to.equal(200);
expect(repo).to.have.property('data');
expect(repo.data).to.have.property('owner').to.have.property('login').to.equal(ownerName);
expect(repo.data).to.have.property('name').to.equal(repoName);
// const client = new github.getOctokit(accessToken.token)
// , repoName = testValues.getTestRepository(TEST_APPLICATION_NAME)
// , ownerName = testValues.getTestRepositoryOwner(TEST_APPLICATION_NAME)
// , repo = await client.rest.repos.get({
// owner: ownerName,
// repo: repoName,
// });

// expect(repo).to.have.property('status').to.equal(200);
// expect(repo).to.have.property('data');
// expect(repo.data).to.have.property('owner').to.have.property('login').to.equal(ownerName);
// expect(repo.data).to.have.property('name').to.equal(repoName);
await testRepositoryToken(accessToken.token);
});
});
})
});

describe('Application token revocation', () => {

let testToken;

beforeEach(async () => {
const repoInstall = await app.getRepositoryInstallation(
testValues.getTestRepositoryOwner(TEST_APPLICATION_NAME),
testValues.getTestRepository(TEST_APPLICATION_NAME)
);

const accessToken = await app.getInstallationAccessToken(repoInstall.id);
expect(accessToken).to.have.property('token');
testToken = accessToken.token;
});

it('should be able to revoke a valid application token', async () => {
await testRepositoryToken(testToken);

const revoked = await gitHubApp.revokeAccessToken(testToken);
expect(revoked).to.be.true;

try {
await testRepositoryToken(testToken);
fail('The token should no longer be valid so should not get here.');
} catch (err) {
expect(err.message).to.contain('Bad credentials');
}
});
});

async function testRepositoryToken(accessToken) {
const client = new github.getOctokit(accessToken)
, repoName = testValues.getTestRepository(TEST_APPLICATION_NAME)
, ownerName = testValues.getTestRepositoryOwner(TEST_APPLICATION_NAME)
;

const repo = await client.rest.repos.get({
owner: ownerName,
repo: repoName,
});

expect(repo).to.have.property('status').to.equal(200);
expect(repo).to.have.property('data');
expect(repo.data).to.have.property('owner').to.have.property('login').to.equal(ownerName);
expect(repo.data).to.have.property('name').to.equal(repoName);
}
});
});
Loading

0 comments on commit 13fafb3

Please sign in to comment.