AlistGo/alist · critical

failed to refresh token: sub not match

Error message

failed to refresh token: sub not match

What it means

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.

Source

Thrown at drivers/aliyundrive_open/util.go:151

		}
	}
	if e.Code != "" {
		return "", "", fmt.Errorf("failed to refresh token: %s", e.Message)
	}
	refresh, access := utils.Json.Get(res.Body(), "refresh_token").ToString(), utils.Json.Get(res.Body(), "access_token").ToString()
	if refresh == "" {
		return "", "", fmt.Errorf("failed to refresh token: refresh token is empty, resp: %s", res.String())
	}
	curSub, err := getSub(d.RefreshToken)
	if err != nil {
		return "", "", err
	}
	newSub, err := getSub(refresh)
	if err != nil {
		return "", "", err
	}
	if curSub != newSub {
		return "", "", errors.New("failed to refresh token: sub not match")
	}
	return refresh, access, nil
}

func getSub(token string) (string, error) {
	segments := strings.Split(token, ".")
	if len(segments) != 3 {
		return "", errors.New("not a jwt token because of invalid segments")
	}
	bs, err := base64.RawStdEncoding.DecodeString(segments[1])
	if err != nil {
		return "", errors.New("failed to decode jwt token")
	}
	return utils.Json.Get(bs, "sub").ToString(), nil
}

func (d *AliyundriveOpen) refreshToken(ctx context.Context) error {
	if d.ref != nil {

View on GitHub (pinned to 843d9dc814)

Solutions

  1. 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
  2. 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
  3. If the account itself changed (family/enterprise migration), wipe both tokens and re-authorize from scratch to re-baseline the `sub`
  4. Never paste refresh tokens between different driver instances or accounts

Example fix

# before — token pasted from another account/client
refresh_token: eyJhbGciOi...subA...

# after — re-run in-browser QR authorization for this storage,
# then keep the freshly issued matching pair together:
refresh_token: eyJhbGciOi...subB...   # same sub as client_id owner
access_token:  eyJhbGciOi...subB...
Defensive patterns

Strategy: validation

Validate before calling

// Before saving config, verify the token's sub matches the intended account
sub, err := getSub(proposedRefreshToken)
if err != nil {
    return fmt.Errorf("token unreadable: %w", err)
}
if existingSub != "" && sub != existingSub {
    return errors.New("refresh token belongs to a different account (sub mismatch)")
}

Type guard

// Go
func isSubMismatch(err error) bool {
    return err != nil && strings.Contains(err.Error(), "sub not match")
}

Try / catch

if _, _, err := d.refreshToken(); isSubMismatch(err) {
    // halt automated retries; re-run the full QR authorization for this storage
    return err
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of AlistGo/alist@843d9dc814 (2026-08-15). Data as JSON: /api/errors/bfeee34e08f16e12. Report an issue: GitHub.