Authenticatie en Autorisatie

Authenticatie en autorisatie

INTRODUCTIE

Voorbereiding

Installatie

Installeer bcrypt

npm install bcrypt

Installeer cookie-parser

npm install cookie-parser

Installeer jsonwebtoken

npm install jsonwebtoken

Voorbereiding

  • Om cookies te kunnen lezen, hebben we de cookieParser nodig. Update je app.js.
app.use(cookieParser());
  • Vergeet niet om de body-parser te importeren
import cookieParser from "cookie-parser";

Velden voor register form

  • Binnen de register-action van de AuthController beschrijven we de velden die we nodig hebben voor register:
    • inputs-array met...
      • firstname, lastname, email, password
  • We halen ook alle beschikbare roles op
  • inputs & roles sturen we als data mee naar de
    register-view
Code-snippet op de volgende slide! -->

Velden voor register form

import Role from "../models/Role.js";

export const register = async (req, res) => {
  const inputs = [
      { name: "firstname", label: "Voornaam", type: "text" },
      { name: "lastname", label: "Achternaam", type: "text" },
      { name: "email", label: "E-mail", type: "text" },
      { name: "password", label: "Password", type: "password" },
  ];

  const roles = await Role.query();

  res.render("register", {
      layout: "layouts/authentication",
      inputs,
      roles,
  });
}
Resultaat in browser, volgende slide -->

Velden voor register form

Authenticatie en autorisatie

REGISTRATIE

Validate Register Form

  • Een nieuw formulier, betekent nieuwe validatie
  • We valideren o.a. of het e-mailadres en wachtwoord conform de regels is.
  • Maak in de folder validation een file AuthRegisterValidation.js
  • Telkens wanneer men registreert, willen we de data controleren. Vul aan met meerdere validators.
import { body } from 'express-validator';

export default [
  body("firstname").notEmpty().withMessage("Voornaam is een verplicht veld."),
  body("lastname").notEmpty().withMessage("Achternaam is een verplicht veld."),
  body("email")
    .notEmpty()
    .withMessage("E-mail is een verplicht veld.")
    .bail()
    .isEmail()
    .withMessage("Onjuist e-mail adres"),
  body("password")
    .isLength({ min: 6 })
    .withMessage("Wachtwoord moet bestaan uit minstens zes tekens."),
  body("role").notEmpty().withMessage("De gebruiker heeft een rol nodig."),
];

Validate Register Form

  • Voeg nu de validators middleware toe vlak voor we de controller postRegister ingaan.
app.post(
  "/register",
  AuthRegisterValidation,
  AuthController.postRegister,
  AuthController.register
);
  • Let op: zorg dat je de validationAuthentication ook importeert in app.js
import AuthRegisterValidation from "./middleware/validation/AuthRegisterValidation.js";

Validate Register Form

  • Eenmaal aangekomen in de postRegister controller kunnen we het resultaat van de validatie opvragen.
export const postRegister = async (req, res, next) => {
  try {
    const errors = validationResult(req);

    // if we have validation errors
    if (!errors.isEmpty()) {
      console.log("We\'ve got some errors, dude...");
    }
    
  } catch(e) {
    next(e.message);
  }
};

Validate Register Form

  • Er zijn nu twee opties
    • De validatie loopt fout, dan moeten we opnieuw de de registratie pagina tonen via next()
    • De validatie loopt goed, dan registreren we de gebruiker en navigeren we naar de login-pagina
  • In het eerste geval gaan hebben we dus nog een extra schakel in de middleware (register):
app.post(
  "/register",
  AuthRegisterValidation,
  AuthController.postRegister,
  AuthController.register
);

Validate Register Form

  • We parsen de fouten en geven de foutmeldingen per input veld door naar de register route via de next() functie. 
if (!errors.isEmpty()) {
    // set the form error fields
    req.formErrorFields = {};
    errors.array().forEach((error) => {
      req.formErrorFields[error.path] = error.msg;
    });

    // set the flash message
    req.flash = {
      type: "danger",
      message: "Er zijn fouten opgetreden",
    };

    return next();
}

Validate Register Form

