¿Cómo implementar el restablecimiento de contraseña?

81

Estoy trabajando en una aplicación en ASP.NET y me preguntaba específicamente cómo podría implementar una Password Resetfunción si quisiera lanzar la mía propia.

Específicamente, tengo las siguientes preguntas:

  • ¿Cuál es una buena forma de generar una identificación única que sea difícil de descifrar?
  • ¿Debería haber un temporizador adjunto? Si es así, ¿cuánto tiempo debería ser?
  • ¿Debo registrar la dirección IP? ¿Incluso importa?
  • ¿Qué información debo solicitar en la pantalla "Restablecer contraseña"? ¿Solo dirección de correo electrónico? ¿O tal vez la dirección de correo electrónico más algún dato que 'conozcan'? (Equipo favorito, nombre del cachorro, etc.)

¿Hay otras consideraciones que deba tener en cuenta?

NB : Otras preguntas han pasado por alto la implementación técnica por completo. De hecho, la respuesta aceptada pasa por alto los detalles sangrientos. Espero que esta pregunta y las respuestas subsiguientes entren en los detalles sangrientos, y espero que al formular esta pregunta de manera mucho más estrecha, las respuestas sean menos "esponjosas" y más "gore".

Editar : Se agradecerían las respuestas que también explican cómo se modelaría y manejaría dicha tabla en SQL Server o cualquier enlace ASP.NET MVC a una respuesta.

George Stocker
fuente
ASP.NET MVC usa el proveedor de autenticación ASP.NET predeterminado, por lo que cualquier muestra de código que encuentre alrededor de ese s debe ser relevante para sus propósitos.
paulwhit

Respuestas:

66

Muchas buenas respuestas aquí, no me molestaré en repetirlas todas ...

Excepto por un problema, que se repite en casi todas las respuestas aquí, aunque es incorrecto:

Las guías son (de manera realista) únicas y estadísticamente imposibles de adivinar.

Esto no es cierto, los GUID son identificadores muy débiles y NO deben usarse para permitir el acceso a la cuenta de un usuario.
Si examina la estructura, obtiene un total de 128 bits como máximo ... lo que no se considera mucho hoy en día.
De los cuales la primera mitad es invariante típico (para el sistema generador), y la mitad de lo que queda depende del tiempo (o algo similar).
Con todo, es un mecanismo muy débil y fácilmente forzado.

¡Así que no uses eso!

En su lugar, simplemente use un generador de números aleatorios criptográficamente fuerte ( System.Security.Cryptography.RNGCryptoServiceProvider) y obtenga al menos 256 bits de entropía sin procesar.

Todo lo demás, como las otras numerosas respuestas proporcionadas.

Ávido
fuente
6
Absolutamente de acuerdo, hasta donde yo sé, los GUID nunca fueron diseñados para ser criptográficamente fuertes e imposibles de adivinar.
Jan Soltis
5
Bien dicho, AFAIK MSDN establece claramente que GUID no debe usarse por motivos de seguridad.
dr. mal
2
Los UUID de la versión 4 se han utilizado en Windows desde 2000: ¿Cómo se generan los GUID de .NET 4? - Desbordamiento de pila . Tienen 122 bits aleatorios, lo que creo que cumple con las recomendaciones del NIST. Hubo una vulnerabilidad muy grave a un ataque local, que según CryptGenRandom - Wikipedia se solucionó en Vista y XP en 2008. Entonces, ¿dónde ve problemas con el uso actual de GUID?
nealmcb
4
Ese blog "Old New Thing" describe los UUID obsoletos de la versión 1 y cita un Borrador de Internet (algo que se supone que nunca debes hacer) que expiró en 1998, 10 años antes de la publicación del blog. Sería escéptico con ellos en el futuro. Peleamos esas batallas hace mucho tiempo, y parece que hemos ganado la mayoría de ellas. Todavía estoy de acuerdo en que usar una llamada API limpia a una fuente
criptoaleatoria
1
Por lo que vale, esto no responde a la pregunta, "cómo restablecer una contraseña". Acaba de vomitar grandes puntos sobre los GUID.
Rex Whitten
67

