Auth0 Home Blog Docs

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

custom-database
database-connections
migrate-users

#1

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?


401 Unauthorized Auth0 - Windows Azure Sql Database
#2

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


#3

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!


#4

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)"
 }


#5

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


#6

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);    
    });
  }


#7

@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.


#8

@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?