JWT Autentisering i Node.js: Steg-for-steg guide

Grunnleggende om sikkerhet: Autentisering og autorisasjon

I hjertet av datasikkerhet finner vi konseptene autentisering og autorisasjon. Autentisering handler om å verifisere identiteten din, som regel ved hjelp av brukernavn og passord. Dette beviser at du er den du utgir deg for å være og gir deg tilgang til systemet som en registrert bruker. Deretter kan autorisasjon gi deg ytterligere tilgangsrettigheter.

Dette prinsippet gjelder også når du logger deg inn på nettjenester ved hjelp av kontoer fra Facebook eller Google.

Denne artikkelen tar for seg hvordan man kan bygge en Node.js API med JWT (JSON Web Tokens) for autentisering. Vi vil bruke følgende verktøy:

  • Express.js
  • MongoDB database
  • Mongoose
  • Dotenv
  • Bcrypt.js
  • Jsonwebtoken

Autentisering versus autorisasjon

Hva er autentisering?

Autentisering er prosessen der man bekrefter brukerens identitet. Dette skjer ved hjelp av legitimasjon som e-post, passord eller sikkerhetstokens. Den innsendte legitimasjonen blir så sammenlignet med en registrert brukers data lagret lokalt eller i en database. Hvis det er samsvar, fullføres autentiseringen og brukeren får tilgang til ressurser.

Hva er autorisasjon?

Autorisasjon følger etter autentiseringen. Det handler om å gi den autentiserte brukeren tilgang til bestemte ressurser i et system eller en applikasjon. For å illustrere dette i vår opplæring, vil innloggede brukere få tilgang til brukerdata, mens ikke-innloggede brukere vil bli nektet tilgang.

Et godt eksempel på autorisasjon er sosiale medieplattformer som Facebook og Twitter. Uten en konto kan du ikke se innholdet. Et annet eksempel er abonnementstjenester; selv om du er logget inn på et nettsted, må du ha et gyldig abonnement for å få tilgang til premium-innhold.

Forutsetninger

Det antas at du har en grunnleggende forståelse av JavaScript, MongoDB og god kjennskap til Node.js før du fortsetter.

Sjekk også at du har Node.js og npm (Node Package Manager) installert. Du kan bekrefte dette ved å åpne kommandolinjen og skrive `node -v` og `npm -v`. Dette vil vise de installerte versjonene, som illustrert nedenfor.

Din versjon kan variere fra det viste bildet. NPM følger med Node.js. Hvis du ikke har installert dette, kan du laste det ned fra NodeJS sin hjemmeside.

For å skrive kode trenger du også et integrert utviklingsmiljø (IDE). I denne artikkelen bruker vi VS Code, men du kan velge en annen om du foretrekker det. Du kan laste ned VS Code fra Visual Studio sin hjemmeside, og velg den versjonen som passer til operativsystemet ditt.

Prosjektoppsett

Start med å opprette en ny mappe, f.eks. kalt «nodeapi», på datamaskinen din. Åpne deretter mappen i VS Code. I VS Code-terminalen, initialiser Node Package Manager ved å skrive:

npm init -y

Sørg for at du er inne i «nodeapi»-mappen før du utfører kommandoen. Denne kommandoen genererer en `package.json`-fil, der vi vil lagre alle prosjektets avhengigheter.

Nå installerer vi alle nødvendige pakker ved å skrive følgende i terminalen:

npm install express dotenv jsonwebtoken mongoose bcryptjs

Etter installasjonen vil du se en struktur med filer og mapper, omtrent som vist nedenfor:

Opprette server og koble til database

Opprett en fil som heter `index.js` og en mappe kalt `config`. Inni `config`-mappen, opprett to filer: `conn.js` (for databasekobling) og `config.env` (for miljøvariabler). Kopier og lim inn koden nedenfor i de respektive filene.

index.js

const express = require('express');
const dotenv = require('dotenv');

//Konfigurer dotenv-filer før bruk av andre bibliotek
dotenv.config({path:'./config/config.env'}); 

