Sesiuna 4 - Serviciul de Utilizatori si Autentificare/Autorizare. Interactiune intre microservicii

Scopul sedintei:

  • Intelegerea procesului de autentificare si autorizare
  • Intelegerea conceptului de middlewares
  • Realizarea serviciului de utilizatori
  • Integrarea componentelor de autentificare si autorizare
  • Interactiunea dintre 2 microservicii prin HTTP

Resurse

Filmuletul si prezentarea ppt pot fi gasite la linkul acesta

Scheletul de cod pentru serviciile implementate poate fi gasit pe urmatorul grup de Gitlab

Practic

Folosind cunostintele de Rest API invatate la laboratorul trecut, am dezvoltat serviciul de utilizatori, care se ocupa si de partea de autentificare si autorizare.

Multe componente sunt reutilizate complet, partea de securitate fiind, insa, noua.

Interactiunea cu parolele (security/passwords/index.js)

Cele doua functii scrise in acest fisier vor fi folosite la register (hash) si la login (compare).

const bcryptjs = require('bcryptjs'); //pachet folosit la criptarea parolelor
 
const hash = async (plainTextPassword) => { // functia care va cripta o parola plaintext
    const salt = await bcryptjs.genSalt(5); // se genereaza salt-ul 
    const hash = await bcryptjs.hash(plainTextPassword, salt); // pe baza saltului si a parolei plainText se genereaza hashul parolei
    return hash
 
};
 
const compare = async (plainTextPassword, hashedPassword) => { // functie care compara o parola text cu o parola hashuita
    const isOk = await bcryptjs.compare(plainTextPassword, hashedPassword);
    return isOk;
};
 
module.exports = {
    hash,
    compare
}

Interactiunea cu token JWT

Interactiuena cu tokenul JWT poate fi impartita in 2 categorii:

  1. configurarea tokenului
  2. filtrele atasate cererilor care se ocupa de manipularea tokenului
Configurarea Tokenului (security/jwt/token.js)
const jwt = require('jsonwebtoken'); //pachetul care se ocupa de tokenuri JWT
 
const {
    ServerError
} = require('../../errors');
 
const {
    getSecret
} = require('docker-secret');
 
const generalOptions = { // optiunile ce tin de identitatea organizatiei care creaza un token 
    issuer: 'Workshop Moby',
    subject: 'Authorization Token Workshop Moby',
    audience: 'Workshop Moby Users'
};
 
const jwtKey = process.env.NODE_ENV === 'development' ? process.env.JWT_SECRET_KEY : getSecret(process.env.JWT_SECRET_KEY_FILE); // cheia este preluata din .env
 
const generateToken = async (payload) => { // functia care se ocupa de generarea tokenului  
    try {
        const encodingOptions = { ...generalOptions, expiresIn: process.env.JWT_EXPIRE_TIME || '1h' } // timpul de expirare este preluat din .env
        const token = await jwt.sign(payload.toPlainObject(), jwtKey, encodingOptions); // tokenul este obtinut apeland functia "sign"
        return token;
    } catch (err) {
        console.error(err);
        throw new ServerError("Error when encoding the token!", 500);
    }
};
 
const verifyAndDecodeData = async (token, ignoreExpiration = false) => { // functia care se ocupa de extragerea datelor din token. O vom folosi in douua circumstante, intr-una fiind nevoie sa se tina cont de expirarea tokenului si in alta nu
    try {
        const decodingOptions = {...generalOptions, ignoreExpiration}
        const decoded = await jwt.verify(token, jwtKey, decodingOptions); // optiunile scoase sunt salvate in obiectul decoded
        return decoded;
    } catch (err) { // handling customizat de erori. Dorim sa intoarcem mesajul si codul clare.
        if (err.message === 'jwt expired') {
            throw new ServerError('Token is expired. Please refresh!', 401);
        } else if (err.message === 'jwt malformed') {
            throw new ServerError('Token is malformed!', 401);
        }
        throw new ServerError("Token is compromised!", 401);
    }
};
 
 
module.exports = {
    generateToken,
    verifyAndDecodeData
};
Filtrele JWT (security/jwt/filters.js)
const {
    verifyAndDecodeData
} = require('./token.js');
 
const {
    ServerError
} = require('../../errors');
 
 
const verifyAuthHeader = (req) => { //verificarea existentei headerului "Authorization"
    if (!req.headers.authorization) {
        throw new ServerError('Authorization Header missing!', 401);
    }
}
 
const authorizeAndExtractToken = async (req, res, next) => { // middleware ce va fi atasat rutelor. Contine, in parametri, obiectele req, res si next
    try {
        verifyAuthHeader(req);
        const token = req.headers.authorization.split(" ")[1]; // extrage tokenul din header. Tokenul este separat prin spatiu de cuvantul Bearer
 
        const decoded = await verifyAndDecodeData(token); // preia obiectul decodat
        req.state = { // salveaza informatiile din token in req.state
            decoded
        };
 
        next(); // prin apelul functiei next(), urmatorul middleware este notificat ca poate sa se execute
    } catch (err) {
        next(err);
    }
};
 
const authorizeAndExtractTokenNoExpire = async (req, res, next) => { // acelasi lucru ca mai sus, doar ca se trimite in plus parametrul de ignoreExpiration = true la functia "verifyAndDecodeData"
    try {
        verifyAuthHeader(req);
        const token = req.headers.authorization.split(" ")[1];
 
        const decoded = await verifyAndDecodeData(token, true);
        req.state = {
            decoded
        };
 
        next();
    } catch (err) {
        next(err);
    }
};
 
module.exports = {
    authorizeAndExtractToken,
    authorizeAndExtractTokenNoExpire
}
Payload-ul JWT (security/jwt/models.js)

