Help build the future of open source observability software Open positions

Check out the open source projects we support Downloads

Grot cannot remember your choice unless you click the consent notice at the bottom.

How to authenticate with third-party APIs in your Grafana app plugin

How to authenticate with third-party APIs in your Grafana app plugin

2024-07-25 7 min

Whether they’re for synthetic monitoring, large-language models, or some other use case, Grafana application plugins are a fantastic way to enhance your overall Grafana experience. Data for these custom experiences can come from a variety of sources, including nested data sources. However, they can also come from third-party APIs, which usually require authentication to access.

This blog post explains, step by step, how to securely authenticate against third-party APIs when developing your Grafana app plugin. By the end, you’ll understand how to handle authentication securely, ensuring that sensitive credentials are protected throughout interactions with third-party APIs.

Note: For those new to Grafana, it might be helpful to understand the distinctions between our panel plugins, data source plugins, and application plugins before continuing with this post. Each plugin type serves a unique purpose and has different capabilities within the Grafana ecosystem. You can find a detailed overview of each type in our developer documentation.

The need for secure authentication with third-party APIs

When integrating with third-party APIs, it’s crucial to secure your connection and use authentication credentials to protect sensitive data from unauthorized access. The mishandling of authentication can expose you to several risks, including credential leaks, man-in-the-middle attacks, replay attacks, impersonation, and compliance violations. This could happen, for example, if you simply store your API authentication credentials in plain text, directly inside your frontend components, and then use them to call a third-party API.

To avoid these risks, it’s essential to implement robust security measures for API credentials and ensure encrypted communications with third-party services.

Secure API authentication in Grafana app plugins

While the following steps will help you securely authenticate against a third-party API within a Grafan app plugin, they can also be used within a backend data source plugin.

There are several best practices to ensure credentials are handled safely and that plugins do not expose sensitive information, either in transit or at rest:

  1. In transit: Ensure you’re using HTTPS at all times to guarantee secure communication.
  2. At rest: Use Grafana’s SecureJsonData plugin configuration to securely store sensitive data like API keys.
  3. Minimize scope of visibility: Use the Resources feature in the Grafana plugin architecture to act as an intermediary for requests to third-party APIs. This lets you securely fetch data in the frontend without exposing credentials to the client.
A diagram depicting best practices to ensure credentials are handled safely.

Handling API keys

Grafana provides a mechanism to store sensitive information, such as API keys or passwords, using secure JSON data fields within the plugin’s configuration. These credentials are encrypted and can only be accessed server-side, ensuring they are not exposed in the browser.

When bootstrapping your Grafana app plugin, using the Create Plugin CLI tool, your app plugin source code will contain a configuration page with an example for storing secure credentials. This can be found in the src/components/AppConfig/AppConfig.tsx file.

A screenshot of the out-of-the-box Grafana app plugin configuration settings page.
The out-of-the-box Grafana app plugin configuration settings page.

You will see that there are two pieces of data being stored as part of the plugin’s configuration: apiUrl and apiKey.

Inside of the configuration page’s source code, you’ll notice the Submit button has an onClick handler that calls the updatePluginAndReload function as shown below:

updatePluginAndReload(plugin.meta.id, {
                enabled,
                pinned,
                jsonData: {
                  apiUrl: state.apiUrl,
                },
                secureJsonData: state.isApiKeySet
                  ? undefined
                  : {
                      apiKey: state.apiKey,
                    },
              })

This call to the updatePluginAndReload function saves the user’s configuration form input in the plugin’s configuration settings. The apiUrl is being stored as plain JSON data, while the apiKey is being stored as secure JSON data. This means the frontend of your app plugin has no way to retrieve the raw value of the apiKey which can therefore only be used by the backend part of your plugin. This ensures that your API credentials remain private and secure.

Remember to run your development Grafana instance and navigate to your app plugin’s configuration page to update the configuration appropriately with your third-party API credentials.

Alternatively, for development purposes, you may want to enter default values for your plugin’s configuration via the provisioning/plugins/app.yaml file, as shown below. This will default your plugin’s configuration to the values entered in this file each time the Docker containers are restarted.

A screenshot showing default values for the plugin's configuration.

As mentioned earlier, it is incredibly important that you use HTTPS to create a secure connection to your API.

Creating a Resources endpoint to call the third-party API

Grafana plugins can use the Resources functionality to allow the frontend to retrieve arbitrary data from the plugin’s backend. What you decide to expose via the Resources endpoints is entirely up to you as the developer, but in this instance, we will use a Resources endpoint to retrieve data from an authenticated third-party API.

