docker/cli · warning

credentials erased successfully, but there was a failure to…

Error message

credentials erased successfully, but there was a failure to revoke the OAuth refresh token with the tenant: %w

What it means

Returned by OAuthManager.Logout after eraseTokensFromStore succeeded but RevokeToken (POST <TenantURL>/oauth/revoke) failed. Locally stored tokens are gone, but the tenant still considers the refresh token valid. The error is wrapped with %w so callers can see the revoke failure reason.

Solutions

  1. Accept the warning if local erasure is sufficient for your threat model (token will expire at tenant naturally).
  2. If you must revoke, manually revoke via the Hub UI (revoke the token/session).
  3. Check network/proxy to /oauth/revoke and retry logout only if a token is still stored.

Example fix

# before
docker logout
# -> credentials erased successfully, but revoke failed
# after: token is gone locally; revoke from Hub UI
# hub.docker.com -> Account Settings -> Security -> revoke the session
# (or ignore; the refresh token will expire per tenant policy)
Defensive patterns

Strategy: fallback

Try / catch

// erase succeeded; revoke failed -> treat as soft warning
if strings.Contains(err.Error(), "credentials erased successfully") { log.Warn(err); return nil }

Prevention

When it happens

Trigger: Logout reaches the revoke step with a network/TLS/HTTP failure (non-200 from /oauth/revoke, or transport error). Because erase already happened, a retry needs a stored token that no longer exists.

Common situations: Transient network blip or tenant outage at the exact revoke call; corporate proxy blocking /oauth/revoke; the refresh token already expired at the tenant so revoke 400s.

Related errors


AI-assisted analysis of docker/cli@4f84911bfe (2026-08-07). Data as JSON: /api/errors/bd054f6c4e6f22d0. Report an issue: GitHub.

Appendix: source

Thrown at internal/oauth/manager/manager.go:184

	if err != nil {
		return err
	}
	if refreshConfig.Password == "" {
		return nil
	}
	parts := strings.Split(refreshConfig.Password, "..")
	if len(parts) != 2 {
		// the token wasn't stored by the CLI, so don't revoke it
		// or erase it from the store/error
		return nil
	}
	// erase the token from the store first, that way
	// if the revoke fails, the user can try to logout again
	if err := m.eraseTokensFromStore(); err != nil {
		return fmt.Errorf("failed to erase tokens: %w", err)
	}
	if err := m.api.RevokeToken(ctx, parts[0]); err != nil {
		return fmt.Errorf("credentials erased successfully, but there was a failure to revoke the OAuth refresh token with the tenant: %w", err)
	}
	return nil
}

const (
	accessTokenKey  = registry.IndexServer + "access-token"
	refreshTokenKey = registry.IndexServer + "refresh-token"
)

func (m *OAuthManager) storeTokensInStore(tokens api.TokenResponse, username string) error {
	return errors.Join(
		m.store.Store(types.AuthConfig{
			Username:      username,
			Password:      tokens.AccessToken,
			ServerAddress: accessTokenKey,
		}),
		m.store.Store(types.AuthConfig{
			Username:      username,

View on GitHub (pinned to 4f84911bfe)