{"record":{"id":"dc17964831451979","repo":"AlistGo/alist","slug":"token-is-invalidated","errorCode":null,"errorMessage":"token is invalidated","messagePattern":"token is invalidated","errorType":"http","errorClass":null,"httpStatus":401,"severity":"error","filePath":"server/common/auth.go","lineNumber":46,"sourceCode":"\t\t\tExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Duration(conf.Conf.TokenExpiresIn) * time.Hour)),\n\t\t\tIssuedAt:  jwt.NewNumericDate(time.Now()),\n\t\t\tNotBefore: jwt.NewNumericDate(time.Now()),\n\t\t}}\n\ttoken := jwt.NewWithClaims(jwt.SigningMethodHS256, claim)\n\ttokenString, err = token.SignedString(SecretKey)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tvalidTokenCache.Set(tokenString, true)\n\treturn tokenString, err\n}\n\nfunc ParseToken(tokenString string) (*UserClaims, error) {\n\ttoken, err := jwt.ParseWithClaims(tokenString, &UserClaims{}, func(token *jwt.Token) (interface{}, error) {\n\t\treturn SecretKey, nil\n\t})\n\tif IsTokenInvalidated(tokenString) {\n\t\treturn nil, errors.New(\"token is invalidated\")\n\t}\n\tif err != nil {\n\t\tif ve, ok := err.(*jwt.ValidationError); ok {\n\t\t\tif ve.Errors&jwt.ValidationErrorMalformed != 0 {\n\t\t\t\treturn nil, errors.New(\"that's not even a token\")\n\t\t\t} else if ve.Errors&jwt.ValidationErrorExpired != 0 {\n\t\t\t\treturn nil, errors.New(\"token is expired\")\n\t\t\t} else if ve.Errors&jwt.ValidationErrorNotValidYet != 0 {\n\t\t\t\treturn nil, errors.New(\"token not active yet\")\n\t\t\t} else {\n\t\t\t\treturn nil, errors.New(\"couldn't handle this token\")\n\t\t\t}\n\t\t}\n\t}\n\tif claims, ok := token.Claims.(*UserClaims); ok && token.Valid {\n\t\treturn claims, nil\n\t}\n\treturn nil, errors.New(\"couldn't handle this token\")","sourceCodeStart":28,"sourceCodeEnd":64,"githubUrl":"https://github.com/AlistGo/alist/blob/843d9dc8149126976b2625911e45a4d3ffd6f2f5/server/common/auth.go#L28-L64","documentation":"Returned by common.ParseToken when IsTokenInvalidated reports the token as invalidated. Tokens are tracked in an in-memory cache: GenerateToken inserts them and InvalidateToken deletes them; a token absent from the cache counts as invalidated. Note this makes every token invalid after a restart or on a different instance, since the cache is not persisted.","triggerScenarios":"Parsing a JWT that was never generated by this process (cache miss), or one explicitly invalidated via InvalidateToken after logout/password change. Also fires for any token after an alist restart because validTokenCache starts empty.","commonSituations":"Client keeps using a token across an alist restart; horizontal deployment where the request hits an instance that did not issue the token; token invalidated server-side by a password change (PwdTS) or logout.","solutions":["Have the client re-authenticate to obtain a fresh token when it receives this error","Catch invalidation at the HTTP layer and return 401 so clients know to re-login","In multi-instance deployments, front the instances with a shared session store or sticky sessions, since the cache is per-process memory"],"exampleFix":"// before\nclaims, err := common.ParseToken(token) // fails after restart\n\n// after\nclaims, err := common.ParseToken(token)\nif err != nil && strings.Contains(err.Error(), \"invalidated\") {\n    c.JSON(http.StatusUnauthorized, gin.H{\"error\": \"token invalidated, please re-login\"})\n    return\n}","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"claims, err := common.ParseToken(tok)\nif err != nil && strings.Contains(err.Error(), \"invalidated\") {\n\tc.AbortWithStatusJSON(401, gin.H{\"error\": \"token invalidated; re-login required\"})\n\treturn\n}","preventionTips":["Clients should auto re-login on 401/invalidated","Remember tokens do not survive server restarts (in-memory cache)"],"tags":["jwt","auth","token","cache","restart"],"backgroundTag":null,"analyzedSha":"843d9dc8149126976b2625911e45a4d3ffd6f2f5","analyzedAt":"2026-08-15T12:14:11.722Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}