Automatic User Migration Script breaks when switching from Node 4 to Node 8

I’ve been using the following Database Connection Login script with the Node 4 runtime to automatically migrate users from my Azure SQL database. The script was copied mostly as is from some Auth0 docs (don’t remember where exactly).

/**
 * See https://auth0.com/docs/users/migrations/automatic
 * 
 * Once all users are migrated, then update this function to a NO-OP:
 *
 * function login (email, password, callback) {
 *   return callback(null, null);
 * }
 * 
 * !! Important !!
 * Once this function is converted to NO-OP, do NOT disable the
 * "Import Users to Auth0" setting on the connection settings page.
 * Keeping this setting enabled will ensure that users are directed
 * to use the new Auth0 database workflow.
 * 
 * Note: Saving this script complains about 'WrongUsernameOrPasswordError'
 * not being defined. This is OK, so save anyway.
 */

function login(email, password, callback) {
  //this example uses the "tedious" library
  //more info here: http://pekim.github.io/tedious/index.html
  var Connection = require('tedious@1.11.0').Connection;
  var Request = require('tedious@1.11.0').Request;
  var TYPES = require('tedious@1.11.0').TYPES;

  var connection = new Connection({
    userName: configuration.legacyUserDatabaseUsername,
    password: configuration.legacyUserDatabasePassword,
    server: configuration.legacyUserDatabaseServer,
    options: {
      database: configuration.legacyUserDatabase,
      encrypt: true,
      rowCollectionOnRequestCompletion: true
    }
  });
  
  console.log(connection);

  var query = "SELECT Id, Email, Password " +
    "FROM dbo.Users WHERE Email = @Email";

  connection.on('debug', function (text) {
    // Uncomment next line in order to enable debugging messages
    //console.log(text);
  }).on('errorMessage', function (text) {
    //console.log(JSON.stringify(text, null, 2));
    return callback(text);
  }).on('infoMessage', function (text) {
    // Uncomment next line in order to enable information messages
    //console.log(JSON.stringify(text, null, 2));
  });

  connection.on('connect', function (err) {
    if (err) {
      return callback(err); }

    var request = new Request(query, function (err, rowCount, rows) {
      if (err) {
        callback(new Error(err));
      } else if (rowCount < 1) {
        // unauthorized
        callback(new WrongUsernameOrPasswordError(email));
      } else {
        validatePassword(password, rows[0][2].value, function(err, isValid) {
          if (!isValid) {
            // unauthorized
            return callback(new WrongUsernameOrPasswordError(email));
          }

          var profile = {
            user_id: rows[0][0].value,
            email: rows[0][1].value
          };
        
          callback(null, profile);
        });
      }
    });

    request.addParameter('Email', TYPES.VarChar, email);
    connection.execSql(request);
  });
  
  /**
   * fixedTimeComparison
   * 
   * Taken unmodified from template:
   *   ASP.NET Membership Provider (MVC4 - Simple Membership)
   *
   * This function gets the password entered by the user, and the original password
   * hash and salt from database and performs an HMAC SHA256 hash.
   *
   * @password      {[string]}      the password entered by the user
   * @originalHash  {[string]}      the original password hashed from the database
   *                                (including the salt).
   * @return        {[bool]}        true if password validates
   */
  function fixedTimeComparison(a, b) {
    var mismatch = (a.length === b.length ? 0 : 1);
    if (mismatch) {
      b = a;
    }

    for (var i = 0, il = a.length; i < il; ++i) {
      var ac = a.charCodeAt(i);
      var bc = b.charCodeAt(i);
      mismatch += (ac === bc ? 0 : 1);
    }

    return (mismatch === 0);
  }
  
  /**
   * validatePassword
   * 
   * Taken unmodified from template:
   *   ASP.NET Membership Provider (MVC4 - Simple Membership)
   *
   * This function gets the password entered by the user, and the original password
   * hash and salt from database and performs an HMAC SHA256 hash.
   *
   * @password      {[string]}      the password entered by the user
   * @originalHash  {[string]}      the original password hashed from the database
   *                                (including the salt).
   * @return        {[bool]}        true if password validates
   */
  function validatePassword(password, originalHash, callback) {
    var iterations = 1000;
    var hashBytes = new Buffer(originalHash, 'base64');
    var salt = hashBytes.slice(1, 17).toString('binary');
    var hash = hashBytes.slice(17, 49);
    crypto.pbkdf2(password, salt, iterations, hash.length, function(err, hashed) {
      if (err) {
        return callback(err);
      }
      var hashedBase64 = new Buffer(hashed, 'binary').toString('base64');

      var isValid = fixedTimeComparison(hash.toString('base64'), hashedBase64);

      return callback(null, isValid);    
    });
  }
}

