Workload identity
A trust lets a job that already has an identity sign in to ADoor. This is standard OpenID Connect workload identity: the same kind of token GitHub Actions uses to sign in to AWS, Google Cloud, and Azure. The job asks its issuer for a short-lived JWT. ADoor checks that the issuer signed it, that it was issued for this ADoor server, and that the subject is one you chose. ADoor then issues its own credential, capped by the access you set. You do not paste a long-lived secret into the job.
ADoor does not special-case a cloud provider. GitHub Actions is the worked example below. Another issuer can be trusted when it signs with RS256, ES256, or PS256, publishes a public HTTPS key set, sets the token audience to the ADoor origin, and includes a jti. There is no separate Cloudflare, AWS, or Google setup.
What you are trusting
You are trusting the issuer to sign only true claims. GitHub fills the sub claim from the job that is actually running: the repository, and the branch, environment, or pull request. Another repository cannot forge your subject, because it does not hold GitHub's signing key.
The key set URL is how ADoor learns that public key. There is no field for a key. ADoor fetches a JWKS document from the URL and checks the token signature against the key named by kid. Pointing that URL at any other address means trusting whoever controls it. ADoor will accept tokens signed by a key published there.
What a trust stores
- A name, for the Access page.
- The issuer. This is the token's
issclaim. GitHub Actions useshttps://token.actions.githubusercontent.com. - The key set URL. GitHub publishes its keys at
https://token.actions.githubusercontent.com/.well-known/jwks. - A subject. The token's
subclaim must match it. An exact string matches only itself. A final*after/or:matches one prefix, sorepo:octo-org/octo-repo:*matches that repository's branches, tags, environments, and pull requests.repo:octo-org*is rejected, because it would also matchrepo:octo-org-evil. - The access the exchanged identity is capped to. On the website, the access you choose is stored on the trust and is the cap. That choice is table access. A workload cannot receive account access: profile, registered apps, creating workspaces, and browser sessions stay with the person. The exchanged identity also cannot exceed your own access at the moment of the exchange. It lasts 15 minutes by default.
The website asks for those fields. The federations.create operation, run while signed in from the Operation catalog in the app, can also require extra claims to match exactly, set the maximum lifetime from 60 seconds up to 30 days with maxTtlSeconds, and choose the kind of connection the job becomes with childRole. The default kind is federated. An exchange can ask for a shorter lifetime with expires_in.
How ADoor checks a token
The job gets a JWT from the issuer, then exchanges it at POST /oauth/token. Two different values are both called an audience:
- The JWT
audclaim must be this ADoor server's origin, with no path and no trailing slash. For the hosted app that ishttps://app.adoor.halmoslabs.com. - The
audiencefield of the exchange request must be the trust id, which ADoor shows after you create the trust. Pass it every time. If you leave it out, the subject has to match exactly one trust before a key set is fetched. Zero matches and several matches return the same error:No trusted issuer matches this token.
ADoor then checks, in order:
- A live trust has this issuer and this server's origin. When you name a trust id, it has to be that trust.
- The signature is RS256, ES256, or PS256.
noneand HMAC (HS*) are rejected. The key is the JWKS entry with the samekid. A key that includes a privatedis ignored. issis the trusted issuer.audcontains this server's origin.exp,nbf, andiatare acceptable, with 60 seconds of clock skew.expminusiatis at most 3600 seconds.- The subject matches, and any exact claims you required match.
jtiis present. ADoor stores it until the token's ownexp. Sending the same token again is rejected and returns no credential.
The key set must be HTTPS, on a public address, with no username or password. Redirects are not followed. ADoor reads at most 256 KiB and waits at most 5 seconds. A localhost or private address is rejected, so a key set has to be published somewhere ADoor can reach. A failed fetch is remembered for 30 seconds.
The new credential cannot exceed the trust's cap or your current access. Revoking the trust makes the next exchange fail, and credentials it already issued fail on their next request.
The exchange can ask for less than the cap by sending authorization_details of type adoor_space_access. Asking for more is rejected. The same signed JWT is also accepted as urn:ietf:params:oauth:grant-type:jwt-bearer (field assertion) and as client_credentials with client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer.
Keep a trust narrow
Prefer one repository and one branch or environment. repo:octo-org/* lets every repository in that organization sign in. Leave out delete and schema changes unless the job needs them. The claims field of federations.create can require an exact claim, for example "repository": "octo-org/octo-repo". The website form does not ask for extra claims.
If the job sets an environment, GitHub puts the environment in sub and does not include the branch. A trust for repo:octo-org/octo-repo:ref:refs/heads/main will not accept that job.
GitHub Actions
GitHub's issuer and key set are the two URLs above. The workflow must allow id-token: write, or GitHub will not mint a token. Request the token with audience set to the ADoor origin. That becomes the JWT aud claim.
GitHub documents the subject formats in its OpenID Connect reference. Use the format your repository actually issues.
Name form, for a repository that still uses it:
- Branch:
repo:octo-org/octo-repo:ref:refs/heads/demo-branch - Environment:
repo:octo-org/octo-repo:environment:Production - Pull request, and only when the job does not set an environment:
repo:octo-org/octo-repo:pull_request
Repositories created after 15 July 2026, and older repositories that opted in, put the owner id and repository id in the subject. GitHub's published branch example is repo:octo-org@123456/octo-repo@456789:ref:refs/heads/main. Keep the environment or pull request suffix after repo:OWNER@OWNER_ID/REPO@REPO_ID, and confirm the exact sub before saving the trust. A custom subject template replaces the whole sub claim. The github/actions-oidc-debugger action prints the claims. GitHub Enterprise Server does not use the immutable format.
In ADoor, open Access, choose Trust an issuer, and confirm the GitHub issuer and key set. Set the subject to one line above, with your organization, repository, and branch or environment. Choose the access the job may have. Copy the trust id.
permissions:
id-token: write
contents: read
jobs:
sync:
runs-on: ubuntu-latest
steps:
- name: Exchange a GitHub token for an ADoor credential
env:
ADOOR_ORIGIN: https://app.adoor.halmoslabs.com
ADOOR_TRUST_ID: PASTE_THE_TRUST_ID
run: |
set -eu
jwt=$(curl -sS \
-H "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \
"$ACTIONS_ID_TOKEN_REQUEST_URL&audience=$ADOOR_ORIGIN" \
| jq -r .value)
response=$(curl -sS -X POST "$ADOOR_ORIGIN/oauth/token" \
-H 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'grant_type=urn:ietf:params:oauth:grant-type:token-exchange' \
--data-urlencode "subject_token=$jwt" \
--data-urlencode 'subject_token_type=urn:ietf:params:oauth:token-type:jwt' \
--data-urlencode "audience=$ADOOR_TRUST_ID")
token=$(jq -r .access_token <<<"$response")
test -n "$token" && test "$token" != null
curl -sS "$ADOOR_ORIGIN/api/identity" -H "Authorization: Bearer $token"
ADOOR_ORIGIN must be the origin only. ADOOR_TRUST_ID is the trust id, not that origin. The last request shows the short-lived credential. The same bearer token is what the API and MCP accept. Each GitHub token can be exchanged once; request a new one for another run.
Revoking a trust
A trust has no credential of its own. Creating one returns its id, not a token, and only an exchange produces a credential. Revoke the trust on Access to stop new exchanges. Credentials it already issued fail on their next request.