Saltar al contenido principal
El script Login implementa la función que se ejecuta cada vez que un usuario debe autenticarse. Recomendamos llamar a esta función login. Este script es obligatorio tanto para la autenticación heredada como para la migración automática. Si la migración automática está configurada para la conexión, el proceso de migración se activa después del primer inicio de sesión correcto del usuario. Auth0 recomienda establecer un user_id permanente en el perfil de usuario devuelto para evitar la creación de usuarios duplicados.

Función de inicio de sesión

La función login debe:
  • Enviar las credenciales del usuario proporcionadas a la API de la base de datos externa.
  • Devolver los datos del perfil del usuario si la autenticación se realiza correctamente.
  • Devolver un error si la autenticación falla.

Definición

La función login acepta tres parámetros y devuelve una función callback:
login(userNameOrEmail, password, callback): function
ParámetroTipoDescripción
userNameOrEmailStringEl username o correo electrónico del usuario.
passwordStringLa contraseña del usuario en texto plano.
callbackFunctionSe usa para pasar errores o datos del perfil a través del flujo de procesamiento.

Ejemplo

Este es un ejemplo en pseudojavascript de cómo podría implementar la función login. Para ver ejemplos específicos de cada lenguaje, consulte Ejemplos de scripts específicos de cada lenguaje.
function login(userNameOrEmail, password, callback) {
  // Enviar credenciales a la API de base de datos externa
  let hashedPassword = hash(password);

  let options = {
    url: "https://example.com/api/authenticate",
    body: {
      email: userNameOrEmail,
      password: hashedPassword
    }
  };

  send(options, (err, profileData) => {
    // Devolver error en el callback si la autenticación no es exitosa
    if (err) {
      return callback(new WrongUsernameOrPasswordError(userNameOrEmail, "My custom error message."));
    } else {
      // Devolver datos del perfil en el callback si la autenticación es exitosa
      let profile = {
        username: profileData.username,
        email: profileData.emailAddress,
        user_id: profileData.userId
      };

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

Cifrado

Evite registrar, almacenar o transferir la credencial de contraseña en cualquier lugar sin cifrar.
Cifre el valor de password con una biblioteca de hash criptográfico como bcrypt para evitar posibles filtraciones de datos.

Ejemplo

bcrypt.hash(password, 10, function (err, hash) {
    if (err) {
        return callback(err);
    } else {
        // Retornar contraseña con hash
    }
});

Función callback

La función callback se utiliza para pasar datos del perfil del usuario o datos de error a través del flujo de procesamiento.

Definición

La función callback acepta hasta dos parámetros y devuelve una función:
callback(error[, profile]): function
ParámetroTipoObligatorioDescripción
errorObjectObligatorioContiene datos de error.
profileObjectOpcionalContiene los datos del perfil del usuario.

Devolver el perfil de usuario

Los datos del perfil que devuelve el script de Login para un usuario deben ser coherentes con los datos del perfil que devuelve el script de Get User.
Si el usuario se autentica correctamente, los datos de su perfil deben devolverse en el objeto profile en formato normalizado. Además de los campos estándar, puede incluir los campos user_metadata, app_metadata y mfa_factors.

Ejemplo

return callback(null, {
    username: "username",
    user_id: "my-custom-db|username@domain.com",
    email: "username@domain.com",
    email_verified: false,
    user_metadata: {
        language: "en"
    },
    app_metadata: {
        plan: "full"
    },
    mfa_factors: [
      {
        phone: {
          value: "+15551234567"
        }
      },
    ]
});

Devolver un error

Si ocurre un error, el parámetro error debe contener información relevante sobre el problema.

Objeto de error personalizado WrongUsernameOrPasswordError

El objeto de error personalizado WrongUsernameOrPasswordError le permite pasar datos que se mostrarán en los registros del Tenant.
Constructor
El constructor WrongUsernameOrPasswordError admite hasta dos parámetros:
new WrongUsernameOrPasswordError(userNameOrEmail[, message]): WrongUsernameOrPasswordError
ParámetroTipoObligatorioDescripción
userNameOrEmailStringObligatorioContiene el username o el correo electrónico del usuario, o un valor null.
messageStringOpcionalContiene información sobre el error.

Devolver un error con username o correo electrónico

Si devuelve un error con un valor en el campo userNameOrEmail, Auth0 registrará un evento de registro fp en el inquilino.
Ejemplo
return callback(new WrongUsernameOrPasswordError(userNameOrEmail, "My custom error message"));
Campo del evento de registro de TenantValor
Codefp
EventoError de inicio de sesión (contraseña incorrecta)
DescripciónMi mensaje de error personalizado

Devolver un error sin username ni correo electrónico

Si devuelve un error con un valor null en el campo userNameOrEmail, Auth0 registrará un evento fu en el registro del inquilino.
Ejemplo
return callback(new WrongUsernameOrPasswordError(null, "My custom error message"));
Campo del evento de registro del TenantValor
Codefu
EventoInicio de sesión fallido (correo electrónico/username no válidos)
DescripciónMy custom error message

Sincroniza los atributos del perfil del usuario en cada inicio de sesión

Habilita la configuración Sync user profile attributes at each login si quieres que Auth0 actualice los campos name, nickname, given_name, family_name y/o picture con los valores devueltos por la base de datos externa en cada inicio de sesión. Si no habilitas esta configuración, los valores devueltos por la base de datos externa la primera vez que el usuario inicie sesión se conservarán en los inicios de sesión posteriores, aunque hayan cambiado en la base de datos externa.

Ejemplos de scripts específicos de cada lenguaje

Auth0 proporciona scripts de ejemplo para usar con los siguientes lenguajes/tecnologías:

JavaScript

function login(email, password, callback) {
  // Este script debe autenticar a un usuario con las credenciales almacenadas en
  // su base de datos.
  // Se ejecuta cuando un usuario intenta iniciar sesión o inmediatamente después de
  // registrarse (como verificación de que el usuario se registró correctamente).
  //
  // Todo lo que devuelva este script se establecerá como parte del perfil del usuario
  // y será visible para cualquiera de los administradores del inquilino. Evite agregar atributos
  // con valores como contraseñas, claves, secretos, etc.
  //
  // El parámetro `password` de esta función está en texto plano. Debe ser
  // hasheado/salteado para coincidir con lo que esté almacenado en su base de datos. Por ejemplo:
  //
  //     var bcrypt = require('bcrypt@0.8.5');
  //     bcrypt.compare(password, dbPasswordHash, function(err, res)) { ... }
  //
  // Hay tres formas en que este script puede finalizar:
  // 1. Las credenciales del usuario son válidas. El perfil de usuario devuelto debe tener
  // el siguiente formato: https://auth0.com/docs/users/normalized/auth0/normalized-user-profile-schema
  //     var profile = {
  //       user_id: ..., // user_id es obligatorio
  //       email: ...,
  //       [...]
  //     };
  //     callback(null, profile);
  // 2. Las credenciales del usuario no son válidas
  //     callback(new WrongUsernameOrPasswordError(email, "my error message"));
  // 3. Ocurrió un error al intentar conectarse a su base de datos
  //     callback(new Error("my error message"));
  //
  // Una lista de módulos de Node.js que pueden referenciarse está disponible aquí:
  //
  //    https://tehsis.github.io/webtaskio-canirequire/
  const msg = 'Please implement the Login script for this database connection ' +
    'at https://manage.auth0.com/#/connections/database';
  return callback(new Error(msg));
}

Proveedor de membresía de ASP.NET (MVC3 - Universal Providers)

function login(email, password, callback) {
  const crypto = require('crypto');
  const sqlserver = require('tedious@11.0.3');
  const Connection = sqlserver.Connection;
  const Request = sqlserver.Request;
  const TYPES = sqlserver.TYPES;
  const connection = new Connection({
    userName: 'the username',
    password: 'the password',
    server: 'the server',
    options: {
      database: 'the db name',
      encrypt: true // for Windows Azure
    }
  });
  /**
   * hashPassword
   *
   * This function creates a hashed version of the password to store in the database.
   *
   * @password  {[string]}      the password entered by the user
   * @return    {[string]}      the hashed password
   */
  function hashPassword(password, salt) {
    // the default implementation uses HMACSHA256 and since Key length is 64
    // and default salt is 16 bytes, Membership will fill the buffer repeating the salt
    const key = Buffer.concat([salt, salt, salt, salt]);
    const hmac = crypto.createHmac('sha256', key);
    hmac.update(Buffer.from(password, 'ucs2'));
    return hmac.digest('base64');
  }
  connection.on('debug', function(text) {
    // if you have connection issues, uncomment this to get more detailed info
    //console.log(text);
  }).on('errorMessage', function(text) {
    // this will show any errors when connecting to the SQL database or with the SQL statements
    console.log(JSON.stringify(text));
  });
  connection.on('connect', function(err) {
    if (err) return callback(err);
    getMembershipUser(email, function(err, user) {
      if (err || !user || !user.profile || !user.password) return callback(err || new WrongUsernameOrPasswordError(email));
      const salt = Buffer.from(user.password.salt, 'base64');
      if (hashPassword(password, salt).toString('base64') !== user.password.password) {
        return callback(new WrongUsernameOrPasswordError(email));
      }
      callback(null, user.profile);
    });
  });

  // Membership Provider implementation used on Microsoft.AspNet.Providers NuGet
  /**
   * getMembershipUser
   *
   * Esta función recibe un username o correo electrónico y devuelve la información
   * del proveedor de membresía del usuario, hashes de contraseña y salt
   *
   * @usernameOrEmail  {[string]}       el username o correo electrónico; el método realizará
   *                                    una consulta en ambos campos con un OR
   *
   * @callback         {[Function]}     el primer argumento será el Error si existe,
   *                                    y el segundo argumento será un objeto de usuario
   */
  function getMembershipUser(usernameOrEmail, done) {
    var user = null;
    const query =
      'SELECT Memberships.UserId, Email, Users.UserName, Password ' +
      'FROM Memberships INNER JOIN Users ' +
      'ON Users.UserId = Memberships.UserId ' +
      'WHERE Memberships.Email = @Username OR Users.UserName = @Username';
    const getMembershipQuery = new Request(query, function(err, rowCount) {
      if (err || rowCount < 1) return done(err);
      done(err, user);
    });
    getMembershipQuery.addParameter('Username', TYPES.VarChar, usernameOrEmail);
    getMembershipQuery.on('row', function(fields) {
      user = {
        profile: {
          user_id: fields.UserId.value,
          nickname: fields.UserName.value,
          email: fields.Email.value,
        },
        password: {
          password: fields.Password.value,
          salt: fields.PasswordSalt.value
        }
      };
    });
    connection.execSql(getMembershipQuery);
  }
}

Proveedor de membresía para ASP.NET (MVC4 - Membresía simple)

function login(email, password, callback) {
  const crypto = require('crypto');
  const sqlserver = require('tedious@11.0.3');
  const Connection = sqlserver.Connection;
  const Request = sqlserver.Request;
  const TYPES = sqlserver.TYPES;
  const connection = new Connection({
    userName: 'the username',
    password: 'the password',
    server: 'the server',
    options: {
      database: 'the db name',
      encrypt: true // for Windows Azure
    }
  });
  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) {
      const ac = a.charCodeAt(i);
      const bc = b.charCodeAt(i);
      mismatch += (ac === bc ? 0 : 1);
    }
    return (mismatch === 0);
  }

  /**
   * validatePassword
   *
   * Esta función obtiene la contraseña ingresada por el usuario y el hash y salt
   * originales de la contraseña almacenados en la base de datos, y realiza un hash HMAC SHA256.
   *
   * @password      {[string]}      la contraseña ingresada por el usuario
   * @originalHash  {[string]}      el hash original de la contraseña de la base de datos
   *                                (incluye el salt).
   * @return        {[bool]}        true si la contraseña es válida
   */
  function validatePassword(password, originalHash, callback) {
    const iterations = 1000;
    const hashBytes = Buffer.from(originalHash, 'base64');
    const salt = hashBytes.slice(1, 17);
    const hash = hashBytes.slice(17, 49);
    crypto.pbkdf2(password, salt, iterations, hash.length, 'sha1', function(err, hashed) {
      if (err) return callback(err);
      const hashedBase64 = Buffer.from(hashed, 'binary').toString('base64');
      const isValid = fixedTimeComparison(hash.toString('base64'), hashedBase64);
      return callback(null, isValid);
    });
  }

  connection.on('debug', function(text) {
    // if you have connection issues, uncomment this to get more detailed info
    //console.log(text);
  }).on('errorMessage', function(text) {
    // this will show any errors when connecting to the SQL database or with the SQL statements
    console.log(JSON.stringify(text));
  });
  connection.on('connect', function(err) {
    if (err) return callback(err);
    getMembershipUser(email, function(err, user) {
      if (err || !user || !user.profile) return callback(err || new WrongUsernameOrPasswordError(email));
      validatePassword(password, user.password, function(err, isValid) {
        if (err || !isValid) return callback(err || new WrongUsernameOrPasswordError(email));
        callback(null, user.profile);
      });
    });
  });

  // Membership Provider implementation used on Microsoft.AspNet.Providers NuGet
  /**
   * getMembershipUser
   *
   * This function gets a username or email and returns a user info, password hashes and salt
   *
   * @usernameOrEamil   {[string]}    the username or email, the method will do a query
   *                                  on both with an OR
   * @callback          {[Function]}  first argument will be the Error if any, and second
   *                                  argument will be a user object
   */
  function getMembershipUser(usernameOrEmail, done) {
    var user = null;
    const query =
      'SELECT webpages_Membership.UserId, UserName, UserProfile.UserName, Password from webpages_Membership ' +
      'INNER JOIN UserProfile ON UserProfile.UserId = webpages_Membership.UserId ' +
      'WHERE UserProfile.UserName = @Username';
    const getMembershipQuery = new Request(query, function(err, rowCount) {
      if (err || rowCount < 1) return done(err);
      done(err, user);
    });
    getMembershipQuery.addParameter('Username', TYPES.VarChar, usernameOrEmail);
    getMembershipQuery.on('row', function(fields) {
      user = {
        profile: {
          user_id: fields.UserId.value,
          nickname: fields.UserName.value,
          email: fields.UserName.value,
        },
        password: fields.Password.value
      };
    });
    connection.execSql(getMembershipQuery);
  }
}

MongoDB

function login(email, password, callback) {
  const bcrypt = require('bcrypt');
  const MongoClient = require('mongodb@3.1.4').MongoClient;
  const client = new MongoClient('mongodb://user:pass@mymongoserver.com');
  client.connect(function (err) {
    if (err) return callback(err);
    const db = client.db('db-name');
    const users = db.collection('users');
    users.findOne({ email: email }, function (err, user) {
      if (err || !user) {
        client.close();
        return callback(err || new WrongUsernameOrPasswordError(email));
      }
      bcrypt.compare(password, user.password, function (err, isValid) {
        client.close();
        if (err || !isValid) return callback(err || new WrongUsernameOrPasswordError(email));
        return callback(null, {
            user_id: user._id.toString(),
            nickname: user.nickname,
            email: user.email
          });
      });
    });
  });
}

MySQL

function login(email, password, callback) {
  const mysql = require('mysql');
  const bcrypt = require('bcrypt');
  const connection = mysql({
    host: 'localhost',
    user: 'me',
    password: 'secret',
    database: 'mydb'
  });
  connection.connect();
  const query = 'SELECT id, nickname, email, password FROM users WHERE email = ?';
  connection.query(query, [ email ], function(err, results) {
    if (err) return callback(err);
    if (results.length === 0) return callback(new WrongUsernameOrPasswordError(email));
    const user = results[0];
    bcrypt.compare(password, user.password, function(err, isValid) {
      if (err || !isValid) return callback(err || new WrongUsernameOrPasswordError(email));
      callback(null, {
        user_id: user.id.toString(),
        nickname: user.nickname,
        email: user.email
      });
    });
  });
}

PostgreSQL

function login(email, password, callback) {
  //este ejemplo usa la librería "pg"
  //más información aquí: https://github.com/brianc/node-postgres
  const bcrypt = require('bcrypt');
  const postgres = require('pg');
  const conString = 'postgres://user:pass@localhost/mydb';
  postgres.connect(conString, function (err, client, done) {
    if (err) return callback(err);
    const query = 'SELECT id, nickname, email, password FROM users WHERE email = $1';
    client.query(query, [email], function (err, result) {
      // NOTA: siempre llama a `done()` aquí para cerrar
      // la conexión a la base de datos
      done();
      if (err || result.rows.length === 0) return callback(err || new WrongUsernameOrPasswordError(email));
      const user = result.rows[0];
      bcrypt.compare(password, user.password, function (err, isValid) {
        if (err || !isValid) return callback(err || new WrongUsernameOrPasswordError(email));
        return callback(null, {
          user_id: user.id,
          nickname: user.nickname,
          email: user.email
        });
      });
    });
  });
}

SQL Server

function login(email, password, callback) {
  //este ejemplo usa la biblioteca "tedious"
  //más información aquí: http://pekim.github.io/tedious/index.html
  const bcrypt = require('bcrypt');
  const sqlserver = require('tedious@11.0.3');
  const Connection = sqlserver.Connection;
  const Request = sqlserver.Request;
  const TYPES = sqlserver.TYPES;
  const connection = new Connection({
    userName:  'test',
    password:  'test',
    server:    'localhost',
    options:  {
      database: 'mydb',
      rowCollectionOnRequestCompletion: true
    }
  });
  const query = 'SELECT Id, Nickname, Email, Password FROM dbo.Users WHERE Email = @Email';
  connection.on('debug', function (text) {
    console.log(text);
  }).on('errorMessage', function (text) {
    console.log(JSON.stringify(text, null, 2));
  }).on('infoMessage', function (text) {
    console.log(JSON.stringify(text, null, 2));
  });
  connection.on('connect', function (err) {
    if (err) return callback(err);
    const request = new Request(query, function (err, rowCount, rows) {
      if (err || rowCount < 1) return callback(err || new WrongUsernameOrPasswordError(email));
      bcrypt.compare(password, rows[0][3].value, function (err, isValid) {
        if (err || !isValid) return callback(err || new WrongUsernameOrPasswordError(email));
        callback(null, {
          user_id: rows[0][0].value,
          nickname: rows[0][1].value,
          email: rows[0][2].value
        });
      });
    });
    request.addParameter('Email', TYPES.VarChar, email);
    connection.execSql(request);
  });
}

Windows Azure SQL Database

function login(email, password, callback) {
  //este ejemplo usa la librería "tedious"
  //más información aquí: http://pekim.github.io/tedious/index.html
  var Connection = require('tedious@11.0.3').Connection;
  var Request = require('tedious@11.0.3').Request;
  var TYPES = require('tedious@11.0.3').TYPES;
  var bcrypt = require('bcrypt');
  var connection = new Connection({
    userName: 'your-user@your-server-id.database.windows.net',
    password: 'the-password',
    server: 'your-server-id.database.windows.net',
    options: {
      database: 'mydb',
      encrypt: true,
      rowCollectionOnRequestCompletion: true
    }
  });
  var query = "SELECT Id, Email, Password " +
    "FROM dbo.Users WHERE Email = @Email";
  connection.on('debug', function (text) {
    // Descomente la siguiente línea para habilitar los mensajes de depuración
    // console.log(text);
  }).on('errorMessage', function (text) {
    console.log(JSON.stringify(text, null, 2));
    return callback(text);
  }).on('infoMessage', function (text) {
    // Descomente la siguiente línea para habilitar los mensajes de información
    // 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) {
        callback(new WrongUsernameOrPasswordError(email));
      } else {
        bcrypt.compare(password, rows[0][2].value, function (err, isValid) {
          if (err) { callback(new Error(err)); }
          else if (!isValid) { callback(new WrongUsernameOrPasswordError(email)); }
          else {
            callback(null, {
              user_id: rows[0][0].value,
              email: rows[0][1].value
            });
          }
        });
      }
    });
    request.addParameter('Email', TYPES.VarChar, email);
    connection.execSql(request);
  });
}

