{"record":{"id":"2f9443355c4dc2d3","repo":"usememos/memos","slug":"unauthenticated","errorCode":"Unauthenticated","errorMessage":"refresh token revoked","messagePattern":"refresh token revoked","errorType":"http","errorClass":null,"httpStatus":401,"severity":"error","filePath":"server/auth/authenticator.go","lineNumber":78,"sourceCode":"// AuthenticateByRefreshToken validates a refresh token against the database.\nfunc (a *Authenticator) AuthenticateByRefreshToken(ctx context.Context, refreshToken string) (*store.User, string, error) {\n\tclaims, err := ParseRefreshToken(refreshToken, []byte(a.secret))\n\tif err != nil {\n\t\treturn nil, \"\", errors.Wrap(err, \"invalid refresh token\")\n\t}\n\n\tuserID, err := util.ConvertStringToInt32(claims.Subject)\n\tif err != nil {\n\t\treturn nil, \"\", errors.Wrap(err, \"invalid user ID in token\")\n\t}\n\n\t// Check token exists in database (revocation check)\n\ttoken, err := a.store.GetUserRefreshTokenByID(ctx, userID, claims.TokenID)\n\tif err != nil {\n\t\treturn nil, \"\", errors.Wrap(err, \"failed to get refresh token\")\n\t}\n\tif token == nil {\n\t\treturn nil, \"\", errors.New(\"refresh token revoked\")\n\t}\n\n\t// Check token not expired\n\tif token.ExpiresAt != nil && token.ExpiresAt.AsTime().Before(time.Now()) {\n\t\treturn nil, \"\", errors.New(\"refresh token expired\")\n\t}\n\n\t// Get user\n\tuser, err := a.store.GetUser(ctx, &store.FindUser{ID: &userID})\n\tif err != nil {\n\t\treturn nil, \"\", errors.Wrap(err, \"failed to get user\")\n\t}\n\tif user == nil {\n\t\treturn nil, \"\", errors.New(\"user not found\")\n\t}\n\tif user.RowStatus == store.Archived {\n\t\treturn nil, \"\", errors.New(\"user is archived\")\n\t}","sourceCodeStart":60,"sourceCodeEnd":96,"githubUrl":"https://github.com/usememos/memos/blob/14d757ce1fb31c78590f374bc042f8dbedbc20d7/server/auth/authenticator.go#L60-L96","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Discard the stored refresh token and re-authenticate (redirect to login)","On the client, clear token state on receiving Unauthenticated from the refresh endpoint instead of retrying","If you administer the instance, confirm whether a session-wide revocation or DB restore explains it"],"exampleFix":"// before\nonRefreshError: retryRefreshWithSameToken()\n// after\nonUnauthenticated: clearTokens(); redirectToLogin()","handlingStrategy":"try-catch","validationCode":"// Client-side: before refreshing, you cannot query revocation; instead ensure you\n// clear tokens atomically on logout in all tabs:\n// auth-state.ts\nbroadcastChannel.postMessage({ type: \"logout\" });\nlocalStorage.removeItem(\"refreshToken\");","typeGuard":null,"tryCatchPattern":"// Treat Unauthenticated from refresh as terminal: purge credentials, do not retry\nif _, _, err := authn.AuthenticateByRefreshToken(ctx, token); err != nil {\n  if status.Code(err) == codes.Unauthenticated {\n    session.Clear() // remove stored tokens\n    return http.Redirect(w, r, \"/login\", http.StatusSeeOther)\n  }\n  return err\n}","preventionTips":["Never retry a refresh after an Unauthenticated response","Clear tokens on logout across all tabs (BroadcastChannel pattern)","Handle revocation as expected behavior after admin session resets, not as a bug"],"tags":["authentication","jwt","refresh-token","session"],"backgroundTag":null,"analyzedSha":"14d757ce1fb31c78590f374bc042f8dbedbc20d7","analyzedAt":"2026-08-15T09:27:36.538Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}