Using NodeJS as auth proxy

I’m trying to use NodeJS to implement an auth proxy that passes authorized requests through to Grafana in a Docker container. It seems to work with one exception. When I attempt to load a dashboard the response from Grafana is strange. I get a 200 status code but when I push the response back up to the client that it renders a Grafana branded 404 page.

The goal is to embed dashboards (in kiosk mode) into our web app and have the nodeJS proxy app bang our website to confirm that the user is logged in by detecting a 302 when loading the login page. I pulled that part out of the code below to simplify things.

The request is sourced from an iframe with a JWT appended to the src URL that includes the data needed for the proxy to assemble the dashboard URL that it uses in the request to Grafana.

NodeJS code:

const fs = require('fs');
const http = require('http');
const express = require('express');
const app = express();
const jwt = require('jsonwebtoken');

const JWT_OPTIONS = {
   algorithms: ['HS256'],
   // probably want to set maxAge at some point
};

let sharedSecret;

function getAccessTokenSecret() {
   return new Promise((resolve, reject) => {
      fs.readFile('grafana.secret', 'utf8', (error, data) => {
         if (error) {
            reject(error);
         } else {
            console.log('Read token: ' + data.trim());
            resolve(data.trim());
         }
      });
   });
}

function startServer() {
   /* Read the secret from disk. */
   getAccessTokenSecret().then((secret) => {
      console.log('Secret: : ' + secret);
      sharedSecret = secret;
      app.listen(80, () => console.log('Proxy listening on port 80!'));
   });
}

function mapHeaders(src, target) {
   for (header in src.headers) {
      target.setHeader(header, src.headers[header]);
   }
}

function forwardRequest(req, decodedJwt, proxiedResponse) {
   let requestPath = '/d/'
      + decodedJwt['dashboard'] 
      + '/'
      + decodedJwt['dashboard_name']
      + '?orgId='
      + decodedJwt['orgid']
      + '&kiosk';
   let proxyHeaders = {};
   proxyHeaders['X-WEBAUTH-USER'] = decodedJwt['site_name'];
   proxyHeaders['Host'] = 'OBFUSCATED.OBFUSCATED.com';

   let options = {
      'host': '172.17.0.2',
      'path': requestPath,
      'port': 3000,
      'rejectUnauthorized': false,
      'requestCert': false,
      'headers': proxyHeaders,
      'setHost': false
   };

   const grafanaReq = http.get(options, (g_response) => {
      mapHeaders(g_response, proxiedResponse);
      proxiedResponse.setHeader('Pragma', 'no-cache');
      proxiedResponse.removeHeader('Content-Security-Policy');
      proxiedResponse.removeHeader('X-Frame-Options');
      proxiedResponse.writeHead(g_response.statusCode);

      g_response.on('data', (data) => {
         console.log('    Got data for dashboard');
         proxiedResponse.write(data);
      });

      g_response.on('end', () => {
         proxiedResponse.end();
         grafanaReq.end();
      });
   });
}

app.get('/d/*', (req, res) => {
   res.on('abort', () => console.log('Response aborted'));
   res.on('close', () => console.log('Response closed'));
   res.on('end', () => console.log('Response ended'));
   if (req.query.hasOwnProperty('access_token')) {
      try {
         // TODO: verify instead of decoding
         let decoded = jwt.decode(req.query.access_token, sharedSecret,
            JWT_OPTIONS);
         forwardRequest(req, decoded, res);
      } catch (ex) {
         res.writeHead(403, {'Content-Type': 'text/plain'});
         res.end('Forbidden');
      }
   } else {
      res.writeHead(403, {'Content-Type': 'text/plain'});
      res.end('Forbidden');
   }
});

/**
 * Allow all requests for Grafana's static resources through.
 * TODO: Is this ok?
 */
