Qué es un sistema de autenticación con token JWT

Antes de empezar vamos a ver rápidamente que es esto de JWT ya que es el sistema de autenticación que vamos a usar.

JWT es un sistema de autenticación que se basa en el uso de tokens. El mecanismo es el siguiente, el usuario hace login en la página y Angular envía al servidor el nombre de usuario y su contraseña. El servidor comprueba si el usuario existe en la base de datos y si la contraseña es correcta y envía a la página web un token generado específicamente para ese usuario. Ese token está firmado por lo que no se puede modificar. La página web guarda ese token y cuando el usuario accede a otra página o ejecuta una acción en la página que dependa del servidor envía el token almacenado.

En resumidas cuentas, se genera un token en servidor, la página se lo guarda en la cookie o en memoria y por cada petición a la API se envía el token para que el servidor compruebe si el usuario tiene permisos. De esta forma se consigue que se puedan proteger llamadas a la API sin necesidad de pasar usuario y contraseña en cada petición (solo se pasa el token).

El servidor genera el token y la página lo usa en cada petición

🗺️ Hoja de ruta

  • Crear y maquetar las vistas de login y registro con formularios
  • Conectar el sistema de registro con la API
  • Conectar el sistema de login con la API
  • Recordar la sesión iniciada al recargar la página

Creando las vistas. Formulario de login y registro

Lo primero que vamos a hacer es maquetar los dos formularios, sin lógica por el momento. Para ello vamos a crear dos componentes, uno para la vista de login y otro para la vista del registro.

Ambos componentes los vamos a añadir a las rutas que tengamos en El router de Vue.

En mi caso en el fichero router.js pongo lo siguiente:

import Vue from "vue";
import Router from "vue-router";

import App from "./App";
import Home from "./views/Home";
import Login from "./views/Login";
import Register from "./views/Register";

Vue.use(Router);

const routes = [
  { path: "/", component: Home },
  { path: "/login", component: Login },
  { path: "/register", component: Register },
];

const router = new Router({
  routes,
});

Simplemente tres rutas, la de la página principal que próximamente protegeremos para que solo entren usuarios logueados, la del formulario del login y la del registro.

De tal forma que lo que hago es crear 3 archivos en la carpeta views: Home.vue, Login.vue y Register.vue

La vista de login (login.vue), la he maquetado de la siguiente forma:

<template>
  <div class="login">
    <h1 class="title">Login in the page</h1>
    <form action class="form">
      <label class="form-label" for="#email">Email:</label>
      <input
        class="form-input"
        type="email"
        id="email"
        required
        placeholder="Email"
      />
      <label class="form-label" for="#password">Password:</label>
      <input
        class="form-input"
        type="password"
        id="password"
        placeholder="Password"
      />
      <input class="form-submit" type="submit" value="Login" />
    </form>
  </div>
</template>

<script>
export default {};
</script>

<style lang="scss" scoped>
.login {
  padding: 2rem;
}
.title {
  text-align: center;
}
.form {
  margin: 3rem auto;
  display: flex;
  flex-direction: column;
  justify-content: center;
  width: 20%;
  min-width: 350px;
  max-width: 100%;
  background: rgba(19, 35, 47, 0.9);
  border-radius: 5px;
  padding: 40px;
  box-shadow: 0 4px 10px 4px rgba(0, 0, 0, 0.3);
}
.form-label {
  margin-top: 2rem;
  color: white;
  margin-bottom: 0.5rem;
  &:first-of-type {
    margin-top: 0rem;
  }
}
.form-input {
  padding: 10px 15px;
  background: none;
  background-image: none;
  border: 1px solid white;
  color: white;
  &:focus {
    outline: 0;
    border-color: #1ab188;
  }
}
.form-submit {
  background: #1ab188;
  border: none;
  color: white;
  margin-top: 3rem;
  padding: 1rem 0;
  cursor: pointer;
  transition: background 0.2s;
  &:hover {
    background: #0b9185;
  }
}
</style>

Lo único que tiene es el formulario de login con unos estilos que me he inventado, de momento no hay nada de lógica en este componente. Siempre recordad de poner los estilos con scoped para que se queden aislados del resto de componentes.

En la imagen se ve un formulario con dos inputs, uno para el email y otro para la contraseña. Además hay un botón de enviar

Ahora vamos a añadir los v-model en los campos para poder sacar el email y la contraseña que el usuario escribe, para ello:

<template>
  <div class="login">
    <h1 class="title">Login in the page</h1>
    <form action class="form">
      <label class="form-label" for="#email">Email:</label>
      <input
        v-model="email"
        class="form-input"
        type="email"
        id="email"
        required
        placeholder="Email"
      />
      <label class="form-label" for="#password">Password:</label>
      <input
        v-model="password"
        class="form-input"
        type="password"
        id="password"
        placeholder="Password"
      />
      <input class="form-submit" type="submit" value="Login" />
    </form>
  </div>
</template>

<script>
export default {
  data: () => ({
    email: "",
    password: "",
  }),
};
</script>

No tiene más, dos v-model apuntando a variables definidas en la sección data del componente para poder recibir los dos valores. Toca preparar el método para enviar la petición de login:

<template>
  <div class="login">
    <h1 class="title">Login in the page</h1>
    <form action class="form" @submit.prevent="login">
      <label class="form-label" for="#email">Email:</label>
      <input
        v-model="email"
        class="form-input"
        type="email"
        id="email"
        required
        placeholder="Email"
      />
      <label class="form-label" for="#password">Password:</label>
      <input
        v-model="password"
        class="form-input"
        type="password"
        id="password"
        placeholder="Password"
      />
      <input class="form-submit" type="submit" value="Login" />
    </form>
  </div>
</template>

<script>
export default {
  data: () => ({
    email: "",
    password: "",
  }),
  methods: {
    login() {
      console.log(this.email);
      console.log(this.password);
    },
  },
};
</script>

De momento solo se escriben los dos valores por consola para probar que se guardan bien en las variables los valores. Nada muy complicado de momento. Vamos a maquetar por último el mensaje de error con un v-if para que se muestre si el usuario ha metido mal el email o la contraseña:

<template>
  <div class="login">
    <h1 class="title">Login in the page</h1>
    <form action class="form" @submit.prevent="login">
      <label class="form-label" for="#email">Email:</label>
      <input
        v-model="email"
        class="form-input"
        type="email"
        id="email"
        required
        placeholder="Email"
      />
      <label class="form-label" for="#password">Password:</label>
      <input
        v-model="password"
        class="form-input"
        type="password"
        id="password"
        placeholder="Password"
      />
      <p v-if="error" class="error">
        Has introducido mal el email o la contraseña.
      </p>
      <input class="form-submit" type="submit" value="Login" />
    </form>
  </div>
</template>

<script>
export default {
  data: () => ({
    email: "",
    password: "",
    error: false,
  }),
  methods: {
    login() {
      console.log(this.email);
      console.log(this.password);
    },
  },
};
</script>

Listo, componente maquetado por el momento, vamos a maquetar el del formulario de registro para prepararlos para conectar al backend.

El componente del registro es igual solo que añadiendo un campo nuevo para que el usuario repita su contraseña:

<template>
  <div class="register">
    <h1 class="title">Sign Up</h1>
    <form action class="form" @submit.prevent="register">
      <label class="form-label" for="#email">Email:</label>
      <input
        v-model="email"
        class="form-input"
        type="email"
        id="email"
        required
        placeholder="Email"
      />
      <label class="form-label" for="#password">Password:</label>
      <input
        v-model="password"
        class="form-input"
        type="password"
        id="password"
        placeholder="Password"
      />
      <label class="form-label" for="#password-repeat"
        >Repite la contraeña:</label
      >
      <input
        v-model="passwordRepeat"
        class="form-input"
        type="password"
        id="password-repeat"
        placeholder="Password"
      />
      <input class="form-submit" type="submit" value="Sign Up" />
    </form>
  </div>
</template>

<script>
export default {
  data: () => ({
    email: "",
    password: "",
    passwordRepeat: "",
  }),
  methods: {
    register() {
      console.log(this.email);
      console.log(this.password);
      console.log(this.passwordRepeat);
    },
  },
};
</script>

<style lang="scss" scoped>
.register {
  padding: 2rem;
}
.title {
  text-align: center;
}
.form {
  margin: 3rem auto;
  display: flex;
  flex-direction: column;
  justify-content: center;
  width: 20%;
  min-width: 350px;
  max-width: 100%;
  background: rgba(19, 35, 47, 0.9);
  border-radius: 5px;
  padding: 40px;
  box-shadow: 0 4px 10px 4px rgba(0, 0, 0, 0.3);
}
.form-label {
  margin-top: 2rem;
  color: white;
  margin-bottom: 0.5rem;
  &:first-of-type {
    margin-top: 0rem;
  }
}
.form-input {
  padding: 10px 15px;
  background: none;
  background-image: none;
  border: 1px solid white;
  color: white;
  &:focus {
    outline: 0;
    border-color: #1ab188;
  }
}
.form-submit {
  background: #1ab188;
  border: none;
  color: white;
  margin-top: 3rem;
  padding: 1rem 0;
  cursor: pointer;
  transition: background 0.2s;
  &:hover {
    background: #0b9185;
  }
}
.error {
  margin: 1rem 0 0;
  color: #ff4a96;
}
</style>
En la imagen se ve un formulario con tres inputs, uno para el email, otro para la contraseña y el último de repetir la contraseña. Además hay un botón de enviar

