sipeed/picoclaw · error

no refresh token available

Error message

no refresh token available

What it means

RefreshAccessToken (pkg/auth/oauth.go:440) refuses to run when cred.RefreshToken is an empty string. The credential was created without a refresh token — typically via LoginPasteToken (API-key auth sets only AccessToken) or an OAuth flow where the provider did not return refresh_token — so there is nothing to refresh with.

Source

Thrown at pkg/auth/oauth.go:440

		return nil, fmt.Errorf("reading device token response: %w", err)
	}

	var tokenResp struct {
		AuthorizationCode string `json:"authorization_code"`
		CodeChallenge     string `json:"code_challenge"`
		CodeVerifier      string `json:"code_verifier"`
	}
	if err := json.Unmarshal(body, &tokenResp); err != nil {
		return nil, err
	}

	redirectURI := cfg.Issuer + "/deviceauth/callback"
	return ExchangeCodeForTokens(cfg, tokenResp.AuthorizationCode, tokenResp.CodeVerifier, redirectURI)
}

func RefreshAccessToken(cred *AuthCredential, cfg OAuthProviderConfig) (*AuthCredential, error) {
	if cred.RefreshToken == "" {
		return nil, fmt.Errorf("no refresh token available")
	}

	data := url.Values{
		"client_id":     {cfg.ClientID},
		"grant_type":    {"refresh_token"},
		"refresh_token": {cred.RefreshToken},
		"scope":         {"openid profile email"},
	}
	if cfg.ClientSecret != "" {
		data.Set("client_secret", cfg.ClientSecret)
	}

	tokenURL := cfg.Issuer + "/oauth/token"
	if cfg.TokenURL != "" {
		tokenURL = cfg.TokenURL
	}

	resp, err := http.PostForm(tokenURL, data)

View on GitHub (pinned to 49183d7e8d)

Solutions

  1. Check cred.AuthMethod and cred.RefreshToken before calling RefreshAccessToken; if empty, prompt re-login via OAuth (LoginDeviceCode / browser flow) to obtain a refresh token
  2. For providers that need it, request offline access (e.g. Google's access_type=offline&prompt=consent — already set in buildAuthorizeURL for accounts.google.com issuers)
  3. If using API-key auth, skip refresh entirely and keep the static AccessToken until it expires or is revoked
  4. Persist the full credential after login so RefreshToken survives restarts

Example fix

// before
refreshed, err := auth.RefreshAccessToken(cred, cfg)

// after
if cred.RefreshToken == "" {
	return nil, fmt.Errorf("cannot refresh: credential has no refresh token (auth method %q); re-login with OAuth", cred.AuthMethod)
}
refreshed, err := auth.RefreshAccessToken(cred, cfg)
Defensive patterns

Strategy: validation

Validate before calling

func canRefresh(cred *auth.AuthCredential) bool {
	return cred != nil && strings.TrimSpace(cred.RefreshToken) != ""
}

// before refreshing:
if !canRefresh(cred) {
	return fmt.Errorf("no refresh token on credential (auth method %q); re-login required", cred.AuthMethod)
}

Type guard

func isNoRefreshTokenError(err error) bool {
	return err != nil && strings.Contains(err.Error(), "no refresh token available")
}

Try / catch

refreshed, err := auth.RefreshAccessToken(cred, cfg)
if err != nil {
	if isNoRefreshTokenError(err) {
		// not retriable: route the user to re-login
		refreshed, err = reloginFlow(cfg)
	}
	if err != nil {
		return err
	}
}

Prevention

When it happens

Trigger: Calling RefreshAccessToken on a credential whose AuthMethod is "token" (pasted API key), or an OAuth credential where the token response omitted refresh_token (offline access not granted).

Common situations: Mixing auth modes: user logged in with a pasted key but code path assumes OAuth; Google-style issuers that require access_type=offline for refresh tokens; provider returned 200 without refresh_token on first exchange; tests constructing AuthCredential literals without RefreshToken.

Related errors


AI-assisted analysis of sipeed/picoclaw@49183d7e8d (2026-08-15). Data as JSON: /api/errors/a8f06eca18dcd29a. Report an issue: GitHub.