Resources endpoints within your app plugin are configured inside the pkg/plugin/resources.go file. Inside this file there is a registerRoutes function that defines which Resources endpoints are available within your app plugin.

To register a new endpoint, define it within the registerRoutes function, as shown below:

func (a *App) registerRoutes(mux *http.ServeMux) {
    mux.HandleFunc("/ping", a.handlePing)
    mux.HandleFunc("/echo", a.handleEcho)
    mux.HandleFunc("/my-new-endpoint", a.handleMyNewEndpoint) // Newly registered endpoint
}

Above we have defined a new Resources endpoint that can be accessed at the Resources location for our plugin by the frontend – for example, /api/plugins/<your-plugin-id>/resources/my-new-endpoint.

This new endpoint is configured to be handled by a new function, a.handleMyNewEndpoint. We must implement this function, which is a standard Go HTTP handler, in order to return data to the frontend when the endpoint is called.

Let’s take a look at an example implementation that gets the API URL and API key from your plugin configuration, and then makes an authenticated HTTP request to the third-party API before returning the data to the client.

func (a *App) handleMyNewEndpoint(w http.ResponseWriter, req *http.Request) {
    // Only allow HTTP GET calls
    if req.Method != http.MethodGet {
   	 http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
   	 return
    }

    // Get the Plugin context
    pCtx := httpadapter.PluginConfigFromContext(req.Context())

    // Get the API Key from the plugin’s secure JSON configuration data
    apiKey, exists := pCtx.AppInstanceSettings.DecryptedSecureJSONData["apiKey"]
    if !exists {
   	http.Error(w, err.Error(), http.StatusInternalServerError)
	return
    }

    // Get the API URL from the plugin’s standard JSON configuration data
    var config pluginConfig
    if err := json.Unmarshal(pCtx.AppInstanceSettings.JSONData, &config); err != nil {
   	http.Error(w, err.Error(), http.StatusInternalServerError)
	return
    }

    if config.ApiUrl == "" {
   	 return nil, errors.New("no api url configured")
    }

    // Define the full API endpoint to call (in this instance the /profile endpoint of the third-party API)
    apiUrl := fmt.Sprintf("%s/profile", config.ApiUrl)

    // Create a new HTTP request to the API
    client := &http.Client{}
    apiReq, err := http.NewRequest("GET", apiUrl, nil)
    if err != nil {
   	http.Error(w, err.Error(), http.StatusInternalServerError)
	return
    }

    // Set the Authorization header using the API Key
    apiReq.Header.Set("Authorization", fmt.Sprintf("Bearer %s", apiKey))

    // Make the request
    apiResp, err := client.Do(apiReq)
    if err != nil {
   	http.Error(w, err.Error(), http.StatusInternalServerError)
	return
    }
    defer apiResp.Body.Close()

    // Read the response body
    body, err := io.ReadAll(apiResp.Body)
    if err != nil {
   	http.Error(w, err.Error(), http.StatusInternalServerError)
	return
    }

    // Write the response body as JSON for the caller to consume
    w.Header().Set("Content-Type", "application/json")
    w.Write(body)
}

The above code uses the plugin’s configuration to call out to the authenticated third-party API’s /profile endpoint and return the data to the client.

Remember, in order for this new Resources endpoint to take effect, the backend source code of the plugin must be rebuilt using the mage -v build:linux command and then the Docker containers must be restarted using Docker Compose.

Calling the Resources endpoint from the frontend

Your plugin’s frontend can now use the newly created Resources endpoint to retrieve data from the authenticated third-party API.

To call the Resources endpoint, use the getBackendSrv().fetch() function, which is part of the @grafana/runtime package. We also use the lastValueFrom function, which is available as part of the rxjs package.

An example of this is shown below:

const response  = getBackendSrv().fetch({
  url: 'api/plugins/myorg-myplugin-app/resources/my-new-endpoint
});

const value = await lastValueFrom(response) as any;
console.log(value);

The above code will make a call to the new Resources endpoint, grab the value from the response, and output it to the browser’s console.

How to learn more

By following the steps above, you can securely authenticate against third-party APIs within your Grafana app plugin, ensuring that your integrations are not only powerful, but secure. This method protects sensitive credentials and maintains the integrity and security of your data flows.

For more information on Grafana plugin development and the Resources API, refer to the Grafana developer portal. Additionally, you can explore our community forums to learn best practices and get support from other Grafana developers.