v1.0.3

Transparent Refresh

Last updated


When the Access Token is expired or missing, Verify() reissues it from the Refresh ID without any work from the caller or the user; this page covers when that happens, the two outcomes, and concurrent behavior.

Triggers


  • The request has no Access Token but has a Refresh ID
  • Signature verification reports the Access Token as expired

Flow


#StepOn failure
1Read refresh:<id> and its TTL; compare fp with this request's fingerprint401 unauthorized
2SETNX lock:refresh:<id> with a 3-second lock429 failed_to_update
3Call CheckAuth(Auth) when configuredReturns false or an error → 401 (empty ErrorTag)
4Compare against thresholds; re-sign only or reissue the full pair—
5DEL the lock after a Lua value check (defer, runs on every outcome)—

Two Outcomes


ConditionResultHow new tokens are delivered
version ≤ MaxVersion and remaining TTL ≥ RefreshIdExpires × RefreshTTLRe-sign the Access Token only: keep the Refresh ID, version + 1, new JTI; refresh:<id> keeps its remaining TTLX-New-Access-Token header + Set-Cookie: access_token
version > MaxVersion, or remaining TTL below the thresholdFull reissue: shrink the old refresh:<id> TTL to 3 seconds, then call Create() for a new Refresh ID and Access TokenOnly Set-Cookie: access_token and refresh_id; X-New-Access-Token is not set

With the default MaxVersion = 5: login sets version = 1, the first 5 refreshes only re-sign, and the 6th refresh reissues the full pair. Re-signing never extends the Refresh ID TTL, so a long-running session eventually reaches the TTL threshold and gets a full reissue.

Concurrent Requests


Only one request at a time can hold the refresh lock for a given Refresh ID; requests arriving while it is held get 429. The lock is released as soon as the refresh finishes, so among several simultaneous requests more than one may refresh successfully and the rest receive 429. On 429 the frontend should retry after a short delay: the retry carries the freshly updated cookies and passes verification directly.

CheckAuth


CheckAuth runs on every refresh (including re-sign only), which makes it the place to check whether a user was suspended or deleted; regular request verification does not call it.
  1. auth, err := goJwt.New(goJwt.Config{
  2. Redis: goJwt.Redis{Host: "localhost", Port: 6379},
  3. CheckAuth: func(a goJwt.Auth) (bool, error) {
  4. active, err := userRepo.IsActive(context.Background(), a.ID)
  5. if err != nil {
  6. return false, fmt.Errorf("check user %s: %w", a.ID, err)
  7. }
  8. return active, nil
  9. },
  10. })

userRepo stands for your application's data layer.

FieldDefaultEffect
MaxVersion5Maximum number of re-sign-only refreshes
RefreshTTL0.5Full reissue once the remaining TTL drops below RefreshIdExpires × RefreshTTL
RefreshIdExpires7 daysTTL of the new Refresh ID on full reissue

See Configuration for every field.
中文