{"record":{"id":"290de38083ec8d90","repo":"mem0ai/mem0","slug":"refresh-token-is-no-longer-valid-290de3","errorCode":null,"errorMessage":"Refresh token is no longer valid.","messagePattern":"Refresh token is no longer valid\\.","errorType":"http","errorClass":"HTTPException","httpStatus":401,"severity":"error","filePath":"server/routers/auth.py","lineNumber":153,"sourceCode":"    user.last_login_at = datetime.now(timezone.utc)\n    db.commit()\n\n    return TokenResponse(\n        access_token=create_access_token(str(user.id), user.role),\n        refresh_token=create_refresh_token(str(user.id), db),\n    )\n\n\n@router.post(\"/refresh\", response_model=TokenResponse)\n@limiter.limit(\"20/minute\")\ndef refresh(request: Request, body: RefreshRequest, db: Session = Depends(get_db)):\n    payload = decode_token(body.refresh_token)\n    if payload.get(\"type\") != \"refresh\":\n        raise HTTPException(status_code=401, detail=\"Invalid token type.\")\n\n    jti = payload.get(\"jti\")\n    if not jti:\n        raise HTTPException(status_code=401, detail=\"Refresh token is no longer valid.\")\n\n    user = db.get(User, payload[\"sub\"])\n    if user is None:\n        raise HTTPException(status_code=401, detail=\"User not found.\")\n\n    consume_refresh_jti(jti, db)\n\n    return TokenResponse(\n        access_token=create_access_token(str(user.id), user.role),\n        refresh_token=create_refresh_token(str(user.id), db),\n    )\n\n\n@router.get(\"/me\", response_model=UserResponse)\ndef me(user: User = Depends(require_auth)):\n    return user\n\n","sourceCodeStart":135,"sourceCodeEnd":171,"githubUrl":"https://github.com/mem0ai/mem0/blob/001c235229be8795e3834520467bd0d661ed8f34/server/routers/auth.py#L135-L171","documentation":"Raised by POST /auth/refresh when the decoded refresh token carries no `jti` (JWT ID) claim. Refresh tokens are one-shot: each is minted with a unique jti that is tracked and consumed on use, so a token without a jti cannot participate in rotation. This typically means the token was created by a code path that skips jti assignment, or the payload was stripped/mangled.","triggerScenarios":"Calling /auth/refresh with a refresh token minted without a jti (older issuer version or custom create_refresh_token path); decoding/re-encoding the token client-side and dropping claims; sending a refresh token signed with the right secret but constructed manually.","commonSituations":"Server upgrade changed create_refresh_token to include jti and rotation, and old clients still hold pre-rotation tokens; a test fixture builds refresh tokens with a helper that omits jti; multiple services share the JWT secret but only one adds jti.","solutions":["Have the user log in again to obtain a refresh token minted by the current create_refresh_token (which embeds a jti)","Audit any code or fixture that constructs refresh tokens and ensure it includes a unique jti claim","If upgrading the auth server, plan for old refresh tokens: let them fail here and require re-login, or issue a migration window"],"exampleFix":"// before (fixture/helper omitting jti)\ndef make_refresh_token(user_id):\n    return jwt.encode({\"sub\": user_id, \"type\": \"refresh\"}, SECRET, algorithm=\"HS256\")\n\n// after\nimport uuid\n\ndef make_refresh_token(user_id):\n    return jwt.encode({\"sub\": user_id, \"type\": \"refresh\", \"jti\": str(uuid.uuid4())}, SECRET, algorithm=\"HS256\")","handlingStrategy":"validation","validationCode":"function hasJti(token) {\n  const payload = JSON.parse(atob(token.split(\".\")[1].replace(/-/g, \"+\").replace(/_/g, \"/\")));\n  return Boolean(payload.jti);\n}\nif (!hasJti(stored.refreshToken)) { clearTokens(); goToLogin(); }","typeGuard":"function isRotatableRefreshToken(token: string): boolean {\n  const payload = JSON.parse(atob(token.split(\".\")[1].replace(/-/g, \"+\").replace(/_/g, \"/\")));\n  return payload.type === \"refresh\" && typeof payload.jti === \"string\" && payload.jti.length > 0;\n}","tryCatchPattern":"catch (e) { if (e.status === 401 && /no longer valid/.test(e.detail)) { clearTokens(); goToLogin(); /* token cannot be reused */ } throw e; }","preventionTips":["Treat this 401 as terminal: do not retry with the same token, clear session state and re-authenticate","Rotate stored tokens atomically: persist the new refresh_token from each TokenResponse before the next request","When building tokens in tests, always include a unique jti claim"],"tags":["auth","jwt","jti","token-rotation","http-401"],"backgroundTag":null,"analyzedSha":"001c235229be8795e3834520467bd0d661ed8f34","analyzedAt":"2026-08-15T01:55:42.685Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}