Auth Bearer Token Caching
Reusing client_credential bearer tokens for the token's full lifetime.
OAuth 2.0 Bearer Token Caching Best Practices
This guide covers best practices for caching and reusing OAuth 2.0 bearer tokens when integrating with Snapdocs APIs. Implementing token caching reduces latency, minimizes load on the authorization server, and improves the overall reliability of your integration.
Overview
Snapdocs APIs use OAuth 2.0 with the client_credentials grant type. When you request a token, the response includes an expires_in field indicating how long the token remains valid (typically 2 hours / 7200 seconds). By caching the token and reusing it for its full lifetime, your application avoids the overhead of fetching a new token on every API call.
Benefits of Token Caching
| Concern | Without Caching | With Caching |
|---|---|---|
| Latency per API call | +200–500ms (token fetch round-trip) | ~0ms (in-memory or cache lookup) |
| Auth server load | 1 token request per API call | 1 token request per ~2 hours |
| Rate limit risk | Higher — more requests to auth server | Negligible |
| Reliability | Additional point of failure per call | Token fetch failure is isolated |
How Token Caching Works
The following diagram illustrates the recommended flow. Your application checks for a cached token before each API call and only requests a new one when the cache is empty or the token has expired.
sequenceDiagram participant Client as Your Application participant Cache as Token Cache (Redis/Memory) participant Auth as Auth0 (Token Endpoint) participant API as Snapdocs API Client->>Cache: Check for cached token Cache-->>Client: Cache MISS Client->>Auth: POST /oauth/token (client_credentials) Auth-->>Client: { access_token, expires_in: 7200 } Client->>Cache: Store token (TTL = expires_in - 60s buffer) Client->>API: API request with Bearer token API-->>Client: 200 OK Note over Client,Cache: Subsequent API call (within token lifetime) Client->>Cache: Check for cached token Cache-->>Client: Cache HIT ✅ Client->>API: API request with Bearer token API-->>Client: 200 OK Note over Client,Auth: Token fetched only when cache is empty or expired
Token Lifecycle Decision Flow
Use this decision diagram to determine when your application should fetch a new token vs. reuse the cached one.
flowchart TD A[API Call Needed] --> B{Cached token exists?} B -- No --> C[Fetch new token from Auth0] C --> D[Cache token with TTL = expires_in - 60s] D --> E[Make API call with Bearer token] B -- Yes --> F{Token expired or near expiry?} F -- No --> E F -- Yes --> C E --> G{Response 401 Unauthorized?} G -- No --> H[Process response] G -- Yes --> I[Invalidate cached token] I --> C
Implementation Guide
Step 1: Store the Token with Expiration Metadata
When you receive a token response from Auth0, store both the access_token and its calculated expiration time.
We recommend subtracting a buffer (e.g., 60 seconds) from the expires_in value, which is in seconds. This ensures your application refreshes the token before it actually expires, avoiding failed API calls due to clock skew or in-flight request timing.
A typical Auth0 token response:
{ "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...", "token_type": "Bearer", "expires_in": 7200 }
Step 2: Check Cache Before Every API Call
Before making any API call, check your cache for a valid token. Only fetch a new one if the cache is empty or the token has expired.
Step 3: Handle 401 Responses Gracefully
If the API returns a 401 Unauthorized, the token may have been revoked or is otherwise invalid. Invalidate your cached token, fetch a new one, and retry the request once.
Code Examples
using System;
using System.Net.Http;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
public class SnapdocsTokenManager
{
private const int BufferSeconds = 60;
private readonly string _clientId;
private readonly string _clientSecret;
private readonly string _audience;
private readonly string _tokenUrl;
private string _accessToken;
private DateTime _expiresAt = DateTime.MinValue;
private readonly SemaphoreSlim _semaphore = new(1, 1);
private static readonly HttpClient _httpClient = new();
public SnapdocsTokenManager(string clientId, string clientSecret,
string audience, string tokenUrl)
{
_clientId = clientId;
_clientSecret = clientSecret;
_audience = audience;
_tokenUrl = tokenUrl;
}
public async Task<string> GetTokenAsync()
{
if (IsTokenValid()) return _accessToken;
await _semaphore.WaitAsync();
try
{
if (IsTokenValid()) return _accessToken;
await FetchNewTokenAsync();
return _accessToken;
}
finally
{
_semaphore.Release();
}
}
public void Invalidate()
{
_accessToken = null;
_expiresAt = DateTime.MinValue;
}
private bool IsTokenValid() =>
_accessToken != null && DateTime.UtcNow < _expiresAt;
private async Task FetchNewTokenAsync()
{
var payload = new
{
client_id = _clientId,
client_secret = _clientSecret,
audience = _audience,
grant_type = "client_credentials"
};
var content = new StringContent(
JsonSerializer.Serialize(payload),
Encoding.UTF8, "application/json");
var response = await _httpClient.PostAsync(_tokenUrl, content);
response.EnsureSuccessStatusCode();
var json = JsonDocument.Parse(await response.Content.ReadAsStringAsync());
_accessToken = json.RootElement.GetProperty("access_token").GetString();
var expiresIn = json.RootElement.TryGetProperty("expires_in", out var exp)
? exp.GetInt32() : 7200;
_expiresAt = DateTime.UtcNow.AddSeconds(expiresIn - BufferSeconds);
}
}Choosing a Cache Backend
Any of the following approaches will work. Choose the one that best fits your existing infrastructure:
| Priority | Approach | Best For | Trade-offs |
|---|---|---|---|
| 1️⃣ | Redis / Memcached | Multi-instance or distributed services | Shared across all instances; requires cache infrastructure |
| 2️⃣ | In-memory (singleton) | Single-instance apps, serverless with warm starts | Simple to implement; lost on restart; not shared across instances |
| 3️⃣ | Database | When neither Redis nor in-memory caching is available | Works with any existing database; slightly higher latency per lookup |
If your application already has Redis or Memcached, that's the ideal choice — the token is shared across all instances and survives restarts. If not, an in-memory singleton is the simplest option and works well for single-instance apps. If neither is readily available, storing the token in a database table with an expiration timestamp is a perfectly valid approach — the small overhead of a database read is far less than fetching a new token from Auth0 on every call.
Multi-Service / Distributed Architecture
If your integration spans multiple services that each call Snapdocs APIs, consider a centralized token service:
flowchart LR subgraph Your Infrastructure SA[Service A] --> TS[Token Service / Cache] SB[Service B] --> TS SC[Service C] --> TS TS --- Redis[(Redis / Shared Cache)] end TS -->|Fetch on cache miss| Auth[Auth0 Token Endpoint] SA -->|Bearer Token| API[Snapdocs API] SB -->|Bearer Token| API SC -->|Bearer Token| API
Estimated Implementation Timeline
| Task | Estimate |
|---|---|
Implement TokenManager class with in-memory cache | 1–2 hours |
Replace direct token fetch calls with TokenManager | 1–2 hours |
| Add 401 retry logic with token invalidation | 1 hour |
| Unit tests for caching, expiry, and thread safety | 2–3 hours |
| Integration / end-to-end validation | 1–2 hours |
| Total | ~1 day |
Token caching is a client-side only change. No modifications are needed on the Snapdocs side. The change is isolated to how your application manages its bearer token — wrap your existing token-fetch logic in a caching layer and reuse the token for its full lifetime.
Summary
- Fetch a token once from Auth0 using the
client_credentialsgrant - Cache the token with a TTL of
expires_in - 60 seconds(buffer for clock skew) - Reuse the cached token for all API calls within the TTL window
- On 401 response, invalidate the cache and fetch a new token
- Thread safety — use a mutex/lock to prevent concurrent token fetches