//Opprett express app
const app = express();

//Bruk express.json for å motta json-data
app.use(express.json());

//Lytt etter server
app.listen(process.env.PORT,() => {
    console.log(`Serveren lytter på port ${process.env.PORT}`);
});

Når du bruker dotenv, er det viktig å konfigurere den i `index.js`-filen før du importerer andre filer som trenger tilgang til miljøvariabler.

conn.js

const mongoose = require('mongoose');

mongoose.connect(process.env.URI, 
    { useNewUrlParser: true,
     useUnifiedTopology: true })
    .then((data) => {
        console.log(`Database koblet til ${data.connection.host}`)
});

config.env

URI = 'mongodb+srv://brukernavn:[email protected]/?retryWrites=true&w=majority'
PORT = 5000

Her er et eksempel med en MongoDB Atlas URI. Du kan også bruke localhost hvis du foretrekker det.

Lage modeller og ruter

En modell representerer strukturen på dataene i MongoDB-databasen og vil lagres som JSON-dokumenter. Vi bruker Mongoose-skjemaer for å definere modellene.

Ruting definerer hvordan applikasjonen svarer på klientforespørsler. Vi bruker Express sin rutingfunksjonalitet til å opprette ruter.

Rutemetoder har typisk to argumenter: ruten og en tilbakringingsfunksjon som definerer hva ruten skal utføre når en forespørsel mottas. Et tredje argument, en mellomvarefunksjon, kan også brukes ved behov, slik som i autentiseringsprosessen. Siden vi skal lage en autentisert API, vil vi også bruke mellomvare for å autorisere og autentisere brukere.

Opprett to nye mapper: `routes` og `models`. Inne i `routes`-mappen, lag en fil som heter `userRoute.js`. I `models`-mappen lager du en fil med navnet `userModel.js`. Kopier så inn følgende kode i de respektive filene:

userModel.js

const mongoose = require('mongoose');

//Oppretter skjema med Mongoose
const userSchema = new mongoose.Schema({
    name: {
        type:String,
        required:true,
        minLength:[4,'Navn må ha minst 4 tegn']
    },
    email:{
        type:String,
        required:true,
        unique:true,
    },
    password:{
        type:String,
        required:true,
        minLength:[8,'Passord må ha minst 8 tegn']
    },
    token:{
        type:String
    }
});

//Oppretter modell
const userModel = mongoose.model('user',userSchema);
module.exports = userModel;

userRoute.js

const express = require('express');
//Oppretter express ruter
const route = express.Router();
//Importerer userModel
const userModel = require('../models/userModel');

//Oppretter register-rute
route.post('/register',(req,res) => {

});
//Oppretter login-rute
route.post('/login',(req,res) => {

});

//Oppretter bruker-rute for å hente brukerdata
route.get('/user',(req,res) => {

});

Implementere rutefunksjonalitet og lage JWT-tokens

Hva er JWT?

JSON Web Tokens (JWT) er et JavaScript-bibliotek for å generere og validere tokens. Det er en åpen standard for sikker informasjonsutveksling mellom en klient og en server. Vi bruker to hovedfunksjoner med JWT: `sign` for å opprette et nytt token og `verify` for å validere tokenet.

Hva er bcryptjs?

Bcrypt.js er en hashing-funksjon utviklet av Niels Provos og David Mazières. Den bruker en hash-algoritme for å kryptere passord. Bcrypt.js har to vanlige funksjoner som vi skal bruke i dette prosjektet: `hash` for å generere en hashverdi, og `compare` for å sammenligne passord.

Implementere rutefunksjonalitet

Tilbakringingsfunksjonen i ruting tar tre argumenter: request (forespørsel), response (svar) og next (neste). Det siste argumentet er valgfritt og brukes når en annen funksjon eller mellomvare skal kalles. Endre filene `userRoute.js`, `config.env` og `index.js` med følgende kode:

userRoute.js

//Importerer nødvendige filer og biblioteker
const express = require('express');
const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');

