Using the API without a browser
Scheduled jobs, servers and CI have no browser and nobody sitting at them. This is the page for them.
There is no way to trade a secret for a token on a machine with nobody present:
the authorization server offers no client_credentials grant. Access always begins
with a person signing in and consenting, and that is deliberate, because it is what
gives the customer a visible grant they can revoke. What you can do is make the
person do it once. After that, a program runs on a refresh token that does not
expire, and never needs a person again.
Three ways to get there, in the order to prefer them.
1. The ts command
ts removes the problem instead of explaining it. It does the discovery, the
sign-in, the storing of the refresh token and the refreshing, and hands any tool a
token to use. It is one command-line program that serves every language, so your
code never has to learn what a refresh token is.
ts auth login https://acme.example.com # once, ever
TOKEN=$(ts auth token) # always valid; refreshes silently
curl -sS https://acme.example.com/api/public/v1/meta \
-H "Authorization: Bearer $TOKEN"
ts auth loginopens a browser when there is one. On a machine without one it uses the device flow below: it prints a code and an address, and you approve from any device that has a browser.ts auth tokenprints a currently valid access token and nothing else, so it is safe inside$(...). When you are not signed in it prints nothing to standard output, says why on standard error, and exits non-zero, so that a script fails at that moment and not later, after sending an empty token and meeting a baffling401. Run scripts withset -e, or check the exit status.ts auth whoamishows what the credential can do here, andts auth logoutforgets it on this machine.- In CI, where there is no file to keep, set
TS_REFRESH_TOKENandTS_TENANT_URLin the environment andtsuses those.
ts is the command-line companion of the open-source migration tool, and comes
with it.
2. The device flow
For a box with no browser and no ts. The authorization server advertises the
grant: look for urn:ietf:params:oauth:grant-type:device_code in its
grant_types_supported, and read the device_authorization_endpoint from its
metadata. This is the standard device authorization grant (RFC 8628): your program
prints a code, and a person approves it from a phone.
Ask for a device code, with the same scopes, user:org:read, offline_access and
resource as in the browser flow:
curl -sS "$DEVICE_AUTHORIZATION_ENDPOINT" \
-d client_id="$CLIENT_ID" \
-d scope="consent:write user:org:read offline_access" \
-d resource="$RESOURCE"
The response, as the standard defines it, has a user_code, a verification_uri,
a device_code, an expires_in and an interval. Print the first two for whoever
is at hand: open this address on any device and enter this code. Meanwhile, poll
the token endpoint no faster than every interval seconds (every five if none is
given):
curl -sS "$TOKEN_ENDPOINT" \
-d grant_type=urn:ietf:params:oauth:grant-type:device_code \
-d device_code="$DEVICE_CODE" \
-d client_id="$CLIENT_ID"
While the person has not finished you get authorization_pending: keep waiting.
slow_down means poll less often. access_denied and expired_token are the
end of this attempt; start over. On success you get the access token and, because you
asked for offline_access, the refresh token. Store the refresh token.
The person who approves must be a member of the customer's organization. If you are
a partner, that is someone at the customer, or your own engineer whom the customer
has added. Confirm with GET /meta that the token is for the organization you meant,
as in the browser flow.
3. The refresh token by hand
For someone who wants no tooling at all. Complete one sign-in by any means (the browser flow from a laptop will do), keep the refresh token it returns, and have the unattended program exchange it for access tokens:
curl -sS "$TOKEN_ENDPOINT" \
-d grant_type=refresh_token \
-d refresh_token="$REFRESH_TOKEN" \
-d client_id="$CLIENT_ID" \
-d resource="$RESOURCE"
Clerk's refresh tokens never expire, so this runs indefinitely. Three habits make it work well:
- Send
resourceevery time. The audience of the token you get back is what the API checks, and one that does not carry ours is refused withtoken_audience_mismatch. - Cache the access token until shortly before
expires_inruns out. Exchanging a refresh token on every request makes every call a round trip to the authorization server first. - Store a new
refresh_tokenif the response carries one, and use it from then on.
Use a service user for production
For anything that matters, do not authorize an integration as a person. Authorize it as a user that exists for the purpose.
The reason is that a grant belongs to whoever signed in. If Dave authorizes the
nightly sync and Dave later leaves, the integration is left depending on someone who
is no longer there. An integration that writes acts with Dave's authority, checked
against Dave's current role on every call. When Dave is demoted, or when he is
deactivated and his access to the organization is removed, the write scopes drop out
of the effective set the next time his role is looked up, and the sync starts failing
with 403 insufficient_scope, at whatever hour it runs and with nobody watching.
A dedicated user keeps the identity institutional:
- One revocation point, and one name on the audit trail, that is not a person.
- Nobody's departure in the critical path.
- The right authority, on purpose. Writes need the Admin role, so if the integration writes, make the service user an Admin (see Scopes). A read-only integration needs no more than a regular member, though under the default policy a member's connection waits for an administrator to approve it.
Ask the customer to add the user, with an address your team controls, such as
integrations@your-company.example. Nobody should sign in as it day to day; it
exists to authorize the integration and to be the person the integration acts as.
The refresh token is the credential
Say it plainly: the refresh token is the credential, and it never expires. Anyone holding it can mint access tokens for as long as the authorization stands. Store it the way you store an SSH key: in a secrets manager, or in a file only the service account can read. Never put it in source control, an image, a log or CI output.
When it might have been exposed, do both of these, because each does a different job:
- Have an administrator revoke the application in Settings, on the API applications tab (titled Connected applications). That is what stops the application being served here, whoever holds the token. It is the revocation point the customer controls.
- Revoke the token itself at the authorization server's
revocation_endpoint(RFC 7009), so it is dead everywhere and not only here.
Deleting your copy of the file does neither. The token still exists wherever else it was copied, and it still works.