Segurança GraphQL com JWTs: Autenticação e Autorização em APIs

GraphQL surge como uma alternativa popular às APIs REST tradicionais, oferecendo uma linguagem de consulta e manipulação de dados mais flexível e eficiente. Com a crescente adoção, a segurança das APIs GraphQL torna-se um aspecto crucial para proteger aplicações contra acessos não autorizados e possíveis violações de dados.

Uma estratégia eficaz para proteger APIs GraphQL é a implementação de JSON Web Tokens (JWTs). JWTs oferecem um método seguro e eficiente para conceder acesso a recursos protegidos e executar ações autorizadas, garantindo uma comunicação segura entre clientes e APIs.

Autenticação e Autorização em APIs GraphQL

Diferentemente das APIs REST, as APIs GraphQL geralmente possuem um único endpoint que permite aos clientes solicitar dinamicamente diferentes quantidades de dados em suas consultas. Esta flexibilidade, embora seja uma grande vantagem, aumenta o risco de possíveis ataques de segurança, como vulnerabilidades de controle de acesso quebradas.

Para mitigar este risco, é fundamental implementar processos robustos de autenticação e autorização, incluindo a definição adequada de permissões de acesso. Ao fazer isso, garante-se que apenas usuários autorizados possam acessar recursos protegidos e, em última análise, reduz-se o risco de potenciais violações de segurança e perda de dados.

O código deste projeto pode ser encontrado no repositório GitHub.

Configurando um Servidor Express.js Apollo

Apollo Server é uma implementação de servidor GraphQL amplamente utilizada para APIs GraphQL. É possível usá-lo para criar facilmente esquemas GraphQL, definir resolvers e gerenciar diferentes fontes de dados para as suas APIs.

Para configurar um Servidor Express.js Apollo, crie e abra uma pasta de projeto:

 mkdir graphql-API-jwt
cd graphql-API-jwt

Em seguida, execute este comando para inicializar um novo projeto Node.js usando npm, o gerenciador de pacotes Node:

 npm init --yes 

Agora, instale estes pacotes.

 npm install apollo-server graphql mongoose jsonwebtokens dotenv 

Por último, crie um arquivo server.js no diretório raiz e configure seu servidor com este código:

 const { ApolloServer } = require('apollo-server');
const mongoose = require('mongoose');
require('dotenv').config();

const typeDefs = require("./graphql/typeDefs");
const resolvers = require("./graphql/resolvers");

const server = new ApolloServer({
    typeDefs,
    resolvers,
    context: ({ req }) => ({ req }),
});

const MONGO_URI = process.env.MONGO_URI;

mongoose
  .connect(MONGO_URI, {
    useNewUrlParser: true,
    useUnifiedTopology: true,
  })
  .then(() => {
    console.log("Conectado ao DB");
    return server.listen({ port: 5000 });
  })
  .then((res) => {
    console.log(`Servidor em execução em ${res.url}`);
  })
  .catch(err => {
    console.log(err.message);
  });

O servidor GraphQL é configurado com os parâmetros typeDefs e resolvers, especificando o esquema e as operações que a API pode manipular. A opção context configura o objeto req para o contexto de cada resolver, o que permitirá ao servidor acessar detalhes específicos da solicitação, como valores de cabeçalho.

Criando um Banco de Dados MongoDB

Para estabelecer a conexão com o banco de dados, primeiro crie um banco de dados MongoDB ou configure um cluster no MongoDB Atlas. Em seguida, copie a string URI de conexão do banco de dados fornecida, crie um arquivo .env e insira a string de conexão da seguinte forma:

 MONGO_URI="<mongo_connection_uri>"

Definindo o Modelo de Dados

Defina um modelo de dados utilizando Mongoose. Crie um novo arquivo models/user.js e insira o seguinte código:

 const {model, Schema} = require('mongoose');

const userSchema = new Schema({
    name: String,
    password: String,
    role: String
});

module.exports = model('user', userSchema);

Definindo o Esquema GraphQL

Em uma API GraphQL, o esquema define a estrutura dos dados que podem ser consultados, e também descreve as operações disponíveis (consultas e mutações) que podem ser realizadas para interagir com os dados através da API.

Para definir um esquema, crie uma nova pasta no diretório raiz do seu projeto e nomeie-a como graphql. Dentro desta pasta, adicione dois arquivos: typeDefs.js e resolvers.js.

No arquivo typeDefs.js, insira o seguinte código:

 const { gql } = require("apollo-server");

const typeDefs = gql`
  type User {
    id: ID!
    name: String!
    password: String!
    role: String!
  }
  input UserInput {
    name: String!
    password: String!
    role: String!
  }
  type TokenResult {
    message: String
    token: String
  }
  type Query {
    users: [User]
  }
  type Mutation {
    register(userInput: UserInput): User
    login(name: String!, password: String!, role: String!): TokenResult
  }
`;

module.exports = typeDefs;

Criando Resolvers para a API GraphQL

As funções de resolver determinam como os dados são recuperados em resposta às consultas e mutações do cliente, bem como outros campos definidos no esquema. Quando um cliente envia uma consulta ou mutação, o servidor GraphQL aciona os resolvers correspondentes para processar e retornar os dados necessários de várias fontes, como bancos de dados ou APIs.

