{"record":{"id":"a8f06eca18dcd29a","repo":"sipeed/picoclaw","slug":"no-refresh-token-available","errorCode":null,"errorMessage":"no refresh token available","messagePattern":"no refresh token available","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"pkg/auth/oauth.go","lineNumber":440,"sourceCode":"\t\treturn nil, fmt.Errorf(\"reading device token response: %w\", err)\n\t}\n\n\tvar tokenResp struct {\n\t\tAuthorizationCode string `json:\"authorization_code\"`\n\t\tCodeChallenge     string `json:\"code_challenge\"`\n\t\tCodeVerifier      string `json:\"code_verifier\"`\n\t}\n\tif err := json.Unmarshal(body, &tokenResp); err != nil {\n\t\treturn nil, err\n\t}\n\n\tredirectURI := cfg.Issuer + \"/deviceauth/callback\"\n\treturn ExchangeCodeForTokens(cfg, tokenResp.AuthorizationCode, tokenResp.CodeVerifier, redirectURI)\n}\n\nfunc RefreshAccessToken(cred *AuthCredential, cfg OAuthProviderConfig) (*AuthCredential, error) {\n\tif cred.RefreshToken == \"\" {\n\t\treturn nil, fmt.Errorf(\"no refresh token available\")\n\t}\n\n\tdata := url.Values{\n\t\t\"client_id\":     {cfg.ClientID},\n\t\t\"grant_type\":    {\"refresh_token\"},\n\t\t\"refresh_token\": {cred.RefreshToken},\n\t\t\"scope\":         {\"openid profile email\"},\n\t}\n\tif cfg.ClientSecret != \"\" {\n\t\tdata.Set(\"client_secret\", cfg.ClientSecret)\n\t}\n\n\ttokenURL := cfg.Issuer + \"/oauth/token\"\n\tif cfg.TokenURL != \"\" {\n\t\ttokenURL = cfg.TokenURL\n\t}\n\n\tresp, err := http.PostForm(tokenURL, data)","sourceCodeStart":422,"sourceCodeEnd":458,"githubUrl":"https://github.com/sipeed/picoclaw/blob/49183d7e8daed0dba89ddbb6fcb60089401d9680/pkg/auth/oauth.go#L422-L458","documentation":"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.","triggerScenarios":"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).","commonSituations":"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.","solutions":["Check cred.AuthMethod and cred.RefreshToken before calling RefreshAccessToken; if empty, prompt re-login via OAuth (LoginDeviceCode / browser flow) to obtain a refresh token","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)","If using API-key auth, skip refresh entirely and keep the static AccessToken until it expires or is revoked","Persist the full credential after login so RefreshToken survives restarts"],"exampleFix":"// before\nrefreshed, err := auth.RefreshAccessToken(cred, cfg)\n\n// after\nif cred.RefreshToken == \"\" {\n\treturn nil, fmt.Errorf(\"cannot refresh: credential has no refresh token (auth method %q); re-login with OAuth\", cred.AuthMethod)\n}\nrefreshed, err := auth.RefreshAccessToken(cred, cfg)","handlingStrategy":"validation","validationCode":"func canRefresh(cred *auth.AuthCredential) bool {\n\treturn cred != nil && strings.TrimSpace(cred.RefreshToken) != \"\"\n}\n\n// before refreshing:\nif !canRefresh(cred) {\n\treturn fmt.Errorf(\"no refresh token on credential (auth method %q); re-login required\", cred.AuthMethod)\n}","typeGuard":"func isNoRefreshTokenError(err error) bool {\n\treturn err != nil && strings.Contains(err.Error(), \"no refresh token available\")\n}","tryCatchPattern":"refreshed, err := auth.RefreshAccessToken(cred, cfg)\nif err != nil {\n\tif isNoRefreshTokenError(err) {\n\t\t// not retriable: route the user to re-login\n\t\trefreshed, err = reloginFlow(cfg)\n\t}\n\tif err != nil {\n\t\treturn err\n\t}\n}","preventionTips":["Check cred.RefreshToken before calling RefreshAccessToken","Request offline access scopes at authorization time","Persist complete credentials (including RefreshToken) immediately after login","Distinguish API-key credentials (AuthMethod 'token') from OAuth ones in refresh logic"],"tags":["oauth","refresh-token","validation","credentials","go"],"backgroundTag":null,"analyzedSha":"49183d7e8daed0dba89ddbb6fcb60089401d9680","analyzedAt":"2026-08-15T21:55:41.315Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}