{"record":{"id":"bfeee34e08f16e12","repo":"AlistGo/alist","slug":"failed-to-refresh-token-sub-not-match","errorCode":null,"errorMessage":"failed to refresh token: sub not match","messagePattern":"failed to refresh token: sub not match","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"critical","filePath":"drivers/aliyundrive_open/util.go","lineNumber":151,"sourceCode":"\t\t}\n\t}\n\tif e.Code != \"\" {\n\t\treturn \"\", \"\", fmt.Errorf(\"failed to refresh token: %s\", e.Message)\n\t}\n\trefresh, access := utils.Json.Get(res.Body(), \"refresh_token\").ToString(), utils.Json.Get(res.Body(), \"access_token\").ToString()\n\tif refresh == \"\" {\n\t\treturn \"\", \"\", fmt.Errorf(\"failed to refresh token: refresh token is empty, resp: %s\", res.String())\n\t}\n\tcurSub, err := getSub(d.RefreshToken)\n\tif err != nil {\n\t\treturn \"\", \"\", err\n\t}\n\tnewSub, err := getSub(refresh)\n\tif err != nil {\n\t\treturn \"\", \"\", err\n\t}\n\tif curSub != newSub {\n\t\treturn \"\", \"\", errors.New(\"failed to refresh token: sub not match\")\n\t}\n\treturn refresh, access, nil\n}\n\nfunc getSub(token string) (string, error) {\n\tsegments := strings.Split(token, \".\")\n\tif len(segments) != 3 {\n\t\treturn \"\", errors.New(\"not a jwt token because of invalid segments\")\n\t}\n\tbs, err := base64.RawStdEncoding.DecodeString(segments[1])\n\tif err != nil {\n\t\treturn \"\", errors.New(\"failed to decode jwt token\")\n\t}\n\treturn utils.Json.Get(bs, \"sub\").ToString(), nil\n}\n\nfunc (d *AliyundriveOpen) refreshToken(ctx context.Context) error {\n\tif d.ref != nil {","sourceCodeStart":133,"sourceCodeEnd":169,"githubUrl":"https://github.com/AlistGo/alist/blob/843d9dc8149126976b2625911e45a4d3ffd6f2f5/drivers/aliyundrive_open/util.go#L133-L169","documentation":"Thrown by aliyundrive_open's token refresh when the freshly issued refresh token carries a `sub` (subject/user identifier) JWT claim that differs from the one in the currently stored refresh token. The Open Platform rotates refresh tokens; if the new token belongs to a different user/account, persisting it would silently switch the storage to another account, so the driver aborts.","triggerScenarios":"refreshToken() succeeds, then getSub() decodes the `sub` claim of both the old and new JWTs and they differ — e.g. the refresh_token in config was issued for user A while the client_id/QR-login context belongs to user B, or a pasted token was later re-issued under a family/enterprise master account.","commonSituations":"Copy-pasting a refresh token from one Aliyun account into a storage configured with another account's client; account merges/family plan conversions changing the subject ID; mixing tokens between the qr endpoint and a different app's client_id; manually editing the refresh_token field after re-login elsewhere.","solutions":["Redo the full authorization flow (QR login) for THIS storage so the refresh token matches the account tied to its client_id — do not hand-mix tokens","Verify the refresh_token in the storage config was issued for the same Aliyun account and the same Open-Platform app (client_id) as the one refreshing it","If the account itself changed (family/enterprise migration), wipe both tokens and re-authorize from scratch to re-baseline the `sub`","Never paste refresh tokens between different driver instances or accounts"],"exampleFix":"# before — token pasted from another account/client\nrefresh_token: eyJhbGciOi...subA...\n\n# after — re-run in-browser QR authorization for this storage,\n# then keep the freshly issued matching pair together:\nrefresh_token: eyJhbGciOi...subB...   # same sub as client_id owner\naccess_token:  eyJhbGciOi...subB...","handlingStrategy":"validation","validationCode":"// Before saving config, verify the token's sub matches the intended account\nsub, err := getSub(proposedRefreshToken)\nif err != nil {\n    return fmt.Errorf(\"token unreadable: %w\", err)\n}\nif existingSub != \"\" && sub != existingSub {\n    return errors.New(\"refresh token belongs to a different account (sub mismatch)\")\n}","typeGuard":"// Go\nfunc isSubMismatch(err error) bool {\n    return err != nil && strings.Contains(err.Error(), \"sub not match\")\n}","tryCatchPattern":"if _, _, err := d.refreshToken(); isSubMismatch(err) {\n    // halt automated retries; re-run the full QR authorization for this storage\n    return err\n}","preventionTips":["Never paste refresh tokens between accounts, apps, or driver instances","Complete the whole auth flow (QR) per storage so tokens and client_id stay paired","If an account migrates (family/enterprise), wipe both tokens and re-authorize from scratch"],"tags":["aliyundrive-open","jwt","sub-claim","account-mismatch","auth"],"backgroundTag":null,"analyzedSha":"843d9dc8149126976b2625911e45a4d3ffd6f2f5","analyzedAt":"2026-08-15T12:14:11.722Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}