Skip to content

Refresh Tokens

Refresh tokens are long-lived credentials used to obtain new access tokens without re-authenticating. They are:

  • 256-bit random (64 hex characters)
  • Stored in D1 (server-side, not in the token itself)
  • Rotated on every use (old token revoked, new one issued)
  • Scoped to a project (cannot be used across projects)
POST https://auth.beshoy.ai/oauth/token
Content-Type: application/json
FieldTypeRequiredDescription
grant_typestringYes"refresh_token"
refresh_tokenstringYesCurrent refresh token
client_idstringYesProject ID
client_secretstringYesProject client secret
Terminal window
curl -X POST https://auth.beshoy.ai/oauth/token \
-H "Content-Type: application/json" \
-d '{
"grant_type": "refresh_token",
"refresh_token": "a1b2c3d4e5f6...",
"client_id": "proj_gym",
"client_secret": "your-client-secret-hex"
}'
{
"access_token": "eyJhbGciOiJIUzI1NiJ9...",
"refresh_token": "f6e5d4c3b2a1...",
"token_type": "Bearer",
"expires_in": 300
}

Every refresh operation follows this sequence:

  1. Look up the refresh token in D1
  2. Verify it belongs to the requesting client_id
  3. Check revocation status (with grace period)
  4. Check expiration
  5. Revoke the old token immediately
  6. Verify user status (blocked check) or PIN status (revoked check)
  7. Issue new access token + new refresh token
  8. Return both

This means a stolen refresh token can only be used once. After that, the legitimate client will fail to refresh (their token was already consumed), signaling a potential breach.

When a token is revoked, there’s a 60-second grace period where it’s still accepted. This handles a common race condition:

Request A: uses refresh_token_1 → rotates to refresh_token_2
Request B: uses refresh_token_1 (sent before A's response arrived)

Without the grace period, Request B would fail. With it, both succeed within the 60-second window.

After 60 seconds, the revoked token is permanently rejected.

During refresh, the auth service checks the user’s status in project_users:

  • status = "active" → refresh succeeds
  • status = "blocked" → returns 403 Account blocked

This means blocking a user takes effect within the access token’s lifetime (5 minutes max).

For PIN-based sessions (is_pin_session = 1):

  • The PIN’s status is checked in project_pins
  • If status = "revoked" → returns 403 PIN revoked
  • Privileges are re-read from the PIN record (reflects any updates)
Session typeRefresh token lifetime
User session7 days
PIN session30 days

Expired tokens are rejected with 401 Refresh token expired.

ErrorStatusCause
Missing refresh_token400No token in request
Invalid refresh token401Token not found, wrong project, or revoked past grace period
Refresh token expired401Token past its expiration date
Account blocked403User’s project status is “blocked”
PIN revoked403PIN has been revoked by admin