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

Authentication

Developer

Callers authenticate with an OAuth 2.0 access token, obtained through the authorization-code flow with PKCE. This page covers the whole of it: where to sign in, what to send, what a token is and is not, why a valid token can still be refused, how access ends, and how to send PATCH, PUT and DELETE through a network that blocks them.

If your code has no browser, read this page for the vocabulary and then Using the API without a browser for the practice.

Who does what

  • The authorization server (Clerk, our identity provider) runs the sign-in and the consent screen and issues and refreshes tokens. We do not issue tokens, keep client secrets, or see a password.
  • This API is the resource server. It verifies each token with the authorization server and then, separately, checks that the customer has let your application in. The second check reads the customer's current decision on every request.
  • Your application is a public client. It has no client secret to protect; PKCE is what protects the code exchange.

1. Discover where to sign in

Two documents tell a client everything it needs, so nothing about the authorization server has to be hard-coded.

The API's protected-resource metadata (RFC 9728), unauthenticated:

curl -sS https://acme.example.com/.well-known/oauth-protected-resource/api/public/v1

It gives resource (the API's canonical address), authorization_servers, scopes_supported and bearer_methods_supported. The address is the host, then /.well-known/oauth-protected-resource, then the API's own path, because the standard puts the well-known segment between the host and the path. It is not under /api/public/v1, and a request for it there is a 404.

An unauthenticated request to a route points you at it:

HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer resource_metadata="https://acme.example.com/.well-known/oauth-protected-resource/api/public/v1"
X-Tstr-Api-Version: v1

The authorization server's own metadata (RFC 8414). Take an entry of authorization_servers and fetch <issuer>/.well-known/oauth-authorization-server. It names the authorization_endpoint, the token_endpoint, the revocation_endpoint and, for callers with no browser, the device_authorization_endpoint. Read the endpoints from it rather than writing them down.

2. Identify your application

There is no registration step and no client secret. Your application's client_id is an HTTPS URL, and that URL serves a JSON document describing the application (a client ID metadata document). The authorization server fetches it when someone signs in and shows what it finds on the consent screen. The authorization server advertises support as client_id_metadata_document_supported.

  • Publish the document somewhere stable that you control. The URL is your application's identity, so changing it makes you a new application to every customer who approved the old one. The document's fields are the authorization server's to define; follow its OAuth applications documentation.
  • Do not depend on dynamic client registration. It is not the supported path, and you should not wait for a registration_endpoint to appear.
  • Token requests from your application carry the client_id and no secret.
  • A grant is recorded per client_id in each organization. Two copies of your application that share a client_id and an organization share one grant: approving or revoking one approves or revokes both.
  • A platform that cannot use a URL as its client identity needs a client that is registered for it in advance, which is arranged with us and not done by the application itself.

3. Ask the person to sign in

Send the person's browser to the authorization_endpoint with these parameters:

ParameterValue
response_typecode
client_idYour metadata document's URL.
redirect_uriA redirect address registered in your metadata document.
scopeSpace-separated: the scopes you need from Scopes, plus user:org:read and offline_access.
code_challenge, code_challenge_methodA PKCE challenge. The method is S256; it is the only one offered.
stateA fresh random value. Check it when the person comes back.
resourceThe resource value from the metadata document, exactly as written.

Three of these are the ones that get left out.

user:org:read. A person can belong to several organizations, and a token is for exactly one of them. This scope is what puts the organization selector on the consent screen. Without it the organization your token ends up bound to is not the person's deliberate choice, and a wrong choice looks like tenant_mismatch later. It is Clerk's own scope, not one of ours, and it is not optional in this flow. Our own scopes are listed in Scopes.

offline_access. Without it the authorization server issues no refresh token, and an integration that has to run unattended has nothing to run on.

resource. It becomes the token's audience, and the API accepts only a token whose audience is its own address. Send it on the authorization request and on every token request, the code exchange and each refresh. A token issued without it, or with a different value (a trailing slash added or removed included), is refused with 401 token_audience_mismatch.

The person who signs in must be a member of the customer's organization: the selector lists the organizations they belong to. Ask for every scope you will ever need now; what an administrator approves is the set you held on your first connection (see Scopes). Authorization servers that support RFC 9207 add the issuer to the redirect back to you (iss); Clerk does, and checking that it is the issuer you expect before you exchange the code costs one comparison.

4. Exchange the code

curl -sS "$TOKEN_ENDPOINT" \
  -d grant_type=authorization_code \
  -d code="$CODE" \
  -d redirect_uri="$REDIRECT_URI" \
  -d client_id="$CLIENT_ID" \
  -d code_verifier="$CODE_VERIFIER" \
  -d resource="$RESOURCE"

The response carries access_token, expires_in and, because you asked for offline_access, a refresh_token.

5. Call the API

GET /api/public/v1/meta HTTP/1.1
Authorization: Bearer <access_token>

The token goes in the Authorization header and nowhere else: not in the query string, not in a body. The metadata document says so as bearer_methods_supported.

Tokens are opaque

Access tokens are opaque strings (today they begin oat_). Your client must not parse, decode or inspect one: there is nothing in it to read, its prefix and length are not part of any contract, and code that reaches inside will break the day they change. Store it as a secret and send it back. What you can learn about a credential, you learn by calling GET /meta.

An access token lasts a day. Do not hard-code that; use expires_in from the response. When it is about to lapse, or the API answers 401 invalid_token, use the refresh token:

curl -sS "$TOKEN_ENDPOINT" \
  -d grant_type=refresh_token \
  -d refresh_token="$REFRESH_TOKEN" \
  -d client_id="$CLIENT_ID" \
  -d resource="$RESOURCE"

Refresh tokens do not expire. If a refresh response carries a new refresh_token, store it and use it next time. A refresh that the authorization server refuses outright, as opposed to one that failed on the network, means the authorization is over and the person has to sign in again.

One organization per token

A token carries one organization. If it is not the organization this tenant belongs to, the API answers 403 with type: tenant_mismatch, and the body never names the other organization. Either the wrong organization was chosen on the consent screen (sign in again and choose the right one) or the request went to another tenant's address (use that organization's). It is a 403 and not a 401 on purpose: signing in again against the same wrong address would only fail the same way.

