mirror of
				https://github.com/node-red/node-red.git
				synced 2025-03-01 10:36:34 +00:00 
			
		
		
		
	Merge pull request #4100 from sroebert/feature/sha-digest-algorithms
Added SHA-256 and SHA-512-256 digest authentication
This commit is contained in:
		| @@ -699,25 +699,43 @@ in your Node-RED user directory (${RED.settings.userDir}). | ||||
|     }); | ||||
|  | ||||
|     const md5 = (value) => { return crypto.createHash('md5').update(value).digest('hex') } | ||||
|     const sha256 = (value) => { return crypto.createHash('sha256').update(value).digest('hex') } | ||||
|     const sha512 = (value) => { return crypto.createHash('sha512').update(value).digest('hex') } | ||||
|  | ||||
|     function digestCompute(algorithm, value) { | ||||
|         var lowercaseAlgorithm = "" | ||||
|         if (algorithm) { | ||||
|             lowercaseAlgorithm = algorithm.toLowerCase().replace(/-sess$/, '') | ||||
|         } | ||||
|  | ||||
|         if (lowercaseAlgorithm === "sha-256") { | ||||
|             return sha256(value) | ||||
|         } else if (lowercaseAlgorithm === "sha-512-256") { | ||||
|             var hash = sha512(value) | ||||
|             return hash.slice(0, 64) // Only use the first 256 bits | ||||
|         } else { | ||||
|             return md5(value) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     function ha1Compute(algorithm, user, realm, pass, nonce, cnonce) { | ||||
|         /** | ||||
|         * RFC 2617: handle both MD5 and MD5-sess algorithms. | ||||
|         * RFC 2617: handle both standard and -sess algorithms. | ||||
|         * | ||||
|         * If the algorithm directive's value is "MD5" or unspecified, then HA1 is | ||||
|         *   HA1=MD5(username:realm:password) | ||||
|         * If the algorithm directive's value is "MD5-sess", then HA1 is | ||||
|         *   HA1=MD5(MD5(username:realm:password):nonce:cnonce) | ||||
|         * If the algorithm directive's value ends with "-sess", then HA1 is | ||||
|         *   HA1=digestCompute(digestCompute(username:realm:password):nonce:cnonce) | ||||
|         *  | ||||
|         * If the algorithm directive's value does not end with "-sess", then HA1 is | ||||
|         *   HA1=digestCompute(username:realm:password) | ||||
|         */ | ||||
|         var ha1 = md5(user + ':' + realm + ':' + pass) | ||||
|         if (algorithm && algorithm.toLowerCase() === 'md5-sess') { | ||||
|             return md5(ha1 + ':' + nonce + ':' + cnonce) | ||||
|         var ha1 = digestCompute(algorithm, user + ':' + realm + ':' + pass) | ||||
|         if (algorithm && /-sess$/i.test(algorithm)) { | ||||
|             return digestCompute(algorithm, ha1 + ':' + nonce + ':' + cnonce) | ||||
|         } else { | ||||
|             return ha1 | ||||
|         } | ||||
|     } | ||||
|  | ||||
|  | ||||
|     function buildDigestHeader(user, pass, method, path, authHeader) { | ||||
|         var challenge = {} | ||||
|         var re = /([a-z0-9_-]+)=(?:"([^"]+)"|([a-z0-9_-]+))/gi | ||||
| @@ -732,10 +750,10 @@ in your Node-RED user directory (${RED.settings.userDir}). | ||||
|         var nc = qop && '00000001' | ||||
|         var cnonce = qop && uuid().replace(/-/g, '') | ||||
|         var ha1 = ha1Compute(challenge.algorithm, user, challenge.realm, pass, challenge.nonce, cnonce) | ||||
|         var ha2 = md5(method + ':' + path) | ||||
|         var ha2 = digestCompute(challenge.algorithm, method + ':' + path) | ||||
|         var digestResponse = qop | ||||
|         ? md5(ha1 + ':' + challenge.nonce + ':' + nc + ':' + cnonce + ':' + qop + ':' + ha2) | ||||
|         : md5(ha1 + ':' + challenge.nonce + ':' + ha2) | ||||
|         ? digestCompute(challenge.algorithm, ha1 + ':' + challenge.nonce + ':' + nc + ':' + cnonce + ':' + qop + ':' + ha2) | ||||
|         : digestCompute(challenge.algorithm, ha1 + ':' + challenge.nonce + ':' + ha2) | ||||
|         var authValues = { | ||||
|             username: user, | ||||
|             realm: challenge.realm, | ||||
|   | ||||
| @@ -31,6 +31,7 @@ var multer = require("multer"); | ||||
| var RED = require("nr-test-utils").require("node-red/lib/red"); | ||||
| var fs = require('fs-extra'); | ||||
| var auth = require('basic-auth'); | ||||
| var crypto = require("crypto"); | ||||
| const { version } = require("os"); | ||||
| const net = require('net') | ||||
|  | ||||
| @@ -163,6 +164,100 @@ describe('HTTP Request Node', function() { | ||||
|         delete process.env.NO_PROXY; | ||||
|     } | ||||
|  | ||||
|     function getDigestPassword() { | ||||
|         return 'digest-test-password'; | ||||
|     } | ||||
|  | ||||
|     function getDigest(algorithm, value) { | ||||
|         var hash; | ||||
|         if (algorithm === 'SHA-256') { | ||||
|             hash = crypto.createHash('sha256'); | ||||
|         } else if (algorithm === 'SHA-512-256') { | ||||
|             hash = crypto.createHash('sha512'); | ||||
|         } else { | ||||
|             hash = crypto.createHash('md5'); | ||||
|         } | ||||
|          | ||||
|         var hex = hash.update(value).digest('hex'); | ||||
|         if (algorithm === 'SHA-512-256') { | ||||
|             hex = hex.slice(0, 64); | ||||
|         } | ||||
|         return hex; | ||||
|     } | ||||
|  | ||||
|     function getDigestResponse(req, algorithm, sess, realm, username, nonce, nc, cnonce, qop) { | ||||
|         var ha1 = getDigest(algorithm, username + ':' + realm + ':' + getDigestPassword()); | ||||
|         if (sess) { | ||||
|             ha1 = getDigest(algorithm, ha1 + ':' + nonce + ':' + cnonce) | ||||
|         } | ||||
|         let ha2 = getDigest(algorithm, req.method + ':' + req.path); | ||||
|         return qop | ||||
|             ? getDigest(algorithm, ha1 + ':' + nonce + ':' + nc + ':' + cnonce + ':' + qop + ':' + ha2) | ||||
|             : getDigest(algorithm, ha1 + ':' + nonce + ':' + ha2); | ||||
|     } | ||||
|  | ||||
|     function handleDigestResponse(req, res, algorithm, sess, qop) { | ||||
|         let realm = "node-red"; | ||||
|         let nonce = "123456"; | ||||
|         let nc = '00000001'; | ||||
|         let algorithmValue = sess ? `${algorithm}-sess` : algorithm; | ||||
|  | ||||
|         let authHeader = req.headers['authorization']; | ||||
|         if (!authHeader) { | ||||
|             let qopField = qop ? `qop="${qop}", ` : ''; | ||||
|  | ||||
|             res.setHeader( | ||||
|                 'WWW-Authenticate', | ||||
|                 `Digest ${qopField}realm="${realm}", nonce="${nonce}", algorithm="${algorithmValue}"` | ||||
|             ); | ||||
|             res.status(401).end(); | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         var authFields = {}; | ||||
|         let re = /([a-z0-9_-]+)=(?:"([^"]+)"|([a-z0-9_-]+))/gi; | ||||
|         for (;;) { | ||||
|             var match = re.exec(authHeader); | ||||
|             if (!match) { | ||||
|                 break; | ||||
|             } | ||||
|             authFields[match[1]] = match[2] || match[3]; | ||||
|         } | ||||
|         console.log(JSON.stringify(authFields)); | ||||
|  | ||||
|         if (qop && authFields['qop'] != qop) { | ||||
|             console.log('test1'); | ||||
|             res.status(401).end(); | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         if ( | ||||
|             !authFields['username'] || | ||||
|             !authFields['response'] || | ||||
|             authFields['realm'] != realm || | ||||
|             authFields['nonce'] != nonce || | ||||
|             authFields['algorithm'] != algorithmValue | ||||
|         ) { | ||||
|             console.log('test2'); | ||||
|             res.status(401).end(); | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         let username = authFields['username']; | ||||
|         let response = authFields['response']; | ||||
|         let cnonce = authFields['cnonce'] || ''; | ||||
|         let expectedResponse = getDigestResponse( | ||||
|             req, algorithm, sess, realm, username, nonce, nc, cnonce, qop | ||||
|         ); | ||||
|         if (!response || expectedResponse.toLowerCase() !== response.toLowerCase()) { | ||||
|             console.log('test3'); | ||||
|             res.status(401).end(); | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         res.status(201).end(); | ||||
|     } | ||||
|  | ||||
|     before(function(done) { | ||||
|  | ||||
|         testApp = express(); | ||||
| @@ -222,6 +317,21 @@ describe('HTTP Request Node', function() { | ||||
|             } | ||||
|             res.json(result); | ||||
|         }); | ||||
|         testApp.get('/authenticate-digest-md5', function(req, res){ | ||||
|             handleDigestResponse(req, res, "MD5", false, false); | ||||
|         }); | ||||
|         testApp.get('/authenticate-digest-md5-sess', function(req, res){ | ||||
|             handleDigestResponse(req, res, "MD5", true, 'auth'); | ||||
|         }); | ||||
|         testApp.get('/authenticate-digest-md5-qop', function(req, res){ | ||||
|             handleDigestResponse(req, res, "MD5", false, 'auth'); | ||||
|         }); | ||||
|         testApp.get('/authenticate-digest-sha-256', function(req, res){ | ||||
|             handleDigestResponse(req, res, "SHA-256", false, 'auth'); | ||||
|         }); | ||||
|         testApp.get('/authenticate-digest-sha-512-256', function(req, res){ | ||||
|             handleDigestResponse(req, res, "SHA-512-256", false, 'auth'); | ||||
|         }); | ||||
|         testApp.get('/proxyAuthenticate', function(req, res){ | ||||
|             // var user = auth.parse(req.headers['proxy-authorization']); | ||||
|             var result = { | ||||
| @@ -2018,6 +2128,100 @@ describe('HTTP Request Node', function() { | ||||
|         }); | ||||
|         */ | ||||
|  | ||||
|         it('should authenticate on server - digest MD5', function(done) { | ||||
|             var flow = [{id:"n1",type:"http request",wires:[["n2"]],method:"GET",ret:"obj",authType:"digest", url:getTestURL('/authenticate-digest-md5')}, | ||||
|                 {id:"n2", type:"helper"}]; | ||||
|             helper.load(httpRequestNode, flow, function() { | ||||
|                 var n1 = helper.getNode("n1"); | ||||
|                 var n2 = helper.getNode("n2"); | ||||
|                 n1.credentials = {user:'xxxuser', password:getDigestPassword()}; | ||||
|                 n2.on("input", function(msg) { | ||||
|                     try { | ||||
|                         msg.should.have.property('statusCode',201); | ||||
|                         done(); | ||||
|                     } catch(err) { | ||||
|                         done(err); | ||||
|                     } | ||||
|                 }); | ||||
|                 n1.receive({payload:"foo"}); | ||||
|             }); | ||||
|         }); | ||||
|  | ||||
|         it('should authenticate on server - digest MD5 sess', function(done) { | ||||
|             var flow = [{id:"n1",type:"http request",wires:[["n2"]],method:"GET",ret:"obj",authType:"digest", url:getTestURL('/authenticate-digest-md5-sess')}, | ||||
|                 {id:"n2", type:"helper"}]; | ||||
|             helper.load(httpRequestNode, flow, function() { | ||||
|                 var n1 = helper.getNode("n1"); | ||||
|                 var n2 = helper.getNode("n2"); | ||||
|                 n1.credentials = {user:'xxxuser', password:getDigestPassword()}; | ||||
|                 n2.on("input", function(msg) { | ||||
|                     try { | ||||
|                         msg.should.have.property('statusCode',201); | ||||
|                         done(); | ||||
|                     } catch(err) { | ||||
|                         done(err); | ||||
|                     } | ||||
|                 }); | ||||
|                 n1.receive({payload:"foo"}); | ||||
|             }); | ||||
|         }); | ||||
|  | ||||
|         it('should authenticate on server - digest MD5 qop', function(done) { | ||||
|             var flow = [{id:"n1",type:"http request",wires:[["n2"]],method:"GET",ret:"obj",authType:"digest", url:getTestURL('/authenticate-digest-md5-qop')}, | ||||
|                 {id:"n2", type:"helper"}]; | ||||
|             helper.load(httpRequestNode, flow, function() { | ||||
|                 var n1 = helper.getNode("n1"); | ||||
|                 var n2 = helper.getNode("n2"); | ||||
|                 n1.credentials = {user:'xxxuser', password:getDigestPassword()}; | ||||
|                 n2.on("input", function(msg) { | ||||
|                     try { | ||||
|                         msg.should.have.property('statusCode',201); | ||||
|                         done(); | ||||
|                     } catch(err) { | ||||
|                         done(err); | ||||
|                     } | ||||
|                 }); | ||||
|                 n1.receive({payload:"foo"}); | ||||
|             }); | ||||
|         }); | ||||
|  | ||||
|         it('should authenticate on server - digest SHA-256', function(done) { | ||||
|             var flow = [{id:"n1",type:"http request",wires:[["n2"]],method:"GET",ret:"obj",authType:"digest", url:getTestURL('/authenticate-digest-sha-256')}, | ||||
|                 {id:"n2", type:"helper"}]; | ||||
|             helper.load(httpRequestNode, flow, function() { | ||||
|                 var n1 = helper.getNode("n1"); | ||||
|                 var n2 = helper.getNode("n2"); | ||||
|                 n1.credentials = {user:'xxxuser', password:getDigestPassword()}; | ||||
|                 n2.on("input", function(msg) { | ||||
|                     try { | ||||
|                         msg.should.have.property('statusCode',201); | ||||
|                         done(); | ||||
|                     } catch(err) { | ||||
|                         done(err); | ||||
|                     } | ||||
|                 }); | ||||
|                 n1.receive({payload:"foo"}); | ||||
|             }); | ||||
|         }); | ||||
|  | ||||
|         it('should authenticate on server - digest SHA-512-256', function(done) { | ||||
|             var flow = [{id:"n1",type:"http request",wires:[["n2"]],method:"GET",ret:"obj",authType:"digest", url:getTestURL('/authenticate-digest-sha-512-256')}, | ||||
|                 {id:"n2", type:"helper"}]; | ||||
|             helper.load(httpRequestNode, flow, function() { | ||||
|                 var n1 = helper.getNode("n1"); | ||||
|                 var n2 = helper.getNode("n2"); | ||||
|                 n1.credentials = {user:'xxxuser', password:getDigestPassword()}; | ||||
|                 n2.on("input", function(msg) { | ||||
|                     try { | ||||
|                         msg.should.have.property('statusCode',201); | ||||
|                         done(); | ||||
|                     } catch(err) { | ||||
|                         done(err); | ||||
|                     } | ||||
|                 }); | ||||
|                 n1.receive({payload:"foo"}); | ||||
|             }); | ||||
|         }); | ||||
|     }); | ||||
|  | ||||
|     describe('file-upload', function() { | ||||
|   | ||||
		Reference in New Issue
	
	Block a user