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 Starship Rewards API.
Token Storage Strategies
Web Applications (Browser)
# Secure token storage patterns with cURL
# Always use HTTPS in production
# Login and receive tokens via secure cookies
curl -X POST {{host}}/auth/login \
-H "Content-Type: application/json" \
-c cookies.txt \
-d '{
"email": "user@example.com",
"password": "secure_password"
}'
# Server sets secure HttpOnly cookies:
# Set-Cookie: access_token=...; HttpOnly; Secure; SameSite=Strict; Max-Age=3600
# Set-Cookie: refresh_token=...; HttpOnly; Secure; SameSite=Strict; Max-Age=2592000
# Use tokens from cookies for authenticated requests
curl -X GET {{host}}/api/protected-endpoint \
-b cookies.txt \
-H "Content-Type: application/json"
# Alternative: Use Authorization header with access token
curl -X GET {{host}}/api/protected-endpoint \
-H "Authorization: Bearer your_access_token_here" \
-H "Content-Type: application/json"<?php
// Store tokens in secure HttpOnly cookies (recommended for web apps)
class TokenManager {
public function setTokens($accessToken, $refreshToken) {
// Store access token in memory/session
$_SESSION['access_token'] = $accessToken;
// Store refresh token in HttpOnly cookie
setcookie('refresh_token', $refreshToken, [
'expires' => time() + (30 * 24 * 60 * 60), // 30 days
'httponly' => true,
'secure' => true,
'samesite' => 'Strict'
]);
}
public function getAccessToken() {
return $_SESSION['access_token'] ?? null;
}
public function getRefreshToken() {
return $_COOKIE['refresh_token'] ?? null;
}
public function clearTokens() {
unset($_SESSION['access_token']);
setcookie('refresh_token', '', [
'expires' => time() - 3600,
'httponly' => true,
'secure' => true
]);
}
}
?>Automatic Token Refresh
Proactive Refresh (Recommended)
# Automatic token refresh with cURL
# Initial login to get tokens
curl -X POST {{host}}/auth/login \
-H "Content-Type: application/json" \
-c cookies.txt \
-d '{
"email": "user@example.com",
"password": "secure_password"
}'
# Check if tokens need refreshing before API calls
# Server responds with token expiration info
curl -X GET {{host}}/auth/token-status \
-b cookies.txt
# Response:
# {
# "expires_in": 295,
# "should_refresh": true,
# "expires_at": "2024-01-15T10:35:00Z"
# }
# Refresh tokens when needed (5 minutes before expiration)
curl -X POST {{host}}/auth/refresh \
-H "Content-Type: application/json" \
-b cookies.txt \
-c cookies.txt \
-d '{
"refresh_token": "stored_refresh_token"
}'
# New tokens are set in cookies automatically
# Use refreshed tokens for API calls
curl -X GET {{host}}/api/protected-endpoint \
-b cookies.txt<?php
class TokenManager {
private $accessToken;
private $refreshToken;
private $expirationTime;
public function setTokens($accessToken, $refreshToken, $expiresIn) {
$this->accessToken = $accessToken;
$this->refreshToken = $refreshToken;
$this->expirationTime = time() + $expiresIn;
// Store in session
$_SESSION['access_token'] = $accessToken;
$_SESSION['token_expires_at'] = $this->expirationTime;
}
public function shouldRefreshToken($bufferSeconds = 300) {
$currentTime = time();
$expirationTime = $_SESSION['token_expires_at'] ?? 0;
return ($expirationTime - $currentTime) <= $bufferSeconds;
}
public function refreshAccessToken() {
$refreshToken = $_COOKIE['refresh_token'] ?? null;
if (!$refreshToken) {
throw new Exception('No refresh token available');
}
$data = json_encode(['refresh_token' => $refreshToken]);
$context = stream_context_create([
'http' => [
'method' => 'POST',
'header' => 'Content-Type: application/json',
'content' => $data
]
]);
$response = file_get_contents('{{host}}/auth/refresh', false, $context);
if ($response === FALSE) {
throw new Exception('Token refresh failed');
}
$tokenData = json_decode($response, true);
$this->setTokens(
$tokenData['access_token'],
$tokenData['refresh_token'] ?? $refreshToken,
$tokenData['expires_in']
);
return $tokenData;
}
public function getValidAccessToken() {
if ($this->shouldRefreshToken()) {
$this->refreshAccessToken();
}
return $_SESSION['access_token'] ?? null;
}
}
?>Token Validation
Client-side token validation helps prevent unnecessary API calls:
# Client-side token validation with cURL
# Decode JWT token payload (requires jq and base64)
# Note: This is for demonstration - normally done server-side
# Extract and decode JWT payload
TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiZXhwIjoxNjQyMjQ4MDAwfQ.signature"
PAYLOAD=$(echo $TOKEN | cut -d'.' -f2)
# Decode base64 payload (add padding if needed)
echo $PAYLOAD | base64 -d 2>/dev/null || echo "${PAYLOAD}===" | base64 -d
# Server-side token validation endpoint
curl -X POST {{host}}/auth/validate-token \
-H "Content-Type: application/json" \
-d '{
"token": "your_access_token_here"
}'
# Response:
# {
# "valid": true,
# "expired": false,
# "expires_at": "2024-01-15T10:30:00Z",
# "should_refresh": true,
# "time_to_expiry": 295
# }
# Check token status before making API calls
curl -X GET {{host}}/auth/token-info \
-H "Authorization: Bearer your_access_token"<?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);
$currentTime = time();
return $payload['exp'] < $currentTime;
} catch (Exception $e) {
return true; // Invalid token format
}
}
public static function shouldRefreshToken($token, $bufferMinutes = 5) {
$expirationTime = self::getTokenExpirationTime($token);
if ($expirationTime === null) return true;
$currentTime = time() * 1000; // Convert to milliseconds
$bufferTime = $bufferMinutes * 60 * 1000;
return ($expirationTime - $currentTime) <= $bufferTime;
}
public static function getTokenExpirationTime($token) {
if (empty($token)) return null;
$parts = explode('.', $token);
if (count($parts) !== 3) return null;
try {
$payload = json_decode(base64_decode(str_replace(['_', '-'], ['/', '+'], $parts[1])), true);
return $payload['exp'] * 1000; // Convert to milliseconds
} catch (Exception $e) {
return null;
}
}
}
?>Error Handling
Implement comprehensive error handling for authentication failures:
# Error handling and retry logic with cURL
# Function to handle authentication errors
handle_auth_error() {
local status_code=$1
local response=$2
case $status_code in
401)
echo "Token expired or invalid - attempting refresh"
# Attempt token refresh
curl -X POST {{host}}/auth/refresh \
-b cookies.txt -c cookies.txt \
-H "Content-Type: application/json"
if [ $? -eq 0 ]; then
echo "Token refresh successful - retrying original request"
return 0 # Success
else
echo "Token refresh failed - please log in again"
return 1 # Failure
fi
;;
403)
echo "Access denied - insufficient permissions"
return 1
;;
429)
# Extract retry-after header if available
retry_after=$(echo "$response" | grep -i "retry-after" | cut -d':' -f2 | tr -d ' ')
echo "Rate limit exceeded - try again in ${retry_after:-60} seconds"
return 1
;;
*)
echo "Request failed with status: $status_code"
return 1
;;
esac
}
# Example usage with error handling
response=$(curl -s -w "\n%{http_code}" {{host}}/api/protected \
-b cookies.txt)
status_code=$(echo "$response" | tail -n1)
body=$(echo "$response" | head -n -1)
if [ "$status_code" -eq 200 ]; then
echo "Success: $body"
else
handle_auth_error "$status_code" "$body"
fi<?php
class AuthenticationManager {
private $tokenManager;
public function __construct($tokenManager) {
$this->tokenManager = $tokenManager;
}
public function handleAuthError($httpCode, $response, $originalUrl, $originalData = null) {
switch ($httpCode) {
case 401:
// Unauthorized - token invalid/expired
if ($this->attemptTokenRefresh()) {
return $this->retryRequest($originalUrl, $originalData);
} else {
$this->initiateLogin();
throw new Exception('Please log in again');
}
case 403:
// Forbidden - valid token but insufficient permissions
throw new Exception('Access denied. Contact support if this is unexpected.');
case 429:
// Rate limited
$retryAfter = $this->getRetryAfterFromHeaders($response) ?: 60;
throw new Exception("Rate limit exceeded. Try again in {$retryAfter} seconds.");
default:
throw new Exception("Request failed with status: {$httpCode}");
}
}
public function attemptTokenRefresh() {
try {
$this->tokenManager->refreshAccessToken();
return true;
} catch (Exception $e) {
error_log('Token refresh failed: ' . $e->getMessage());
return false;
}
}
}
?>Summary
- Store tokens securely: Use HttpOnly cookies for web apps, OS keychain for native apps
- Implement proactive refresh: Refresh tokens before they expire to avoid API interruptions
- Validate tokens client-side: Check expiration before making API calls
- Handle errors gracefully: Implement retry logic and fallback to login when necessary
- Never store sensitive tokens in localStorage: This makes them vulnerable to XSS attacks
For more advanced topics, see Session Management and Security Considerations.