Axios

async function loginAsync(email, password, callback) {
  //debe actualizarse cuando haya nuevas versiones de axios disponibles (https://auth0-extensions.github.io/canirequire/#axios)
  const axios = require("axios@0.22.0");

  let response;

  try {
    response = await axios.post(
      //almacene la URL de la API en la configuración de la conexión para un mejor soporte de entornos SDLC
      configuration.baseAPIUrl + "/login",
      //credenciales del usuario enviadas como cuerpo de la solicitud
      {
        email: email,
        password: password,
      },
      {
        timeout: 10000, //finaliza la llamada correctamente si la solicitud expira para que el script pueda ejecutar el callback necesario
        headers: {
          //protege la llamada a la API con la apiKey almacenada en la configuración de la conexión.
          //es un enfoque rápido y sencillo, aunque usar tokens M2M es más seguro ya que
          // un secreto no debe compartirse entre el cliente y la API.
          "x-api-key": configuration.apiKey,
        },
      }
    );
  } catch (e) {
    if (e.response.status === 404) {
      //se asume que la API devuelve 404 cuando el correo electrónico/username/contraseña no son válidos
      return callback(
        new WrongUsernameOrPasswordError(email, "Invalid credentials provided.")
      );
    }
    //callback para cualquier otro tipo de error
    return callback(new Error(e.message));
  }

  try {
    let user = response.data;

    //si usa múltiples conexiones de base de datos personalizadas en su tenant, agregue como prefijo
    //al user_id una clave específica de la conexión, p. ej.: "connName|" + user.user_id
    //esto garantiza IDs de usuario únicos en todas las conexiones de base de datos
    return callback(null, {
      user_id: user.user_id,
      email: user.email,
    });
  } catch (e) {
    return callback(new Error(e.message));
  }
}