🎉 Pues listo, primera tarea completada, hacemos commit y pasamos a la siguiente.

✅ Crear y maquetar las vistas de login y registro con formularios

Conectando el registro al servidor

Lógicamente para poder conectar el login y registro necesitamos un servidor (encargado de almacenar los usuarios en base de datos ya que desde javascript no se puede). Para ello necesitas una API, puedes crear tú mismo una API usando alguna tecnología de backend como nodejs, java o python. Como de lo que se trata es de aprender, para este ejemplo voy a usar esta API de pruebas ya creada que tú también puedes usar.

La API en cuestión es la de: https://reqres.in/. Te permite mandar peticions de login y registro además de muchas otras para hacer pruebas.

Lo primero que vamos a hacer es conectar el registro, para ello voy a crear una carpeta llamada logic dentro de la carpeta src del proyecto. Yo la he llamado logic pero la puedes llamar como quieras. Dentro de esa carpeta voy a crear un archivo llamado auth.js en el que voy a colocar la petición de registro y la de login.

Por el momento dentro del archivo de auth.js voy a crear esto:

import axios from "axios";

const ENDPOINT_PATH = "https://reqres.in/api/";

export default {
  register(email, password) {
    const user = { email, password };
    return axios.post(ENDPOINT_PATH + "regiser", user);
  },
};

Lo primero es importar axios, si no lo tienes instalado en el proyecto ejecuta:

npm install axios --save

Lo siguiente que hago es definir una constante para endpoint de la API. Luego exporto un objeto para poder usar desde fuera este fichero con el método de registro de usuarios.

Dentro del método de registro se construye el objeto user que se enviará en la petición POST de registro de usuarios. Por último se llama a axios para que haga el POST y devuelva la promesa.

Vamos ahora a usar este fichero desde el componente de registro. Lo primero que se hace es importar el fichero encima del export default del componente:

// ... import auth from "@/logic/auth"; export default { data: () => ({ // ...

¿Recuerdas el métoodo que creaste que simplemente tenía los console.log? Pues hay que cambiar eso por esto otro:

// ...
methods: {
  register() {
    auth.register(this.email, this.password).then(response => {
      console.log(response);
    })
  }
}
// ...

Como el método register del archivo auth devuelve una promesa lo que hay que hacer es crear a continuación el then para capturar la respuesta asíncrona.

Si ejecutas el código y escribes en el formulario el email eve.holt@reqres.in y la contraseña pistol y le das a registrar verás que se devuelve la respuesta de la API, en este caso un 201 created.

La forma de resolver la asincronía con el then está bien pero creo que con async/await queda todo más claro:

methods: {
  async register() {
    const response = await auth.register(this.email, this.password);
    console.log(response);
  }
}

Por último podemos crear una variable en el data para mostrar error si el usuario ha metido mal el usuario y contraseña. Para capturar el error en la petición podemos usar try/catch. De paso vamos a poner que si el registro es correcto lleve al usuario a la página de inicio. Veamos como queda todo el componente:

<template>
  <div class="register">
    <h1 class="title">Sign Up</h1>
    <form action class="form" @submit.prevent="register">
      <label class="form-label" for="#email">Email:</label>
      <input
        v-model="email"
        class="form-input"
        type="email"
        id="email"
        required
        placeholder="Email"
      />
      <label class="form-label" for="#password">Password:</label>
      <input
        v-model="password"
        class="form-input"
        type="password"
        id="password"
        placeholder="Password"
      />
      <label class="form-label" for="#password-repeat"
        >Repite la contraeña:</label
      >
      <input
        v-model="passwordRepeat"
        class="form-input"
        type="password"
        id="password-repeat"
        placeholder="Password"
      />
      <input class="form-submit" type="submit" value="Sign Up" />
    </form>
  </div>
</template>

