Tencent/WeKnora · error

OAuth token response did not contain an access_token

Error message

OAuth token response did not contain an access_token

What it means

SaveToken persists a transport.Token but refuses to store a nil token or one with an empty AccessToken — such a token is unusable for authenticated requests. This error indicates the OAuth token endpoint response was malformed or the exchange produced no access token.

Source

Thrown at internal/mcp/oauth_tokenstore.go:88

	}
	if row == nil || row.AccessToken == "" {
		return nil, transport.ErrNoToken
	}
	return &transport.Token{
		AccessToken:  row.AccessToken,
		RefreshToken: row.RefreshToken,
		TokenType:    row.TokenType,
		ExpiresAt:    row.ExpiresAt,
	}, nil
}

// SaveToken persists a freshly issued or refreshed token.
func (s *dbTokenStore) SaveToken(ctx context.Context, token *transport.Token) error {
	if err := ctx.Err(); err != nil {
		return err
	}
	if token == nil || token.AccessToken == "" {
		return fmt.Errorf("OAuth token response did not contain an access_token")
	}
	if token.TokenType == "" {
		token.TokenType = "Bearer"
	}
	expiresAt := token.ExpiresAt
	if expiresAt.IsZero() && token.ExpiresIn > 0 {
		expiresAt = time.Now().Add(time.Duration(token.ExpiresIn) * time.Second)
	}
	principal := s.principal.Normalize()
	return s.repo.SaveTokenForPrincipal(ctx, &types.MCPOAuthToken{
		TenantID:      s.tenantID,
		PrincipalType: principal.Type,
		PrincipalID:   principal.ID,
		UserID:        principal.StorageID(),
		ServiceID:     s.serviceID,
		AccessToken:   token.AccessToken,
		RefreshToken:  token.RefreshToken,
		TokenType:     token.TokenType,

View on GitHub (pinned to 988cbb0330)

Solutions

  1. Inspect the token exchange response before SaveToken: log (redacted) status code and body to see whether the provider returned an error like invalid_grant.
  2. Verify client_id/client_secret, redirect_uri, and authorization code are correct and the code was not already consumed (codes are single-use).
  3. Check your provider's token response field name matches what transport.Token parsing expects (access_token).
  4. Never call SaveToken after a failed exchange — gate it on a successful, non-empty exchange result.

Example fix

// before
tok, _ := exchange(ctx, code)
store.SaveToken(ctx, tok)
// after
tok, err := exchange(ctx, code)
if err != nil { return fmt.Errorf("token exchange failed: %w", err) }
if tok == nil || tok.AccessToken == "" {
    return fmt.Errorf("exchange returned no access token; check provider error response")
}
return store.SaveToken(ctx, tok)
Defensive patterns

Strategy: validation

Validate before calling

if token == nil || token.AccessToken == "" {
    return fmt.Errorf("refusing to save token: exchange returned no access_token")
}

Type guard

func hasAccessToken(t *transport.Token) bool {
    return t != nil && t.AccessToken != ""
}

Try / catch

if err := store.SaveToken(ctx, token); err != nil {
    if strings.Contains(err.Error(), "did not contain an access_token") {
        return fmt.Errorf("token exchange failed; verify provider credentials and error response: %w", err)
    }
    return err
}

Prevention

When it happens

Trigger: SaveToken called with a nil *transport.Token, or with a token whose AccessToken field is empty (provider returned error/JSON without access_token, or the token struct was constructed without the access token).

Common situations: OAuth provider returned 4xx with a JSON body the transport parsed into an empty token; wrong token endpoint or client credentials caused an error response; provider uses a nonstandard field name for the access token; network proxy stripped the response body.

Related errors


AI-assisted analysis of Tencent/WeKnora@988cbb0330 (2026-09-02). Data as JSON: /api/errors/dfbc157cfe3ce740. Report an issue: GitHub.