Authentication
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_endpointto appear. - Token requests from your application carry the
client_idand no secret. - A grant is recorded per
client_idin each organization. Two copies of your application that share aclient_idand 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:
| Parameter | Value |
|---|---|
response_type | code |
client_id | Your metadata document's URL. |
redirect_uri | A redirect address registered in your metadata document. |
scope | Space-separated: the scopes you need from Scopes, plus user:org:read and offline_access. |
code_challenge, code_challenge_method | A PKCE challenge. The method is S256; it is the only one offered. |
state | A fresh random value. Check it when the person comes back. |
resource | The 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:
| Channel | In the application | In production, at our edge |
|---|---|---|
Native PATCH, PUT, DELETE | Works. | Works. The default. |
POST with Tstr-Method | Works. | Works. Use it behind a network that blocks the native verbs. |
POST with X-HTTP-Method-Override | Still 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 string | Not 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 see | Usually | Fix |
|---|---|---|
401 token_audience_mismatch | resource 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_token | The access token expired, or the authorization it came from was ended. | Refresh. If the refresh is refused, sign in again. |
401 authentication_required | No Authorization: Bearer ... header, or a malformed one. | Send the header. The scheme name is not case-sensitive. |
403 tenant_mismatch | The token is for another organization. | Sign in again and choose the right organization, or use that organization's address. |
403 insufficient_scope | The 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 types | The customer's side has to act. | See Conventions. |
503 authorization_unavailable | We could not check authorization just now. | Retry shortly. Do not sign in again. |
503 public_api_not_configured | This deployment has not been set up to serve the API. | Retrying does not help; tell the customer. |
Related guides
- Getting started
- Scopes
- Conventions
- Using the API without a browser
- Connected applications: the administrator's side of approval and revocation