Clasa ce este folosita pentru impachetarea payload-ului JWT

class JwtPayload {
    constructor(id) {
        this.userId = id;
    }
 
    toPlainObject (){ // functia "sign" nu stie sa lucreze cu clase, ci cu obiecte json simple, asa ca trebuie returnat un obiect simplu
        return {
            userId: this.userId
        }
    }
}
 
module.exports = {
    JwtPayload
}

Clientul HTTP (http-client/index.js)

Deoarece microserviciile sunt entitati/procese separate, ele comunica prin retea, nu in process. Pentru acest lucru, este nevoie de utilizarea unei forme de comunicare peste HTTP, sau event-based, prin cozi de mesaje. La acest workshop vom lucra exclusiv prin comunicare peste HTTP.

In NodeJS, comunicarea prin HTTP se poate face in multe moduri, insa cel mai uzual este prin folosirea pachetului axios. Mai mult, noi am scris un wrapper peste acest pachet care se ocupa si de error handling.

const axios = require('axios').default;
 
const {
    ServerError
} = require('../errors');
 
const sendRequest = async (options) => { // wrapper peste apelul de axios. Parametrul este un obiect care contine cel putin unu camp, url. Documentatia o gasiti pe pagina pachetului axios
    try {
        const { data } = await axios(options); // daca se realizeaza cu succes, este intors un obiect ce contine campul data unde se afla informatia efectiva 
        return data;
    } catch (err) {
        if (err.isAxiosError) { // eroarea aruncata de axios are proprietatea specifica "isAxiosError"
            throw new ServerError(err.response.data.message, err.response.status);
        }
        throw err;
    }
}
 
module.exports = {
    sendRequest
}

Utilizarea filtrelor JWT

Filtrele JWT sunt atasate rutelor. Un exemplu de utilizare il puteti gasi in token/controllers.js. Un alt exemplu este in users/controllers.js

// users/controllers.js
 
Router.delete('/', authorizeAndExtractToken, async (req, res) => { // observati cum filtrul JWT este atasat cererii
    const {
        userId
    } = req.state.decoded;
 
    await remove(userId);
 
    res.status(200).end();
});

Interactiunea dintre microserviciul Library si microserviciul Auth

In microserviciul Library am creat un nou filtru de autorizare.

Rolul acestui filtru este de a se conecta la microservicul de autorizare. Se trimite o cerere HTTP catre microserviciul de autorizare, prin care forwardeaza headerul ce contine tokenul JWT.

Microserviciul de autorizare despacheteaza tokenul si returneaza id-ul utilizatorului care a facut cererea si permisiunea acestuia in sistem.

Ruta din microserviciul de autorizare responsabila pentru autorizarea cererilor provenite de la alte microservicii

// auth-microservice/token/controllers.js
 
Router.get('/authorizeAndReturnDetails', authorizeAndExtractToken, async (req, res) => {
    const {
        userId
    } = req.state.decoded;
 
    const role = await getRole(userId);
 
    res.json({
        userId,
        role
    });
});

Filtrul din microserviciul library responsabil cu trimiterea cererilor de autorizare catre microserviciul de autorizare. Filtrul este o functie curry (functie ce returneaza alta functie).

const authorizeUser = (...roles) => { //poate accepta mai mult de un singur rol (de exemplu 'ADMIN', 'READER')
 
    return async (req, res, next) => { //returneaza un middleware clasic ce contine req, res, next
        if (!req.headers.authorization) {
            throw new ServerError("Authorization Header missing!", 403);
        }
 
        const options = { //creaza obiectul options ce va fi trimis clientului de http
            url: `http://${process.env.AUTH_SERVICE}/api/token/authorizeAndReturnDetails`, //IP-ul si PORT-ul serviciului de auth sunt preluate din .env. Observati ruta.
            headers: {
                'Authorization': req.headers.authorization
            }
        }
 
        const {
            userId,
            role
        } = await sendRequest(options);
 
        if (!roles.includes(role)) { // se verifica daca rolul primit face parte din rolurile date ca parametru
            throw new ServerError(`${role} is not allowed for this method!`, 403);
        }
 
        req.state = {
            userId
        }
 
        next(); // se apeleaza next pentru a se trece la urmatorul middleware
    }
}

Exemplu de utilizare in cadrul microserviciului Library:

const {
    ADMIN_ROLE,
    READER_ROLE
} = process.env; //preluarea valorilor celor 2 roluri din .env
 
Router.get('/:id', authorizeUser(ADMIN_ROLE, READER_ROLE), async (req, res) => { //introducerea filtrului pentru ambele roluri. Observati cum, de fapt, este pus un apel de functie. Motivul este datorat comportamentului de functie curry (functie in functie) pe care l-am folosit pentru a returna middleware-ul ce contine (req, res, next)
    const {
        id
    } = req.params;
 
    const author = await getById(id);
 
    res.json(author);
 
});

Baza de date

Baza de date functioneaza la fel ca la microserviciul Library. Aveti un folder database-deploy in care veti efectua comanda

docker-compose up

Exercitii

Singurul exercitiu este sincronizarea dintre stergerea unui utilizator cu stergerea tuturor cartilor pe care acesta le-a introdus in sistem. Pentru acest lucru, trebuie sa urmariti TODO-urile puse in scheletul de cod.

moby/backend/04.txt · Last modified: 2020/07/30 12:29 by alexandru.hogea
CC Attribution-Share Alike 3.0 Unported
www.chimeric.de Valid CSS Driven by DokuWiki do yourself a favour and use a real browser - get firefox!! Recent changes RSS feed Valid XHTML 1.0