├── .gitignore ├── .gitmodules ├── LICENCE ├── README.md ├── app.js ├── config.js ├── lib ├── basic-auth.js ├── cas-auth.js ├── es-proxy.js ├── google-oauth.js └── gr-proxy.js └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | grafana-auth-proxy.pm2.json 2 | config.js.orig 3 | node_modules 4 | npm-debug.log 5 | .DS_Store 6 | meta.json 7 | .idea 8 | coverage.html 9 | lib-cov 10 | .coverage_data 11 | reports 12 | html-report 13 | build 14 | cobertura-coverage.xml 15 | metadata 16 | *.sublime-workspace 17 | *.sublime-project 18 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "grafana"] 2 | path = grafana 3 | url = https://github.com/torkelo/grafana.git 4 | -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) <2013> 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | grafana Authentication Proxy 2 | ============ 3 | 4 | Hosts the latest [grafana](https://github.com/torkelo/grafana) and elasticsearch behind Google OAuth2, Basic Authentication or CAS Authentication with NodeJS and Express. 5 | 6 | - A proxy between Elasticsearch, grafana and user client 7 | - Support Elasticsearch which protected by basic authentication, only grafana-authentication-proxy knows the user/passwd 8 | - Compatible with the latest grafana 9 | - Enhanced authentication methods. Now support Google OAuth2, BasicAuth(multiple users supported) and CAS Authentication for the clients 10 | - Per-user grafana index supported. now you can use index grafana-int-userA for user A and grafana-int-userB for user B 11 | - Inspired by and based on [kibana-authentication-proxy](https://github.com/fangli/kibana-authentication-proxy), 99% from this is at the moment written by them, thanks:) 12 | 13 | *We NO LONGER support third-party plugins such as `Bigdesk` or `Head` since it is hard to test and maintain* 14 | 15 | Installation 16 | ===== 17 | 18 | ``` 19 | # git clone https://github.com/strima/grafana-authentication-proxy.git 20 | # cd grafana-authentication-proxy/ 21 | # git submodule init 22 | # git submodule update 23 | # npm install 24 | 25 | // You may want to update the built-in grafana to the latest version, just run 26 | # cd grafana && git checkout master && git pull 27 | 28 | // Then edit config.js, make sure you have everything checked in the config file 29 | // and run! 30 | # node app.js 31 | ``` 32 | 33 | Configuration 34 | ============= 35 | 36 | All settings are placed in /config.js, hack it as you go. 37 | 38 | ### Elasticsearch backend configurations 39 | 40 | - ``es_host``: *The host of ElasticSearch* 41 | - ``es_port``: *The port of ElasticSearch* 42 | - ``es_using_ssl``: *If the ES is using SSL(https)?* 43 | - ``es_username``: *(optional) The basic authentication user of ES server, leave it blank if no basic auth applied* 44 | - ``es_password``: *(optional) The password of basic authentication of ES server, leave it blank if no basic auth applied* 45 | 46 | ### Client settings 47 | 48 | - ``base_path``: *The base path to appear outwards (e.g. /grafana, or "" for /)* 49 | - ``listen_port``: *The listen port of grafana* 50 | - ``brower_cache_maxage``: *The browser cache max-Age controll, for a better loading speed* 51 | - ``enable_ssl_port``: *Enable SSL or not?* 52 | - ``listen_port_ssl``: *If enable_ssl_port set to true, this is the port of SSL* 53 | - ``ssl_key_file``: *Point to the ssl key file* 54 | - ``ssl_cert_file``: *Point to the ssl certification file* 55 | - ``grafana_es_index``: *The ES index for saving grafana dashboards, now per-user configurations supported. using %user% instead of the username* 56 | - ``which_auth_type_for_grafana_index``: *Where the variable %user% comes from? which authentication type you want to use for it?* 57 | - ``cookie_secret``: *The secret token for cookies. replace it with a random string for security* 58 | 59 | ### Client authentication settings 60 | 61 | We currently support 3 auth methods: Google OAuth2, BasicAuth and CAS, you can use one of them or all of them. it depends on the configuration you have. 62 | 63 | ***1. Google OAuth2*** 64 | 65 | - ``enable_google_oauth``: *Enable or not?* 66 | - ``client_id``: *The client ID of Google OAuth2, leave empty if you don't want to use it* 67 | - ``client_secret``: *The client secret of Google OAuth2* 68 | - ``allowed_emails``: *An emails list for the authorized users, should like `["a@b.com", "*@b.com", "*"]`*. All google users in the list will be allowed to access grafana. 69 | 70 | **Important** 71 | 72 | Google OAuth2 needs authorized redirect URIs for your app, please add it first as below, ``http://YOUR-grafana-SITE:[listen_port]/auth/google/callback`` in production or ``http://localhost:[listen_port]/auth/google/callback`` for local test 73 | 74 | ***2. Basic Authentication*** 75 | 76 | - ``enable_basic_auth``: *Enable or not?* 77 | - ``basic_auth_users``: *A list of user/passwd, see the comments in config.js for help. leave empty if you won't use it* 78 | - ``basic_auth_file``: *if is specified and exists, the user password combinations are read from the named file and overrule the here defined settings from array basic_auth_users. File format is one combination per line split by first appearing colon 79 | 80 | ***3. CAS Auth*** 81 | 82 | - ``enable_cas_auth``: *Enable or not?* 83 | - ``cas_server_url``: *Point to the CAS server URL* 84 | 85 | Resources 86 | ========= 87 | - The original proxy project of [kibana-proxy](https://github.com/hmalphettes/kibana-proxy) 88 | - The original authentication proxy project of [kibana-authentication-proxy](https://github.com/fangli/kibana-authentication-proxy) 89 | - [grafana](http://grafana.org/) or (https://github.com/torkelo/grafana) and [Elasticsearch](https://github.com/elasticsearch/elasticsearch) 90 | 91 | 92 | Contributing 93 | ============ 94 | - Fork it 95 | - Create your feature branch (git checkout -b my-new-feature) 96 | - Commit your changes (git commit -am 'Add some feature') 97 | - Push to the branch (git push origin my-new-feature) 98 | - Create new Pull Request 99 | 100 | 101 | Releases 102 | ======== 103 | - Minor Changes which are also found in pull request https://github.com/fangli/kibana-authentication-proxy/pull/18 104 | - Initial forked from https://github.com/fangli/kibana-authentication-proxy 105 | 106 | 107 | License 108 | ======= 109 | grafana Authentication Proxy is freely distributable under the terms of the MIT license. 110 | 111 | Copyright (c) 2014 strima 112 | 113 | See LICENCE for details. 114 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Hosts the latest grafana and elasticsearch behind Google OAuth2 Authentication 3 | * with nodejs and express. 4 | * License: MIT 5 | * Copyright: Funplus Game Inc. 6 | * Author: strima 7 | * Original Author: Fang Li. 8 | * Project: https://github.com/strima/grafana-authentication-proxy 9 | */ 10 | 11 | var express = require('express'); 12 | var https = require('https'); 13 | var http = require('http'); 14 | var fs = require('fs'); 15 | 16 | var config = require('./config'); 17 | var app = express(); 18 | 19 | app.use(express.logger()); 20 | 21 | console.log('Server starting...'); 22 | 23 | if (!config.base_path) { 24 | config.base_path=""; 25 | console.log("No base_path specified in config so using /"); 26 | } 27 | 28 | app.use(express.cookieParser()); 29 | app.use(express.session({ secret: config.cookie_secret })); 30 | 31 | // Authentication 32 | function readAndInitBasicAuthFile() { 33 | config.basic_auth_users=new Array(); 34 | var basic_auth_users=fs.readFileSync(config.basic_auth_file,'utf8'); 35 | var userpass=basic_auth_users.split('\n'); 36 | for (var userpass_index in userpass) { 37 | var uspa=userpass[userpass_index].match(/^([^:]+):(.+)/); 38 | if (uspa) { 39 | config.basic_auth_users[config.basic_auth_users.length]={"user": uspa[1], "password": uspa[2]}; 40 | } 41 | } 42 | } 43 | if (config.enable_basic_auth && config.basic_auth_file && fs.existsSync(config.basic_auth_file)) { 44 | console.log('basic_auth_file defined and found, so reading it ...'); 45 | readAndInitBasicAuthFile(); 46 | fs.watchFile(config.basic_auth_file, { persistent: true, interval: 5007 }, function(curr,prev) { 47 | if (curr.mtime.getTime() != prev.mtime.getTime()) { 48 | console.log('BASIC AUTH File was changed, so reloading values'); 49 | readAndInitBasicAuthFile(); 50 | } 51 | }); 52 | } 53 | 54 | require('./lib/basic-auth').configureBasic(express, app, config); 55 | require('./lib/google-oauth').configureOAuth(express, app, config); 56 | require('./lib/cas-auth.js').configureCas(express, app, config); 57 | 58 | // Setup ES proxy 59 | require('./lib/es-proxy').configureESProxy(app, config.es_host, config.es_port, 60 | config.es_username, config.es_password, config.base_path); 61 | 62 | // Setup Graphite proxy 63 | require('./lib/gr-proxy').configureGRProxy(app, config.gr_host, config.gr_port, 64 | config.gr_username, config.gr_password, config.base_path); 65 | 66 | // Serve config.js for grafana 67 | // We should use special config.js for the frontend and point the ES to __es/ 68 | app.get(config.base_path + '/config.js', grafanaconfigjs); 69 | 70 | // Serve all grafana frontend files 71 | app.use(express.compress()); 72 | app.use(config.base_path + '/', express.static(__dirname + '/grafana/src', {maxAge: config.brower_cache_maxage || 0})); 73 | 74 | 75 | run(); 76 | 77 | function run() { 78 | if (config.enable_ssl_port === true) { 79 | var options = { 80 | key: fs.readFileSync(config.ssl_key_file), 81 | cert: fs.readFileSync(config.ssl_cert_file), 82 | }; 83 | https.createServer(options, app).listen(config.listen_port_ssl); 84 | console.log('Server listening on ' + config.listen_port_ssl + '(SSL)'); 85 | } 86 | http.createServer(app).listen(config.listen_port); 87 | console.log('Server listening on ' + config.listen_port); 88 | } 89 | 90 | function grafanaconfigjs(req, res) { 91 | var graphiteUrl = config.graphiteUrl; 92 | function getGrafanaIndex() { 93 | var raw_index = config.grafana_es_index; 94 | var user_type = config.which_auth_type_for_grafana_index; 95 | var user; 96 | if (raw_index.indexOf('%user%') > -1) { 97 | if (user_type === 'google') { 98 | user = req.googleOauth.id; 99 | } else if (user_type === 'basic') { 100 | user = req.user; 101 | } else if (user_type === 'cas') { 102 | user = req.session.cas_user_name; 103 | } else { 104 | user = 'unknown'; 105 | } 106 | return raw_index.replace(/%user%/gi, user); 107 | } else { 108 | return raw_index; 109 | } 110 | } 111 | 112 | res.setHeader('Content-Type', 'application/javascript'); 113 | res.end("define(['settings'], " + 114 | "function (Settings) {'use strict'; return new Settings({elasticsearch: '" + config.base_path + "/__es', graphiteUrl: '" + config.base_path + "/__gr', default_route : '/dashboard/file/default.json'," + 115 | "grafana_index: '" + 116 | getGrafanaIndex() + 117 | "', timezoneOffset: null, panel_names: ['text','graphite'] }); });"); 118 | } 119 | -------------------------------------------------------------------------------- /config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 3 | //////////////////////////////////// 4 | // ElasticSearch Backend Settings 5 | //////////////////////////////////// 6 | "es_host": "localhost", // The host of Elastic Search 7 | "es_port": 9200, // The port of Elastic Search 8 | "es_using_ssl": false, // If the ES is using SSL(https)? 9 | "es_username": "", // The basic authentication user of ES server, leave it blank if no basic auth applied 10 | "es_password": "", // The password of basic authentication of ES server, leave it blank if no basic auth applied. 11 | 12 | "base_path": "/grafana", 13 | 14 | //////////////////////////////////// 15 | // Proxy server configurations 16 | //////////////////////////////////// 17 | // Which port listen to 18 | "listen_port": 9202, 19 | // Control HTTP max-Age header. Whether the browser cache static grafana files or not? 20 | // 0 for no-cache, unit in millisecond, default to 0 21 | // We strongly recommand you set to a larger number such as 2592000000(a month) to get a better loading speed 22 | "brower_cache_maxage": 0, 23 | // Enable SSL protocol 24 | "enable_ssl_port": false, 25 | // The following settings are valid only when enable_ssl_port is true 26 | "listen_port_ssl": 4444, 27 | // Use absolute path for the key file 28 | "ssl_key_file": "POINT_TO_YOUR_SSL_KEY", 29 | // Use absolute path for the certification file 30 | "ssl_cert_file": "POINT_TO_YOUR_SSL_CERT", 31 | 32 | // The ES index for saving grafana dashboards 33 | // default to "grafana-int" 34 | // With the default configuration, all users will use the same index for grafana dashboards settings, 35 | // But we support using different grafana settings for each user. 36 | // If you want to use different grafana indices for individual users, use %user% instead of the real username 37 | // Since we support multiple authentication types(google, cas or basic), you must decide which one you gonna use. 38 | 39 | // Bad English:D 40 | // For example: 41 | // Config "grafana_es_index": "grafana-int-for-%user%", "which_auth_type_for_grafana_index": "basic" 42 | // will use grafana index settings like "grafana-int-for-demo1", "grafana-int-for-demo2" for user demo1 and demo2. 43 | // in this case, if you enabled both Google Oauth2 and BasicAuth, and the username of BasicAuth is the boss. 44 | "grafana_es_index": "grafana-dash-%user%", // "grafana-int-%user%" 45 | "which_auth_type_for_grafana_index": "basic", // google, cas or basic 46 | 47 | /** 48 | * graphite-web url: 49 | * For Basic authentication use: http://username:password@domain.com 50 | * Basic authentication requires special HTTP headers to be configured 51 | * in nginx or apache for cross origin domain sharing to work (CORS). 52 | * Check install documentation on github 53 | */ 54 | "graphiteUrl": "http://"+window.location.hostname+":8080", 55 | 56 | //////////////////////////////////// 57 | // Security Configurations 58 | //////////////////////////////////// 59 | // Cookies secret 60 | // Please change the following secret randomly for security. 61 | "cookie_secret": "REPLACE_WITH_A_RANDOM_STRING_PLEASE", 62 | 63 | 64 | //////////////////////////////////// 65 | // grafana Authentication Settings 66 | // Currently we support 3 different auth methods: Google OAuth2, Basic Auth and CAS SSO. 67 | // You can use one of them or both 68 | //////////////////////////////////// 69 | 70 | 71 | // ================================= 72 | // Google OAuth2 settings 73 | // Enable? true or false 74 | // When set to false, google OAuth will not be applied. 75 | "enable_google_oauth": false, 76 | // We use the following redirect URI: 77 | // http://YOUR-grafana-SITE:[listen_port]/auth/google/callback 78 | // Please add it in the google developers console first. 79 | // The client ID of Google OAuth2 80 | "client_id": "", 81 | "client_secret": "", // The client secret of Google OAuth2 82 | "allowed_emails": ["*"], // An emails list for the authorized users 83 | 84 | 85 | // ================================= 86 | // Basic Authentication Settings 87 | // The following config is different from the previous basic auth settings. 88 | // It will be applied on the client who access grafana. 89 | // Enable? true or false 90 | "enable_basic_auth": false, 91 | // If basic_auth_file is specified and exists, the user password combinations 92 | // are read from the named file and overrule the here defined settings from 93 | // array basic_auth_users. 94 | // File format is one combination per line split by first appearing colon 95 | // e.g. 96 | // user1:password1 97 | // user2:password2 98 | "basic_auth_file": "", 99 | // Multiple user/passwd supported 100 | // The User&Passwd list for basic auth 101 | "basic_auth_users": [ 102 | {"user": "demo1", "password": "pwd1"}, 103 | {"user": "demo1", "password": "pwd2"}, 104 | ], 105 | 106 | 107 | // ================================= 108 | // CAS SSO Login 109 | // Enable? true or false 110 | "enable_cas_auth": false, 111 | // Point to the CAS authentication URL 112 | "cas_server_url": "https://point-to-the-cas-server/cas", 113 | 114 | }; 115 | -------------------------------------------------------------------------------- /lib/basic-auth.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Configure Basic Authentication 3 | * Config parameters: 4 | * - basic_auth_users: should be a list like 5 | * [ 6 | * {"user":"test1", "password":"psw1"}, 7 | * {"user":"test2", "password":"psw2"}, 8 | * ... 9 | * ] 10 | * When no basic_auth_users presented, no authentication applied. 11 | */ 12 | exports.configureBasic = function(express, app, config) { 13 | if (!config.enable_basic_auth) { 14 | console.log('Warning: No basic authentication presented'); 15 | return; 16 | } else { 17 | console.log('Info: HTTP Basic Authentication applied'); 18 | } 19 | 20 | app.use(express.basicAuth(function(user, pass) { 21 | for (var i in config.basic_auth_users) { 22 | var cred = config.basic_auth_users[i]; 23 | if ((cred["user"] === user) && (cred["password"] === pass)){ 24 | return true; 25 | } 26 | } 27 | return false; 28 | })); 29 | 30 | }; 31 | -------------------------------------------------------------------------------- /lib/cas-auth.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Configure CAS Authentication 3 | * When no cas_server_url presented, no CAS authentication applied. 4 | */ 5 | 6 | 7 | exports.configureCas = function(express, app, config) { 8 | 9 | if (!config.enable_cas_auth) { 10 | console.log('Warning: No CAS authentication presented'); 11 | return; 12 | } else { 13 | console.log('Info: CAS Authentication applied'); 14 | } 15 | 16 | app.use(function(req, res, next) { 17 | if (req.url.indexOf('/auth/cas/login') === 0 || req.session.cas_user_name) { 18 | return next(); 19 | } else { 20 | res.redirect('/auth/cas/login'); 21 | } 22 | }); 23 | 24 | config.cas_server_url = config.cas_server_url.replace(/\s+$/,''); 25 | 26 | app.get('/auth/cas/login', function (req, res) { 27 | var service_url = req.protocol + "://" + req.get('host') + req.url; 28 | 29 | var CAS = require('cas'); 30 | var cas = new CAS({base_url: config.cas_server_url, service: service_url}); 31 | 32 | var cas_login_url = config.cas_server_url + "/login?service=" + service_url; 33 | 34 | var ticket = req.param('ticket'); 35 | if (ticket) { 36 | cas.validate(ticket, function(err, status, username) { 37 | if (err || !status) { 38 | // Handle the error 39 | res.send( 40 | "You may have logged in with invalid CAS ticket or permission denied.
" + 41 | "Try again" 42 | ); 43 | } else { 44 | // Log the user in 45 | req.session.cas_user_name = username; 46 | res.redirect("/"); 47 | } 48 | }); 49 | } else { 50 | if (!req.session.cas_user_name) { 51 | res.redirect(cas_login_url); 52 | } else { 53 | res.redirect("/"); 54 | } 55 | } 56 | }); 57 | 58 | }; 59 | -------------------------------------------------------------------------------- /lib/es-proxy.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Proxies the request to elasticsearch 3 | * node-http-proxy worked really well until it met elasticsearch deployed on cloudfoundry 4 | * hence this small proxy and naive proxy based on: 5 | * http://www.catonmat.net/http-proxy-in-nodejs/ 6 | */ 7 | var http = require('http'); 8 | 9 | function proxyRequest(request, response, host, port, user, password, getProxiedRequestPath, isUI) { 10 | var filteredHeaders = {}; 11 | Object.keys(request.headers).forEach(function(header) { 12 | if (header === 'host') { 13 | //most necessary: 14 | filteredHeaders[header] = host; 15 | } else if (header !== 'cookie' && 16 | (isUI === true || (header !== 'referer' && 17 | header !== 'user-agent' && header !== 'accept-language'))) { 18 | //avoid leaking unecessay info and save some room 19 | filteredHeaders[header] = request.headers[header]; 20 | } 21 | }); 22 | if (user) { 23 | var auth = 'Basic ' + new Buffer(user + ':' + password).toString('base64'); 24 | filteredHeaders.authorization = auth; 25 | } 26 | 27 | var options = { 28 | path: getProxiedRequestPath(request), 29 | method: request.method, 30 | hostname: host, 31 | port: port, 32 | headers: filteredHeaders 33 | }; 34 | if (user) { 35 | options.auth = password ? user + ':' + password : user; 36 | } 37 | 38 | var proxyReq = http.request(options); 39 | 40 | proxyReq.addListener('error', function(err){ 41 | response.status(500).send('Unable to process your request, ' + err.code); 42 | console.log('ElasticSearch Server Error: ' + err.code); 43 | }); 44 | 45 | proxyReq.addListener('response', function(proxyResp) { 46 | var http10 = request.httpVersionMajor === 1 && request.httpVersionMinor < 1; 47 | if(http10 && proxyResp.headers['transfer-encoding'] !== undefined){ 48 | //filter headers 49 | var headers = proxyResp.headers; 50 | delete proxyResp.headers['transfer-encoding']; 51 | var buffer = ""; 52 | 53 | //buffer answer 54 | proxyResp.addListener('data', function(chunk) { 55 | buffer += chunk; 56 | }); 57 | proxyResp.addListener('end', function() { 58 | headers['Content-length'] = buffer.length;//cancel transfer encoding "chunked" 59 | response.writeHead(proxyResp.statusCode, headers); 60 | response.write(buffer, 'binary'); 61 | response.end(); 62 | }); 63 | } else { 64 | //send headers as received 65 | response.writeHead(proxyResp.statusCode, proxyResp.headers); 66 | 67 | //easy data forward 68 | proxyResp.addListener('data', function(chunk) { 69 | response.write(chunk, 'binary'); 70 | }); 71 | proxyResp.addListener('end', function() { 72 | response.end(); 73 | }); 74 | } 75 | }); 76 | 77 | //proxies to SEND request to real server 78 | request.addListener('data', function(chunk) { 79 | proxyReq.write(chunk, 'binary'); 80 | }); 81 | request.addListener('end', function() { 82 | proxyReq.end(); 83 | }); 84 | } 85 | 86 | exports.configureESProxy = function(app, esHost, esPort, esUser, esPassword, basePath) { 87 | app.use(basePath + "/__es", function(request, response, next) { 88 | proxyRequest(request, response, esHost, esPort, esUser, esPassword, 89 | function getProxiedRequestPath(request) { 90 | return request.url; 91 | }); 92 | }); 93 | app.use(basePath + "/_plugin", function(request, response, next) { 94 | proxyRequest(request, response, esHost, esPort, esUser, esPassword, 95 | function getProxiedRequestPath(request) { 96 | return request.originalUrl; 97 | }, true); 98 | }); 99 | }; 100 | -------------------------------------------------------------------------------- /lib/google-oauth.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Configure google oauth passport's. 3 | * Config parameters: 4 | * - client_id: the application ID 5 | * - client_secret: the applicatin secrete 6 | * - allowed_emails: a comma separated listed of patterns 7 | * each pattern can be '*': anything, 8 | * '*@domain': any email in the domain 9 | * 'an@email': a specific email. 10 | */ 11 | exports.configureOAuth = function(express, app, config) { 12 | if (!config.enable_google_oauth) { 13 | console.log('Warning: No Google OAuth2 presented'); 14 | return; 15 | } else { 16 | console.log('Info: Google OAuth2 Authentication applied'); 17 | } 18 | 19 | var validateUser = function(passportProfile) { 20 | var validEmail; 21 | passportProfile.emails.some(function(email) { 22 | email = email.value; 23 | config.allowed_emails.some(function(patt) { 24 | if (patt === email) { 25 | validEmail = email; 26 | return true; 27 | } 28 | if (patt === '*') { 29 | validEmail = email; 30 | return true; 31 | } 32 | if ('*' + email.slice(email.indexOf('@')) === patt) { 33 | validEmail = email; 34 | return true; 35 | } 36 | }); 37 | if (validEmail) { 38 | return true; 39 | } 40 | }); 41 | return validEmail; 42 | }; 43 | 44 | var GoogleStrategy = require('passport-google-oauth').OAuth2Strategy; 45 | var passport = require('passport'); 46 | var scope = config.scope || [ 'https://www.googleapis.com/auth/userinfo.email' ]; 47 | 48 | var passportIsSet = false; 49 | 50 | var lazySetupPassport = function(req) { 51 | passportIsSet = true; 52 | 53 | var protocol = (req.connection.encrypted || req.headers['x-forwarded-proto'] == "https" ) ? "https" : "http"; 54 | 55 | //not doing anything with this: 56 | //it will try to serialize the users in the session. 57 | passport.serializeUser(function(user, done) { 58 | done(null, user); 59 | }); 60 | passport.deserializeUser(function(obj, done) { 61 | done(null, obj); 62 | }); 63 | 64 | var callbackUrl = protocol + "://" + req.headers.host + "/auth/google/callback"; 65 | passport.use(new GoogleStrategy({ 66 | clientID: config.client_id, clientSecret: config.client_secret, callbackURL: callbackUrl 67 | }, function(accessToken, refreshToken, profile, done) { 68 | var validEmail = validateUser(profile); 69 | if (!validEmail) { 70 | done(null, false, { message: 'not an authorized email ' + profile.emails[0] }); 71 | } else { 72 | done(null, profile); 73 | } 74 | })); 75 | 76 | app.get('/auth/google', passport.authenticate( 77 | 'google', 78 | { scope: scope, }), 79 | function(req, res) { 80 | // The request will be redirected to Google for authentication, so 81 | // this function will not be called. 82 | }); 83 | 84 | app.get('/auth/google/callback', passport.authenticate( 85 | 'google', 86 | { failureRedirect: req.session.beforeLoginURL || '/' }), 87 | function(req, res) { 88 | // Successful authentication, redirect home. 89 | req.session.authenticated = true; 90 | res.redirect(req.session.beforeLoginURL || '/'); 91 | }); 92 | 93 | }; 94 | 95 | app.use(express.urlencoded()); 96 | app.use(express.json()); 97 | app.use(require('connect-restreamer')()); 98 | app.use(function(req, res, next) { 99 | if (req.url.indexOf('/auth/google') === 0 || req.session.authenticated) { 100 | return next(); 101 | } 102 | if (!passportIsSet) { 103 | lazySetupPassport(req); 104 | } 105 | 106 | req.session.beforeLoginURL = req.url; 107 | res.redirect('/auth/google'); 108 | }); 109 | app.use(passport.initialize({ userProperty: 'googleOauth' })); 110 | app.use(passport.session()); 111 | }; 112 | -------------------------------------------------------------------------------- /lib/gr-proxy.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Proxies the request to graphite 3 | */ 4 | var http = require('http'); 5 | 6 | function proxyRequest(request, response, host, port, user, password, getProxiedRequestPath, isUI) { 7 | var filteredHeaders = {}; 8 | Object.keys(request.headers).forEach(function(header) { 9 | if (header === 'host') { 10 | //most necessary: 11 | filteredHeaders[header] = host; 12 | } else if (header !== 'cookie' && 13 | (isUI === true || (header !== 'referer' && 14 | header !== 'user-agent' && header !== 'accept-language'))) { 15 | //avoid leaking unecessay info and save some room 16 | filteredHeaders[header] = request.headers[header]; 17 | } 18 | }); 19 | if (user) { 20 | var auth = 'Basic ' + new Buffer(user + ':' + password).toString('base64'); 21 | filteredHeaders.authorization = auth; 22 | } 23 | 24 | var options = { 25 | path: getProxiedRequestPath(request), 26 | method: request.method, 27 | hostname: host, 28 | port: port, 29 | headers: filteredHeaders 30 | }; 31 | if (user) { 32 | options.auth = password ? user + ':' + password : user; 33 | } 34 | 35 | var proxyReq = http.request(options); 36 | 37 | proxyReq.addListener('error', function(err){ 38 | response.status(500).send('Unable to process your request, ' + err.code); 39 | console.log('GRAPHITE Server Error: ' + err.code); 40 | }); 41 | 42 | proxyReq.addListener('response', function(proxyResp) { 43 | var http10 = request.httpVersionMajor === 1 && request.httpVersionMinor < 1; 44 | if(http10 && proxyResp.headers['transfer-encoding'] !== undefined){ 45 | //filter headers 46 | var headers = proxyResp.headers; 47 | delete proxyResp.headers['transfer-encoding']; 48 | var buffer = ""; 49 | 50 | //buffer answer 51 | proxyResp.addListener('data', function(chunk) { 52 | buffer += chunk; 53 | }); 54 | proxyResp.addListener('end', function() { 55 | headers['Content-length'] = buffer.length;//cancel transfer encoding "chunked" 56 | response.writeHead(proxyResp.statusCode, headers); 57 | response.write(buffer, 'binary'); 58 | response.end(); 59 | }); 60 | } else { 61 | //send headers as received 62 | response.writeHead(proxyResp.statusCode, proxyResp.headers); 63 | 64 | //easy data forward 65 | proxyResp.addListener('data', function(chunk) { 66 | response.write(chunk, 'binary'); 67 | }); 68 | proxyResp.addListener('end', function() { 69 | response.end(); 70 | }); 71 | } 72 | }); 73 | 74 | //proxies to SEND request to real server 75 | request.addListener('data', function(chunk) { 76 | proxyReq.write(chunk, 'binary'); 77 | }); 78 | request.addListener('end', function() { 79 | proxyReq.end(); 80 | }); 81 | } 82 | 83 | exports.configureGRProxy = function(app, grHost, grPort, grUser, grPassword, basePath) { 84 | app.use(basePath + "/__gr", function(request, response, next) { 85 | proxyRequest(request, response, grHost, grPort, grUser, grPassword, 86 | function getProxiedRequestPath(request) { 87 | return request.url; 88 | }); 89 | }); 90 | }; 91 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "grafana-authentication-proxy", 3 | "version": "1.1.0", 4 | "description": "Hosts the latest grafana and elasticsearch behind Google OAuth2, Basic Auth or CAS Authentication", 5 | "main": "app.js", 6 | "scripts": { 7 | "test": "" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "http://github.com/strima/grafana-authentication-proxy" 12 | }, 13 | "keywords": [ 14 | "grafana", 15 | "elasticsearch", 16 | "authentication", 17 | "oauth2", 18 | "basic authentication" 19 | ], 20 | "author": "strima", 21 | "license": "MIT", 22 | "copyright": "strima", 23 | "dependencies": { 24 | "express": "3.*", 25 | "passport": "*", 26 | "passport-google-oauth": "*", 27 | "connect-restreamer": "*", 28 | "cas": "*" 29 | }, 30 | "devDependencies": { 31 | }, 32 | "bugs": { 33 | "url": "https://github.com/strima/grafana-authentication-proxy/issues" 34 | } 35 | } 36 | --------------------------------------------------------------------------------