Autentificación con jwt, apollo server y mongodb.

September 22, 2019 • ☕️ 5 minutos de lectura

⚠️Requisitos mínimos Debemos tener instalado nodejs y un servidor mongodb, además algunos de conocimientos básicos sobre graphql, como Scheme, Types, Query, Mutations…

Este artículo veremos cómo configurar la autorización de nuestro GraphQL, para que nuestro Schema pueda decidir que usuarios pueden realizar solicitudes a nuestra API.

Nos basaremos en la documentación oficial, que no explica como pasar nuestro usuario al contexto.

Poner la información del usuario en el contexto

Usaremos json web tokens para verificar las credenciales de nuestro usuario, para ello debemos pasar nuestro token en un encabezado de autorización HTTP, para que nuestro servidor se encargue de decodificar el token y si todo es correcto devolvernos nuestro usuario.

const { ApolloServer } = require('apollo-server');

 const server = new ApolloServer({ 
 typeDefs,
 resolvers,
 context: ({ req }) => {
   // get the user token from the headers
   const token = req.headers.authorization || '';
  
   // Obtenemos el usuario a traves del token.
   const user = getUser(token);
  
   // Añadimos el usuario al contexto
   return { user };
 },
});

server.listen().then(({ url }) => {
 console.log(`🚀 Server ready at ${url}`)
});

Iniciando el proyecto…

Como siempre usaremos npm init para iniciar nuestro proyecto y poder usar npm, poder instalar nuestras dependencias.

npm install apollo-sever axios bcryptjs bluebird body-parser crypto dotenv express graphql jwt-simple moment mongoose morgan uuidv
  "dependencies": {
    "apollo-server": "^2.8.1",
    "axios": "^0.19.0",
    "bcryptjs": "^2.4.3",
    "bluebird": "^3.5.5",
    "body-parser": "^1.19.0",
    "crypto": "^1.0.1",
    "dotenv": "^8.0.0",
    "express": "^4.17.1",
    "graphql": "^14.4.2",
    "jwt-simple": "^0.5.6",
    "moment": "^2.24.0",
    "mongoose": "^5.6.9",
    "morgan": "^1.9.1",
    "uuidv4": "^4.0.0"
  },

config.js

module.exports = {
  db: process.env.MONDODB || "mongodb://localhost/api",
  SECRET_TOKEN: process.env.SECRET_TOKEN || "miclavedetokens"
};

index.js

"use strict";

const { ApolloServer } = require("apollo-server");
const mongoose = require("mongoose");
const config = require("./config");
const resolvers = require("./lib/resolvers");
const service = require("./services");

mongoose.Promise = require("bluebird");
mongoose.set("useCreateIndex", true);
mongoose
  .connect(config.db, {
    useNewUrlParser: true,
    promiseLibrary: require("bluebird")
  })
  .then(() => console.log("connection succesful"))
  .catch(err => console.error("Error connection MongoDB"));

const typeDefs = `type Query {
    users: [User!]
    user(id: ID!): User
    posts: [Post!]
    post(id: ID!): Post
  }
  
  type User {
    id: ID!
    username: String!
    name: String!
    email: String!
    phone: String!
    posts: [Post!]!
  }
  
  type Post {
    id: ID!
    title: String!
    body: String!
    author: User!
  }
  query {
    users {
      id
      name
    }
  }
  
  type Mutation {
    "Añadir Usuario"
    signup (username: String!, email: String!, password: String!): String
    "Hacer login"
    login (email: String!, password: String!): String
  }`;

const server = new ApolloServer({
  typeDefs,
  resolvers,
  context: async ({ req }) => {
    let authToken = null;
    let user = null;

    try {
      authToken = req.headers.authorization;

      if (authToken) {
        user = await service.decodeToken(authToken);
      }
    } catch (e) {
      console.warn(`No se pudo autenticar el token: ${authToken}`);
    }
    return {
      authToken,
      user
    };
  }
});

server.listen().then(({ url }) => {
  console.log(`🚀 Servidor corriendo en: ${url}`);
});

models/user.js

"use strict";

const mongoose = require("mongoose");
const Schema = mongoose.Schema;
const bcrypt = require("bcryptjs");
const crypto = require("crypto");

const UserSchema = new Schema({
  uuid: String,
  email: { type: String, unique: true, lowercase: true },
  username: String,
  phone: String,
  avatar: String,
  dni: String,
  publicidad: Boolean,
  role: String,
  password: String,
  resetCode: String,
  resetExpiryTime: Date,
  signupDate: { type: Date, default: Date.now() },
  lastLogin: Date
});

