Token Management
Best practices for storing, refreshing, and managing authentication tokens
Token Management Best Practices
This guide covers production-ready token management strategies for secure and reliable authentication with the API.
Token Expiration Timeline
| Token | Lifetime | Refresh Strategy |
|---|---|---|
| Access Token | 1 hour | Refresh 5-10 minutes before expiry |
| Refresh Token | 7 days | Re-authenticate when expired |
Token Storage Strategies
Server-Side Storage (Recommended for B2B)
For server-to-server integrations, store tokens securely in your backend infrastructure:
# Store tokens in environment variables or secure files
# Never store tokens in log files or version control
# Login and store tokens
RESPONSE=$(curl -s -X POST {{host}}/auth/login \
-H "Content-Type: application/json" \
-d '{
"username": "your_api_username",
"password": "your_api_password"
}')
# Extract tokens from the response
ACCESS_TOKEN=$(echo $RESPONSE | jq -r '.data.access_token')
REFRESH_TOKEN=$(echo $RESPONSE | jq -r '.data.refresh_token')
EXPIRES_AT=$(echo $RESPONSE | jq -r '.data.access_expires_at')
# Use the access token for API calls
curl -X GET {{host}}/api/v1/products \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json"<?php
// Option 1: In-memory (simplest, for single-request scripts)
class TokenStorage {
private $accessToken;
private $refreshToken;
private $expiresAt;
public function storeTokens($tokenData) {
$this->accessToken = $tokenData['access_token'];
$this->refreshToken = $tokenData['refresh_token'];
$this->expiresAt = strtotime($tokenData['access_expires_at']);
}
public function getAccessToken() {
return $this->accessToken;
}
public function getRefreshToken() {
return $this->refreshToken;
}
public function shouldRefresh($bufferSeconds = 300) {
return (time() + $bufferSeconds) >= $this->expiresAt;
}
}
// Option 2: Redis storage (recommended for long-running applications)
class RedisTokenStorage {
private $redis;
private $prefix;
public function __construct($redis, $clientId) {
$this->redis = $redis;
$this->prefix = "api_tokens:{$clientId}";
}
public function storeTokens($tokenData) {
$this->redis->hMSet($this->prefix, [
'access_token' => $tokenData['access_token'],
'refresh_token' => $tokenData['refresh_token'],
'expires_at' => strtotime($tokenData['access_expires_at'])
]);
// Set TTL to match refresh token lifetime (7 days)
$this->redis->expire($this->prefix, 7 * 24 * 60 * 60);
}
public function getAccessToken() {
return $this->redis->hGet($this->prefix, 'access_token');
}
public function getRefreshToken() {
return $this->redis->hGet($this->prefix, 'refresh_token');
}
public function shouldRefresh($bufferSeconds = 300) {
$expiresAt = (int) $this->redis->hGet($this->prefix, 'expires_at');
return (time() + $bufferSeconds) >= $expiresAt;
}
}
?>Automatic Token Refresh
Implement proactive token refresh to avoid API interruptions. The example below is a self-contained client that handles login, refresh, and automatic retry:
# The access_expires_at field tells you when the token expires
# Parse it and refresh before expiry
# Login and capture token details
RESPONSE=$(curl -s -X POST {{host}}/auth/login \
-H "Content-Type: application/json" \
-d '{
"username": "your_api_username",
"password": "your_api_password"
}')
ACCESS_TOKEN=$(echo $RESPONSE | jq -r '.data.access_token')
REFRESH_TOKEN=$(echo $RESPONSE | jq -r '.data.refresh_token')
EXPIRES_AT=$(echo $RESPONSE | jq -r '.data.access_expires_at')
# Check if token needs refreshing
EXPIRES_EPOCH=$(date -d "$EXPIRES_AT" +%s 2>/dev/null || date -j -f "%Y-%m-%dT%H:%M:%SZ" "$EXPIRES_AT" +%s)
CURRENT_EPOCH=$(date +%s)
BUFFER=300 # 5 minutes
if [ $((EXPIRES_EPOCH - CURRENT_EPOCH)) -le $BUFFER ]; then
echo "Token expiring soon, refreshing..."
REFRESH_RESPONSE=$(curl -s -X POST {{host}}/auth/refresh \
-H "Content-Type: application/json" \
-d "{\"refresh_token\": \"$REFRESH_TOKEN\"}")
# Update stored tokens with new values
ACCESS_TOKEN=$(echo $REFRESH_RESPONSE | jq -r '.data.access_token')
REFRESH_TOKEN=$(echo $REFRESH_RESPONSE | jq -r '.data.refresh_token')
EXPIRES_AT=$(echo $REFRESH_RESPONSE | jq -r '.data.access_expires_at')
fi
# Make API call with valid token
curl -X GET {{host}}/api/v1/products \
-H "Authorization: Bearer $ACCESS_TOKEN"<?php
class APIClient {
private $host;
private $accessToken;
private $refreshToken;
private $accessExpiresAt;
private $username;
private $password;
public function __construct($host, $username, $password) {
$this->host = $host;
$this->username = $username;
$this->password = $password;
}
public function authenticate() {
$data = json_encode([
'username' => $this->username,
'password' => $this->password
]);
$ch = curl_init($this->host . '/auth/login');
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode !== 200) {
throw new Exception('Authentication failed');
}
$result = json_decode($response, true);
$this->updateTokens($result['data']);
}
public function request($url, $method = 'GET', $data = null) {
$token = $this->getValidAccessToken();
$headers = [
'Authorization: Bearer ' . $token,
'Content-Type: application/json'
];
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
if ($data) {
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
}
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
// If we get a 401, try refreshing once and retry
if ($httpCode === 401) {
$this->refresh();
return $this->request($url, $method, $data);
}
return json_decode($response, true);
}
private function getValidAccessToken() {
if ($this->shouldRefresh()) {
$this->refresh();
}
return $this->accessToken;
}
private function shouldRefresh() {
if (!$this->accessExpiresAt) return true;
// Refresh 5 minutes (300 seconds) before expiry
return (time() + 300) >= $this->accessExpiresAt;
}
private function refresh() {
if (!$this->refreshToken) {
$this->authenticate();
return;
}
$data = json_encode(['refresh_token' => $this->refreshToken]);
$ch = curl_init($this->host . '/auth/refresh');
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode === 200) {
$result = json_decode($response, true);
$this->updateTokens($result['data']);
} else {
// Refresh failed (expired or revoked), re-authenticate
$this->authenticate();
}
}
private function updateTokens($tokenData) {
$this->accessToken = $tokenData['access_token'];
$this->refreshToken = $tokenData['refresh_token'];
$this->accessExpiresAt = strtotime($tokenData['access_expires_at']);
}
}
// Usage
$api = new APIClient('{{host}}', 'your_api_username', 'your_api_password');
$api->authenticate();
// Tokens are automatically refreshed before expiry
$products = $api->request('{{host}}/api/v1/products');
$categories = $api->request('{{host}}/api/v1/categories');
?>Client-Side Token Validation
You can validate tokens locally by decoding the JWT payload to check expiration without making an API call:
# Decode JWT payload to check expiration locally
# JWT format: header.payload.signature
TOKEN="$ACCESS_TOKEN"
PAYLOAD=$(echo $TOKEN | cut -d'.' -f2)
# Add padding and decode base64
DECODED=$(echo "$PAYLOAD" | base64 -d 2>/dev/null || echo "${PAYLOAD}==" | base64 -d)
echo "Token payload:"
echo $DECODED | jq .
# Check expiration
EXP=$(echo $DECODED | jq -r '.exp')
NOW=$(date +%s)
if [ $NOW -ge $EXP ]; then
echo "Token is expired"
else
REMAINING=$((EXP - NOW))
echo "Token valid for $REMAINING more seconds"
fi<?php
class TokenValidator {
public static function isTokenExpired($token) {
if (empty($token)) return true;
$parts = explode('.', $token);
if (count($parts) !== 3) return true;
try {
$payload = json_decode(
base64_decode(str_replace(['-', '_'], ['+', '/'], $parts[1])),
true
);
return $payload['exp'] < time();
} catch (Exception $e) {
return true;
}
}
public static function getTimeUntilExpiry($token) {
if (empty($token)) return 0;
$parts = explode('.', $token);
if (count($parts) !== 3) return 0;
try {
$payload = json_decode(
base64_decode(str_replace(['-', '_'], ['+', '/'], $parts[1])),
true
);
return max(0, $payload['exp'] - time());
} catch (Exception $e) {
return 0;
}
}
public static function shouldRefresh($token, $bufferMinutes = 5) {
$remaining = self::getTimeUntilExpiry($token);
return $remaining <= ($bufferMinutes * 60);
}
}
// Usage
if (TokenValidator::shouldRefresh($accessToken)) {
echo "Token should be refreshed soon\n";
}
$remaining = TokenValidator::getTimeUntilExpiry($accessToken);
echo "Token valid for " . round($remaining / 60) . " more minutes\n";
?>Error Recovery Patterns
Handling 401 Responses
When an API call returns 401, attempt a token refresh before retrying:
- Receive 401 on an API call
- Attempt to refresh the token using
/auth/refresh - If refresh succeeds, retry the original request with the new token
- If refresh fails (401), re-authenticate with
/auth/login - If login also fails, surface the error to the caller
Handling 403 Responses
A 403 response indicates the request IP is not in the client's whitelist. This cannot be resolved by refreshing tokens — contact your account administrator to update the IP whitelist.
Best Practices
- Track expiration times: Parse
access_expires_atfrom login/refresh responses and refresh proactively - Refresh before expiry: Refresh 5-10 minutes before the access token expires
- Handle refresh failures: If the refresh token is also expired, fall back to full re-authentication
- Use token rotation: Always store the new refresh token from each refresh response — the old one is revoked
- Avoid concurrent refreshes: If multiple threads/processes share tokens, use locking to prevent simultaneous refresh attempts
- Store tokens securely: Use encrypted storage (database, Redis, vault) — never log token values
- Single retry on 401: Attempt one refresh + retry cycle per request, then re-authenticate
For credential storage and security patterns, see Security Considerations.