Skip to content
Last updated

Refresh Token Flow

Access tokens expire after 1 hour. Use the refresh token to obtain new access tokens without requiring user re-authentication.

Why Refresh Tokens?

Short-lived access tokens limit the impact of token compromise. Refresh tokens enable:

  • Seamless user experience without repeated logins
  • Enhanced security through token rotation
  • Controlled access revocation

Implementation

Request New Access Token

When your access token expires (HTTP 401 responses), use the refresh token:

POST https://api.askara.ai/oauth2/token
Content-Type: application/x-www-form-urlencoded

grant_type=refresh_token&refresh_token=REFRESH_TOKEN&client_id=CLIENT_ID&client_secret=CLIENT_SECRET

The request body must be form-encoded, as defined by the OAuth2 specification (RFC 6749).

Response:

{
  "token_type": "Bearer",
  "expires_in": 3600,
  "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
  "refresh_token": "def502003f8a8c7..."
}

Important: The response includes a new refresh token. Update your stored refresh token immediately: the old one is invalidated. For 10 seconds after a refresh, the old token is still accepted, so that a lost response does not lock your application out. After that delay, it is rejected with invalid_grant.

Token Rotation

Each refresh token is single-use. This rotation mechanism:

  • Rejects the reuse of an old refresh token (after the 10-second delay described above)
  • Limits the window of vulnerability

Rotation Flow

┌─────────────────────┐
│ Old Refresh Token   │
└──────────┬──────────┘
           │
           ▼
┌─────────────────────┐
│ POST /oauth2/token  │
└──────────┬──────────┘
           │
           ├──────────────────────┐
           │                      │
           ▼                      ▼
┌──────────────────┐   ┌──────────────────┐
│ New Access Token │   │ New Refresh Token│
└────────┬─────────┘   └────────┬─────────┘
         │                      │
         └──────────┬───────────┘
                    │
                    ▼
         ┌──────────────────┐
         │ Update Database  │
         └────────┬─────────┘
                  │
                  ▼
      ┌─────────────────────────┐
      │ Old Token Invalidated   │
      └─────────────────────────┘

Error Handling

Invalid Refresh Token

The API answers with HTTP 400:

{
  "error": "invalid_grant",
  "error_description": "The refresh token is invalid.",
  "hint": "Token has been revoked"
}

Rely on the error value only: error_description and hint are meant for debugging and their wording may change.

Causes:

  • Token already used (rotation)
  • Token revoked by user
  • Token expired (refresh tokens are valid for 30 days)

Invalid client credentials are reported differently, with the invalid_client error (HTTP 401).

Action: Redirect user to re-authenticate via Authorization Code Flow.

Too Many Refreshes

Refresh requests are limited to 10 per minute and per user. Beyond that, the API answers 429 Too Many Requests with an error too_many_requests and a Retry-After header (in seconds). A refresh is only needed about once per hour, so this limit should never be reached in normal use.

Revoked Access

If a user revokes your application's access, all tokens become invalid. Implement proper error handling to detect this scenario.

Security Best Practices

Storage

  • Encrypt refresh tokens in your database
  • Never expose refresh tokens to client-side code
  • Use separate encryption keys from other sensitive data

Rotation Implementation

function refreshAccessToken($oldRefreshToken) {
    $response = $httpClient->post('https://api.askara.ai/oauth2/token', [
        'form_params' => [
            'grant_type' => 'refresh_token',
            'refresh_token' => $oldRefreshToken,
            'client_id' => getenv('ASKARA_CLIENT_ID'),
            'client_secret' => getenv('ASKARA_CLIENT_SECRET')
        ]
    ]);

    $tokens = json_decode($response->getBody(), true);

    // Atomic update: only commit if both tokens are updated
    $db->beginTransaction();
    try {
        $db->update('oauth_tokens', [
            'access_token' => $tokens['access_token'],
            'refresh_token' => $tokens['refresh_token'],
            'expires_at' => time() + $tokens['expires_in']
        ]);
        $db->commit();
    } catch (Exception $e) {
        $db->rollback();
        throw $e;
    }

    return $tokens['access_token'];
}

Expiration Handling

Check token expiration before making API requests to avoid unnecessary failures:

function getValidAccessToken() {
    $tokenData = $db->getTokenData();

    // Refresh if token expires within 5 minutes
    if ($tokenData['expires_at'] < time() + 300) {
        return refreshAccessToken($tokenData['refresh_token']);
    }

    return $tokenData['access_token'];
}

Automatic Retry Pattern

Implement automatic token refresh on 401 responses:

function makeApiRequest($url, $data) {
    $accessToken = getValidAccessToken();

    try {
        return $httpClient->post($url, [
            'headers' => ['Authorization' => 'Bearer ' . $accessToken],
            'json' => $data
        ]);
    } catch (UnauthorizedException $e) {
        // Token expired during request, refresh and retry once
        $accessToken = refreshAccessToken($db->getRefreshToken());
        return $httpClient->post($url, [
            'headers' => ['Authorization' => 'Bearer ' . $accessToken],
            'json' => $data
        ]);
    }
}

Monitoring

Track refresh token usage to detect anomalies:

  • Unusually frequent refreshes (potential token theft)
  • Failed refresh attempts (compromised credentials)
  • Geographic anomalies (access from unexpected locations)

Next Steps