Implementera användarautentisering i Express.js med JWT

By rik

GraphQL har etablerat sig som ett populärt alternativ till den mer traditionella RESTful API-arkitekturen. Det erbjuder en flexibel och effektiv metod för dataförfrågningar och manipulationer av API:er. Med dess ökande användning blir det avgörande att prioritera säkerheten för GraphQL API:er för att skydda applikationer mot obehörig åtkomst och potentiella dataintrång.

En effektiv metod för att öka säkerheten i GraphQL API:er är att använda JSON Web Tokens (JWT). JWT ger en säker och effektiv mekanism för att tillåta åtkomst till skyddade resurser och utföra auktoriserade operationer, vilket säkerställer en skyddad kommunikation mellan klienter och API:er.

Autentisering och auktorisering i GraphQL API:er

I motsats till REST API:er, använder GraphQL API:er vanligtvis en enda slutpunkt. Detta tillåter klienter att dynamiskt begära olika datamängder i sina förfrågningar. Även om denna flexibilitet är en styrka, ökar det också risken för potentiella säkerhetsangrepp, såsom sårbarheter i åtkomstkontrollen.

För att minska dessa risker är det viktigt att implementera robusta autentiserings- och auktoriseringsprocesser, inklusive en korrekt definition av åtkomstbehörigheter. Genom detta säkerställs att endast behöriga användare får tillgång till skyddade resurser, vilket i slutändan minskar risken för potentiella säkerhetsincidenter och dataförlust.

Du hittar koden för det här projektet i dess GitHub-repository.

Konfigurera en Express.js Apollo-server

Apollo Server är en ofta använd GraphQL-serverimplementation för GraphQL API:er. Den gör det enkelt att bygga GraphQL-scheman, definiera resolvers och hantera olika datakällor för dina API:er.

För att konfigurera en Express.js Apollo Server, skapa och öppna en projektkatalog:

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

Kör sedan följande kommando för att initiera ett nytt Node.js-projekt med npm, Node Package Manager:

 npm init --yes 

Installera nu dessa paket:

 npm install apollo-server graphql mongoose jsonwebtokens dotenv 

Slutligen, skapa en `server.js`-fil i rotkatalogen och konfigurera din server med följande kod:

 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("Ansluten till databasen");
    return server.listen({ port: 5000 });
  })
  .then((res) => {
    console.log(`Servern körs på ${res.url}`);
  })
  .catch(err => {
    console.log(err.message);
  });

GraphQL-servern konfigureras med parametrarna `typeDefs` och `resolvers`, som specificerar schemat och de operationer som API:et kan hantera. `context`-alternativet ställer in `req`-objektet i kontexten för varje resolver, vilket gör att servern får tillgång till förfrågningsspecifika detaljer, som rubrikvärden.

Skapa en MongoDB-databas

För att etablera databasanslutningen, skapa först en MongoDB-databas eller konfigurera ett kluster på MongoDB Atlas. Kopiera sedan den medföljande URI-strängen för databasanslutningen, skapa en `.env`-fil och ange anslutningssträngen enligt följande:

 MONGO_URI="<mongo_connection_uri>"

Definiera datamodellen

Definiera en datamodell med Mongoose. Skapa en ny `models/user.js`-fil och inkludera följande kod:

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

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

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

Definiera GraphQL-schemat

I ett GraphQL API definierar schemat strukturen för de data som kan efterfrågas, samt beskriver de tillgängliga operationerna (frågor och mutationer) som kan utföras för att interagera med data via API:et.

För att definiera ett schema, skapa en ny mapp i rotkatalogen för ditt projekt och namnge den `graphql`. Lägg till två filer i den här mappen: `typeDefs.js` och `resolvers.js`.

Inkludera följande kod i filen `typeDefs.js`:

 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;

Skapa resolvers för GraphQL API

Resolverfunktioner bestämmer hur data hämtas som svar på klientförfrågningar och mutationer, såväl som andra fält som definieras i schemat. När en klient skickar en fråga eller mutation, triggar GraphQL-servern motsvarande resolver för att bearbeta och returnera nödvändig data från olika källor, såsom databaser eller andra API:er.

