diff --git a/.github/workflows/pomerium-verify.yml b/.github/workflows/pomerium-verify.yml new file mode 100644 index 0000000..09d07cc --- /dev/null +++ b/.github/workflows/pomerium-verify.yml @@ -0,0 +1,81 @@ +name: Verify Pomerium with GitHub OIDC + +on: + push: + branches: [main] + workflow_dispatch: + inputs: + url: + description: URL to open with the GitHub OIDC JWT + required: false + default: https://verify.sv.sandbox.pomerium.io + audience: + description: Audience to request for the GitHub OIDC JWT + required: false + default: https://verify.sv.sandbox.pomerium.io + +permissions: + contents: read + id-token: write + +jobs: + verify: + runs-on: ubuntu-latest + steps: + - name: Authenticate to Pomerium with GitHub OIDC + uses: actions/github-script@v9 + env: + DEFAULT_POMERIUM_URL: https://verify.sv.sandbox.pomerium.io + DEFAULT_POMERIUM_AUDIENCE: https://verify.sv.sandbox.pomerium.io + INPUT_URL: ${{ inputs.url }} + INPUT_AUDIENCE: ${{ inputs.audience }} + with: + script: | + const targetUrl = process.env.INPUT_URL || process.env.DEFAULT_POMERIUM_URL; + const audience = process.env.INPUT_AUDIENCE || process.env.DEFAULT_POMERIUM_AUDIENCE; + + core.info(`Requesting GitHub OIDC token for audience: ${audience}`); + const idToken = await core.getIDToken(audience); + core.setSecret(idToken); + + // Print the token's header and claims, never the token itself: the + // signature is what makes it usable, and this repository's logs are public. + const decodeSegment = (segment) => + JSON.parse(Buffer.from(segment, 'base64url').toString('utf8')); + const [rawHeader, rawPayload] = idToken.split('.'); + const header = decodeSegment(rawHeader); + const claims = decodeSegment(rawPayload); + + core.startGroup('GitHub OIDC token - header'); + core.info(JSON.stringify(header, null, 2)); + core.endGroup(); + + core.startGroup('GitHub OIDC token - claims'); + core.info(JSON.stringify(claims, null, 2)); + core.endGroup(); + + const asUTC = (s) => new Date(s * 1000).toISOString(); + core.info(`Signed with ${header.alg} (key id ${header.kid})`); + core.info(`iss=${claims.iss}`); + core.info(`aud=${claims.aud}`); + core.info(`sub=${claims.sub}`); + core.info(`repository=${claims.repository} ref=${claims.ref} event=${claims.event_name}`); + core.info(`valid ${asUTC(claims.iat)} -> ${asUTC(claims.exp)} (${claims.exp - claims.iat}s)`); + + core.info(`Opening ${targetUrl} with the GitHub OIDC JWT`); + const response = await fetch(targetUrl, { + headers: { + Authorization: `Bearer ${idToken}`, + Accept: 'application/json', + }, + redirect: 'manual', + }); + + core.info(`Received ${response.status} ${response.statusText}`); + + if (!response.ok) { + const location = response.headers.get('location'); + const body = (await response.text()).slice(0, 4000); + const redirectHint = location ? `\nRedirected to: ${location}` : ''; + core.setFailed(`Request to ${targetUrl} failed with ${response.status} ${response.statusText}${redirectHint}\n${body}`); + } diff --git a/README.md b/README.md index daec3ff..62e2e8b 100644 --- a/README.md +++ b/README.md @@ -1 +1,17 @@ -# github-actions-jwt-test \ No newline at end of file +# github-actions-jwt-test + +This repository includes a GitHub Actions workflow that requests a GitHub OIDC JWT and uses it to open `https://verify.sv.sandbox.pomerium.io`. + +## Workflow + +- Workflow file: `.github/workflows/pomerium-verify.yml` +- Triggered on every push and with manual `workflow_dispatch` +- Requests `id-token: write` so the job can mint a GitHub OIDC JWT +- Sends the JWT in the `Authorization` header to the target URL + +## Manual run + +You can run the workflow manually and optionally override: + +- `url` (defaults to `https://verify.sv.sandbox.pomerium.io`) +- `audience` (defaults to `https://verify.sv.sandbox.pomerium.io`)