app.get('*', (req, res) => {
   var grafanaOptions = {
      'host': '172.17.0.2',
      'path': req.url,
      'port': 3000,
      'rejectUnauthorized': false,
      'requestCert': false,
   };

   const grafanaReq = http.get(grafanaOptions, (grafanaResponse) => {
      mapHeaders(grafanaResponse, res);

      grafanaResponse.on('data', (data) => {
         res.write(data);
      });

      grafanaResponse.on('end', () => {
         res.end();
         grafanaReq.end();
      });
   });
});

startServer();

NodeJS output:

Proxy listening on port 80!
Got dashboard request for /d/?access_token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE1MzE4Nzg0NjMsIm5iZiI6MTUzMTg3ODQ2MywiZXhwIjoxNTMxOTY0ODYzLCJzaXRlX25hbWUiOiJndW5uZXJhdXRvbW90aXZlIiwidXNlcmlkIjoyMzUsIm9yZ2lkIjoxLCJkYXNoYm9hcmRfbmFtZSI6ImRlLWZhY3RvcnktdmlldyIsImRhc2hib2FyZCI6IjNoMzVxNlNteiIsImRvenVraXNlc3Npb24iOiIzZWFmZmU5NmYxOGUyM2VhZDM4ZmMxNzNkYTY2ZDFkNCJ9.zPX8GtsGcLbgA90uOh384FXXvY9ch4g4zTaEr168erI
    decodedJwt { iat: 1531878463,
  nbf: 1531878463,
  exp: 1531964863,
  site_name: 'gunnerautomotive',
  userid: 235,
  orgid: 1,
  dashboard_name: 'de-factory-view',
  dashboard: '3h35q6Smz'}
    Retrieving grafana asset at /d/3h35q6Smz/de-factory-view?orgId=1&kiosk
    Setting Options:{"host":"172.17.0.2","path":"/d/3h35q6Smz/de-factory-view?orgId=1&kiosk","port":3000,"rejectUnauthorized":false,"requestCert":false,"headers":{"X-WEBAUTH-USER":"gunnerautomotive","Host":"grafana.<OBFUSCATED>.com"},"setHost":false}
    Grafana request headers: {"x-webauth-user":"gunnerautomotive","host":"grafana.<OBFUSCATED>.com"}
    Status code: 200
    Grafana response headers: {"content-type":"text/html; charset=UTF-8","set-cookie":["grafana_sess=049e5895b7c9d230; Path=/; HttpOnly"],"date":"Wed, 18 Jul 2018 13:38:40 GMT","connection":"close","transfer-encoding":"chunked"}
    Proxied response headers: {"x-powered-by":"Express","content-type":"text/html; charset=UTF-8","set-cookie":["grafana_sess=049e5895b7c9d230; Path=/; HttpOnly"],"date":"Wed, 18 Jul 2018 13:38:40 GMT","connection":"close","transfer-encoding":"chunked","pragma":"no-cache"}
    Got data for dashboard
    Got data for dashboard
Finished grafana dashboard response

Any feed back is greatly appreciated!

1 Like

I think this has something to do with not passing posts through maybe? Anyone have any thoughts?

When I load the dashboard directly I can see in the Grafana logs that after…
t=2018-07-18T18:12:05+0000 lvl=info msg="Request Completed" logger=context userId=4 orgId=1 uname=<OBFUSCATED> method=GET path=/d/3h35q6Smz/de-factory-view status=200 remote_addr=74.82.136.115 time_ms=11 size=13881 referer=

I see a second get come through on the API that passes the dashboard id on the uri…
t=2018-07-18T18:12:06+0000 lvl=info msg=“Request Completed” logger=context userId=4 orgId=1 uname= method=GET path=/api/dashboards/uid/3h35q6Smz status=200 remote_addr=74.82.136.115 time_ms=4 size=43749 referer=“http:///d/3h35q6Smz/de-factory-view?orgId=1&kiosk”``

but when I hit it through my proxy the first get comes through but not the second… but I do see assets pulled in.

Update: Was not processing POST requests.

Hi,
i have the same problem but i can’t write the right POST requests