Seguridad de Spring: Funcionalidad de Olvido de Contraseña

S

Introducción

Internet se está orientando cada vez más a los servicios con más negocios y empresas que presentan ofertas que se pueden proporcionar o acceder en línea. Esto requiere que los usuarios creen muchas cuentas en muchas plataformas diferentes para los servicios que obtienen en línea. Dichos servicios van desde compras en línea hasta servicios basados ​​en suscripción, como ofertas de música y entretenimiento y también servicios educativos que incluyen cursos y paquetes de tutoriales.

Los usuarios de Internet terminan creando muchas cuentas diferentes para diferentes plataformas y no se recomienda usar la misma contraseña en todos los servicios. Esto conlleva la carga de recordar múltiples contraseñas diferentes para múltiples cuentas y, desafortunadamente, algunas se filtran y se olvidan porque, después de todo, somos humanos.

Olvidar las contraseñas es un problema real al que se enfrentan los usuarios y, como desarrolladores de sistemas y plataformas, podemos facilitar a nuestros usuarios la gestión de sus contraseñas al ofrecer una funcionalidad que les permita restablecer sus contraseñas de forma segura en caso de que las olviden. Esto ayudará a mejorar la retención de clientes en nuestra plataforma, ya que pueden estar seguros de que, en caso de que pierdan su contraseña, no habrán perdido su cuenta.

En esta publicación, exploraremos cómo Spring Security nos ayuda no solo a proteger nuestras aplicaciones basadas en Spring, sino que también ayuda a nuestros usuarios a recuperar sus contraseñas perdidas de una manera fácil y segura.

Protección de aplicaciones web con Spring Security

Spring Security es un marco que es fácil de ampliar y personalizar y se centra en la provisión de funciones de autenticación y control de acceso para aplicaciones basadas en Spring. Maneja la autenticación y autorización y también ayuda a proteger las aplicaciones Java contra vulnerabilidades de seguridad y ataques comunes, como la fijación de sesiones, el secuestro de clics y la falsificación de solicitudes entre sitios, entre otros.

Spring Security también se puede usar para simplificar las instalaciones de administración de cuentas en la aplicación Java Enterprise a través de características como la provisión de un marco de autorización OAuth2 para permitir que los usuarios usen plataformas de terceros para identificarse en nuestras aplicaciones Java.

Esto se implementa más comúnmente a través del inicio de sesión social, donde podemos usar nuestras cuentas en plataformas como Facebook, Twitter, Google y Github para acceder e identificarnos en otras plataformas.

OpenID es un protocolo de autenticación promovido por OpenID Foundation que es descentralizado y estándar, que se puede usar para iniciar sesión en varios sitios web sin tener que crear nuevas contraseñas. OpenID es compatible con Spring Security y se puede utilizar para facilitar el registro y el acceso a nuestras aplicaciones Java para nuestros usuarios finales.

En el caso de organizaciones que utilizan el protocolo LDAP (Protocolo ligero de acceso a directorios) como servicio de autenticación y repositorio central de información del usuario, Spring Security proporciona la funcionalidad para integrar LDAP en su aplicación basada en Spring.

Esto permitirá a los miembros actuales de las organizaciones acceder de forma segura a nuevas aplicaciones con sus credenciales existentes sin tener que crear un nuevo conjunto de credenciales.

Gestión de contraseñas en Spring Security

Hemos visto un subconjunto de la funcionalidad que ofrece Spring Security y hace aún más por nuestras contraseñas. Spring Security toma las credenciales de un usuario y las convierte en un token que se pasa a una AuthenticationManagerinstancia para validar las credenciales. En caso de que la contraseña sea incorrecta, Spring Security proporcionará comentarios sobre el error y el proceso no continuará más allá de ese punto.

Después de la validación, se establece un contexto de seguridad y ahora el usuario se considera autenticado. El contexto de seguridad también define la función del usuario en el sistema y se puede utilizar para determinar el nivel de acceso permitido, ya sea como administrador o como usuario normal.

Spring Security utiliza una PasswordEncoderinterfaz para codificar o transformar contraseñas para facilitar el almacenamiento seguro de las credenciales. Se supone que esta contraseña codificada es unidireccional y el proceso de verificación implica comparar la contraseña que el usuario ha proporcionado con la que está almacenada, y si coinciden, los detalles son correctos.

