How to authenticate with third-party APIs in your Grafana app plugin
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:
- In transit: Ensure you’re using HTTPS at all times to guarantee secure communication.
- At rest: Use Grafana’s SecureJsonData plugin configuration to securely store sensitive data like API keys.
- 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.
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.
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.
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.