A successful GET /meta is the positive form of the same check.

A valid token is not access

The customer decides which applications may use their account. Clerk's consent screen can be reached by any member of the organization, and we cannot add a step to it, so the API keeps its own record of every application and serves only those that the organization's access policy or an administrator has let in.

The first request from an application the organization has not seen creates that record, in the state the access policy dictates, and that first request is answered with no data unless the policy lets the application straight in. Under the default policy an administrator's own sign-in is approved at once and anyone else's waits. The answers an application can get are the four governance 403s in Conventions, and each tells you a different person to talk to.

None of them is a 401, because the token is genuine and signing in again changes nothing. And a pending application is not stuck: once an administrator approves it, the same token is served, with no new sign-in.

How access ends and changes

Several things end or reduce an application's access. None of them is "the" way, and they do not all work alike.

An administrator revokes the application. In Settings, on the Connected applications tab, an administrator can revoke it, with a recorded reason, and from that moment the application is not served. A revocation is remembered. The application's next request is answered as though it were new, with one difference: nothing admits it automatically. Where a new application would have been approved at once, because an administrator signed in or because the organization lets anyone connect, a revoked one is held for approval again instead. That is a 403 grant_pending_approval, not a refusal outright, and the application appears in the administrator's list under Waiting for approval. Undoing a revoke is as deliberate as making one: an administrator has to approve it a second time, after which the same token is served again.

What revoking does not do is cancel the token at the authorization server. The token is still valid there and a refresh token never expires, so a refresh still succeeds. A successful refresh is not evidence of access; the API's answer is. Treat a revocation as the customer telling you to stop, and do not keep retrying. If you want your tokens dead everywhere, revoke them yourself at the authorization server's revocation_endpoint (RFC 7009); you hold them, so you can. The API remembers a successful check of a token for a short time (currently up to 60 seconds), so a token revoked there can keep being accepted that long.

The organization switches third-party access off. Every application is refused at once, ones already approved included, and nothing is revoked or deleted. When access is turned back on, approved applications work again as they were.

The person who authorized it loses their standing. An application acts with the authority of whoever signed in, checked against that person's current role in the organization. If they are demoted, write scopes leave the effective set the next time the role is looked up. If they leave and their access is removed, the credential belongs to someone who is no longer there. See the service-user advice in Using the API without a browser.

Tokens lapse. An access token expires after a day; the refresh token behind it does not.

Sending PATCH, PUT and DELETE

Send native verbs. They work at our edge, and they are the default.

Some corporate networks intercept PATCH, PUT and DELETE, answer them with a timeout, or keep the request and lose its body. For those, every route also accepts a POST that carries the intended verb in a header:

POST /api/public/v1/{route} HTTP/1.1
Tstr-Method: PATCH
Authorization: Bearer <access_token>
Content-Type: application/json

{ "...": "..." }

The URL, body and other headers are exactly what the native request would have carried. Tstr-Method takes PATCH, PUT or DELETE (any capitalization) and nothing else: it rewrites to PATCH, PUT and DELETE only. There is no way to send a GET as a POST. A POST with Tstr-Method: GET is not a GET; it is a request for a POST route, and where there is none it gets 404 unknown_route. Only a POST is rewritten; the header on any other method is ignored.

The two conventional channels below are not available, and how they fail is the part to read carefully:

ChannelIn the applicationIn production, at our edge
Native PATCH, PUT, DELETEWorks.Works. The default.
POST with Tstr-MethodWorks.Works. Use it behind a network that blocks the native verbs.
POST with X-HTTP-Method-OverrideStill works. The application honours this older header.Rejected before it reaches the API by the firewall in front of it (rule 920450).
POST with ?_method=PATCH in the query stringNot implemented: the request stays a POST, so a route with no POST handler answers 404 unknown_route.Rejected by the firewall as well (rule 920650).

X-HTTP-Method-Override dies at the edge; the application is not what stops it. That is exactly why it is a trap: against a local instance, or any deployment without the firewall, it appears to work, and then it fails only in production. Test against the real address, and use Tstr-Method. A request stopped at the edge never reaches the API, so what comes back is not the error format described in Conventions and carries no X-Tstr-Api-Version header.

If something is wrong

You seeUsuallyFix
401 token_audience_mismatchresource was missing, or is not the metadata document's resource character for character.Request a token with the exact resource, on the authorization request and the token request.
401 invalid_tokenThe access token expired, or the authorization it came from was ended.Refresh. If the refresh is refused, sign in again.
401 authentication_requiredNo Authorization: Bearer ... header, or a malformed one.Send the header. The scheme name is not case-sensitive.
403 tenant_mismatchThe token is for another organization.Sign in again and choose the right organization, or use that organization's address.
403 insufficient_scopeThe credential lacks a scope this route needs.The WWW-Authenticate header lists every scope the route needs. See Scopes for why a scope you asked for may be missing.
403 with one of the four governance typesThe customer's side has to act.See Conventions.
503 authorization_unavailableWe could not check authorization just now.Retry shortly. Do not sign in again.
503 public_api_not_configuredThis deployment has not been set up to serve the API.Retrying does not help; tell the customer.