Authenticating Github workflows with oauth2-proxy04 Jul 2023 | #tech
oauth2-proxy is often used to handle user authentication for apps, however non-human users (e.g. CI workflows) are often unable to complete the OIDC flow. In this post I will show how to configure oauth2-proxy to trust Github’s OIDC provider and use that JWT to authenticate workflows and give them access to the app behind the proxy.
1. Figure out the JWT issuer URL
We are using the Github OIDC feature that allows workflows to obtain a Github-signed JWT.
The Github docs say that this is
https://token.actions.githubusercontent.com for github.com, and
https://HOSTNAME/_services/token for Github Enterprise.
To limit the scope of the token to this specific use-case, we also need to pick an audience.
This should be a unique, non-secret value.
I will pick
2. Configure the oauth2-proxy
Oauth2-proxy supports skipping the OIDC flow if a JWT is passed in a header. To configure this we need to add the following two config options:
--extra-jwt-issuers config flag holds a list of
When using a different issuer, make sure it has
$ISSUER/.well-known/jwks.json, e.g. github.com has the former.
3. Configure the Github action workflow to obtain and use the JWT
name: Test Github JWT with oauth2-proxy on: push # permission can be added at job level or workflow level permissions: id-token: write # This is required for requesting the JWT contents: read # This is required for actions/checkout, that this example actually doesn't use, but real code probably will jobs: test: runs-on: ubuntu-latest steps: - name: Test run: | GH_JWT=$(curl -H "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" "$ACTIONS_ID_TOKEN_REQUEST_URL&audience=szabo-jp-example-app" | jq -j -c '.value') curl -v -H "Authorization: Bearer $GH_JWT" https://your-app-behind-oauth2-proxy.example.com/
Make sure to pass the same audience to the call that you configured with the oauth2-proxy!
The response is a json, so we use
jq to get only the value.
Note the use of
-j which avoids quoting the token value (a pretty hard to debug issue, as Github filters the token value in workflow logs).
If this fails, make sure
jq is installed on the runner.
Once the JWT is obtained, we can pass it to the oauth2-proxy via the
Authorization: Bearer header.
If it’s not working, check the oauth2-proxy logs. You might find a message like
[2023/07/04 07:07:27] [jwt_session.go:51] Error retrieving session from token in Authorization header: no valid bearer token found in authorization header [2023/07/04 07:07:27] [oauthproxy.go:866] No valid authentication in request. Initiating login.
I found that it’s the easiest to check the project’s source to see why a certain error is returned.
If everything is working you should see a log message like this:
127.0.0.6 - 3e98af6c-2d10-4b53-fa52-7a7a89f6b824 - repo:markszabo/markszabo.github.io:ref:refs/heads/testing-github-jwt [2023/07/04 07:07:56] your-app-behind-oauth2-proxy.example.com GET / "/debug" HTTP/1.1 "curl/7.81.0" 200 3306 0.055
5. Identity of the workflow
--pass-user-headers config option is set, oauth2-proxy passes the authenticated user’s identity in the headers
But now that we are skipping the OIDC flow, what value do these headers get?
The log output earlier already hinted at it:
X-Forwarded-Email: repo:markszabo/markszabo.github.io:ref:refs/heads/testing-github-jwt X-Forwarded-User: repo:markszabo/markszabo.github.io:ref:refs/heads/testing-github-jwt
When configuring authorization in your app based on these headers, make sure to include the trailing colon.
strings.HasPrefix(authHeaderVakue, "repo:markszabo/markszabo.github.io") will also match other repositories, e.g.
markszabo/markszabo.github.io-other-repo, so use
strings.HasPrefix(authHeaderVakue, "repo:markszabo/markszabo.github.io:") to avoid this.