Stormpath

function login(username, password, callback) {
  // Reemplaza {yourStormpathClientId} con tu ID de Stormpath
  var url = 'https://api.stormpath.com/v1/applications/{yourStormpathClientId}/loginAttempts';
  // Agrega tu ID de cliente y secreto de la API de Stormpath
  var apiCredentials = {
    user : '{yourStormpathApiId}',
    password: '{yourStormpathApiSecret}'
  };
  // Stormpath requiere que las credenciales del usuario se pasen como un mensaje codificado en base64
  var credentials = Buffer.from(username + ':' + password).toString('base64');
  // Realiza una solicitud POST para autenticar a un usuario
  request({
    url: url,
    method: 'POST',
    auth: apiCredentials,
    json: {
      type: 'basic',
      // Se pasan las credenciales codificadas en base64
      value: credentials
    }
  }, function (error, response, body) {
    // Si la respuesta es exitosa, continuamos
    if (response.statusCode !== 200) return callback();
    // Una respuesta exitosa devolverá una URL para obtener la información del usuario
    var accountUrl = body.account.href;
    // Realiza una segunda solicitud para obtener la información del usuario.
    request({
      url: accountUrl,
      auth: apiCredentials,
      json: true
    }, function (errorUserInfo, responseUserInfo, bodyUserInfo) {
      // Si obtenemos una respuesta exitosa, la procesamos
      if (responseUserInfo.statusCode !== 200) return callback();
      // Para obtener el identificador del usuario, eliminamos la ruta de la API de Stormpath
      var id = bodyUserInfo.href.replace('https://api.stormpath.com/v1/accounts/', '');
      // Por último, definimos los datos que queremos almacenar en Auth0 y migramos al usuario
      return callback(null, {
        user_id : id,
        username: bodyUserInfo.username,
        email: bodyUserInfo.email,
        // Establecemos email_verified en true porque asumimos que, si era un usuario
        // válido en Stormpath, ya verificó su correo electrónico.
        // Si este campo no se establece, el usuario recibirá un correo electrónico
        // pidiéndole que verifique su cuenta. Puedes decidir cómo manejar esto según tu caso de uso
        email_verified: true
        // Agrega cualquier campo adicional que desees trasladar desde Stormpath
      });
    });
  });
}

Más información