Para mejorar la seguridad de las contraseñas de los usuarios, Spring Security permite a los desarrolladores usar funciones unidireccionales (o hashes) para codificar contraseñas como Bcrypt , Argon2 , Scrypt y PBKDF2 . Estas funciones requieren muchos recursos, pero su propósito es ser unidireccionales y dificultar a los atacantes descifrar y extraer las credenciales de los usuarios.

Si desea leer más sobre este tema, puede consultar el artículo Codificación de contraseña con Spring Security.

Las prácticas de almacenamiento de contraseñas seguirán cambiando con el tiempo para mejorar la seguridad de los métodos actuales y es por esta razón que Spring Security introdujo la DelegatingPasswordEncoderinterfaz en la versión 5.0+.

Garantiza que la codificación de contraseñas utilice los métodos recomendados actualmente para el almacenamiento de contraseñas y permite la actualización de las funciones de codificación en el futuro. También nos permite usar diferentes funciones de codificación para diferentes contraseñas y esto se puede distinguir usando un identificador prefijado en la contraseña codificada. Por ejemplo, si usamos Bcrypt para codificar nuestras contraseñas, la salida sería, por ejemplo:

{bcrypt}$2a$12$rBvYrRneJjT/pmXakMbBg.vA1jUCpEMPE5z2tY3/4kyFw.KoiZA6C

En ausencia del prefijo de identificación, se utiliza una función de codificación predeterminada.

Así es como Spring maneja las contraseñas y la seguridad en general, pero ¿qué sucede cuando nuestros usuarios olvidan sus credenciales?

Envío de correos electrónicos en Spring Boot

Los sistemas y plataformas utilizan ampliamente los correos electrónicos para identificar a los usuarios y también para enviarles comunicaciones y promociones. Esto hace que las direcciones de correo electrónico sean importantes para la identidad de un usuario de Internet que termina teniendo pocas cuentas de correo electrónico para uso personal. Esto significa que la cuenta de correo electrónico de un usuario es fácil y fácilmente accesible para ellos, ya que la usan con frecuencia. El correo electrónico es, por tanto, una de las mejores vías para ayudar a los usuarios a recuperar sus contraseñas.

Para permitir que nuestros usuarios restablezcan sus contraseñas, nuestra aplicación basada en Spring debería poder enviar correos electrónicos a los usuarios. A través de estos correos electrónicos, podemos proporcionar tokens o enlaces o instrucciones a nuestros usuarios que detallan cómo pueden recuperar sus cuentas.

Java proporciona la biblioteca javax.mail , que también se conoce como JavaMail, que podemos usar para enviar correos electrónicos a nuestros usuarios cada vez que olvidan sus contraseñas. La JavaMailbiblioteca nos permite redactar y enviar correos electrónicos a través de varios protocolos, como SMTP (Protocolo simple de transferencia de correo), POP (Protocolo de oficina postal) o IMAP (Protocolo de acceso a mensajes de Internet).

Nos permite enviar correos electrónicos de texto sin formato, así como mensajes que contienen HTML, lo que hace que nuestros correos electrónicos sean llamativos y atractivos a la vista. Incluso podemos enviar correos electrónicos con archivos adjuntos cuando usamos la JavaMailbiblioteca.

Si está interesado en obtener más información sobre el envío de correos electrónicos en Java, lo tenemos cubierto.

Otras bibliotecas que puede usar para enviar correos electrónicos en Java incluyen:

Implementación

Reunamos ahora todo lo anterior en un proyecto y ayudemos a nuestros usuarios a recuperar sus contraseñas con facilidad.

El objetivo principal de este artículo fue brindar orientación sobre la funcionalidad de restablecimiento de contraseña. Antes de que podamos restablecer las contraseñas, debemos permitir que los usuarios se registren, confirmen sus cuentas por correo electrónico e inicien sesión en sus cuentas confirmadas.

El artículo Spring Security: Registro de verificación de correo electrónico cubre el registro de usuario y la confirmación de cuentas a través de un token enviado al correo electrónico del usuario. Con el ánimo de mantener este artículo centrado en la funcionalidad de restablecimiento de contraseña, bifurcaremos y ampliaremos este proyecto en GitHub que se basa en ese artículo y agregaremos nuestra nueva funcionalidad que incluye la funcionalidad de inicio de sesión y restablecimiento de contraseña.

Resumen