<script>
import auth from "@/logic/auth";
export default {
  data: () => ({
    email: "",
    password: "",
    passwordRepeat: "",
    error: false,
  }),
  methods: {
    async register() {
      try {
        await auth.register(this.email, this.password);
        this.$router.push("/");
      } catch (error) {
        console.log(error);
      }
    },
  },
};
</script>

<style lang="scss" scoped>
.register {
  padding: 2rem;
}
.title {
  text-align: center;
}
.form {
  margin: 3rem auto;
  display: flex;
  flex-direction: column;
  justify-content: center;
  width: 20%;
  min-width: 350px;
  max-width: 100%;
  background: rgba(19, 35, 47, 0.9);
  border-radius: 5px;
  padding: 40px;
  box-shadow: 0 4px 10px 4px rgba(0, 0, 0, 0.3);
}
.form-label {
  margin-top: 2rem;
  color: white;
  margin-bottom: 0.5rem;
  &:first-of-type {
    margin-top: 0rem;
  }
}
.form-input {
  padding: 10px 15px;
  background: none;
  background-image: none;
  border: 1px solid white;
  color: white;
  &:focus {
    outline: 0;
    border-color: #1ab188;
  }
}
.form-submit {
  background: #1ab188;
  border: none;
  color: white;
  margin-top: 3rem;
  padding: 1rem 0;
  cursor: pointer;
  transition: background 0.2s;
  &:hover {
    background: #0b9185;
  }
}
.error {
  margin: 1rem 0 0;
  color: #ff4a96;
}
</style>

Listo, sistema de registro de usuarios terminado, tarea completad, toca hacer commit.

✅ Conectar el sistema de registro con la API

Sistema de login

Una vez tenemos el sistema de registro el de login debería ser más fácil porque básicamente es repetir lo mismo que antes solo que cambiando la ruta del endpoint.

En el fichero de auth.js añadimos:

import axios from "axios";

const ENDPOINT_PATH = "https://reqres.in/api/";

export default {
  register(email, password) {
    const user = { email, password };
    return axios.post(ENDPOINT_PATH + "regiser", user);
  },
  login(email, password) {
    const user = { email, password };
    return axios.post(ENDPOINT_PATH + "login", user);
  },
};

Y en el componente de login llamamos a este fichero de la misma manera que en el registro, solo que cuando haya error activamos la variable error a true para que en el formulario se muestre un error avisando de que el email o la contraseña están mal:

<template>
  <div class="login">
    <h1 class="title">Login in the page</h1>
    <form action class="form" @submit.prevent="login">
      <label class="form-label" for="#email">Email:</label>
      <input
        v-model="email"
        class="form-input"
        type="email"
        id="email"
        required
        placeholder="Email"
      />
      <label class="form-label" for="#password">Password:</label>
      <input
        v-model="password"
        class="form-input"
        type="password"
        id="password"
        placeholder="Password"
      />
      <p v-if="error" class="error">
        Has introducido mal el email o la contraseña.
      </p>
      <input class="form-submit" type="submit" value="Login" />
    </form>
    <p class="msg">
      ¿No tienes cuenta?
      <router-link to="/register">Regístrate</router-link>
    </p>
  </div>
</template>

<script>
import auth from "@/logic/auth";
export default {
  data: () => ({
    email: "",
    password: "",
    error: false,
  }),
  methods: {
    async login() {
      try {
        await auth.login(this.email, this.password);
        this.$router.push("/");
      } catch (error) {
        this.error = true;
      }
    },
  },
};
</script>

<style lang="scss" scoped>
.login {
  padding: 2rem;
}
.title {
  text-align: center;
}
.form {
  margin: 3rem auto;
  display: flex;
  flex-direction: column;
  justify-content: center;
  width: 20%;
  min-width: 350px;
  max-width: 100%;
  background: rgba(19, 35, 47, 0.9);
  border-radius: 5px;
  padding: 40px;
  box-shadow: 0 4px 10px 4px rgba(0, 0, 0, 0.3);
}
.form-label {
  margin-top: 2rem;
  color: white;
  margin-bottom: 0.5rem;
  &:first-of-type {
    margin-top: 0rem;
  }
}
.form-input {
  padding: 10px 15px;
  background: none;
  background-image: none;
  border: 1px solid white;
  color: white;
  &:focus {
    outline: 0;
    border-color: #1ab188;
  }
}
.form-submit {
  background: #1ab188;
  border: none;
  color: white;
  margin-top: 3rem;
  padding: 1rem 0;
  cursor: pointer;
  transition: background 0.2s;
  &:hover {
    background: #0b9185;
  }
}
.error {
  margin: 1rem 0 0;
  color: #ff4a96;
}
.msg {
  margin-top: 3rem;
  text-align: center;
}
</style>