export const register = async (req, res) => {
  // input fields
  const inputs = [
    {
      name: "firstname",
      label: "Voornaam",
      type: "text",
      value: req.body?.firstname ? req.body.firstname : "",
      err: req.formErrorFields?.firstname ? req.formErrorFields.firstname : "",
    },
    {
      name: "lastname",
      label: "Achternaam",
      type: "text",
      value: req.body?.lastname ? req.body.lastname : "",
      err: req.formErrorFields?.lastname ? req.formErrorFields.lastname : "",
    },
    {
      name: "email",
      label: "E-mail",
      type: "text",
      value: req.body?.email ? req.body.email : "",
      err: req.formErrorFields?.email ? req.formErrorFields.email : "",
    },
    {
      name: "password",
      label: "Password",
      type: "password",
      value: req.body?.password ? req.body.password : "",
      err: req.formErrorFields?.password ? req.formErrorFields.password : "",
    },
  ];

  // get the roles
  const roles = await Role.query();
  const flash = req.flash || {};

  // render the register page
  res.render("register", {
    layout: "layouts/authentication",
    inputs,
    roles,
    flash,
  });
};

Afhandelen errors in register

  • Zijn alle velden correct, dan valideren we ook of de gebruiker al dan niet al bestaat.
[...]
else {
      const user = await User.query().findOne({ email: req.body.email });
      const role = await Role.query().findOne({ id: req.body.role });

      // validate if the role exists in the database
      if (!role) {
        req.flash = { type: "danger", message: "Deze rol bestaat niet." };
        req.formErrorFields = { role: "Deze rol bestaat niet." };
        return next();
      }
      // validate if the user already exists
      if (user) {
        req.flash = { type: "danger", message: "Dit e-mail adres is al in gebruik." };
        req.formErrorFields = { email: "Dit e-mail adres is al in gebruik." };
        return next();
      }

      // temp res.send
      res.send('no errors, registrate the user')
    }
[...]

Extra validatie Register Form

Authenticatie en autorisatie

HASHING

Hashing refers to the process of generating a fixed-size output from an input of variable size using the mathematical formulas known as hash functions. This technique determines an index or location for the storage of an item in a data structure.

Hashing turns plain text into a unique code, which can't be reverted into a readable form

  • Encryptie: tweerichting
    • Decryptie m.b.v. een sleutel
  • Hashing: eenrichting
    1. Hash collision is onwaarschijnlijk
    2. De input vinden o.b.v. de hash is onwaarschijnlijk
    3. Output heeft een vaste lengte

Hashing ≠ encryptie

  • Checksum-verificatie van downloads
  • Digital signatures
  • Git commit ID (SHA-1)
  • Wachtwoorden opslaan in een database
  • ...

Hashing-toepassingen

Hashing met bcrypt

  • Via de .hash() methode kan je van een wachtwoord een hash maken
    • eerste argument = wachtwoord
    • tweede argument = aantal salt rounds
const hashedPassword = bcrypt.hash("geheim123", 10);

Hashing met bcrypt

  • Salt rounds: performance vs. security
    • Factor 10: calculation is done 1000 times (2 ^ 10)
  • Eens je de hash hebt, bewaren we de volledige user veilig in de database door een nieuwe record aan te maken.

Hashing met bcrypt

import bcrypt from "bcrypt";

export const postRegister = async (req, res, next) => {

  [...]
   
  const hashedPassword = bcrypt.hashSync(req.body.password, 10);

  await User.query().insert({
    firstname: req.body.firstname,
    lastname: req.body.lastname,
    email: req.body.email,
    password: hashedPassword,
    role_id: parseInt(req.body.role),
  });

  res.redirect("/login");
};
  • Hashing is niet omkeerbaar, dus je kan het niet "dehashen" om de waarde van het wachtwoord te achterhalen.
  • Om later (bij een login) te weten of het wachtwoord correct is, zullen we telkens de ingegeven waarde hashen en vergelijken met de bewaarde, originele hash
  • In bcrypt doe je dat met de .compare() functie

Hashing met bcrypt

// Load hash from your password DB.
const isPassCorrect = bcrypt.compare("secret789", hash);
// --> false
// // Load hash from your password DB.
const isOtherPassCorrect = bcrypt.compare("geheim123", hash);
// --> true
  • Log-in pagina
    • Velden toevoegen: e-mail, paswoord
  • Nieuwe middleware toevoegen
    • Valideer of een gebruiker met dit e-mailadres bestaat
      • Nee: toon foutboodschap
      • Ja: vergelijk hash van paswoord met de hash uit DB
        • Niet gelijk: toon foutboodschap
        • Gelijk: redirect naar homepagina

Oefening - login

Validatie, authenticatie en autorisatie

AUTHENTICATIE

Authenticatie is het proces waarbij iemand nagaat of een gebruiker, een andere computer of applicatie daadwerkelijk is wie hij beweert te zijn.

