Implementace ověření uživatele v Express.js pomocí JWT

GraphQL se stává stále populárnější alternativou k tradičnímu REST API, nabízející flexibilní a efektivní způsob pro dotazování a manipulaci s daty. S jeho rozšiřujícím se využitím roste i potřeba zabezpečení GraphQL API, aby se předešlo neoprávněnému přístupu a úniku citlivých dat.

Jednou z efektivních metod zabezpečení GraphQL API je implementace JSON Web Tokenů (JWT). JWT představují bezpečný a efektivní způsob, jak udělovat přístup k chráněným zdrojům a autorizovat akce, zajišťující bezpečnou komunikaci mezi klienty a API.

Autentizace a autorizace v GraphQL API

Na rozdíl od REST API, GraphQL API obvykle disponují jedním koncovým bodem, což klientům umožňuje dynamicky požadovat různá množství dat v rámci jednoho dotazu. I když je tato flexibilita velkou výhodou, nese s sebou i riziko potenciálních bezpečnostních útoků, včetně zranitelností spojených s kontrolou přístupu.

Pro minimalizaci těchto rizik je klíčové implementovat robustní ověřovací a autorizační mechanismy, včetně správného nastavení přístupových oprávnění. Tím zajistíte, že pouze autorizovaní uživatelé budou mít přístup k chráněným zdrojům, a tím snížíte riziko potenciálních narušení bezpečnosti a ztráty dat.

Zdrojový kód tohoto projektu je dostupný na GitHub úložišti.

Nastavení Express.js Apollo Serveru

Apollo Server je široce používaná implementace GraphQL serveru. Umožňuje snadno vytvářet GraphQL schémata, definovat resolvery a spravovat různé datové zdroje pro vaše API.

Pro nastavení Express.js Apollo Serveru nejdříve vytvořte a otevřete složku projektu:

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

Poté spusťte následující příkaz pro inicializaci nového Node.js projektu pomocí npm, Node package manageru:

 npm init --yes 

Následně nainstalujte potřebné balíčky.

 npm install apollo-server graphql mongoose jsonwebtoken dotenv 

Nakonec vytvořte soubor server.js v kořenové složce a nastavte server pomocí tohoto kódu:

 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("Připojeno k DB");
    return server.listen({ port: 5000 });
  })
  .then((res) => {
    console.log(`Server běží na ${res.url}`);
  })
  .catch(err => {
    console.log(err.message);
  });

GraphQL server je nastaven s parametry typeDefs a resolvers, které definují schéma a operace, které API zvládne. Volba kontextu konfiguruje objekt req do kontextu každého resolveru, což serveru umožňuje přístup k podrobnostem specifickým pro daný požadavek, jako jsou hlavičky.

Vytvoření MongoDB databáze

Pro vytvoření databázového spojení nejprve vytvořte novou MongoDB databázi nebo nastavte cluster v Atlasu MongoDB. Poté zkopírujte URI připojovací řetězec, vytvořte soubor .env a uložte do něj připojovací řetězec v následujícím formátu:

 MONGO_URI="<mongo_connection_uri>"

Definice datového modelu

Pro definici datového modelu použijte Mongoose. Vytvořte nový soubor models/user.js a vložte následující kód:

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

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

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

Definice GraphQL schématu

V GraphQL API definuje schéma strukturu dat, na která je možné se dotazovat, a také dostupné operace (dotazy a mutace), pomocí kterých lze s daty interagovat.

Pro definování schématu vytvořte novou složku v kořenovém adresáři projektu a pojmenujte ji graphql. Do této složky vložte dva soubory: typeDefs.js a resolvers.js.

Do souboru typeDefs.js vložte následující kód:

 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;

Vytvoření resolverů pro GraphQL API

Funkce resolveru určují, jakým způsobem se data načítají v reakci na klientské dotazy a mutace, a také další pole definovaná v schématu. Když klient odešle dotaz nebo mutaci, GraphQL server spustí odpovídající resolver, který zpracuje a vrátí požadovaná data z různých zdrojů, jako jsou databáze nebo jiná API.

Pro implementaci autentizace a autorizace pomocí JWT tokenů definujte resolvery pro mutace registrace a přihlášení. Tyto resolvery budou zpracovávat procesy registrace a ověřování uživatelů. Dále vytvořte resolver pro načítání dat, který bude přístupný pouze ověřeným a autorizovaným uživatelům.

