{"record":{"id":"1bef0ff87eed6311","repo":"bytedance/deer-flow","slug":"token-invalid","errorCode":"TOKEN_INVALID","errorMessage":"Token revoked (password changed)","messagePattern":"Token revoked \\(password changed\\)","errorType":"http","errorClass":"HTTPException","httpStatus":401,"severity":"error","filePath":"backend/app/gateway/deps.py","lineNumber":777,"sourceCode":"\n    payload = decode_token(access_token)\n    if isinstance(payload, TokenError):\n        raise HTTPException(\n            status_code=401,\n            detail=AuthErrorResponse(code=token_error_to_code(payload), message=f\"Token error: {payload.value}\").model_dump(),\n        )\n\n    provider = get_local_provider()\n    user = await provider.get_user(payload.sub)\n    if user is None:\n        raise HTTPException(\n            status_code=401,\n            detail=AuthErrorResponse(code=AuthErrorCode.USER_NOT_FOUND, message=\"User not found\").model_dump(),\n        )\n\n    # Token version mismatch → password was changed, token is stale\n    if user.token_version != payload.ver:\n        raise HTTPException(\n            status_code=401,\n            detail=AuthErrorResponse(code=AuthErrorCode.TOKEN_INVALID, message=\"Token revoked (password changed)\").model_dump(),\n        )\n\n    return user\n\n\nasync def require_admin_user(request: Request, *, detail: str) -> None:\n    \"\"\"Require the authenticated caller to be an admin user.\n\n    ``AuthMiddleware`` normally stamps ``request.state.user`` before the request\n    reaches a router. Falling back to the strict dependency keeps the route safe\n    in tests or alternative ASGI compositions that mount a router without the\n    global middleware. ``detail`` is the route-specific 403 message.\n\n    Centralising this here means a future change to the admin definition (e.g.\n    allowing an internal system role, adding audit logging, or switching to a\n    permission-based check) lands in one place instead of drifting across the","sourceCodeStart":759,"sourceCodeEnd":795,"githubUrl":"https://github.com/bytedance/deer-flow/blob/1dd6ba1acb03700589994b0366c5d1c7d05e2eff/backend/app/gateway/deps.py#L759-L795","documentation":"HTTP 401 with code TOKEN_INVALID: the token's ver (token_version) claim does not match user.token_version in the store. DeerFlow increments a user's token_version on password change, which retroactively revokes all previously issued JWTs; a mismatch means this token predates the password change.","triggerScenarios":"Authenticated call using a token issued before the most recent password change for that user — e.g. an old tab, a second device, or a saved session that kept the pre-rotation cookie.","commonSituations":"User changed password on device A; device B still sends the old cookie; password reset flow bumped token_version; automated tests reusing fixtures with stale ver claims after a password-update step.","solutions":["Re-login on the affected client — the new token carries the current token_version","If this hits every client after a routine deploy (not a password change), check whether something is incorrectly bumping token_version (e.g. a user-update code path writing the field unconditionally)","In tests, re-mint tokens after any user password/version mutation"],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":"null","typeGuard":"null","tryCatchPattern":"if (e.status === 401 && e.detail?.code === 'TOKEN_INVALID') {\n  showNotice('Password changed elsewhere — please sign in again');\n  logout();\n}","preventionTips":["Handle TOKEN_INVALID as 're-login required', never as a retryable error","In multi-device setups, expect 401 storms on other devices after any password change","Tests: re-mint tokens after exercising password-change endpoints"],"tags":["auth","http-401","token-revocation","password-change","jwt"],"backgroundTag":null,"analyzedSha":"1dd6ba1acb03700589994b0366c5d1c7d05e2eff","analyzedAt":"2026-08-14T21:20:34.804Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}