EDITAR 2012/05/22: Como seguimiento de esta popular respuesta, ya no uso GUID en este procedimiento. Al igual que la otra respuesta popular, ahora uso mi propio algoritmo de hash para generar la clave para enviar la URL. Esto también tiene la ventaja de ser más corto. Mire en System.Security.Cryptography para generarlos, que generalmente también uso un SALT.

Primero, no restablezca inmediatamente la contraseña del usuario.

Primero, no restablezca inmediatamente la contraseña del usuario cuando lo soliciten. Esta es una brecha de seguridad, ya que alguien podría adivinar direcciones de correo electrónico (es decir, su dirección de correo electrónico en la empresa) y restablecer las contraseñas a su antojo. Las mejores prácticas en estos días generalmente incluyen un enlace de "confirmación" enviado a la dirección de correo electrónico del usuario, confirmando que desean restablecerla. Este enlace es donde desea enviar el enlace de clave única. Envío el mío con un enlace como:domain.com/User/PasswordReset/xjdk2ms92

Sí, establezca un tiempo de espera en el enlace y almacene la clave y el tiempo de espera en su backend (y sal si está usando uno). Los tiempos de espera de 3 días son la norma, y ​​asegúrese de notificar al usuario de 3 días a nivel web cuando soliciten reiniciar.

Utilice una clave hash única

Mi respuesta anterior decía usar un GUID. Ahora estoy editando esto para aconsejar a todos que usen un hash generado aleatoriamente, por ejemplo, usando el RNGCryptoServiceProvider. Y asegúrese de eliminar las "palabras reales" del hash. Recuerdo una llamada telefónica especial a las 6 am en la que una mujer recibió cierta palabra "c" en su clave hash "se supone que sea aleatorio" que hizo un desarrollador. Doh!

Todo el procedimiento

  • El usuario hace clic en "restablecer" la contraseña.
  • Se solicita al usuario un correo electrónico.
  • El usuario ingresa el correo electrónico y hace clic en enviar. No confirme ni niegue el correo electrónico, ya que esto también es una mala práctica. Simplemente diga: "Hemos enviado una solicitud de restablecimiento de contraseña si el correo electrónico está verificado". o algo críptico por igual.
  • Usted crea un hash a partir de RNGCryptoServiceProvider, lo almacena como una entidad separada en una ut_UserPasswordRequeststabla y lo vincula al usuario. Así que esto para que pueda rastrear solicitudes antiguas e informar al usuario que los enlaces más antiguos han expirado.
  • Envíe el enlace al correo electrónico.

El usuario obtiene el enlace, me gusta http://domain.com/User/PasswordReset/xjdk2ms92y hace clic en él.

Si se verifica el enlace, solicita una nueva contraseña. Simple, y el usuario puede establecer su propia contraseña. O establezca su propia contraseña críptica aquí e infórmeles de su nueva contraseña aquí (y envíelos por correo electrónico).

