Exchanges a valid refresh token for a new JWT access token and refresh token pair. This endpoint implements token rotation: the old refresh token is revoked and a new one is issued.
Endpoint
Rate Limits
No explicit rate limit configured (subject to global API rate limits).
This endpoint does not have the same aggressive rate limiting as /auth/start and /auth/exchange because refresh tokens are cryptographically secure and single-use.
Request Body
Refresh token obtained from /auth/exchange or a previous /auth/refresh call. Format : 48-byte base64url-encoded random string (64 characters)Validation : Minimum 10 charactersSecurity : The token is hashed (SHA-256) before database lookup. Only the hash is stored server-side.Expiration : Default 30 days (configurable via REFRESH_TTL_DAYS)
Response
Returns a complete token pair with the same structure as POST /v1/auth/exchange .
New JWT access token for authenticating API requests. Format : JSON Web Token (JWT) signed with HS256TTL : Default 15 minutes (configurable via JWT_ACCESS_TTL_MINUTES)Usage : Include in Authorization: Bearer <accessToken> header
New opaque refresh token. IMPORTANT : The old refresh token is now revoked. Format : 48-byte base64url-encoded random string (64 characters)TTL : Default 30 days from nowStorage : Replace the old refresh token with this new one
Access token lifetime in seconds. Default : 900 (15 minutes)
Authenticated user profile information. User’s verified email address.
Slack user ID (e.g., U0123456789).
Slack workspace/team ID (e.g., T0123456789).
Example Request
curl -X POST https://api.rs-tunnel.example.com/v1/auth/refresh \
-H "Content-Type: application/json" \
-d '{
"refreshToken": "mB92K27uhbUJU1p1r_wW1gFWFOEjXk_dBjftJeZ4CVP-Ab3kDf9mPqRsTuVwXyZ"
}'
Example Response
{
"accessToken" : "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI1NWM3YzQzZS1hMzJhLTRiZGEtOGU4Yi0zZjJjNmE5ZTEyM2UiLCJlbWFpbCI6ImFsaWNlQGV4YW1wbGUuY29tIiwic2xhY2tVc2VySWQiOiJVMDEyMzQ1Njc4OSIsInNsYWNrVGVhbUlkIjoiVDAxMjM0NTY3ODkiLCJpYXQiOjE3MDkwNDEwMDAsImV4cCI6MTcwOTA0MTkwMH0.7xY2zA1bC3dE5fG7hI9jK8fJ3kLmN5pQ9rS2tU4vW6" ,
"refreshToken" : "Ab3kDf9mPqRsTuVwXyZ_mB92K27uhbUJU1p1r_wW1gFWFOEjXk_dBjftJeZ4CVP" ,
"expiresInSec" : 900 ,
"profile" : {
"email" : "[email protected] " ,
"slackUserId" : "U0123456789" ,
"slackTeamId" : "T0123456789"
}
}
401 Invalid Refresh Token
{
"code" : "INVALID_REFRESH_TOKEN" ,
"message" : "Refresh token is invalid or expired."
}
{
"code" : "INVALID_INPUT" ,
"message" : "Invalid token refresh payload." ,
"details" : {
"fieldErrors" : {
"refreshToken" : [ "String must contain at least 10 character(s)" ]
}
}
}
Error Codes
Code HTTP Status Description INVALID_REFRESH_TOKEN401 Refresh token not found, revoked, or expired INVALID_INPUT400 Request body validation failed
Token Rotation Security
This endpoint implements refresh token rotation for enhanced security:
Old token revoked : The refresh token in the request is immediately revoked
New token issued : A fresh refresh token is generated and stored
Single-use enforcement : Attempting to reuse an old refresh token will fail with INVALID_REFRESH_TOKEN
Critical : Always store the new refreshToken from the response. The old token is permanently invalidated and cannot be recovered.
Why Token Rotation?
Refresh token rotation mitigates several attack vectors:
Token theft detection : If an attacker steals a refresh token and uses it, the legitimate client’s next refresh attempt will fail, indicating compromise
Reduced blast radius : Even if a token is stolen, it’s only valid until the next legitimate refresh
Replay attack prevention : Captured tokens cannot be replayed after legitimate use
Proactive Refresh Strategy
For uninterrupted service, refresh tokens before the access token expires:
Token Manager with Proactive Refresh
Python Token Manager
class TokenManager {
private accessToken : string | null = null ;
private refreshToken : string | null = null ;
private expiresAt : number = 0 ;
private refreshPromise : Promise < void > | null = null ;
async getAccessToken () : Promise < string > {
const now = Date . now ();
const bufferMs = 120_000 ; // Refresh 2 minutes before expiry
// Check if token needs refresh
if ( now >= this . expiresAt - bufferMs ) {
// Prevent concurrent refresh calls
if ( ! this . refreshPromise ) {
this . refreshPromise = this . refresh (). finally (() => {
this . refreshPromise = null ;
});
}
await this . refreshPromise ;
}
if ( ! this . accessToken ) {
throw new Error ( 'No access token available' );
}
return this . accessToken ;
}
private async refresh () : Promise < void > {
if ( ! this . refreshToken ) {
throw new Error ( 'No refresh token available' );
}
const response = await fetch ( 'https://api.rs-tunnel.example.com/v1/auth/refresh' , {
method: 'POST' ,
headers: { 'Content-Type' : 'application/json' },
body: JSON . stringify ({ refreshToken: this . refreshToken }),
});
if ( ! response . ok ) {
throw new Error ( 'Token refresh failed' );
}
const tokens = await response . json ();
this . accessToken = tokens . accessToken ;
this . refreshToken = tokens . refreshToken ; // Store new refresh token!
this . expiresAt = Date . now () + tokens . expiresInSec * 1000 ;
// Persist to storage
await this . saveTokens ();
}
private async saveTokens () : Promise < void > {
// Implement secure storage based on platform
localStorage . setItem ( 'accessToken' , this . accessToken ! );
localStorage . setItem ( 'refreshToken' , this . refreshToken ! );
localStorage . setItem ( 'expiresAt' , String ( this . expiresAt ));
}
}
Handling Refresh Failures
If refresh fails with 401 INVALID_REFRESH_TOKEN, the user must re-authenticate:
try {
const tokens = await refreshTokens ( oldRefreshToken );
saveTokens ( tokens );
} catch ( error ) {
if ( error . code === 'INVALID_REFRESH_TOKEN' ) {
// Refresh token expired or revoked - require re-authentication
console . log ( 'Session expired. Please log in again.' );
await initiateLogin (); // Restart full OAuth flow
} else {
// Network or other error - retry with backoff
console . error ( 'Token refresh failed:' , error );
}
}
Logout and Token Revocation
To invalidate a refresh token (e.g., during logout), use the /auth/logout endpoint:
curl -X POST https://api.rs-tunnel.example.com/v1/auth/logout \
-H "Authorization: Bearer <accessToken>" \
-H "Content-Type: application/json" \
-d '{
"refreshToken": "optional-specific-token-to-revoke"
}'
Omit refreshToken to revoke all refresh tokens for the authenticated user.
The /auth/logout endpoint requires authentication (Authorization: Bearer <accessToken> header). See the full API reference for details.
Security Best Practices
Secure storage : Store refresh tokens in platform-appropriate secure storage:
Web: httpOnly cookies (preferred) or encrypted storage
CLI: Encrypted file with restricted permissions (0600)
Mobile: OS keychain/keystore
Never log tokens : Refresh tokens are sensitive credentials. Never log them or include them in error messages.
Implement retry logic : Handle transient network errors with exponential backoff, but do NOT retry 401 INVALID_REFRESH_TOKEN errors.
Monitor for anomalies : Track refresh patterns. Unexpected refresh failures may indicate token theft.
Rotate regularly : The default 30-day refresh token TTL balances security and UX. Consider shorter TTLs for high-security environments.
Next Steps
Authentication Overview Learn about the complete authentication flow
Create Tunnel Use your access token to create a tunnel