Trusting other issuers
Promoters keep their line-ups in git, and they'd like to publish them on every push, instead of emailing files to the staff. The box office already streams line-ups into its importer, in Requests. Now promoters should reach that importer from GitHub Actions, without a password or a shared secret anywhere.
GitHub Actions can hand every workflow run a short-lived token, signed by GitHub, that says which repository and branch it runs for. The box office only has to trust GitHub as an issuer.
Trusting GitHub
GitHub publishes its keys at a fixed address, so IssuerKeys::Published points straight at them. The audience is what workflows ask GitHub to put into their tokens: the box office's own address.
use verifies_tokens_from_issuer;
use IssuerKeys;
;Promoters
The staff register each promoter with the repository it publishes from:
$ box_office add-promoter "Riverside Promotions" --repository riverside/lineups
Riverside Promotions publishes from riverside/lineupsadd-promoter is a small command that creates the Promoter through the PromoterRepository.
Recognizing the workflow
The promoter is an authenticated user, and its provider reads GitHub's token with #[bearer_token(issuer = github_actions)]. GitHub's tokens are ID tokens, so the profile is IdTokenProfile. Only pushes to main may publish, and anything else is stopped with Interrupted:
use Deserialize;
pub async Publishing a line-up
The streaming loop from Requests moved into a ConcertFeed singleton, so the staff's admin route and the promoters' public route share it. The public route asks for the promoter, and passes the body on:
Margaret recognizes the caller before it reads a single byte of the body. An anonymous upload of 20 megabytes is turned away right away, with a 401, without the box office ever receiving the file:
$ curl -i -T big-file.jsonl https://tickets.example.com/lineup
HTTP/1.1 401 Unauthorized
www-authenticate: BearerExchanging tokens
This works, but it ties the import route to GitHub. Some promoters use other CI systems, and every new one would mean another provider on the route. Instead, the box office can exchange GitHub's token for one of its own, with OAuth 2.0 Token Exchange. The route then only ever sees box office tokens.
Declare a resource for line-up imports, and admit a public client that may exchange tokens for it:
use issues_resource_tokens;
;use admits_oauth_client;
use ClientAuthenticationMethod;
;The client needs no secret, because the GitHub token it brings is the proof. Which GitHub tokens are good enough is up to you. A singleton with #[exchanges_tokens_from] decides, and picks the subject of the new token: the promoter's id.
The promoter's provider now reads box office tokens for the lineup_imports resource, and finds the promoter by the token's subject:
&self,
token: ,
) The route itself doesn't change. A promoter's workflow asks GitHub for a token, exchanges it at the box office's token endpoint, and publishes:
name: Publish the line-up
on:
push:
branches:
permissions:
id-token: write
contents: read
jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Exchange the GitHub token for a box office token
run: |
github_token=$(curl -sS -H "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \
"$ACTIONS_ID_TOKEN_REQUEST_URL&audience=https://tickets.example.com" | jq -r .value)
box_office_token=$(curl -sS https://tickets.example.com/token \
-d grant_type=urn:ietf:params:oauth:grant-type:token-exchange \
-d client_id=lineup-ci \
-d audience=https://tickets.example.com/lineup \
-d subject_token_type=urn:ietf:params:oauth:token-type:id_token \
--data-urlencode "subject_token=$github_token" | jq -r .access_token)
echo "::add-mask::$box_office_token"
echo "BOX_OFFICE_TOKEN=$box_office_token" >> "$GITHUB_ENV"
- name: Publish
run: |
curl -sS --fail-with-body -X PUT https://tickets.example.com/lineup \
-H "Authorization: Bearer $BOX_OFFICE_TOKEN" \
--data-binary @lineup.jsonlA token that doesn't verify never reaches your exchanger:
$ curl https://tickets.example.com/token -d grant_type=urn:ietf:params:oauth:grant-type:token-exchange \
-d client_id=lineup-ci -d audience=https://tickets.example.com/lineup \
-d subject_token_type=urn:ietf:params:oauth:token-type:id_token -d subject_token=garbage
{"error":"invalid_request","error_description":"the subject token fails verification"}Supporting another CI system now means trusting its issuer and adding one more exchanger. The import route never changes again.