El proyecto utiliza el marco Spring junto con Spring Security junto con Thymeleaf como motor de plantillas del lado del servidor. Hibernate se usa para interactuar con una base de datos MySQL para guardar los detalles de los usuarios.

Un usuario accede al sistema e inmediatamente se le solicita que se registre proporcionando sus detalles, incluidos sus nombres, dirección de correo electrónico para verificación y una contraseña. En las siguientes secciones, comenzaremos desde allí y agregaremos la funcionalidad de inicio de sesión primero, luego agregaremos la funcionalidad de restablecimiento de contraseña para ayudar a nuestros usuarios a restablecer sus contraseñas.

Cambios y adiciones

Hay algunas cosas que cambiaremos en este proyecto y las destacaré a medida que avancemos. Primero, actualizaremos la versión Spring de 1.5.4a 2.1.4.RELEASEen nuestro pom.xml. También actualizaremos la versión del conector MySQL a la versión 8.0.13para que el proyecto se ejecute sin problemas con la versión Spring actualizada.

Si bien la versión anterior aún funciona, la actualización de las dependencias garantiza que podamos aprovechar la nueva funcionalidad que se ha introducido en la versión posterior. Además, algunos problemas que podríamos enfrentar se eliminarán mediante el uso de paquetes actualizados.

Necesitaremos configurar nuestra base de datos local y actualizar el applications.propertiesarchivo para que se adapte a nuestra nueva configuración. También necesitaremos actualizar la configuración del correo electrónico para facilitar el envío de correos electrónicos:

# add these new properties
spring.mail.transport.protocol=smtp
spring.mail.from.email=<your-email-goes-here>

# modify these properties with your credentials
spring.mail.username=<your-email-goes-here>
spring.mail.password=<password-goes-here>

# update our database configuration
spring.datasource.url=jdbc:mysql://localhost:3306/demodb?allowPublicKeyRetrieval=true&useSSL=false
spring.datasource.username=<database-username>
spring.datasource.password=<database-password>

Con esta configuración modificada, podemos ejecutar el proyecto y registrarnos como nuevos usuarios. El correo electrónico proporcionado en application.propertiesaparecerá como el remitente del correo electrónico que contiene el token de confirmación.

Funcionalidad de inicio de sesión

Podemos registrarnos y confirmar nuestra cuenta por correo electrónico en este momento. Antes de que podamos restablecer nuestras contraseñas, deberíamos poder iniciar sesión, y cuando olvidemos nuestra contraseña, deberíamos poder restablecerla. Para implementar la funcionalidad de inicio de sesión, comenzaremos creando las plantillas, luego la vista en el controlador.

Nuestra página de inicio de sesión tendrá campos para el correo electrónico y la contraseña, y una sección oculta para mostrar cualquier mensaje cuando sea necesario. Dichos mensajes pueden incluir notificar al usuario cuando proporciona credenciales no válidas o cuando el usuario no existe en el sistema. También crearemos una plantilla adicional que se mostrará al iniciar sesión correctamente, que servirá como nuestra página de inicio.

En nuestra carpeta de plantillas, agregamos la página de inicio de sesión:

<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
    <head> <title>Login</title> </head>
    <body>
        <center> <span th:text="${message}"></span> <br/> </center>
        <center>
            <form action="#" th:action="@{/login}" th:object="${user}" method="post">
                <table>
                    <tr>
                        <td><label for="emailId">Email</label></td>
                        <td><input th:field="*{emailId}" type="text" name="emailId"></input></td>
                    </tr>
                    <tr>
                        <td><label for="password">Password</label></td>
                        <td><input th:field="*{password}" type="password" name="password"></input></td>
                    </tr>
                    <tr>
                        <td><input type="reset" value="Clear"/></td>
                        <td><input type="submit" value="Submit"></input></td>
                    </tr>
                </table>
            </form>

            <a href="/forgot-password">Forgot Password?</a>
        </center>
    </body>
</html>

Este es un formulario que toma una dirección de correo electrónico y una contraseña y envía esa información a nuestro controlador en el /loginpunto final para su verificación. También hay un Forgot Password?enlace, que implementaremos más adelante.

Después de iniciar sesión correctamente, notificamos al usuario y lo redirigimos a la página de inicio, que en nuestro caso será simplemente successLogin.html:

<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml"
  xmlns:th="http://www.thymeleaf.org">
    <head> <title>Login Success</title> </head>
    <body>
        <center> <span th:text="${message}"></span> </center>
    </body>
</html>

Extendamos nuestro UserAccountControllerque se encuentra en la controllercarpeta para incluir la funcionalidad de inicio de sesión.

Primero, importaremos la BCryptPasswordEncoderclase para codificar nuestras contraseñas y crearemos una instancia para encriptar y comparar nuestras contraseñas. Inicialmente, el proyecto guardó las contraseñas sin procesar en la base de datos, lo modificaremos para tener las contraseñas encriptadas cuando el usuario se registre, ya que guardar las contraseñas sin procesar no es una buena práctica.

Para la funcionalidad de inicio de sesión, implementaremos una función para mostrar la página con el formulario de inicio de sesión y otra función para recibir las credenciales, verificarlas y notificar cualquier problema o llevar al usuario a la página siguiente si se ha realizado correctamente. Si el correo electrónico proporcionado no existe en nuestra base de datos, también se lo notificaremos al usuario.

BCryptPasswordEncoderproporciona la matches(rawPassword, encodedPassword)función que nos ayuda a comparar la contraseña proporcionada con la que tenemos en los registros. Usamos este método en lugar de codificar la contraseña sin procesar y comparar, ya que cada vez se usa una sal diferente y una comparación directa fallaría todo el tiempo.

Primero agregamos la nueva importación:

import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

Y luego incluye estos cambios:

    // Instantiate our encoder
    BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(12);

    // Right before saving the user on registration, we encode the password
    user.setPassword(encoder.encode(user.getPassword()));
    userRepository.save(user);

    // Function to display login page
    @RequestMapping(value="/login", method=RequestMethod.GET)
    public ModelAndView displayLogin(ModelAndView modelAndView, User user) {
        modelAndView.addObject("user", user);
        modelAndView.setViewName("login");
        return modelAndView;
    }

    // Function to handle the login process
    @RequestMapping(value="/login", method=RequestMethod.POST)
    public ModelAndView loginUser(ModelAndView modelAndView, User user) {
        User existingUser = userRepository.findByEmailIdIgnoreCase(user.getEmailId());
        if (existingUser != null) {
            // Use encoder.matches to compare raw password with encrypted password

            if (encoder.matches(user.getPassword(), existingUser.getPassword())){
                // Successfully logged in
                modelAndView.addObject("message", "Successfully logged in!");
                modelAndView.setViewName("successLogin");
            } else {
                // Wrong password
                modelAndView.addObject("message", "Incorrect password. Try again.");
                modelAndView.setViewName("login");
            }
        } else {
            modelAndView.addObject("message", "The email provided does not exist!");
            modelAndView.setViewName("login");
        }
        return modelAndView;
    }

Cuando ejecutamos nuestro proyecto, esta es la página resultante cuando navegamos hasta el /loginpunto final:

Si las credenciales son incorrectas, se muestra un mensaje con el error en la parte superior del formulario y, si son válidas, se redirige al usuario a una página que muestra un mensaje de éxito.

Ahora nuestro usuario puede iniciar sesión con sus credenciales, pero ¿qué pasa cuando las olvida? El Forgot Password?enlace viene al rescate.

Funcionalidad de contraseña olvidada

Cuando un usuario olvida su contraseña, puede solicitar que se restablezca haciendo clic en el Forgot Password?enlace. Se le pedirá al usuario que proporcione la dirección de correo electrónico con la que se registró y se generará un token y se enviará a la dirección de correo electrónico como parte del enlace.

Una vez que el usuario haga clic en el enlace de restablecimiento, validaremos el token y redirigiremos al usuario a una página donde puede ingresar una nueva contraseña para su cuenta. Ahora guardaremos esta nueva contraseña después de haber confirmado que el usuario tiene acceso a la dirección de correo electrónico que proporcionó. Ahora podrán iniciar sesión con las credenciales actualizadas.

Crearemos la plantilla de contraseña olvidada donde el usuario ingresará su dirección de correo electrónico a través de la forgotPassword.htmlplantilla:

<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml"
 xmlns:th="http://www.thymeleaf.org">
    <head> <title>Forgot Password</title> </head>
    <body>
        <center>
            <form action="#" th:action="@{/forgot-password}" th:object="${user}" method="post">
                <table>
                    <tr>
                        <td><label for="emailId">Email</label></td>
                        <td><input th:field="*{emailId}" type="text" name="emailId"></input></td>
                    </tr>
                    <tr>
                        <td><input type="reset" value="Clear"/></td>
                        <td><input type="submit" value="Reset Password"></input></td>
                    </tr>
                </table>
            </form>
        </center>
    </body>
</html>

Crearemos una successForgotPassword.htmlplantilla adicional para mostrar el mensaje de éxito cuando la contraseña se haya restablecido correctamente y esté presente en la base de código completa vinculada a continuación.

Con la plantilla en su lugar, permítanos actualizar nuestra UserAccountControllerpara manejar esta nueva funcionalidad. Tendremos una función para mostrar el formulario, y otra para recibir el correo electrónico, crear un token y enviar un correo electrónico al usuario con el enlace para restablecer la contraseña del usuario.

Las adiciones a nuestro controlador incluyen:

    // Display the form
    @RequestMapping(value="/forgot-password", method=RequestMethod.GET)
    public ModelAndView displayResetPassword(ModelAndView modelAndView, User user) {
        modelAndView.addObject("user", user);
        modelAndView.setViewName("forgotPassword");
        return modelAndView;
    }

    // Receive the address and send an email
    @RequestMapping(value="/forgot-password", method=RequestMethod.POST)
    public ModelAndView forgotUserPassword(ModelAndView modelAndView, User user) {
        User existingUser = userRepository.findByEmailIdIgnoreCase(user.getEmailId());
        if (existingUser != null) {
            // Create token
            ConfirmationToken confirmationToken = new ConfirmationToken(existingUser);

            // Save it
            confirmationTokenRepository.save(confirmationToken);

            // Create the email
            SimpleMailMessage mailMessage = new SimpleMailMessage();
            mailMessage.setTo(existingUser.getEmailId());
            mailMessage.setSubject("Complete Password Reset!");
            mailMessage.setFrom("[email protected]");
            mailMessage.setText("To complete the password reset process, please click here: "
              + "http://localhost:8082/confirm-reset?token="+confirmationToken.getConfirmationToken());

            // Send the email
            emailSenderService.sendEmail(mailMessage);

            modelAndView.addObject("message", "Request to reset password received. Check your inbox for the reset link.");
            modelAndView.setViewName("successForgotPassword");

        } else {
            modelAndView.addObject("message", "This email address does not exist!");
            modelAndView.setViewName("error");
        }
        return modelAndView;
    }

Ahora podemos empaquetar y ejecutar nuestro proyecto nuevamente usando el mvn spring-boot:runcomando. Cuando hacemos clic en el Forgot Password?enlace, podemos ver un formulario con un campo de correo electrónico. Cuando completamos nuestra dirección de correo electrónico registrada, recibimos el siguiente correo electrónico:

Hasta ahora, hemos podido recibir una solicitud de restablecimiento de contraseña y hemos enviado un correo electrónico al usuario con un enlace para restablecer su contraseña.

Para implementar la siguiente parte de la funcionalidad de restablecimiento de contraseña, necesitaremos crear una plantilla que reciba la nueva contraseña del usuario. Se parecerá a la página de inicio de sesión, la única diferencia importante es que el campo de correo electrónico será de solo lectura.

La nueva plantilla resetPassword:

<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml"
 xmlns:th="http://www.thymeleaf.org">
    <head> <title>Reset Password</title> </head>
    <body>
        <center>
            <h2>Enter new password:</h2>
            <form action="#" th:action="@{/reset-password}" th:object="${user}" method="post">
                <table>
                    <tr>
                        <td><label for="emailId">Email</label></td>
                        <td><input th:field="*{emailId}" type="text" name="emailId" readonly></input></td>
                    </tr>
                    <tr>
                        <td><label for="password">Password</label></td>
                        <td><input th:field="*{password}" type="password" name="password"></input></td>
                    </tr>
                    <tr>
                        <td><input type="reset" value="Clear"/></td>
                        <td><input type="submit" value="Submit"></input></td>
                    </tr>
                </table>
            </form>
        </center>
    </body>
</html>

Prellenaremos la dirección de correo electrónico del usuario en un campo de solo lectura y luego dejaremos que el usuario ingrese su nueva contraseña.