//Oppretter express ruter
const route = express.Router();
//Importerer userModel
const userModel = require('../models/userModel');

//Oppretter register-rute
route.post("/register", async (req, res) => {

    try {
        const { name, email, password } = req.body;
        //Sjekker om data er tom
        if (!name || !email || !password) {
            return res.json({ message: 'Vennligst fyll inn alle detaljene' });
        }

        //Sjekker om brukeren allerede finnes
        const userExist = await userModel.findOne({ email: req.body.email });
        if (userExist) {
            return res.json({ message: 'Bruker finnes allerede med den e-postadressen' });
        }
        //Krypter passordet
        const salt = await bcrypt.genSalt(10);
        const hashPassword = await bcrypt.hash(req.body.password, salt);
        req.body.password = hashPassword;
        const user = new userModel(req.body);
        await user.save();
        const token = await jwt.sign({ id: user._id }, process.env.SECRET_KEY, {
            expiresIn: process.env.JWT_EXPIRE,
        });
        return res.cookie({ 'token': token }).json({ success: true, message: 'Bruker registrert', data: user });
    } catch (error) {
        return res.json({ error: error });
    }
});

//Oppretter login-rute
route.post('/login', async (req, res) => {
    try {
        const { email, password } = req.body;
        //Sjekker om data er tom
        if (!email || !password) {
            return res.json({ message: 'Vennligst fyll inn alle detaljene' });
        }
        //Sjekker om brukeren finnes
        const userExist = await userModel.findOne({email:req.body.email});
        if(!userExist){
            return res.json({message:'Feil brukernavn eller passord'})
        }
        //Sjekker passord
        const isPasswordMatched = await bcrypt.compare(password,userExist.password);
        if(!isPasswordMatched){
            return res.json({message:'Feil brukernavn eller passord'});
        }
        const token = await jwt.sign({ id: userExist._id }, process.env.SECRET_KEY, {
            expiresIn: process.env.JWT_EXPIRE,
        });
        return res.cookie({"token":token}).json({success:true,message:'Innlogging vellykket'});
    } catch (error) {
        return res.json({ error: error });
    }
});

//Oppretter bruker-rute for å hente brukerdata
route.get('/user', async (req, res) => {
    try {
        const user  = await userModel.find();
        if(!user){
            return res.json({message:'Ingen brukere funnet'});
        }
        return res.json({user:user});
    } catch (error) {
        return res.json({ error: error });
    }
});

module.exports = route;

Husk å bruke try-catch blokker når du bruker Async-funksjoner, da dette hindrer uhåndterte avvisningsfeil.

config.env

URI = 'mongodb+srv://brukernavn:[email protected]/?retryWrites=true&w=majority'
PORT = 5000
SECRET_KEY = "en-hemmelig-nøkkel-her"
JWT_EXPIRE = 2d

index.js

const express = require('express');
const dotenv = require('dotenv');

//Konfigurerer dotenv filer over andre biblioteker og filer
dotenv.config({path:'./config/config.env'});
require('./config/conn');

//Oppretter express app
const app = express();
const route = require('./routes/userRoute');

//Bruk express.json for å motta json data
app.use(express.json());
//Bruk ruter
app.use('/api', route);

//Lytt til server
app.listen(process.env.PORT,() => {
    console.log(`Serveren lytter på port ${process.env.PORT}`);
});

Opprette mellomvare for å autentisere bruker

Hva er mellomvare?

Mellomvare er en funksjon som har tilgang til request-objektet, response-objektet, og `next`-funksjonen i request-response-syklusen. `next`-funksjonen kalles når funksjonen er ferdig. `next()` brukes når en annen tilbakringingsfunksjon eller mellomvare skal utføres.

Opprett en ny mappe kalt `middleware` og en fil inni der kalt `auth.js`. Legg til følgende kode:

auth.js

const userModel = require('../models/userModel');
const jwt = require('jsonwebtoken');

