diff --git a/lib/requester.js b/lib/requester.js index 1a5668a0..bb063426 100644 --- a/lib/requester.js +++ b/lib/requester.js @@ -2,7 +2,7 @@ * Copyright © 2015-2025 Progress Software Corporation and/or its subsidiaries or affiliates. All Rights Reserved. */ 'use strict'; -var createAuthInitializer = require('www-authenticate'); +var createAuthInitializer = require('./www-authenticate/www-authenticate'); var Kerberos = require('./optional.js') .libraryProperty('kerberos', 'Kerberos'); var Multipart = require('multipart-stream'); diff --git a/lib/www-authenticate/md5.js b/lib/www-authenticate/md5.js new file mode 100644 index 00000000..3e6de8bf --- /dev/null +++ b/lib/www-authenticate/md5.js @@ -0,0 +1,7 @@ +var crypto= require('crypto'); + +function md5(s) { + return crypto.createHash('md5').update(s).digest('hex'); +} + +module.exports= md5; \ No newline at end of file diff --git a/lib/www-authenticate/parsers.js b/lib/www-authenticate/parsers.js new file mode 100644 index 00000000..65e2be1d --- /dev/null +++ b/lib/www-authenticate/parsers.js @@ -0,0 +1,109 @@ +var ParseAuth= /(\w+)\s+(.*)/ // -> scheme, params + , Separators= /([",=])/ + ; + +function parse_params(header) { + // This parser will definitely fail if there is more than one challenge + var tok, last_tok, _i, _len, key, value; + var state= 0; //0: token, + var m= header.split(Separators) + for (_i = 0, _len = m.length; _i < _len; _i++) { + last_tok= tok; + tok = m[_i]; + if (!tok.length) continue; + switch (state) { + case 0: // token + key= tok.trim(); + state= 1; // expect equals + continue; + case 1: // expect equals + if ('=' != tok) return 'Equal sign was expected after '+key; + state= 2; + continue; + case 2: // expect value + if ('"' == tok) { + value= ''; + state= 3; // expect quoted + continue; + } + else { + this.parms[key]= value= tok.trim(); + state= 9; // expect comma or end + continue; + } + case 3: // handling quoted string + if ('"' == tok) { + state= 8; // end quoted + continue; + } + else { + value+= tok; + state= 3; // continue accumulating quoted string + continue; + } + case 8: // end quote encountered + if ('"' == tok) { + // double quoted + value+= '"'; + state= 3; // back to quoted string + continue; + } + if (',' == tok) { + this.parms[key]= value; + state= 0; + continue; + } + else { + return 'Unexpected token ('+tok+') after '+value+'"'; + } + continue; + case 9: // expect commma + if (',' != tok) return 'Comma expected after '+value; + state= 0; + continue; + } + } + switch (state) { // terminal state + case 0: // Empty or ignoring terminal comma + case 9: // Expecting comma or end of header + return; + case 8: // Last token was end quote + this.parms[key]= value; + return; + default: + return 'Unexpected end of www-authenticate value.'; + } +} + +function Parse_WWW_Authenticate(to_parse) +{ + var m= to_parse.match(ParseAuth); + this.scheme= m[1]; + this.parms= {}; + var err= this.parse_params(m[2]); + if (err) { + this.scheme= ''; + this.parms= {}; + this.err= err; + } +} + +function Parse_Authentication_Info(to_parse) +{ + this.scheme= 'Digest'; + this.parms= {}; + var err= this.parse_params(to_parse); + if (err) { + this.scheme= ''; + this.parms= {}; + this.err= err; + } +} + +Parse_Authentication_Info.prototype.parse_params= parse_params; +Parse_WWW_Authenticate.prototype.parse_params= parse_params; + +module.exports = { + WWW_Authenticate: Parse_WWW_Authenticate, + Authentication_Info: Parse_Authentication_Info +}; diff --git a/lib/www-authenticate/user-credentials.js b/lib/www-authenticate/user-credentials.js new file mode 100644 index 00000000..25bc405d --- /dev/null +++ b/lib/www-authenticate/user-credentials.js @@ -0,0 +1,46 @@ +var md5= require('./md5'); + +/* + * Hide the password. Uses the password to form authorization strings, + * but provides no interface for exporting it. + */ +function user_credentials(username,password,options) { + if (username.is_user_credentials && + typeof username.basic === 'function' && + typeof username.digest === 'function' + ) { + return username; + } + + var basic_string= options && options.hide_basic ? + '' + : + (!password && password !== '' ? + Buffer.from(username, "ascii").toString("base64") + : + Buffer.from(username+':'+password, "ascii").toString("base64") + ) + function Credentials() + { + this.username= username; + } + Credentials.prototype.basic= function() + { + return basic_string; + } + Credentials.prototype.digest= function(realm) + { + return !password && password !== '' ? + md5(username+':'+realm) + : + md5(username+':'+realm+':'+password) + } + Credentials.prototype.is_user_credentials= function() + { + return true; + } + + return new Credentials; +} + +module.exports= user_credentials; diff --git a/lib/www-authenticate/www-authenticate.js b/lib/www-authenticate/www-authenticate.js new file mode 100644 index 00000000..f09c16ba --- /dev/null +++ b/lib/www-authenticate/www-authenticate.js @@ -0,0 +1,218 @@ +/* + * www-authenticate + * https://github.com/randymized/www-authenticate + * + * Copyright (c) 2013 Randy McLaughlin + * Licensed under the MIT license. + */ + +/* +* Copyright © 2015-2025 Progress Software Corporation and/or its subsidiaries or affiliates. All Rights Reserved. +*/ + +'use strict'; + +var crypto= require('crypto') + , parsers= require('./parsers') + , md5= require('./md5') + , user_credentials= require('./user-credentials') + , basic_challenge= { + statusCode: 401, + headers: { + 'www-authenticate': 'Basic realm="sample"' + } + } + ; + +function hex8(num) +{ + return ("00000000" + num.toString(16)).slice(-8); +} + +var www_authenticator = function(username,password,options) +{ + if (2 == arguments.length && toString.call(password) != '[object String]') { + options= password; + password= null; + } + var credentials= user_credentials(username,password) + var cnonce; + if (options) { + if (toString.call(options.cnonce) == '[object String]') + cnonce= options.cnonce; + } + if (cnonce === void 0) cnonce= crypto.pseudoRandomBytes(8).toString('hex'); + + /** + * @typedef {Object} Authenticator + * @property {any} [err] + * @property {function(string=, string=): string} [authorize] + * @property {any} [parms] + * @property {string} [cnonce] + */ + + /** + * Parses the WWW-Authenticate header. + * @param {string} www_authenticate + * @returns {Authenticator} + */ + var parse_header= function(www_authenticate) + { + function Authenticator() + { + function note_error(err) + { + this.err= err + } + var nc= 0; + + var parsed= new parsers.WWW_Authenticate(www_authenticate); + if (parsed.err) return note_error(parsed.err); + var auth_parms= this.parms= parsed.parms; + this.cnonce= cnonce; + + switch(parsed.scheme) { + case 'Basic': + var auth_string= 'Basic '+credentials.basic(); + this.authorize= function() { + return auth_string; + }; + return; + case 'Digest': + var realm= auth_parms.realm; + if (!realm) { + return note_error("Realm not found in www-authenticate header."); + } + + var ha1= + credentials.digest(realm); + var nonce= auth_parms.nonce; + if (!nonce) { + return note_error("Nonce not found in www-authenticate header."); + } + + var fixed= 'Digest username="'+credentials.username+'",'+ + ' realm="'+realm+'",'+ + ' nonce="'+nonce+'",'; + var qop= auth_parms.qop; + if (!qop) { + this.authorize= function(method,digestURI) { + var ha2= md5(method+':'+digestURI); + return fixed+ + ' uri="'+digestURI+'",'+ + ' response="'+md5(ha1+':'+nonce+':'+ha2)+'",'; + }; + return; + } + else { + var qopa= qop.split(','); + var q, x, _i, _len; + for (_i = 0, _len = qopa.length; _i < _len; _i++) { + if ('auth' === qopa[_i]) { + var opaque= auth_parms.opaque; + var algorithm= auth_parms.algorithm; + if (algorithm) { + fixed+= ' algorithm="'+algorithm+'",'; + } + else { + algorithm= 'MD5'; + } + var a1= 'MD5-sess' == algorithm ? + md5(ha1+':'+nonce+':'+cnonce) + : + ha1; + this.authorize= function(method,digestURI) { + var ha2= md5(method+':'+digestURI); + nc= nc+1; + var hexed_nc= hex8(nc); + var s= fixed+ + ' uri="'+digestURI+'",'+ + ' qop=auth,'+ + ' nc='+hexed_nc+','+ + ' cnonce="'+cnonce+'",'+ + ' response="'+md5(a1+':'+nonce+':'+hexed_nc+':'+cnonce+':auth:'+ha2)+'"'; + if (opaque) { + s+= ', opaque="'+opaque+'"'; + } + return s; + }; + return; + } + return note_error('Server does not accept any supported quality of protection techniques.'); + } + } + break; + default: + return note_error("Unknown scheme"); + } + } + + return new Authenticator(); + }; + + parse_header.authenticator= new HigherLevel(credentials,options); // deprecated + return parse_header; +}; + + +function HigherLevel(credentials,options) +{ + this.credentials= credentials + this.options= options + if (options && options.sendImmediately) { + this.sendImmediately= true; + } +} +HigherLevel.prototype.get_challenge= function(request) { + if (401 == request.statusCode && 'www-authenticate' in request.headers) { + if (!this.parse_header) { + this.parse_header= www_authenticator(this.credentials,this.options) + } + this.challenge= this.parse_header(request.headers['www-authenticate']) + return this.challenge.err; + } +} +HigherLevel.prototype._challenge= function() { + if (!this.challenge) { + if (this.sendImmediately) { + // simulate receipt of a basic challenge + this.get_challenge(basic_challenge) + return this.challenge + } + else return; // simply won't produce an 'Authorization' header + } + return this.challenge; +} +HigherLevel.prototype.authentication_string= function(method,digestURI) { + var challenge= this._challenge(); + if (!challenge) return; // simply won't produce an 'Authorization' header + if (challenge.err) return challenge.err; + return challenge.authorize(method,digestURI); +} +HigherLevel.prototype.authenticate_headers= function(headers,method,digestURI) { + var challenge= this._challenge(); + if (!challenge) return; // simply won't produce an 'Authorization' header + if (challenge.err) return challenge.err; + headers.authorization= challenge.authorize(method,digestURI); +} +HigherLevel.prototype.authenticate_request_options= function(request_options) { + var challenge= this._challenge(); + if (!challenge) return; // simply won't produce an 'Authorization' header + if (challenge.err) return challenge.err; + if (!request_options.headers) request_options.headers= {}; + request_options.headers.authorization= challenge.authorize(request_options.method,request_options.path); +} + +module.exports = www_authenticator; +module.exports.parsers= parsers; +module.exports.user_credentials= user_credentials; +module.exports.basic_challenge= basic_challenge; +module.exports.authenticator= function(username,password,options) +{ + if (2 == arguments.length && toString.call(password) != '[object String]') { + options= password; + password= null; + } + var credentials= user_credentials(username,password) + return new HigherLevel(credentials,options); +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 40d7d9ed..42423f9b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,8 +20,7 @@ "json-text-sequence": "^1.0.1", "multipart-stream": "^2.0.1", "qs": "^6.11.0", - "through2": "^4.0.2", - "www-authenticate": "^0.6.3" + "through2": "^4.0.2" }, "devDependencies": { "@jsdoc/salty": "0.2.3", @@ -5765,14 +5764,6 @@ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, - "node_modules/www-authenticate": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/www-authenticate/-/www-authenticate-0.6.3.tgz", - "integrity": "sha512-8VkdLBJiBh5aXlJvcVaPykwSI//OA+Sxw7g84vIyCqoqlXtLupGNhyXxbgVuZ7g5ZS+lCJ4bTtcw/gJciqEuAg==", - "engines": { - "node": ">= 0.8.0" - } - }, "node_modules/xml": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/xml/-/xml-1.0.1.tgz", @@ -10121,11 +10112,6 @@ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, - "www-authenticate": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/www-authenticate/-/www-authenticate-0.6.3.tgz", - "integrity": "sha512-8VkdLBJiBh5aXlJvcVaPykwSI//OA+Sxw7g84vIyCqoqlXtLupGNhyXxbgVuZ7g5ZS+lCJ4bTtcw/gJciqEuAg==" - }, "xml": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/xml/-/xml-1.0.1.tgz", diff --git a/package.json b/package.json index e4ba353f..444db78e 100644 --- a/package.json +++ b/package.json @@ -38,8 +38,7 @@ "json-text-sequence": "^1.0.1", "multipart-stream": "^2.0.1", "qs": "^6.11.0", - "through2": "^4.0.2", - "www-authenticate": "^0.6.3" + "through2": "^4.0.2" }, "repository": { "type": "git", diff --git a/test-basic/digestauth-fips-nomd5load.js b/test-basic/digestauth-fips-nomd5load.js new file mode 100644 index 00000000..c90ca73d --- /dev/null +++ b/test-basic/digestauth-fips-nomd5load.js @@ -0,0 +1,54 @@ +/* +* Copyright © 2015-2025 Progress Software Corporation and/or its subsidiaries or affiliates. All Rights Reserved. +*/ +'use strict'; + +const should = require('should'); + +describe('FIPS test - ensure MD5 hash digester object is not loaded by default on require of www-authenticate module', function () { + it('should not automatically load MD5 digest algorithm function when requiring www-authenticate module', function () { + /** + * Attempt to load/require the www-authenticate module after applying a monkey-patch + * to the crypto.createHash function to intercept any attempts to create an MD5 hash + * digester object. + * This is to simulate a FIPS-enabled environment where MD5 is not allowed. + * + * First, create a monkey-patch to intercept calls to crypto.createHash + * and throw an Error object if the code attempts to create an + * MD5 hashing algorithm object. + * + * We undo this monkey-patch after the test to avoid side effects on all the other tests. + * + * To simulate the require/load, we first delete the module from Node's require cache + * and then require it again, which forces a reload of the module. + */ + delete require.cache[require.resolve('../lib/www-authenticate/www-authenticate')]; + delete require.cache[require.resolve('../lib/www-authenticate/md5')]; + const crypto = require('crypto'); + const originalCreateHash = crypto.createHash; + + crypto.createHash = function (algorithm, ...args) { + if (algorithm.toLowerCase() === 'md5') { + throw new Error('FIPS emulation: MD5 digest algorithm is not allowed on this system!'); + } + return originalCreateHash.call(this, algorithm, ...args); + }; + + try { // we must ensure the createHash function is restored after the test + + // Verify MD5 detection works + (() => crypto.createHash('md5')).should.throw('FIPS emulation: MD5 digest algorithm is not allowed on this system!'); + + // Require the module - should not call to get MD5 digester so should not throw + (() => require('../lib/www-authenticate/md5')).should.not.throw(); + (() => require('../lib/www-authenticate/www-authenticate')).should.not.throw(); + + } finally { + // Restore the original createHash function to avoid side effects + // This MUST execute to avoid breaking other tests! + crypto.createHash = originalCreateHash; + } + }); +}); + +