För att implementera autentisering och auktorisering med JSON Web Tokens (JWT), definiera resolvers för registrerings- och inloggningsmutationerna. Dessa kommer att hantera processerna för användarregistrering och autentisering. Skapa sedan en datahämtningsfråga som endast är tillgänglig för autentiserade och auktoriserade användare.

Men först, definiera funktionerna för att generera och verifiera JWT. Börja med att lägga till följande import i filen `resolvers.js`.

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

Se till att lägga till den hemliga nyckel som du använder för att signera JSON-webbtokens till `.env`-filen.

 SECRET_KEY = '<my_Secret_Key>'; 

För att generera en autentiseringstoken, inkludera följande funktion, som också specificerar unika attribut för JWT-token, t.ex. utgångstiden. Dessutom kan du införliva andra attribut som utfärdats vid tidpunkten baserat på dina specifika applikationskrav.

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

  return token;
}

Implementera nu tokenverifieringslogiken för att validera JWT-tokens som inkluderas i efterföljande HTTP-förfrågningar.

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

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

Den här funktionen tar en token som indata, verifierar dess giltighet med den angivna hemliga nyckeln och returnerar den avkodade token om den är giltig. Annars ges ett felmeddelande som indikerar en ogiltig token.

Definiera API-resolvers

För att definiera resolvers för GraphQL API måste du beskriva de specifika operationerna som den ska hantera, i det här fallet användarregistrering och inloggningsoperationer. Skapa först ett resolverobjekt som ska lagra resolverfunktionerna, och definiera sedan följande mutationsoperationer:

 const resolvers = {
  Mutation: {
    register: async (_, { userInput: { name, password, role } }) => {
      if (!name || !password || !role) {
        throw new Error('Namn, lösenord och roll krävs');
     }

      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('Kunde inte skapa användare');
      }
    },
    login: async (_, { name, password }) => {
      try {
        const user = await User.findOne({ name: name });

        if (!user) {
          throw new Error('Användaren hittades inte');
       }

        if (password !== user.password) {
          throw new Error('Fel lösenord');
        }

        const token = generateToken(user);

        if (!token) {
          throw new Error('Kunde inte generera token');
        }

        return {
          message: 'Inloggningen lyckades',
          token: token,
        };
      } catch (error) {
        console.error(error);
        throw new Error('Inloggningen misslyckades');
      }
    }
  },

Registreringsmutationen hanterar registreringsprocessen genom att lägga till nya användardata i databasen. Inloggningsmutationen hanterar användarinloggningar. Vid lyckad autentisering genereras en JWT-token och ett framgångsmeddelande returneras i svaret.

Inkludera nu frågeresolvern för att hämta användardata. För att säkerställa att den här frågan endast är tillgänglig för autentiserade och auktoriserade användare, inkludera auktoriseringslogik för att begränsa åtkomsten till endast användare med en administratörsroll.

I princip kommer frågan först att kontrollera tokens giltighet och sedan användarrollen. Om auktoriseringskontrollen lyckas fortsätter resolverfrågan att hämta och returnera användardata från databasen.

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

        if (decodedToken.role !== 'Admin') {
          throw new ('Ej behörig. Endast administratörer har tillgång till den här datan.');
        }

        const users = await User.find({}, { name: 1, _id: 1, role:1 });
        return users;
      } catch (error) {
        console.error(error);
        throw new Error('Kunde inte hämta användare');
      }
    },
  },
};

Starta slutligen utvecklingsservern:

 node server.js 

Utmärkt! Fortsätt nu och testa funktionaliteten hos API:et med hjälp av Apollo Server API-sandlådan i din webbläsare. Du kan till exempel använda registreringsmutationen för att lägga till nya användardata i databasen, och sedan inloggningsmutationen för att autentisera användaren.

Lägg slutligen till JWT-token till behörighetsrubriksektionen och fortsätt med att fråga databasen efter användardata.

Säkra GraphQL API:er

Autentisering och auktorisering är avgörande komponenter för att skydda GraphQL API:er. Trots det är det viktigt att inse att de i sig själva kanske inte är tillräckliga för att garantera en fullständig säkerhet. Det är lämpligt att implementera ytterligare säkerhetsåtgärder som indatavalidering och kryptering av känslig information.

Genom att anta en omfattande säkerhetsstrategi kan du skydda dina API:er mot en mängd potentiella angrepp.