siyuan-note/siyuan · error

OAuth token endpoint returned no access token

Error message

OAuth token endpoint returned no access token

What it means

The token endpoint returned a 2xx response, but the JSON body's `access_token` field was empty. SiYuan refuses to treat a token response as valid when the access token itself is missing, since the subsequent API call would have no credential to send.

Source

Thrown at kernel/mcp/client/oauth.go:687

	}
	defer resp.Body.Close()
	body, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
	if err != nil {
		return nil, nil, err
	}
	if resp.StatusCode < 200 || resp.StatusCode >= 300 {
		tokenErr := &oauthTokenError{}
		if json.Unmarshal(body, tokenErr) != nil || tokenErr.Code == "" {
			return nil, nil, fmt.Errorf("OAuth token endpoint returned %s", resp.Status)
		}
		return nil, tokenErr, tokenErr
	}
	result := &oauthTokenResponse{}
	if err = json.Unmarshal(body, result); err != nil {
		return nil, nil, err
	}
	if result.AccessToken == "" {
		return nil, nil, fmt.Errorf("OAuth token endpoint returned no access token")
	}
	if result.TokenType != "" && !strings.EqualFold(result.TokenType, "Bearer") {
		return nil, nil, fmt.Errorf("OAuth token endpoint returned unsupported token type %q", result.TokenType)
	}
	return result, nil, nil
}

func applyOAuthClientAuthentication(values url.Values, req *http.Request, credential oauthCredential) {
	switch credential.TokenAuthMethod {
	case "client_secret_basic":
		if req != nil {
			req.SetBasicAuth(url.QueryEscape(credential.ClientID), url.QueryEscape(credential.ClientSecret))
		}
	default:
		if values != nil {
			values.Set("client_id", credential.ClientID)
			if credential.TokenAuthMethod == "client_secret_post" && credential.ClientSecret != "" {
				values.Set("client_secret", credential.ClientSecret)

View on GitHub (pinned to 251596fc0d)

Solutions

  1. Capture and print the raw token-endpoint response body to confirm it is a valid OAuth token JSON with an `access_token` field.
  2. Verify the endpoint URL is the token endpoint, not a userinfo or introspection endpoint.
  3. Re-check the `grant_type` and parameters sent so the IdP actually issues an access token.
Defensive patterns

Strategy: validation

Validate before calling

// Decode the token response defensively and require access_token before use.
var tok oauthTokenResponse
if err := json.Unmarshal(body, &tok); err != nil {
    return fmt.Errorf("token response not JSON: %w", err)
}
if strings.TrimSpace(tok.AccessToken) == "" {
    return fmt.Errorf("no access_token in body: %s", string(body))
}

Prevention

When it happens

Trigger: `oauthTokenResponse.AccessToken` is empty after a successful `json.Unmarshal`. The IdP returned 200 with a body that omits `access_token` (e.g. it returned only an `id_token`, an error wrapped in 200, or a non-OAuth JSON shape).

Common situations: The server is not actually an OAuth 2.0 token endpoint (pointed at a userinfo or userinfo-like endpoint). The grant returned an error that the IdP wrapped with a 200 status. The `Accept` header negotiation returned a non-token content type whose JSON lacks the field.

Related errors


AI-assisted analysis of siyuan-note/siyuan@251596fc0d (2026-08-12). Data as JSON: /api/errors/c0d68be921a16b77. Report an issue: GitHub.