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!