Skip to Content

Refresh Token Flow 🔄

Access tokens are short-lived on purpose — a leaked token expires quickly. The Refresh Token flow lets your application obtain a fresh access token without sending the user back through login, keeping sessions seamless while the tokens stay short-lived.

You receive a refresh token when a user authenticates with the Authorization Code flow or the Device Code flow. Store it, and exchange it whenever the access token expires.

[!TIP] If you use @faable/auth-js, you can skip this page entirely: the SDK refreshes tokens automatically in the background. Read on if you’re curious or implementing the flow yourself.


📸 How It Works

Every refresh returns a new refresh token (rotation). Always store the latest one and use it for the next refresh.


🚀 Automatic Refresh with @faable/auth-js

The SDK manages the whole lifecycle: it stores the session, runs a background ticker, and exchanges the refresh token before the access token expires — across tabs, transparently.

import { createClient } from '@faable/auth-js' const auth = createClient({ domain: 'your-domain.auth.faable.link', clientId: 'YOUR_CLIENT_ID' }) // That's it — sessions refresh automatically in the background. const { data } = await auth.getSession()

Forcing a Manual Refresh

Need a guaranteed-fresh token — for example right after a server-side change to the user’s claims? Call refreshSession():

const { data, error } = await auth.refreshSession() if (!error) { console.log('New access token:', data.session.access_token) }

🛠️ Refreshing over HTTP

Implementing it yourself (a backend, a CLI, a non-JS stack)? It’s a single request.

  • Endpoint: https://your-domain.auth.faable.link/oauth/token
  • Method: POST
  • Content-Type: application/x-www-form-urlencoded or application/json

Request Parameters

ParameterRequiredDescription
grant_typeYesMust be refresh_token.
client_idYesYour application’s Client ID.
refresh_tokenYesThe most recent refresh token you received.
scopeNoSpace-separated. May only narrow the original grant — scopes not present in the original token are dropped (RFC 6749 §6).

The token’s audience is preserved automatically from the original grant — you don’t (and can’t) change it on refresh.

Example with curl

curl --request POST \ --url 'https://your-domain.auth.faable.link/oauth/token' \ --header 'content-type: application/x-www-form-urlencoded' \ --data 'grant_type=refresh_token' \ --data 'client_id=YOUR_CLIENT_ID' \ --data 'refresh_token=YOUR_REFRESH_TOKEN'

Response

{ "access_token": "eyJhbGciOiJSUzI1NiIs...", "id_token": "eyJhbGciOiJSUzI1NiIs...", "refresh_token": "v1.NEW_ROTATED_TOKEN...", "token_type": "Bearer", "expires_in": 86400 }
FieldDescription
access_tokenFresh JWT, same aud as the original grant.
id_tokenRefreshed OIDC identity token. Keeps the original nonce, auth_time, and session id (sid) per the OIDC spec.
refresh_tokenA new, rotated refresh token. Replace the stored one with this value.
expires_inNew access token lifetime in seconds (from the target API’s token_lifetime, default 86400 = 24 h).

⚠️ Common Errors

Errors use the standard OAuth 2.0 format: { "error": "...", "error_description": "..." }.

StatusErrorCause / Fix
400Missing refresh_tokenInclude the parameter in the request body.
400Bad decoded tokenThe value isn’t a valid refresh token. Check you’re not sending an access token by mistake.
400invalid_grantThe API (audience) the token was issued for no longer exists. Stop retrying and re-authenticate the user.
429Rate limit exceededYou’re refreshing too often. Refresh only when the access token is about to expire — or let @faable/auth-js schedule it for you.

[!IMPORTANT] Refresh only when needed — not on every request. The token endpoint is rate-limited per client, and a refresh loop (e.g. refreshing a 24-hour token every minute) will get throttled with 429 responses.


❓ FAQ

Where do I get a refresh token in the first place?

From a user login: the Authorization Code flow and the Device Code flow both return one alongside the access token.

Why is a new refresh token returned every time?

Rotation limits the damage of a leaked token: each refresh token is superseded by the next one. Always persist the latest value.

Can I extend the scopes when refreshing?

No — RFC 6749 §6 only allows narrowing. Scopes not granted originally are silently dropped. To gain new scopes, send the user through login again.

Does the Client Credentials flow use refresh tokens?

No. Machine-to-machine clients simply request a new token with their credentials when the current one expires.

Do I have to implement any of this in a browser app?

No — use @faable/auth-js and it’s fully automatic, including multi-tab session sync.


Last updated on