Si abres la página de login y pruebas con el email: eve.holt@reqres.in y la contraseña cityslicka te debería llevar a la página principal ya que la llamada al login ha salido bien. Si pones otro email y contraseña te debería salir el mensaje de error en pantalla.

Pues otra tarea terminada. Commit y a por la siguiente:

✅ Conectar el sistema de login con la API

Recordando la sesión iniciada mediante cookie

Vamos con una parte fundamental en todo sistema de login, el de guardar el usuario cuando se loguea en una cookie o en el localstorage para que los componentes puedan pintar información del usuario logueado.

Para este ejemplo voy a optar por usar la librería de js cookie, para ello lo primero es decargarlo mediante npm:

npm install js-cookie --save

Ahora lo que voy a hacer es crear dos métodos más dentro del archivo de auth.js. Uno de ellos para guardar el usuario logueado y el otro para recuperarlo desde las cookies.

import axios from "axios";
import Cookies from "js-cookie";

const ENDPOINT_PATH = "https://reqres.in/api/";

export default {
  setUserLogged(userLogged) {
    Cookies.set("userLogged", userLogged);
  },
  getUserLogged() {
    return Cookies.get("userLogged");
  },
  register(email, password) {
    const user = { email, password };
    return axios.post(ENDPOINT_PATH + "regiser", user);
  },
  login(email, password) {
    const user = { email, password };
    return axios.post(ENDPOINT_PATH + "login", user);
  },
};

Lo que vamos a hacer ahora en la vista de login es que si la petición de login sale bien tenemos que guardar el usuario en la cookie. El método de login quedaría de esta forma:

...
async login() {
  try {
    await auth.login(this.email, this.password);
    const user = {
      email: this.email
    };
    auth.setUserLogged(user);
    this.$router.push("/");
  } catch (error) {
    console.log(error);
    this.error = true;
  }
}
...

En el componente de registro podemos hacer lo propio en caso de que queramos que en nuestra aplicación cuando un usuario se registre se autologuee también.

Ahora en la página principal podemos mostrar el usuario logueado, para ello:

<template>
  <div class="home">
    <navigation/>
    <h1>Home</h1>
    <p v-if="userLogged">User loggued: {{userLogged}}</p>
  </div>
</template>

<script>
import Navigation from "../components/Navigation";
import auth from "@/logic/auth";
export default {
  name: "Home",
  components: {
    navigation: Navigation
  },
  computed: {
    userLogged() {
      return auth.getUserLogged();
    }
  }
};
</script>

<style>
    </style>

Pues listo, si has hecho login y refrescas la página verás que el usuario logueado sigue estando porque se guarda en las cookies. Para terminar podemos crear en el archivo auth.js un método para cerrar sesión que simplemente borre la cookie:

deleteUserLogged() {
  Cookies.remove('userLogged');
}

ATENCIÓN: No recomiendo guardar toda la información del usuario en la cookie, pero lo hecho así en este ejemplo para demostrar cómo guardar cosas en las cookies. En estos casos lo que se suele hacer es guardar un token y no todo el usuario. Más info de esto si buscas JWT

Pues la última tarea terminada también. Commit y listo.

La página principal la he dejado sin estilos, pero puedes aprovechar para cambiarlos.

Te dejo el proyecto subido a Codesanbox para que puedas jugar con él:

🖥️ Proyecto completo en codesanbox

Y para terminar te pongo deberes por si quieres seguir aprendiendo:

  • Mostrar aviso y comprobación en el registro si las dos contraseñas no coinciden
  • Deshabilitar el botón con otros estilos mientras que el usuario no meta el email y las contraseñas no coincidan
  • Meter más campos en el registro (nombre de usuario por ejemplo) y guardar más información del usuario en las cookies

Conclusiones

Como he dicho muchas veces, mira siempre la consola del navegador porque muchas veces falla algo y no nos damos cuenta hasta que leemos el mensaje ahí.

Espero que te haya gustado este ejemplo práctico y espero que te haya servido para afianzar los conocimientos que ya tenías de Vue. Con esto deberías ser capaz de crear cosas bastante interesantes a partir de aquí.

Echa un vistazo a los Filtros de Vue y a las Directivas de Vue para que todavía le saques mucho más partido a Vue.