usememos/memos · error

Unauthenticated

Unauthenticated

Error message

refresh token revoked

What it means

During refresh-token authentication, the JWT signature and claims validated, but no row with the token's TokenID exists in the database for that user — meaning the token was revoked (user logged out, sessions invalidated, or admin revoked sessions) after it was issued. Returned as Unauthenticated.

Source

Thrown at server/auth/authenticator.go:78

// AuthenticateByRefreshToken validates a refresh token against the database.
func (a *Authenticator) AuthenticateByRefreshToken(ctx context.Context, refreshToken string) (*store.User, string, error) {
	claims, err := ParseRefreshToken(refreshToken, []byte(a.secret))
	if err != nil {
		return nil, "", errors.Wrap(err, "invalid refresh token")
	}

	userID, err := util.ConvertStringToInt32(claims.Subject)
	if err != nil {
		return nil, "", errors.Wrap(err, "invalid user ID in token")
	}

	// Check token exists in database (revocation check)
	token, err := a.store.GetUserRefreshTokenByID(ctx, userID, claims.TokenID)
	if err != nil {
		return nil, "", errors.Wrap(err, "failed to get refresh token")
	}
	if token == nil {
		return nil, "", errors.New("refresh token revoked")
	}

	// Check token not expired
	if token.ExpiresAt != nil && token.ExpiresAt.AsTime().Before(time.Now()) {
		return nil, "", errors.New("refresh token expired")
	}

	// Get user
	user, err := a.store.GetUser(ctx, &store.FindUser{ID: &userID})
	if err != nil {
		return nil, "", errors.Wrap(err, "failed to get user")
	}
	if user == nil {
		return nil, "", errors.New("user not found")
	}
	if user.RowStatus == store.Archived {
		return nil, "", errors.New("user is archived")
	}

View on GitHub (pinned to 14d757ce1f)

Solutions

  1. Discard the stored refresh token and re-authenticate (redirect to login)
  2. On the client, clear token state on receiving Unauthenticated from the refresh endpoint instead of retrying
  3. If you administer the instance, confirm whether a session-wide revocation or DB restore explains it

Example fix

// before
onRefreshError: retryRefreshWithSameToken()
// after
onUnauthenticated: clearTokens(); redirectToLogin()
Defensive patterns

Strategy: try-catch

Validate before calling

// Client-side: before refreshing, you cannot query revocation; instead ensure you
// clear tokens atomically on logout in all tabs:
// auth-state.ts
broadcastChannel.postMessage({ type: "logout" });
localStorage.removeItem("refreshToken");

Try / catch

// Treat Unauthenticated from refresh as terminal: purge credentials, do not retry
if _, _, err := authn.AuthenticateByRefreshToken(ctx, token); err != nil {
  if status.Code(err) == codes.Unauthenticated {
    session.Clear() // remove stored tokens
    return http.Redirect(w, r, "/login", http.StatusSeeOther)
  }
  return err
}

Prevention

When it happens

Trigger: Client retries a refresh with a token whose row was deleted: after logout on another device, after 'sign out all sessions', after an admin deactivates sessions, or after a DB restore/rotation that dropped the refresh-token table rows.

Common situations: Multiple tabs/devices where one logged out and a stale tab attempts refresh; iOS app resuming with an old token; database restored from backup without the refresh-token rows; race between logout and an in-flight token refresh.

Related errors


AI-assisted analysis of usememos/memos@14d757ce1f (2026-08-15). Data as JSON: /api/errors/2f9443355c4dc2d3. Report an issue: GitHub.