UserSchema.pre("save", function(next) {
  let user = this;
  var salt = bcrypt.genSaltSync(10);
  var hash = bcrypt.hashSync(user.password, salt);
  user.password = hash;
  next();
});

UserSchema.methods.gravatar = function() {
  if (!this.email) return `https://gravatar.com/avatar/?s=200&d=retro`;

  const md5 = crypto
    .createHash("md5")
    .update(this.email)
    .digest("hex");
  return `https://gravatar.com/avatar/${md5}?s=200&d=retro`;
};

UserSchema.methods.comparePassword = function(candidatePassword, cb) {
  bcrypt.compare(candidatePassword, this.password, function(err, res) {
    cb(null, res);
  });
};

module.exports = mongoose.model("User", UserSchema);

lib/resolvers.js

const User = require("../models/user");
const service = require("../services");
const uuidv4 = require("uuid/v4");
const axios = require("axios");
const bcrypt = require("bcryptjs");
const baseURL = `https://jsonplaceholder.typicode.com`;
const resolvers = {
  Query: {
    users: (parent, args, context) => {
      if (!context.user) return null;
      return axios.get(`${baseURL}/users`).then(res => res.data);
    },
    user: (parent, args) => {
      const { id } = args;
      return axios.get(`${baseURL}/users/${id}`).then(res => res.data);
    },
    posts: (parent, args, context) => {
      // if (!context.user) return null;
      // if (!context.user || !context.user.roles.includes('admin')) return null;
      return axios.get(`${baseURL}/posts`).then(res => res.data);
    },
    post: (parent, args) => {
      const { id } = args;
      return axios.get(`${baseURL}/posts/${id}`).then(res => res.data);
    }
  },
  Post: {
    author: parent => {
      const { id } = parent;
      return axios.get(`${baseURL}/users/${id}/todos`).then(res => res.data);
    }
  },
  User: {
    posts: parent => {
      const { id } = parent;
      return axios.get(`${baseURL}/posts/${id}/todos`).then(res => res.data);
      // return fetch(`${baseURL}/posts/${id}/todos`).then(res => res.json());
    }
  },
  Mutation: {
    // Handle user signup
    async signup(_, { username, email, password }) {
      var user = new User();
      user.uuid = uuidv4();
      user.username = username;
      user.email = email;
      user.password = password;
      user.avatar = user.gravatar();
      User.findOne({ email: email }, function(err, existingUser) {
        if (existingUser) {
          throw new Error(`Ya existe un usuario con esa cuenta de correo`);
        } else {
          user.save(function(err, user) {
            if (err) {
              console.log(err);
              throw new Error(err);
            } else {
              return { token: service.createToken(user) };
            }
          });
        }
      });
      // devolvemos json web token
      return service.createToken(user);
    },

    // Handles user login
    async login(_, { email, password }) {
      const user = await User.findOne({ email: email });
      if (!user) {
        throw new Error("No user with that email");
      }
      const valid = await bcrypt.compareSync(password, user.password);

      if (!valid) {
        throw new Error("Incorrect password");
      }
      return service.createToken(user);
    }
  }
};

module.exports = resolvers;

services/index.js

"use strict";

const jwt = require("jwt-simple");
const moment = require("moment");
const config = require("../config");

function createToken(user) {
  const payload = {
    sub: {
      _id: user._id,
      email: user.email,
      username: user.username
    },
    iat: moment().unix(),
    exp: moment()
      .add(14, "days")
      .unix()
  };
  return jwt.encode(payload, config.SECRET_TOKEN);
}

function decodeToken(bearer) {
  const token = bearer.split(" ")[1];
  const payload = jwt.decode(token, config.SECRET_TOKEN);

  if (payload.exp <= moment().unix()) {
    reject({
      status: 401,
      message: "El token ha expirado"
    });
  }
  return payload.sub;
}

module.exports = {
  createToken,
  decodeToken
};

Uso:

Para usarlo solo tendremos que ejecutar npm start y acceder a http://localhost:4000/ para realizar nuestras pruebas.

SignUp:

mutation {
  signup(username: "Nombre de Usuario", email: "tu@mail.com", password:"12345")
  }

Login:

mutation {
  login(email: "tu@mail.com", password:"12345")
  }

Private endpoint:

{
  posts {
    title
  }
}

Http headers

{
  "Authorization": "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOnsiX2lkIjoiNWQ1ODc4MjE5MDM4NDQ1NGZkY2E1ZjFiIiwiZW1haWwiOiJlbm1hc2thQGdtYWlsLmNvbSJ9LCJpYXQiOjE1NjYwOTgzNTEsImV4cCI6MTU2NzMwNzk1MX0.8Kq0iZVsF5c55FGjT-dYtz0_aX6-_pXO0l0ona9QZuo"
}