docker/cli · error

failed to store tokens

Error message

failed to store tokens: %w

What it means

Returned by OAuthManager.Login when storeTokensInStore fails. After parsing claims, the manager persists both the access token (under accessTokenKey) and refresh token (refreshTokenKey, as '<refresh>..<access>' split) into the credential store; if either Store call errors, login fails wrapped with %w.

Solutions

  1. Ensure the credential helper is installed and unlocked (e.g. `pass init <gpg-key>`, unlock keychain).
  2. Set credsStore/credHelpers correctly in ~/.docker/config.json, or remove them to use the file store.
  3. Check write permissions and free space on ~/.docker/config.json.
  4. Re-run `docker login` after fixing the store.

Example fix

# before: pass not initialized -> failed to store tokens
pass init A1B2C3D4
# config.json uses credsStore=pass; ensure helper exists
{
  "credsStore": "pass"
}
docker login
# fallback: use file store (remove credsStore) for CI
Defensive patterns

Strategy: try-catch

Validate before calling

// ensure the credential store is writable before login
if err := store.Store(types.AuthConfig{Username:"_probe", Password:"", ServerAddress:"_probe"}); err != nil { return err }

Try / catch

if isStoreError(err) { // locked keyring / missing helper
    configureFileStore(); retry login
}

Prevention

When it happens

Trigger: The credential store backend (e.g. pass/darwin keychain/wincred/secretservice/file) rejects the write — locked keyring, missing helper binary, permission denied on ~/.docker/config.json, disk full.

Common situations: GNOME keyring locked; `pass` not initialized (no gpg key); credential helper binary missing; read-only home; corrupt config.json; CI with no usable secret store.

Related errors


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

Appendix: source

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

	_, _ = fmt.Fprint(w, "\nWaiting for authentication in the browser…\n")
	var tokenRes api.TokenResponse
	select {
	case <-ctx.Done():
		return nil, errors.New("login canceled")
	case err := <-waitForTokenErrChan:
		return nil, fmt.Errorf("failed waiting for authentication: %w", err)
	case tokenRes = <-tokenResChan:
	}

	claims, err := oauth.GetClaims(tokenRes.AccessToken)
	if err != nil {
		return nil, fmt.Errorf("failed to parse token claims: %w", err)
	}

	err = m.storeTokensInStore(tokenRes, claims.Domain.Username)
	if err != nil {
		return nil, fmt.Errorf("failed to store tokens: %w", err)
	}

	pat, err := m.api.GetAutoPAT(ctx, m.audience, tokenRes)
	if err != nil {
		return nil, err
	}

	return &types.AuthConfig{
		Username:      claims.Domain.Username,
		Password:      pat,
		ServerAddress: registry.IndexServer,
	}, nil
}

// Logout fetches the refresh token from the store and revokes it
// with the configured oauth tenant. The stored access and refresh
// tokens are then erased from the store.
// If the refresh token is not found in the store, an error is not

View on GitHub (pinned to 4f84911bfe)