Embed and authenticate Grafana in a iframe

Grafana

Grafana needs to be configured to allow header based auth from the auth proxy

Grafana config:

[auth.proxy]
enabled = true
# HTTP Header name that will contain the username or email
header_name = X-WEBAUTH-USER
# HTTP Header property, defaults to `username` but can also be `email`
header_property = username
# Set to `true` to enable auto sign up of users who do not exist in Grafana DB. Defaults to `true`.
auto_sign_up = false
# Limit where auth proxy requests come from by configuring a list of IP addresses.
# This can be used to prevent users spoofing the X-WEBAUTH-USER header.
# Example `whitelist = 192.168.1.1, 192.168.1.0/24, 2001::23, 2001::0/120`
whitelist = <reverse proxy server ip>

Apache

Then Apache needs to be configured as a auth proxy

following is the Apache config:


# grafanaservice Proxy for embedding grafana into webpage
# Rewrite HTTP -> HTTPS
<Virtualhost *:80>
  ServerName grafanaservice.example.com  
  RedirectPermanent / https://grafanaservice.example.com
</Virtualhost>

<Virtualhost *:443>
  ServerName grafanaservice.example.com

  SSLEngine On
  SSLCertificateFile /etc/apache2/ssl.crt/example.com.crt
  SSLCertificateKeyFile /etc/apache2/ssl.key/example.com.key

  ErrorLog /var/log/apache2/grafanaservice_error.log
  CustomLog /var/log/apache2/grafanaservice_access.log combined   

  # exclude the service worker from proxying
  # the service worker file must be accesible from outside on the root of the subdomain (grafanaservice.example.com/sw)
  # we had to strip the extension ".js" to make it work (some wierd apache bug?)
  Alias /sw "/srv/www/htdocs/serviceworker/sw.js"
  ProxyPass /sw !

  #service worker installer script (again without ".html"):
  Alias /install_sw "/srv/www/htdocs/serviceworker/install_sw.html"
  ProxyPass /install_sw !

  # CORS headers.
  Header always set Access-Control-Allow-Origin "example.com"
  Header always set Access-Control-Allow-Methods "POST, GET, OPTIONS, DELETE, PUT"
  Header always set Access-Control-Max-Age "1000"
  Header always set Access-Control-Allow-Headers "x-requested-with, Content-Type, origin, authorization, accept, client-security-token"

  # Added a rewrite to respond with a 200 SUCCESS on every OPTIONS request.
  # Grafana does not handle options requests, but xhr (its probably xhr) only works if response is 200.
  RewriteEngine On
  RewriteCond %{REQUEST_METHOD} OPTIONS
  RewriteRule ^(.*)$ $1 [R=200,L]

  RequestHeader unset Authorization

  ProxyRequests Off
  ProxyPass / http://<grafana server ip>:3000/
  ProxyPassReverse / http://<grafana server ip>:3000/
</Virtualhost>
⚠️ IMPORTANT: Change SSL certificate path and domains.

i've not yet tested how much of this is actually necessary, since i had to test some stuff (header unset authorization, 200 on OPTIONS)

Service Worker

ServiceWorkers run on a per-subdomain basis. This means the service worker for authenticating needs to be hosted on the same subdomain as grafana. ServiceWorkers also stay installed even if they are not loaded on the current page (you can load the service worker on the home page using the following 0 size iframe, and authenticate multiple grafana iframes on pages that dont load the service worker themselfes)

Then the service worker that handles the authorization needs to be coded to get the username from somewhere, in this case an argument from the URL This is made more secure by adding a few random characters at the end of the username so it cannot be easily guessed and spoofed by someone else

First the service worker needs to be installed, this happens through an iframe with 0 size that just installs the service worker, because the service worker can't be installed from another domain. This iframe should be placed in a way, that ensures it is loaded before the grafana iframe.

<iframe src="grafanaservice.example.com/install_sw" width="0" height="0"></iframe>
⚠️ IMPORTANT: This iframe needs to be loaded before the page that embeds Grafana is opened to give the service worker time to install.

The install_sw.html:

<!DOCTYPE html>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />

<body>
    <script>
        if ('serviceWorker' in navigator) {
            navigator.serviceWorker.register('/sw').then(function (reg) {
                if (reg.installing) {
                    console.log('Service worker installing');
                } else if (reg.waiting) {
                    console.log('Service worker installed');
                } else if (reg.active) {
                    console.log('Service worker active');
                }
            }).catch(function (error) {
                // registration failed
                console.log('Registration failed with ' + error);
            });;
        }

    </script>
</body>

The console log messages can be left out if undesired

Then you need to define the service worker sw.js:

importScripts('https://cdn.jsdelivr.net/npm/idb-keyval@3/dist/idb-keyval-iife.min.js');

self.addEventListener('fetch', event => {
  event.waitUntil(saveUsername(event));
  event.respondWith(idbKeyval.get("username").then(username => {
    request = event.request;
    if (typeof username === 'string' || username instanceof String) {
      request = editRequest(request, username);
    }
    return fetch(request); // return request, edited or not
  })); // respond with header
});

async function saveUsername(event) {
  request = event.request;
  referrer = new URL(request.referrer); // make url object to easily extract hostname
  splitted = referrer.host.split("."); // extract hostname and split
  domain = splitted.slice(splitted.length - 2, splitted.length).join("."); // get last 2 elements of splitted hostname as string, this is the base domain
  if (domain === "example.com") { // check against base domain
    url = request.url;
    //console.log("url is: " + url);
    username = new URL(url).searchParams.get("username"); // set username from query parameter
    if (typeof username === 'string' || username instanceof String) {
      await idbKeyval.set("username", username);
      //console.log("store username: " + username);
    }
  }
}

function editRequest(request, username) {
  const newRequest = new Request(request, {
    method: request.method,
    headers: request.headers,
    mode: 'same-origin', // need to set this properly
    credentials: request.credentials,
    redirect: 'manual'   // let browser handle redirects
  })
  request = newRequest;
  request.headers.append("X-WEBAUTH-USER", username); // add user header
  // console.log("login to grafana with: " + username);
  return request;
}
⚠️ IMPORTANT: Change the domain on line 20 to your base domain.

Here is where the magic happens. We add a event listener to fetch, which catches all requests made by grafana. If the referrer is the example.com we save the username that the iframe has as a html query parameter in IndexedDB. Then we edit the request to contain the X-WEBAUTH-USER header with the correct username.

There are still some things to work out, like how to protect the admin account but all in all this works quite nicely.

Grafana Embed

Now we can send the username as a query parameter to be picked up by the service worker, in this case the example username myuser_sadf237sad1 is used.

⚠️ IMPORTANT: Using this the username is enough to log in. To prevent unauthorized access to user accounts, you need to prevent people from knowing or guessing usernames they should not know.

To embed grafana a normal iframe is suffecient:

<iframe id="myiframe" width="100%" src="https://grafanaservice.example.com?refresh=30s&theme=light&kiosk=tv&username=myuser_sadf237sad1"></iframe>

Keep in mind that the username argument is only read by the service worker and is not used by Grafana, which means this wont work without the ServiceWorker

Recap

  1. We've changed the grafana config to allow login without password from our webserver IP
  2. We've changed the Apache config to ProxyPass grafana, but also make /install_sw and /sw available on the same subdomain.
  3. We've added a 0 size iframe that opens grafanaservice.example.com/install_sw to install the ServiceWorker
  4. We've made a ServiceWorker that adds the X-WEBAUTH-USER header to all grafana requests.
  5. We've embedded the grafana panel we want in a iframe.

Now you should be able to embed as many panels as you want with as many users as you want into your website!