const isAuthenticated = async (req,res,next) => {
    try {
        const {token} = req.cookies;
        if(!token){
            return next('Vennligst logg inn for å få tilgang til data');
        }
        const verify = await jwt.verify(token,process.env.SECRET_KEY);
        req.user = await userModel.findById(verify.id);
        next();
    } catch (error) {
        return next(error);
    }
};

module.exports = isAuthenticated;

Installer cookie-parser-biblioteket for å konfigurere `cookieParser` i appen din. `cookieParser` hjelper deg med å få tilgang til tokenet som er lagret i informasjonskapslene. Uten `cookieParser` vil du ikke få tilgang til informasjonskapslene fra overskriftene i request-objektet. Skriv følgende i terminalen for å laste ned `cookie-parser`:

npm i cookie-parser

Nå som `cookieParser` er installert, konfigurer appen din ved å endre `index.js`-filen og legg til mellomvare i «/user/» ruten.

index.js-filen

const cookieParser = require('cookie-parser');
const express = require('express');
const dotenv = require('dotenv');

//Konfigurer dotenv filer over andre biblioteker og filer
dotenv.config({path:'./config/config.env'});
require('./config/conn');
//Opprett express app
const app = express();
const route = require('./routes/userRoute');

//Bruk express.json for å motta json data
app.use(express.json());
//Konfigurerer cookie-parser
app.use(cookieParser());

//Bruk ruter
app.use('/api', route);

//Lytt til server
app.listen(process.env.PORT,() => {
    console.log(`Serveren lytter på port ${process.env.PORT}`);
});

userRoute.js

//Importerer nødvendige filer og biblioteker
const express = require('express');
const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');
const isAuthenticated = require('../middleware/auth');

//Oppretter express ruter
const route = express.Router();
//Importerer userModel
const userModel = require('../models/userModel');

//Oppretter bruker-rute for å hente brukerdata
route.get('/user', isAuthenticated, async (req, res) => {
    try {
        const user = await userModel.find();
        if (!user) {
            return res.json({ message: 'Ingen brukere funnet' });
        }
        return res.json({ user: user });
    } catch (error) {
        return res.json({ error: error });
    }
});

module.exports = route;

Ruten «/user» er nå kun tilgjengelig for innloggede brukere.

Sjekker API-ene på POSTMAN

Før du sjekker API-er, må du endre `package.json`-filen og legge til følgende:

"scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "node index.js",
    "dev": "nodemon index.js"
  },

Du kan starte serveren med `npm start`, men dette vil kun kjøre serveren én gang. For å holde serveren kjørende mens du endrer filer, trenger du `nodemon`. Last ned `nodemon` ved å skrive følgende i terminalen:

npm install -g nodemon

Flagget `-g` vil laste ned `nodemon` globalt på systemet, slik at du ikke trenger å laste det ned for hvert nye prosjekt.

For å starte serveren, skriv `npm run dev` i terminalen. Du vil få et resultat som dette:

Koden din er nå fullført, og serveren kjører. Du kan sjekke APIene med POSTMAN.

Hva er POSTMAN?

POSTMAN er et verktøy for å designe, bygge, utvikle og teste API-er.

Hvis du ikke har lastet ned POSTMAN, last ned herfra: Postman sin hjemmeside.

Åpne POSTMAN og lag en ny samling med navnet `nodeAPItest`, og inni den lag tre forespørsler: `register`, `login` og `user`. Det skal se omtrent slik ut:

Når du sender JSON-data til `localhost:5000/api/register`, vil du få et resultat som ligner på dette:

Siden vi oppretter og lagrer tokenet i informasjonskapsler ved registrering, kan du få brukerdetaljene ved å spørre «/api/user»-ruten. Du kan teste de andre forespørslene i POSTMAN.

Du finner fullstendig kode på min GitHub-konto.

Konklusjon

I denne opplæringen har du lært hvordan man bruker autentisering i en Node.js API ved hjelp av JWT-tokens. Du har også lært hvordan å gi brukere tilgang til brukerdata.

GOD KODING!