After upgrading my Auth0 tenant to the Node 8 runtime, the above script stopped working. My logs show the following exception.

{
  "code": 500,
  "error": "Script generated an unhandled asynchronous exception.",
  "details": "TypeError: The \"digest\" argument is required and must not be undefined",
  "name": "TypeError",
  "message": "The \"digest\" argument is required and must not be undefined",
  "stack": "TypeError: The \"digest\" argument is required and must not be undefined\n    at pbkdf2 (crypto.js:694:11)\n    at Object.exports.pbkdf2 (crypto.js:682:10)\n    at validatePassword (/data/io/46303d62-70ee-4b5b-88ea-dc5f46963494/webtask.js:138:12)\n    at Request.userCallback (/data/io/46303d62-70ee-4b5b-88ea-dc5f46963494/webtask.js:68:9)\n    at Request.callback (/data/_verquire/tedious/1.11.0/node_modules/tedious/lib/request.js:30:27)\n    at Connection.message (/data/_verquire/tedious/1.11.0/node_modules/tedious/lib/connection.js:283:29)\n    at Connection.dispatchEvent (/data/_verquire/tedious/1.11.0/node_modules/tedious/lib/connection.js:752:59)\n    at MessageIO.<anonymous> (/data/_verquire/tedious/1.11.0/node_modules/tedious/lib/connection.js:685:22)\n    at emitNone (events.js:106:13)\n    at MessageIO.emit (events.js:208:7)"
}

I’ve tried using later versions of the “tedious” package, but then I get errors saying that the script cannot find those versions. What needs to be updated in this script in order for it to run in Node 8?

I’m also having the same issue here with the same code snippet. Any help?

Good morning @mostafa.abdelrazek and @bugged , can you both please try adding the following variable declaration to the code when you get a minute? Please let me know if this ends up helping the challenge you both are facing.

var jwt = require('jsonwebtoken');

Thanks in advance!

Thanks @James.Morrison for your help.

Where would you want me to put this variable? because when I put it at the beginning of my code it
showed warning: redifinition of 'jwt' and resulted in the same error:


{ "code": 500, 
  "error": "Script generated an unhandled asynchronous exception.", 
  "details": "TypeError: The \"digest\" argument is required and must not be undefined", 
  "name": "TypeError", 
  "message": "The \"digest\" argument is required and must not be undefined", 
  "stack": "TypeError: The \"digest\" argument is required and must not be undefined\n at pbkdf2 (crypto.js:694:11)\n at Object.exports.pbkdf2 (crypto.js:682:10)\n at validatePassword (/data/io/19e09ab6-d46d-4b19-be3c-b5088e6c1b92/webtask.js:139:12)\n at Request.userCallback (/data/io/19e09ab6-d46d-4b19-be3c-b5088e6c1b92/webtask.js:71:9)\n at Request.callback (/data/_verquire/tedious/1.11.0/node_modules/tedious/lib/request.js:30:27)\n at Connection.message (/data/_verquire/tedious/1.11.0/node_modules/tedious/lib/connection.js:283:29)\n at Connection.dispatchEvent (/data/_verquire/tedious/1.11.0/node_modules/tedious/lib/connection.js:752:59)\n at MessageIO.<anonymous> (/data/_verquire/tedious/1.11.0/node_modules/tedious/lib/connection.js:685:22)\n at emitNone (events.js:106:13)\n at MessageIO.emit (events.js:208:7)"
 }

@James.Morrison adding that to my script made no difference.

Hi @bugged and @James.Morrison

