Skip to main content
POST
/
v1
/
auth
/
refresh
Refresh Access Token
curl --request POST \
  --url https://api.example.com/v1/auth/refresh \
  --header 'Content-Type: application/json' \
  --data '
{
  "refreshToken": "<string>"
}
'
{
  "accessToken": "<string>",
  "refreshToken": "<string>",
  "expiresInSec": 123,
  "profile": {
    "email": "<string>",
    "slackUserId": "<string>",
    "slackTeamId": "<string>"
  }
}
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

POST /v1/auth/refresh

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

refreshToken
string
required
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.
accessToken
string
required
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
refreshToken
string
required
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
expiresInSec
integer
required
Access token lifetime in seconds.Default: 900 (15 minutes)
profile
object
required
Authenticated user profile information.

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

200 Success
{
  "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."
}
400 Invalid Input
{
  "code": "INVALID_INPUT",
  "message": "Invalid token refresh payload.",
  "details": {
    "fieldErrors": {
      "refreshToken": ["String must contain at least 10 character(s)"]
    }
  }
}

Error Codes

CodeHTTP StatusDescription
INVALID_REFRESH_TOKEN401Refresh token not found, revoked, or expired
INVALID_INPUT400Request body validation failed

Token Rotation Security

This endpoint implements refresh token rotation for enhanced security:
  1. Old token revoked: The refresh token in the request is immediately revoked
  2. New token issued: A fresh refresh token is generated and stored
  3. 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:
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

  1. 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
  2. Never log tokens: Refresh tokens are sensitive credentials. Never log them or include them in error messages.
  3. Implement retry logic: Handle transient network errors with exponential backoff, but do NOT retry 401 INVALID_REFRESH_TOKEN errors.
  4. Monitor for anomalies: Track refresh patterns. Unexpected refresh failures may indicate token theft.
  5. 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

Build docs developers (and LLMs) love