IceWhaleTech/CasaOS · error

refresh token is empty

Error message

refresh token is empty

What it means

During the OneDrive OAuth code exchange (grant_type=authorization_code), the token endpoint responded successfully but the parsed response contained an empty refresh_token field. The driver requires a refresh token for long-lived access, so it refuses to proceed. This typically means the authorization flow did not request the offline_access scope.

Source

Thrown at drivers/onedrive/util.go:89

	var resp base.TokenResp
	var e TokenErr

	res, err := base.RestyClient.R().SetResult(&resp).SetError(&e).SetFormData(map[string]string{
		"grant_type":    "authorization_code",
		"client_id":     d.ClientID,
		"client_secret": d.ClientSecret,
		"code":          d.Code,
		"redirect_uri":  d.RedirectUri,
	}).Post(url)
	if err != nil {
		return err
	}
	logger.Info("get refresh token", zap.String("res", res.String()))
	if e.Error != "" {
		return fmt.Errorf("%s", e.ErrorDescription)
	}
	if resp.RefreshToken == "" {
		return errors.New("refresh token is empty")
	}
	d.RefreshToken, d.AccessToken = resp.RefreshToken, resp.AccessToken
	return nil
}

func (d *Onedrive) _refreshToken() error {
	url := d.GetMetaUrl(true, "") + "/common/oauth2/v2.0/token"
	var resp base.TokenResp
	var e TokenErr

	res, err := base.RestyClient.R().SetResult(&resp).SetError(&e).SetFormData(map[string]string{
		"grant_type":    "refresh_token",
		"client_id":     d.ClientID,
		"client_secret": d.ClientSecret,
		"redirect_uri":  d.RedirectUri,
		"refresh_token": d.RefreshToken,
	}).Post(url)
	if err != nil {

View on GitHub (pinned to 0d3b2f444e)

Solutions

  1. Ensure the authorize URL includes scope 'offline_access Files.ReadWrite.All' (offline_access is what makes the token endpoint issue a refresh_token).
  2. Generate a fresh authorization code and re-run the exchange immediately — codes are single-use and short-lived (~10 minutes).
  3. Verify redirect_uri in the token request exactly matches the one registered/used during authorization.
  4. Inspect the logged response body (logger.Info "get refresh token") to see what the endpoint actually returned.

Example fix

// before
config.AuthUrl = "https://login.microsoftonline.com/common/oauth2/v2.0/authorize?client_id=" + client_id + "&response_type=code&redirect_uri=...&scope=files.readwrite.all&state=..."

// after
config.AuthUrl = "https://login.microsoftonline.com/common/oauth2/v2.0/authorize?client_id=" + client_id + "&response_type=code&redirect_uri=...&scope=offline_access+files.readwrite.all&state=..."
Defensive patterns

Strategy: validation

Validate before calling

// Before exchanging, verify the authorize flow used offline_access
if !strings.Contains(authUrl, "offline_access") {
	return errors.New("authorize URL must include offline_access scope")
}

Try / catch

if err := d.GetRefreshToken(); err != nil {
	if strings.Contains(err.Error(), "refresh token is empty") {
		// re-run authorization flow with correct scope; do not retry exchange with same code
	}
	return err
}

Prevention

When it happens

Trigger: Calling the initial token exchange with d.Code (the OAuth authorization code) when the authorize URL lacks the 'offline_access' scope, the code is invalid/expired, or the response was an error page that parsed to empty fields. Only triggered on first-time setup when RefreshToken is empty.

Common situations: Misconfigured AuthUrl missing 'offline_access' in the scope parameter; reusing an already-consumed authorization code; a tenant admin policy that strips refresh tokens; redirect_uri mismatch causing a soft-failed response.

Related errors


AI-assisted analysis of IceWhaleTech/CasaOS@0d3b2f444e (2026-08-15). Data as JSON: /api/errors/65b09e067995c25a. Report an issue: GitHub.