It worked with me when I modified the Validate password function to be like this


  function validatePassword(password, originalHash, callback) {
    var jwt = require('jsonwebtoken');
    var iterations = 1000;
    var hashBytes = new Buffer(originalHash, 'base64');
    var salt = hashBytes.slice(1, 17).toString('binary');
    var hash = hashBytes.slice(17, 49);
    
    crypto.pbkdf2(password, salt, iterations, hash.length, jwt, function(err, hashed) {
      
      if (err) {
        return callback(err);
      }
      
      var hashedBase64 = new Buffer(hashed, 'binary').toString('base64');

      var isValid = fixedTimeComparison(hash.toString('base64'), hashedBase64);

      return callback(null, isValid);    
    });
  }

@mostafa.abdelrazek and @James.Morrison, I modified the validatePassword method as suggested, and I no longer get the digest error. But now I consistently get 401 responses when I “Try” this script in Node 8, yet it works fine in Node 4. So there’s still something working differently between those two runtimes with regards to this script.

@bugged and @James.Morrison at the time of my reply, it was working fine. But today I’m facing exact the same issue 401 - Unauthorized. it seams there is a problem with the hashing algorithm. It does not generate the same hash it was created in the ASP.NET Core Identity !
did you find any solution to this?

When you get a moment @bugged and @mostafa.abdelrazek can you share your .net hashing code? Thanks in advance!

@James.Morrison it looks like we’re just using whatever default hashing logic that is used by

// Type: Microsoft.AspNet.Identity.UserManager`2
// Assembly: Microsoft.AspNet.Identity.Core, Version=2.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35

which has a dependency on

// Type: Microsoft.AspNet.Identity.PasswordHasher
// Assembly: Microsoft.AspNet.Identity.Core, Version=2.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35

which has a dependency on

// Type: Microsoft.AspNet.Identity.Crypto
// Assembly: Microsoft.AspNet.Identity.Core, Version=2.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35

which has the following implementation

public static string HashPassword(string password)
    {
      if (password == null)
        throw new ArgumentNullException(nameof (password));
      byte[] salt;
      byte[] bytes;
      using (Rfc2898DeriveBytes rfc2898DeriveBytes = new Rfc2898DeriveBytes(password, 16, 1000))
      {
        salt = rfc2898DeriveBytes.Salt;
        bytes = rfc2898DeriveBytes.GetBytes(32);
      }
      byte[] inArray = new byte[49];
      Buffer.BlockCopy((Array) salt, 0, (Array) inArray, 1, 16);
      Buffer.BlockCopy((Array) bytes, 0, (Array) inArray, 17, 32);
      return Convert.ToBase64String(inArray);
    }

Does that help?

1 Like

I tried changing the validatePassword function from

var salt = hashBytes.slice(1, 17).toString('binary');
var hash = hashBytes.slice(17, 49);

to

var salt = hashBytes.slice(1, 16).toString('binary');
var hash = hashBytes.slice(17, 32);

to match the Microsoft.AspNet.Identity.Crypto implementation but had no luck with the result.

1 Like

Thanks for sharing @bugged, we’ll take a look and investigate!

Replace the validatePassword function in Node 8 with the following code to make it equivalent to its Node 4 counterpart:

  function validatePassword(password, originalHash, callback) {
    var iterations = 1000;
    var hashBytes = new Buffer(originalHash, 'base64');
    var salt = hashBytes.slice(1, 17);
    var hash = hashBytes.slice(17, 49);
    crypto.pbkdf2(password, salt, iterations, hash.length, 'sha1', function(err, hashed) {
      if (err) {
        return callback(err);
      }
      var hashedBase64 = new Buffer(hashed, 'binary').toString('base64');

      var isValid = fixedTimeComparison(hash.toString('base64'), hashedBase64);

      return callback(null, isValid);    
    });
  }

The salt = line is different (no more .toString('binary') because of encoding differences in Node 8) and also you need to specify the digest parameter before the callback when calling crypto.pbkdf2 before the callback function. 'sha1' was the default used in Node 4.

2 Likes

@nicolas_sabena @James.Morrison that works. Thanks!

CC @mostafa.abdelrazek

1 Like

Thanks for confirming everyone.

We are working on an improved set of custom DB connection templates. While the ASP.Net templates are largely unchanged (because we need compatibility with existing data) I’ve already submitted these modifications to make the ASP.Net MVC 4 templates compatible with Node 8. So the above code should be available directly from the Dashboard templates shortly.

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.