Para implementar autenticação e autorização usando JSON Web Tokens (JWTs), defina resolvers para as mutações de registro e login. Elas cuidarão dos processos de registro e autenticação do usuário. Em seguida, crie um resolver de consulta de busca de dados que estará acessível apenas a usuários autenticados e autorizados.

Mas primeiro defina as funções para gerar e verificar os JWTs. No arquivo resolvers.js, comece adicionando as seguintes importações.

 const User = require("../models/user");
const jwt = require('jsonwebtoken');
const secretKey = process.env.SECRET_KEY;

Certifique-se de adicionar a chave secreta que você usará para assinar tokens web JSON no arquivo .env.

 SECRET_KEY = '<minha_Chave_Secreta>'; 

Para gerar um token de autenticação, inclua a seguinte função, que também especifica atributos exclusivos para o token JWT, por exemplo, o tempo de expiração. Além disso, você pode incorporar outros atributos, como os emitidos no momento, com base nos requisitos específicos da sua aplicação.

 function generateToken(user) {
  const token = jwt.sign(
   { id: user.id, role: user.role },
   secretKey,
   { expiresIn: '1h', algorithm: 'HS256' }
 );

  return token;
}

Agora, implemente a lógica de verificação de token para validar os tokens JWT incluídos em solicitações HTTP subsequentes.

 function verifyToken(token) {
  if (!token) {
    throw new Error('Token não fornecido');
  }

  try {
    const decoded = jwt.verify(token, secretKey, { algorithms: ['HS256'] });
    return decoded;
  } catch (err) {
    throw new Error('Token inválido');
  }
}

Esta função receberá um token como entrada, verificará sua validade utilizando a chave secreta especificada e retornará o token decodificado se for válido, caso contrário, lançará um erro indicando um token inválido.

Definindo os Resolvers da API

Para definir os resolvers da API GraphQL, é necessário delinear as operações específicas que ela irá gerenciar, neste caso, as operações de registro e login do usuário. Primeiro, crie um objeto resolver que conterá as funções resolver e, em seguida, defina as seguintes operações de mutação:

 const resolvers = {
  Mutation: {
    register: async (_, { userInput: { name, password, role } }) => {
      if (!name || !password || !role) {
        throw new Error('Nome, senha e função são obrigatórios');
     }

      const newUser = new User({
        name: name,
        password: password,
        role: role,
      });

      try {
        const response = await newUser.save();

        return {
          id: response._id,
          ...response._doc,
        };
      } catch (error) {
        console.error(error);
        throw new Error('Falha ao criar usuário');
      }
    },
    login: async (_, { name, password }) => {
      try {
        const user = await User.findOne({ name: name });

        if (!user) {
          throw new Error('Usuário não encontrado');
       }

        if (password !== user.password) {
          throw new Error('Senha incorreta');
        }

        const token = generateToken(user);

        if (!token) {
          throw new Error('Falha ao gerar token');
        }

        return {
          message: 'Login bem-sucedido',
          token: token,
        };
      } catch (error) {
        console.error(error);
        throw new Error('Falha no login');
      }
    }
  },

A mutação de registro trata do processo de cadastro adicionando os novos dados do usuário ao banco de dados. Enquanto a mutação de login gerencia os logins de usuários – no caso de autenticação bem-sucedida, ela irá gerar um token JWT, e também retornar uma mensagem de sucesso na resposta.

Agora inclua o resolver de consulta para recuperar dados do usuário. Para garantir que esta consulta esteja acessível apenas a usuários autenticados e autorizados, inclua uma lógica de autorização para restringir o acesso apenas a usuários com a função de administrador.

Essencialmente, a consulta verificará primeiro a validade do token e, em seguida, a função do usuário. Se a verificação de autorização for bem-sucedida, a consulta do resolver irá buscar e retornar os dados dos usuários do banco de dados.

   Query: {
    users: async (parent, args, context) => {
      try {
        const token = context.req.headers.authorization || '';
        const decodedToken = verifyToken(token);

        if (decodedToken.role !== 'Admin') {
          throw new ('Não autorizado. Somente administradores podem acessar estes dados.');
        }

        const users = await User.find({}, { name: 1, _id: 1, role:1 });
        return users;
      } catch (error) {
        console.error(error);
        throw new Error('Falha ao buscar usuários');
      }
    },
  },
};

Finalmente, inicie o servidor de desenvolvimento:

 node server.js 

Ótimo! Agora vá em frente e teste a funcionalidade da API utilizando o sandbox da API do Apollo Server no seu navegador. Por exemplo, você pode usar a mutação de registro para adicionar novos dados de usuário ao banco de dados, e depois a mutação de login para autenticar o usuário.

Por último, adicione o token JWT à seção de cabeçalho de autorização e prossiga para consultar o banco de dados em busca de dados do usuário.

Protegendo APIs GraphQL

Autenticação e autorização são componentes cruciais para proteger APIs GraphQL. No entanto, é importante reconhecer que, por si só, podem não ser suficientes para garantir uma segurança abrangente. Você deve implementar medidas de segurança adicionais, como validação de entrada e criptografia de dados confidenciais.

Ao adotar uma abordagem de segurança abrangente, você pode proteger suas APIs contra diversos ataques potenciais.