En este punto, se introducirán dos nuevos puntos finales:

  • /confirm-reset: maneja la verificación del token y, en caso de éxito, redirigirá al usuario al siguiente punto final
  • /reset-password: muestra el formulario anterior, recibe las nuevas credenciales y las actualiza en la base de datos

Agreguemos estos nuevos puntos finales en nuestro controlador de la siguiente manera:

    // Endpoint to confirm the token
    @RequestMapping(value="/confirm-reset", method= {RequestMethod.GET, RequestMethod.POST})
    public ModelAndView validateResetToken(ModelAndView modelAndView, @RequestParam("token")String confirmationToken) {
        ConfirmationToken token = confirmationTokenRepository.findByConfirmationToken(confirmationToken);

        if (token != null) {
            User user = userRepository.findByEmailIdIgnoreCase(token.getUser().getEmailId());
            user.setEnabled(true);
            userRepository.save(user);
            modelAndView.addObject("user", user);
            modelAndView.addObject("emailId", user.getEmailId());
            modelAndView.setViewName("resetPassword");
        } else {
            modelAndView.addObject("message", "The link is invalid or broken!");
            modelAndView.setViewName("error");
        }
        return modelAndView;
    }

    // Endpoint to update a user's password
    @RequestMapping(value = "/reset-password", method = RequestMethod.POST)
    public ModelAndView resetUserPassword(ModelAndView modelAndView, User user) {
        if (user.getEmailId() != null) {
            // Use email to find user
            User tokenUser = userRepository.findByEmailIdIgnoreCase(user.getEmailId());
            tokenUser.setPassword(encoder.encode(user.getPassword()));
            userRepository.save(tokenUser);
            modelAndView.addObject("message", "Password successfully reset. You can now log in with the new credentials.");
            modelAndView.setViewName("successResetPassword");
        } else {
            modelAndView.addObject("message","The link is invalid or broken!");
            modelAndView.setViewName("error");
        }
        return modelAndView;
    }

Con estos nuevos cambios, podemos ejecutar el proyecto y hacer clic en el enlace que venía en el correo electrónico para restablecer la contraseña que se envió anteriormente. El resultado es:

Cuando ingresamos nuestra nueva contraseña, recibimos un mensaje de éxito. Nuestra contraseña se ha actualizado y podemos probar esta nueva contraseña navegando a la página de inicio de sesión e iniciando sesión con las nuevas credenciales.

¡Funciona! Nuestros usuarios ahora pueden restablecer sus contraseñas olvidadas a través de enlaces enviados a su dirección de correo electrónico desde nuestra aplicación web Spring Boot.

Conclusión

Hemos aprendido de las diversas formas en que Spring Security puede proporcionar funciones de autenticación y control de acceso para ayudarnos a proteger nuestras aplicaciones basadas en Spring de una manera fácilmente extensible y personalizable.

También hemos entendido cómo Spring Security maneja las contraseñas de nuestros usuarios a través de varios algoritmos para codificar y almacenar de forma segura la contraseña de manera que sea indescifrable para un atacante. Destacamos brevemente cómo podemos enviar correos electrónicos en Spring y, en nuestro caso, usamos este conocimiento para enviar correos electrónicos para confirmar las cuentas de nuestros usuarios cuando se están registrando y también recuperando su cuenta. Esta funcionalidad de correo electrónico también se puede utilizar para enviar notificaciones de inicio de sesión o marcar actividades sospechosas en las cuentas de los usuarios.

Finalmente, ampliamos un proyecto de registro de correo electrónico de Spring agregando la funcionalidad de inicio de sesión y restablecimiento de contraseña para ayudar a nuestros usuarios en caso de que no puedan recordar sus credenciales.

La base de código completa y final de este proyecto está disponible aquí en Github .

 

About the author

Ramiro de la Vega

Bienvenido a Pharos.sh

Soy Ramiro de la Vega, Estadounidense con raíces Españolas. Empecé a programar hace casi 20 años cuando era muy jovencito.

Espero que en mi web encuentres la inspiración y ayuda que necesitas para adentrarte en el fantástico mundo de la programación y conseguir tus objetivos por difíciles que sean.

Add comment

Sobre mi

Últimos Post

Etiquetas

Esta web utiliza cookies propias para su correcto funcionamiento. Al hacer clic en el botón Aceptar, aceptas el uso de estas tecnologías y el procesamiento de tus datos para estos propósitos. Más información
Privacidad