Authenticatie - inloggen

  • Zorg er eerst voor dat ook je login formulier wordt voorzien van validatie door je login en postLogin controller verder aan te vullen.
  • Controleer daarbij ook of de gebruiker bestaat.

JWT

  • Om ervoor te zorgen dat een user de homepagina te zien krijgt op voorwaarde dat deze is ingelogd, gebruiken we een zogenaamd JSON Web Token ofwel JWT.

Client
Aanmelden

Server
JWT

JWT

  • Een token wordt bewaard op de client als een http-only cookie, dat betekent dat ze niet door client-side javascript kan worden onderschept (en dus ook niet door malafide scripts).
  • Bij elke request wordt de token verstuurd naar de server en kan de server er van uitgaan dat een gebruiker correct is aangemeld.

JWT

  • We maken een token met de jsonwebtoken module.
 // create the webtoken
const token = jwt.sign(
  { userId: user.id, email: user.email },
  process.env.TOKEN_SALT,
  { expiresIn: '1h' }
);

// add token to cookie in response
res.cookie('token', token, { httpOnly: true });

// redirect to home page
res.redirect('/');
  • Salt: encryption key (in this case)

Validatie, authenticatie en autorisatie

AUTORISATIE

Authenticatie

  • Bewijzen wie je bent bv.
    • E-mailadres, wachtwoord
    • Single sign-on
    • Passkeys
    • ...

 

Autorisatie

  • Machtigingen verlenen aan een geauthenticeerde gebruiker bv.
    • Rollen: admin, super admin, manager, standaard gebruiker, lezer ...
    • Operaties: lezen, schrijven, verwijderen ...

Autoriseren

  • Telkens wanneer er een actie is dat een anonieme
    ( = niet ingelogde) gebruiker niet mag uitvoeren, doen we aan autorisatie
    • = controleren of de gebruiker ingelogd is en de nodige rechten heeft 
    • vb profiel bekijken 👀, bestelling maken 💰, ...
  • We bekijken de JSON WEB TOKEN (opgeslagen in een cookie 🍪 )
  • Daarvoor schrijven we een stukje middleware, we kiezen in dit voorbeeld voor de naam: jwtAuth

jwtAuth middleware

  • We schrijven een nieuwe middleware functie die we chainen vòòr we de controller aanspreken.

 

  • Maak een nieuwe file in de middleware folder, jwtAuth.js.
import jwt from 'jsonwebtoken';

export const jwtAuth = (req, res, next) => {
  const token = req.cookies.token;
  try {
   	const userPayload = jwt.verify(token, process.env.TOKEN_SALT);
    const user = await User.query()
      .findOne({id: userPayload.userId,})
      .withGraphFetched("role");

    // set this in client's request
    req.user = user;
    next();
  } catch(e) {
    res.clearCookie('token');
    return res.redirect('/login');
  }
}
app.get('/', jwtAuth, ShopController.index);

jwtAuth middleware

  • We controleren of gebruiker correct  aangemeld
    • jwt.verify()
  • Indien correct aangemeld, dan kunnen we door naar de volgende schakel in de middleware ketting met next().
  • Indien de token expired is,
    dan komen we terecht in de catch blok.
    1. We verwijderen de token op de client
    2. We navigeren terug naar de login pagina

Validatie, authenticatie en autorisatie

OEFENINGEN

Oefening

In deze oefening breid je je applicatie uit met een gastenboek voor Georgette in de bloemetjes te zetten.
Trouwe klanten kunnen wel of niet, (naargelang hun rol) een berichtje nalaten

  • Je maakt een entity Message aan. 
    • Primary Key: id
    • Subject (varchar)
    • Comment (text)
    • Foreign Key user_id
      • een many-to-one relatie met de
        User-entity

Oefening

  • Gebruik de rol van de user, render énkel hetgeen uit wat de ingelogde user mag zien, conform diens rol.
    • Gebruik hiervoor Handlebars, schrijf eventueel een custom block helper 💪🏻.
  • Controleer bij elke POST/GET/PUT of de gebruiker toegang heeft om de HTTP method uit te voeren, indien niet, geef je de respectievelijke HTTP error code terug:
    • Bijv. 401: Niet geautoriseerd
    • Controleer deze lijst op specifiekere error codes.

PGM3/6 - Authenticatie & Authorisatie

By Frederick Roegiers

PGM3/6 - Authenticatie & Authorisatie

  • 72