eduncan911
fuente
1
Me preguntaba, si la contraseña de usuario real tiene hash, ¿por qué generar una nueva clave HASH? ¿No sería correcto enviar un correo electrónico al usuario con un enlace para restablecer la contraseña pasando la contraseña hash? La contraseña hash no se puede revertir, cuando el usuario hace clic en el enlace, el servidor recibirá la contraseña hash, la comparará con la almacenada real y luego permitirá que el usuario cambie la contraseña.
Daniel
Y otra cosa muy buena de esto, es que no necesita configurar el tiempo de espera, una vez que el usuario ha cambiado la contraseña, el enlace anterior ya no será válido automáticamente, porque se cambió la contraseña hash almacenada en la base de datos.
Daniel
@Daniel, esa es una muy mala idea. Creo que necesitas buscar en Google el término "ataques de fuerza bruta". Además, la razón por la que SÍ desea que expire es en caso de que el correo electrónico de alguien se vea comprometido un año después (y nunca lo restablezca), el pirata informático obtiene los derechos para cambiar la contraseña.
eduncan911
@ educan911. Conozco los ataques de fuerza bruta, pero también, para tener acceso a la clave hash, la persona con malas intenciones debe tener acceso al correo electrónico, y si tiene acceso a eso, no es necesario revertir la contraseña hash. Además, para que sea casi imposible, puede hash la contraseña hash, o incluso mejor, hash la contraseña con algo más. No estoy en desacuerdo contigo, solo estoy tratando de hacer una lluvia de ideas al respecto
Daniel
8

Primero, necesitamos saber lo que ya sabe sobre el usuario. Obviamente, tienes un nombre de usuario y una contraseña antigua. ¿Qué más sabes? ¿Tienes dirección de correo electrónico? ¿Tienes datos sobre la flor favorita del usuario?

Suponiendo que tiene un nombre de usuario, una contraseña y una dirección de correo electrónico que funcione, debe agregar dos campos a su tabla de usuario (asumiendo que es una tabla de base de datos): una fecha llamada new_passwd_expire y una cadena new_passwd_id.

Suponiendo que tiene la dirección de correo electrónico del usuario, cuando alguien solicita un restablecimiento de contraseña, actualiza la tabla de usuarios de la siguiente manera:

new_passwd_expire = now() + some number of days
new_passwd_id = some random string of characters (see below)

A continuación, envía un correo electrónico al usuario en esa dirección:

Querido fulano de tal

Alguien ha solicitado una nueva contraseña para la cuenta de usuario <nombre de usuario> en <nombre de su sitio web>. Si solicitó este restablecimiento de contraseña, siga este enlace:

http://example.com/yourscript.lang?update= < new_password_id >

Si ese enlace no funciona, puede ir a http://example.com/yourscript.lang e ingresar lo siguiente en el formulario: <new_password_id>

Si no solicitó un restablecimiento de contraseña, puede ignorar este correo electrónico.

Gracias, yada yada

Ahora, codificando yourscript.lang: este script necesita un formulario. Si la actualización de var pasó a la URL, el formulario solo solicita el nombre de usuario y la dirección de correo electrónico del usuario. Si la actualización no se aprueba, solicita el nombre de usuario, la dirección de correo electrónico y el código de identificación enviado en el correo electrónico. También solicita una nueva contraseña (dos veces, por supuesto).

Para verificar la nueva contraseña del usuario, verifica que el nombre de usuario, la dirección de correo electrónico y el código de identificación coincidan, que la solicitud no haya expirado y que las dos nuevas contraseñas coincidan. Si tiene éxito, cambie la contraseña del usuario a la nueva contraseña y borre los campos de restablecimiento de contraseña de la tabla de usuarios. También asegúrese de cerrar la sesión del usuario / borrar las cookies relacionadas con el inicio de sesión y redirigir al usuario a la página de inicio de sesión.

Básicamente, el campo new_passwd_id es una contraseña que solo funciona en la página de restablecimiento de contraseña.

Una mejora potencial: podría eliminar <nombre de usuario> del correo electrónico. "Alguien ha solicitado un restablecimiento de contraseña para una cuenta en esta dirección de correo electrónico ..." Por lo tanto, el nombre de usuario es algo que solo el usuario sabe si el correo electrónico es interceptado. No comencé de esa manera porque si alguien está atacando la cuenta, ya conoce el nombre de usuario. Esta oscuridad adicional detiene los ataques de oportunidad de intermediarios en caso de que alguien malintencionado intercepte el correo electrónico.

En cuanto a tus preguntas:

Generando la cadena aleatoria: no necesita ser extremadamente aleatorio. Cualquier generador de GUID o incluso md5 (concat (salt, current_timestamp ())) es suficiente, donde salt es algo en el registro del usuario como se creó la cuenta de marca de tiempo. Tiene que ser algo que el usuario no pueda ver.

timer: Sí, lo necesitas solo para mantener tu base de datos sana. No es realmente necesario más de una semana, pero al menos 2 días, ya que nunca se sabe cuánto puede durar un retraso de correo electrónico.

Dirección IP: dado que el correo electrónico puede demorarse días, la dirección IP solo es útil para el registro, no para la validación. Si desea registrarlo, hágalo, de lo contrario no lo necesita.

Pantalla de reinicio: ver arriba.

Espero que lo cubra. Buena suerte.

jmucchiello
fuente
¿No podría un atacante potencial usar el MD5 de la marca de fecha actual para ingresar?
George Stocker
Recomiendo encarecidamente no enviar una contraseña por correo electrónico por cable. La mayoría de los usuarios dejan estos correos electrónicos sin eliminar, lo cual es una brecha de seguridad; a algunos de ellos les gustará simplemente copiarlos y pegarlos cada vez de sus correos electrónicos 'favoritos'. ¿Qué pasa si el certificado del servidor de correo de la empresa de los usuarios caduca y se detecta el tráfico? Para minimizar esta posible violación es (1) establecer un tiempo de expiración corto para esta contraseña en particular - 1 hora, y (2) forzar al usuario a actualizarla en el próximo inicio de sesión.
Ognyan Dimitrov
Ognyan, la contraseña enviada por correo electrónico solo funciona una vez. Deben cambiar su contraseña después de iniciar sesión y el correo electrónico no contiene el nombre de inicio de sesión del usuario. Entonces, no, no pueden simplemente copiarlo y pegarlo cada vez. No eliminar el correo electrónico no es un problema de seguridad, ya que es solo una cadena de letras / números sin sentido que no le dará NADA al atacante después de restablecer la contraseña.
jmucchiello
3

Es probable que un GUID enviado a la dirección de correo electrónico registrada sea suficiente para la mayoría de las aplicaciones corrientes, con un tiempo de espera aún mejor.

Después de todo, si el buzón de correo electrónico del usuario se ha visto comprometido (es decir, un pirata informático tiene el inicio de sesión / contraseña para la dirección de correo electrónico), no hay mucho que pueda hacer al respecto.

EJ Brennan
fuente
2

Puede enviar un correo electrónico al usuario con un enlace. Este enlace contendría una cadena difícil de adivinar (como GUID). En el lado del servidor, también almacenaría la misma cadena que envió al usuario. Ahora, cuando el usuario presiona el enlace, puede encontrar en su entrada de base de datos la misma cadena secreta y restablecer su contraseña.

Sergej Andrejev
fuente
Sería útil contar con más detalles.
George Stocker
2

1) Para generar la identificación única, puede usar Secure Hash Algorithm. 2) temporizador adjunto? ¿Quiso decir un vencimiento para el enlace de restablecimiento de pwd? Sí, puede tener un conjunto de Caducidad 3) Puede solicitar más información además del ID de correo electrónico para validar ... Como la fecha de nacimiento o algunas preguntas de seguridad 4) También puede generar caracteres aleatorios y solicitar ingresarlos junto con la solicitud .. para asegurarse de que la solicitud de contraseña no esté automatizada por algún software espía o cosas por el estilo ..

Chico de Java
fuente
0

Creo que la guía de Microsoft para ASP.NET Identity es un buen comienzo.

https://docs.microsoft.com/en-us/aspnet/identity/overview/features-api/account-confirmation-and-password-recovery-with-aspnet-identity

Código que uso para ASP.NET Identity:

Web.Config:

<add key="AllowedHosts" value="example.com,example2.com" />

AccountController.cs:

[Route("RequestResetPasswordToken/{email}/")]
[HttpGet]
[AllowAnonymous]
public async Task<IHttpActionResult> GetResetPasswordToken([FromUri]string email)
{
    if (!ModelState.IsValid)
        return BadRequest(ModelState);

    var user = await UserManager.FindByEmailAsync(email);
    if (user == null)
    {
        Logger.Warn("Password reset token requested for non existing email");
        // Don't reveal that the user does not exist
        return NoContent();
    }

    //Prevent Host Header Attack -> Password Reset Poisoning. 
    //If the IIS has a binding to accept connections on 80/443 the host parameter can be changed.
    //See https://security.stackexchange.com/a/170759/67046
    if (!ConfigurationManager.AppSettings["AllowedHosts"].Split(',').Contains(Request.RequestUri.Host)) {
            Logger.Warn($"Non allowed host detected for password reset {Request.RequestUri.Scheme}://{Request.Headers.Host}");
            return BadRequest();
    }

    Logger.Info("Creating password reset token for user id {0}", user.Id);

    var host = $"{Request.RequestUri.Scheme}://{Request.Headers.Host}";
    var token = await UserManager.GeneratePasswordResetTokenAsync(user.Id);
    var callbackUrl = $"{host}/resetPassword/{HttpContext.Current.Server.UrlEncode(user.Email)}/{HttpContext.Current.Server.UrlEncode(token)}";

    var subject = "Client - Password reset.";
    var body = "<html><body>" +
               "<h2>Password reset</h2>" +
               $"<p>Hi {user.FullName}, <a href=\"{callbackUrl}\"> please click this link to reset your password </a></p>" +
               "</body></html>";

    var message = new IdentityMessage
    {
        Body = body,
        Destination = user.Email,
        Subject = subject
    };

    await UserManager.EmailService.SendAsync(message);

    return NoContent();
}

[HttpPost]
[Route("ResetPassword/")]
[AllowAnonymous]
public async Task<IHttpActionResult> ResetPasswordAsync(ResetPasswordRequestModel model)
{
    if (!ModelState.IsValid)
        return NoContent();

    var user = await UserManager.FindByEmailAsync(model.Email);
    if (user == null)
    {
        Logger.Warn("Reset password request for non existing email");
        return NoContent();
    }            

    if (!await UserManager.UserTokenProvider.ValidateAsync("ResetPassword", model.Token, UserManager, user))
    {
        Logger.Warn("Reset password requested with wrong token");
        return NoContent();
    }

    var result = await UserManager.ResetPasswordAsync(user.Id, model.Token, model.NewPassword);

    if (result.Succeeded)
    {
        Logger.Info("Creating password reset token for user id {0}", user.Id);

        const string subject = "Client - Password reset success.";
        var body = "<html><body>" +
                   "<h1>Your password for Client was reset</h1>" +
                   $"<p>Hi {user.FullName}!</p>" +
                   "<p>Your password for Client was reset. Please inform us if you did not request this change.</p>" +
                   "</body></html>";

        var message = new IdentityMessage
        {
            Body = body,
            Destination = user.Email,
            Subject = subject
        };

        await UserManager.EmailService.SendAsync(message);
    }

    return NoContent();
}

public class ResetPasswordRequestModel
{
    [Required]
    [Display(Name = "Token")]
    public string Token { get; set; }

    [Required]
    [Display(Name = "Email")]
    public string Email { get; set; }

    [Required]
    [StringLength(100, ErrorMessage = "The {0} must be at least {2} characters long.", MinimumLength = 10)]
    [DataType(DataType.Password)]
    [Display(Name = "New password")]
    public string NewPassword { get; set; }

    [DataType(DataType.Password)]
    [Display(Name = "Confirm new password")]
    [Compare("NewPassword", ErrorMessage = "The new password and confirmation password do not match.")]
    public string ConfirmPassword { get; set; }
}
Ogglas
fuente