Nejprve definujte funkce pro generování a ověřování JWT tokenů. V souboru resolvers.js začněte přidáním následujících importů.

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

Nezapomeňte do souboru .env přidat tajný klíč, který se použije pro podepisování JWT tokenů.

 SECRET_KEY = '<muj_tajny_klic>'; 

Pro generování autentizačního tokenu vložte následující funkci, která také definuje jedinečné atributy pro JWT token, například dobu platnosti. Dle potřeby je možné přidat další atributy, jako například čas vydání tokenu.

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

  return token;
}

Nyní implementujte logiku ověření tokenu pro validaci JWT tokenů obsažených v následných HTTP požadavcích.

 function verifyToken(token) {
  if (!token) {
    throw new Error('Token nebyl poskytnut');
  }

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

Tato funkce přijme token jako vstup, ověří jeho platnost pomocí tajného klíče a v případě platnosti tokenu vrátí dekódovaný token, v opačném případě vyvolá chybu indikující neplatný token.

Definice API resolverů

Pro definování resolverů pro GraphQL API je nutné nastínit specifické operace, které bude server zpracovávat, v tomto případě operace registrace a přihlášení uživatele. Nejprve vytvořte objekt resolveru, který bude obsahovat funkce resolverů a poté definujte následující mutační operace:

 const resolvers = {
  Mutation: {
    register: async (_, { userInput: { name, password, role } }) => {
      if (!name || !password || !role) {
        throw new Error('Jméno, heslo a role jsou povinné');
     }

      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('Nepodařilo se vytvořit uživatele');
      }
    },
    login: async (_, { name, password }) => {
      try {
        const user = await User.findOne({ name: name });

        if (!user) {
          throw new Error('Uživatel nenalezen');
       }

        if (password !== user.password) {
          throw new Error('Nesprávné heslo');
        }

        const token = generateToken(user);

        if (!token) {
          throw new Error('Nepodařilo se vygenerovat token');
        }

        return {
          message: 'Přihlášení proběhlo úspěšně',
          token: token,
        };
      } catch (error) {
        console.error(error);
        throw new Error('Přihlášení se nezdařilo');
      }
    }
  },

Registrační mutace zpracovává proces registrace, ukládá nová uživatelská data do databáze. Přihlašovací mutace naopak obstarává přihlašování uživatelů – v případě úspěšné autentizace vygeneruje JWT token a v odpovědi vrátí zprávu o úspěchu.

Nyní přidejte resolver pro dotaz pro načítání uživatelských dat. Aby byl tento dotaz přístupný pouze ověřeným a autorizovaným uživatelům, implementujte autorizační logiku, která omezí přístup pouze na uživatele s rolí administrátora.

Dotaz nejprve zkontroluje platnost tokenu a poté ověří uživatelskou roli. Pokud je autorizační kontrola úspěšná, dotaz resolveru pokračuje v načítání dat uživatelů z databáze a vrací je.

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

        if (decodedToken.role !== 'Admin') {
          throw new Error('Neautorizováno. Pouze administrátoři mají přístup k těmto datům.');
        }

        const users = await User.find({}, { name: 1, _id: 1, role:1 });
        return users;
      } catch (error) {
        console.error(error);
        throw new Error('Nepodařilo se načíst uživatele');
      }
    },
  },
};

Nakonec spusťte vývojový server:

 node server.js 

Výborně! Nyní můžete otestovat funkčnost API pomocí GraphQL playgroundu ve vašem prohlížeči. Můžete například použít registrační mutaci pro přidání nových uživatelských dat do databáze a poté ověřit uživatele pomocí přihlašovací mutace.

Nakonec přidejte JWT token do sekce hlavičky autorizace a pokračujte v dotazování databáze pro získání uživatelských dat.

Zabezpečení GraphQL API

Autentizace a autorizace jsou základní komponenty pro zabezpečení GraphQL API. Je však důležité si uvědomit, že samy o sobě nemusí stačit pro zajištění komplexní bezpečnosti. Doporučuje se implementovat další bezpečnostní opatření, jako je validace vstupů a šifrování citlivých dat.

Přijetím komplexního bezpečnostního přístupu můžete ochránit vaše API před různými potenciálními útoky.