Token lifecycle
Access token expiry, refresh tokens and what to store for each connection.
Conventions
$API_URLstands for theapi_urlof the connected license, received during the authorization flow. Values in capitals, such asYOUR_CLIENT_ID, are placeholders.
Access tokens
Access tokens are Bearer tokens, sent in the Authorization header of every request. They expire 1 hour after they are issued (expires_in: 3600, in seconds).
Refresh tokens
Every access token is issued together with a refresh token. When the access token expires, or shortly before, use the refresh token to obtain a new pair of access token and refresh token, without involving the user.
POST {api_url}/oauth2/access_token
Content-Type: application/x-www-form-urlencoded
| Parameter | Description |
|---|---|
client_id | Your integration's client ID. |
client_secret | Your integration's client secret. |
refresh_token | The current refresh token. |
grant_type | Must be refresh_token. |
curl -X POST "$API_URL/oauth2/access_token" \
-H "Content-Type: application/x-www-form-urlencoded" \
--data-urlencode "client_id=YOUR_CLIENT_ID" \
--data-urlencode "client_secret=YOUR_CLIENT_SECRET" \
--data-urlencode "refresh_token=8xLOxBtZp8r2tN3kq..." \
--data-urlencode "grant_type=refresh_token"The response has the same format as the initial token exchange and contains a new access_token and a new refresh_token.
Refresh tokens are single-useA refresh token can be used only once. Always save the new refresh token returned with the new access token, replacing the previous one. If you lose it, the user has to authorize your integration again.
Refresh tokens remain valid until the user revokes them or uninstalls your integration. After that, the user has to go through the authorization flow again.
What to store for each connection
Store, on your server and for each connected license:
| Value | Why |
|---|---|
api_url | Base URL for every request and token refresh of this license. |
access_token | Current Bearer token. |
refresh_token | Needed to obtain the next token pair. |
| Expiry time | Computed from expires_in, to refresh before the token expires. |
Treat tokens like passwords: encrypt them at rest and never write them to logs.
Recommended practices
- Refresh proactively, a few minutes before the expiry time, rather than waiting for a
401response. - On a
401 Unauthorizedresponse, refresh once and retry the request. If the refresh fails, mark the connection as disconnected and ask the user to authorize again. - Serialise refreshes per connection, with a lock or a queue. Two processes refreshing at the same time with the same refresh token will make one of them fail, since the token can only be used once.
- Save the new token pair before using the new access token, so that a crash cannot lose the only valid refresh token.
// C#: refresh a connection's tokens (call under a per-connection lock)
var response = await http.PostAsync($"{connection.ApiUrl}/oauth2/access_token",
new FormUrlEncodedContent(new Dictionary<string, string>
{
["client_id"] = clientId,
["client_secret"] = clientSecret,
["refresh_token"] = connection.RefreshToken,
["grant_type"] = "refresh_token"
}));
response.EnsureSuccessStatusCode();
var tokens = await response.Content.ReadFromJsonAsync<TokenResponse>();
connection.AccessToken = tokens.AccessToken;
connection.RefreshToken = tokens.RefreshToken;
connection.ExpiresAt = DateTimeOffset.UtcNow.AddSeconds(tokens.ExpiresIn);
await repository.SaveAsync(connection); // persist before using the new token
record TokenResponse(
[property: JsonPropertyName("access_token")] string AccessToken,
[property: JsonPropertyName("refresh_token")] string RefreshToken,
[property: JsonPropertyName("expires_in")] int ExpiresIn);Updated about 1 hour ago