{"record":{"id":"6170322521a2942b","repo":"ruvnet/RuView","slug":"token-has-expired","errorCode":null,"errorMessage":"Token has expired","messagePattern":"Token has expired","errorType":"http","errorClass":"HTTPException","httpStatus":401,"severity":"error","filePath":"plans/phase2-architecture/api-architecture.md","lineNumber":1580,"sourceCode":"        \"\"\"Verify JWT token\"\"\"\n        token = credentials.credentials\n        \n        try:\n            payload = jwt.decode(token, self.secret_key, algorithms=[self.algorithm])\n            user_id = payload.get('sub')\n            scopes = payload.get('scopes', [])\n            \n            if user_id is None:\n                raise HTTPException(\n                    status_code=status.HTTP_401_UNAUTHORIZED,\n                    detail=\"Invalid authentication credentials\",\n                    headers={\"WWW-Authenticate\": \"Bearer\"},\n                )\n            \n            return {'user_id': user_id, 'scopes': scopes}\n            \n        except jwt.ExpiredSignatureError:\n            raise HTTPException(\n                status_code=status.HTTP_401_UNAUTHORIZED,\n                detail=\"Token has expired\",\n                headers={\"WWW-Authenticate\": \"Bearer\"},\n            )\n        except jwt.JWTError:\n            raise HTTPException(\n                status_code=status.HTTP_401_UNAUTHORIZED,\n                detail=\"Invalid token\",\n                headers={\"WWW-Authenticate\": \"Bearer\"},\n            )\n    \n    def require_scopes(self, required_scopes: List[str]):\n        \"\"\"Decorator to require specific scopes\"\"\"\n        async def scope_checker(token_data: dict = Depends(self.verify_token)):\n            user_scopes = token_data.get('scopes', [])\n            \n            for scope in required_scopes:\n                if scope not in user_scopes:","sourceCodeStart":1562,"sourceCodeEnd":1598,"githubUrl":"https://github.com/ruvnet/RuView/blob/4685618388a5e49fad5b3005806f3bdd6a7c25c3/plans/phase2-architecture/api-architecture.md#L1562-L1598","documentation":"HTTP 401 raised when jwt.decode throws jwt.ExpiredSignatureError, i.e. the token's 'exp' claim is in the past. The signature was valid; the credential simply aged out. The response includes WWW-Authenticate: Bearer per RFC 6750 so clients know to re-authenticate.","triggerScenarios":"Sending any Bearer token whose exp timestamp is earlier than the server's current time: sessions older than the token TTL, tokens cached in localStorage beyond expiry, or clock skew where the client machine (or server) has a wrong clock.","commonSituations":"Long-running SPAs or notebooks holding tokens past their TTL; NTP drift between token issuer and API server; short-lived access tokens used without a refresh flow.","solutions":["Obtain a fresh token via the login/token endpoint and retry the request","Implement a refresh-token flow so clients silently renew before exp","Verify clocks: ensure both issuer and API server run NTP; skew makes valid tokens appear expired","If a slightly longer session is acceptable, increase the exp delta at token creation (weigh security)"],"exampleFix":"# before\nasync def call_api():\n    return await client.get('/pose', headers=bearer_headers(old_token))\n\n# after\nasync def call_api():\n    resp = await client.get('/pose', headers=bearer_headers(old_token))\n    if resp.status_code == 401 and resp.json()['detail'] == 'Token has expired':\n        old_token = await refresh_or_login()\n        resp = await client.get('/pose', headers=bearer_headers(old_token))\n    return resp","handlingStrategy":"try-catch","validationCode":"import jwt, time\nunverified = jwt.decode(token, options={'verify_signature': False})\nif unverified.get('exp', 0) <= time.time() + 30:\n    token = await refresh_or_login()  # renew with margin instead of letting the server 401","typeGuard":null,"tryCatchPattern":"try:\n    result = await call_api(token)\nexcept HTTPException as e:\n    if e.status_code == 401 and e.detail == 'Token has expired':\n        token = await refresh_or_login()\n        result = await call_api(token)  # single retry with the fresh token\n    else:\n        raise","preventionTips":["Refresh tokens slightly before exp instead of waiting for the 401","Run NTP on issuers and API servers to avoid skew-induced expiries","Treat 'Token has expired' as a routine client-side event (refresh + retry once), not a fatal error"],"tags":["python","fastapi","jwt","authentication","token-expiry","http-401"],"backgroundTag":null,"analyzedSha":"4685618388a5e49fad5b3005806f3bdd6a7c25c3","analyzedAt":"2026-08-16T06:09:40.886Z","schemaVersion":2},"datasetVersion":"2026-08-16T08:17:34.114Z"}