Free live webinar

Same team, more pipeline: AI-powered marketing for Dynamics 365 · Wed 11 Nov · 1:00pm ET · with Microsoft MVP Mark SmithSame team, more pipeline · 11 Nov

Save my seat

Using the API without a browser

Developer

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 login opens 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 token prints 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 baffling 401. Run scripts with set -e, or check the exit status.
  • ts auth whoami shows what the credential can do here, and ts auth logout forgets it on this machine.
  • In CI, where there is no file to keep, set TS_REFRESH_TOKEN and TS_TENANT_URL in the environment and ts uses 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 resource every time. The audience of the token you get back is what the API checks, and one that does not carry ours is refused with token_audience_mismatch.
  • Cache the access token until shortly before expires_in runs out. Exchanging a refresh token on every request makes every call a round trip to the authorization server first.
  • Store a new refresh_token if 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:

  1. 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.
  2. 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.