Poglavje 8 P3.3 Preverjanje istovetnosti uporabnika
P3.3
8.1 Način avtentikacije z uporabo MEAN sklada tehnologij
Obstoječo aplikacijo želimo nadgraditi s funkcionalnostjo prijave uporabnikov pred možnostjo komentiranja izbrane lokacije. To je pomemben vidik spletnih aplikacij, saj je obvladovanje uporabnikov (angl. Identity Management) in njihovih sej (angl. session) pogosta zahteva. Kot prikazuje slika 8.1, se bomo v kontekstu uporabe MEAN tehnologij omenjenega problema lotili s pristopom od spodaj navzgor. Najprej bomo posodobili shemo podatkovne baze, potem sledi posodobitev API-ja in na koncu še prilagoditev uporabniškega vmesnika.
Slika 8.1: Dodajanje sloja avtentikacije na različnih ravneh obstoječe aplikacije - podatkovna baza, API in odjemalska SPA aplikacija
Mogoče se nam na začetku zdi vpeljava avtentikacije z uporabo MEAN sklada tehnologij nemogoča oz. težka, saj je večji del izvorne kode v aplikaciji SPA, ki se posreduje spletnemu brskalniku in se tam tudi izvaja. Zato bomo v nadaljevanju postopoma prikazali način, ki ga bomo uporabili.
8.1.1 Klasični pristop avtentikacije pri strežniški aplikaciji
Pri klasičnem pristopu preverjanja istovetnosti identitete uporabnika oz. avtentikacija (angl. Authentication) s strežniško aplikacijo se celotna funkcionalnost nahaja v programski kodi na strani strežnika. Ko se uporabnik prijavi v sistem, se uporabniško ime in geslo posredujeta na strežnik, ki ju le-ta preveri v podatkovni bazi. Če sta podatka ustrezna, strežnik nastavi ustrezno zastavico oz. parameter v uporabniški seji na strani strežnika, s čimer določi, da je uporabnik uspešno prijavljen.
Strežnik lahko nastavi tudi piškotek (angl. cookie) v uporabniškem brskalniku s podatki o seji. To je pogosta praksa, ki pa tehnično ni potrebna za obvladovanje sej prijavljenih uporabnikov, saj je strežnik tisti, ki vzdržuje vse pomembne podatke. Takšen proces vzpostavitve seje je prikazan na sliki 8.2.
Slika 8.2: Vzpostavitev seje pri klasičnem pristopu avtentikacije
Ko uporabnik zahteva zaščiteno vsebino ali poskuša posredovati podatke na strežnik, mora strežnik preveriti veljavnost uporabniške seje in se odločiti, ali uporabniku dovoli izvesti zahtevano akcijo.
Slika 8.3: Preverjanje veljavnosti seje ob zahtevi podatkov pri klasičnem pristopu avtentikacije
Preverjanje veljavnosti seje ob zahtevi podatkov in ob shranjevanju podatkov je prikazano na slikah 8.3 in 8.4.
Slika 8.4: Preverjanje veljavnosti seje ob shranjevanju podatkov pri klasičnem pristopu avtentikacije
Klasični pristop avtentikacije ni najbolj primeren za MEAN sklad tehnologij. Težava je predvsem v odvisnosti od rezervacije strežniških virov za vsakega uporabnika z namenom vzdrževanja seje. Že v uvodnih predavanjih smo omenili, da Node.js in Express ne vzdržujeta seje za vsakega uporabnika posebej, saj se celotna aplikacija za vse uporabnike izvaja v 1 niti.
V nadaljevanju bomo zato predstavili bolj kompleksen scenarij z dodajanjem avtentikacije na SPA aplikacijo na eni strani, ki dostopa do podatkov preko REST API-ja.
8.1.2 Avtentikacija z MEAN skladom tehnologij
Pri avtentikaciji z MEAN skladom tehnologij imamo naslednji težavi:
- API je brez stanja, saj Express in Node.js ne poznata koncepta uporabniških sej,
- aplikacijska logika se nahaja na strani odjemalca in se izvaja v brskalniku, tako da kode, ki se posreduje odjemalcu, ne moremo enostavno omejiti.
Dobra praksa pri obvladovanju te težave je uporaba JSON spletnega žetona (angl. JSON Web Token (JWT)). JWT je JSON objekt, šifriran v obliki niza, kjer uporabnik neposredno ne more razbrati njegove vsebine, lahko pa ga dekodira strežnik ter ga uporabi v aplikaciji.
Slika 8.5 prikazuje proces prijave oz. generiranje JWT. Uporabnik najprej posreduje svoje prijavne podatke strežniku preko API-ja, nato strežnik preveri veljavnost v podatkovni bazi in vrne žeton nazaj brskalniku. Brskalnik pridobljeni žeton shrani za ponovno uporabo. Postopek je zelo podoben klasičnemu pristopu, kjer namesto shranjevanja uporabniške seje na strežniku shranimo žeton na odjemalcu.
Slika 8.5: Generiranje JSON spletnega žetona pri MEAN pristopu avtentikacije
Ko uporabnik uporablja aplikacijo, mora imeti možnost spreminjanja pogleda oz. strani in aplikacija se mora zavedati, ali mu je zahtevana akcija dovoljena ali ne. Kot prikazuje slika 8.6, aplikacija dekodira JWT in uporabi rezultat za prikaz ustreznih podatkov uporabniku. Na tem mestu se kaže jasna razlika s tradicionalnim pristopom, saj se strežnik ne zaveda, kaj uporabnik počne, vse dokler ne zahteva dostopa do podatkov preko API-ja.
Slika 8.6: Preverjanje veljavnosti žetona ob zahtevi podatkov pri MEAN pristopu avtentikacije
Če je posamezen del aplikacije zaščiten in je dostop dovoljen le prijavljenim uporabnikom, potem je velika verjetnost, da je s tem povezan dostop do podatkovne baze, ki ga lahko izvedejo le prijavljeni uporabniki. Glede na to, da je API brez stanja, se le-ta ne zaveda, kdo je klic zahteval, razen če tega podatka eksplicitno ne posredujemo. Na tem mestu pa zopet pride v uporabo JWT. Kot prikazuje slika 8.7, se žeton posreduje na API dostopno točko, ki žeton dekodira, še preden preveri, ali ima uporabnik pravico zahtevati to funkcionalnost.
Slika 8.7: Preverjanje veljavnosti žetona ob shranjevanju podatkov pri MEAN pristopu avtentikacije
8.2 Kreiranje sheme uporabnikov v MongoDB
Uporabniška imena in gesla je smiselno shranjevati v podatkovno bazo. Za ta namen bomo uporabili kar obstoječo podatkovno bazo MongoDB, kjer bomo kreirali novo shemo za uporabnike.
Namig: Gesla v podatkovni bazi ne smemo nikoli shranjevati v obliki navadnega besedila, saj to predstavlja veliko varnostno tveganje, če kdo pridobi nepooblaščen dostop do podatkovne baze.
8.2.1 Enosmerno šifriranje gesel
V primeru naše aplikacije lahko izvedemo enosmerno šifriranje gesel. S tem dosežemo enostavno preverjanje ustreznosti gesla in onemogočimo ostalim, da na enostaven način dešifrirajo geslo. Ob prijavi uporabnika lahko aplikacija šifrira podano geslo in preveri, ali se ujema s shranjeno vrednostjo.
Vendar samo šifriranje ni dovolj. Če bi npr. več uporabnikov uporabilo “geslo” za njihovo geslo (kar se dogaja), potem bi bila šifrirana vrednost vsakega gesla enaka. Vsak napadalec, ki bi dobil dostop do podatkovne baze, bi lahko opazil vzorec in identificiral potencialna šibka gesla.
Zato pri šifriranju uporabljamo koncept naključne vrednosti (angl. salt) s predstavitvijo naključnih znakov, ki jih aplikacija generira za vsakega uporabnika ter jih pred postopkom šifriranja združi z dejanskim geslom. Rezultat šifriranja je zgoščena vrednost (angl. hash), kot to prikazuje slika 8.8.
Slika 8.8: Zgoščena vrednost nastane v postopku šifriranja na podlagi uporabniškega gesla in naključne vrednosti
V podatkovni bazi namesto shranjevanja gesla raje hranimo naključno vrednost in zgoščeno vrednost. S tem pristopom zagotavljamo, da so vse zgoščene vrednosti enolične in gesla ustrezno zaščitena.
8.2.2 Kreiranje sheme z Mongoose
Shemo bomo opredelili v ločeni datoteki api/models/users.js in jo najprej vključili v api/models/db.js, ki v okviru implementacije API-ja skrbi za povezavo s podatkovno bazo.
api/models/db.js
Pri opredelitvi sheme v api/models/users.js zahtevamo elektronski naslov, ime, zgoščeno in naključno vrednost. Za elektronski naslov zahtevamo, da je enoličen in obvezen, ime je obvezno, medtem ko sta zgoščena in naključna vrednost podatkovnega tipa niz in ravno tako obvezni.
Poskrbimo tudi za povezovanje sheme z modelom.
api/models/users.js
const mongoose = require("mongoose");
const usersSchema = new mongoose.Schema({
email: { type: String, unique: true, required: [true, "Email is required!"] },
name: { type: String, required: [true, "Name is required!"] },
hash: { type: String, required: [true, "Hash is required!"] },
salt: { type: String, required: [true, "Salt is required!"] },
});
mongoose.model("User", usersSchema, "Users");Elektronski naslov in ime bomo pridobili iz registracijskega obrazca uporabnika, zgoščeno in naključno vrednost pa bo kreirala aplikacija na podlagi podanega uporabniškega gesla, ki ga bo uporabnik tudi vnesel na registracijski obrazec.
8.2.3 Šifriranje uporabniškega gesla
Ker je šifriranje podatkov pogosta zahteva, je v Node.js vgrajen modul crypto, ki ga bomo uporabili tudi mi.
api/models/users.js
Mongoose nam omogoča dodajanje metod neposredno v shemo in metode so izpostavljene kot metode modela. S pomočjo teh metod pridobimo neposredni dostop do atributov modela. Za naše potrebe bomo pripravili metodo setPassword. Pri šifriranju uporabniškega gesla bomo najprej generirali naključno vrednost s pomočjo metode randomBytes, ki generira kriptografsko močan naključen niz podatkov. Nato bomo na podlagi gesla in naključne vrednosti s pomočjo metode pbkdf2Sync generirali zgoščeno vrednost (dolžina ključa je \(64\) pri \(1000\) iteracijah).
api/models/users.js
usersSchema.methods.setPassword = function (password) {
this.salt = crypto.randomBytes(16).toString("hex");
this.hash = crypto
.pbkdf2Sync(password, this.salt, 1000, 64, "sha512")
.toString("hex");
};S tem dosežemo, da gesla nikjer ne shranimo neposredno, tudi v pomnilniku ne.
8.2.4 Preverjanje ustreznosti podanega gesla
Pomembno je podpreti tudi funkcionalnost preverjanja ustreznosti podanega gesla, ko se uporabnik poskuša prijaviti v sistem. Ker smo geslo šifrirali, ga ne moremo dešifrirati, ampak ponovno uporabimo isti postopek šifriranja s podanim geslom in naključno vrednostjo iz podatkovne baze za podanega uporabnika. To funkcionalnost implementiramo v obliki metode validPassword.
api/models/users.js
8.2.5 Generiranje JSON spletnega žetona (JWT)
JWT se uporablja za posredovanje podatkov, v našem primeru med API-jem na strežniku in SPA aplikacijo v brskalniku. JWT lahko uporabimo tudi na strežniku, ki je JWT zgeneriral, da avtenticira uporabnika ob njegovih dodatnih zahtevah na strežnik.
JWT je sestavljen iz 3 delov, na videz naključnih nizov, ki so ločeni s piko (.). Takšen niz je lahko zelo dolg, kot prikazuje naslednji primer.
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJfaWQiOiI1NTZiZWRmNDhmOTUzOTViMTlhNjc1ODgiLCJlbWFpbCI6InNpbW9uQGZ1bGxz
dGFja3RyYWluaW5nLmNvbSIsIm5hbWUiOiJTaW1vbiBIb2xtZXMiLCJleHAiOjE0MzUwNDA0MTgsImlhdCI6MTQzNDQzNTYxOH0.GD7UrfnLk
295rwvIrCikbkAKctFFoRCHotLYZwZpdlE
JWT vsebinsko sestavljajo naslednji deli:
- glava135 je kodiran JSON objekt, ki vsebuje tip in uporabljen šifrirni algoritem,
- koristna vsebina136 je kodiran JSON objekt, ki vsebuje dejanske podatke,
- podpis137 je šifrirana zgoščena vrednost glave in koristne vsebine z uporabo “skritega gesla”, ki ga pozna le strežnik.
Pomembno je izpostaviti, da glava in koristna vsebina nista šifrirani, ampak kodirani. To pomeni, da jih lahko brskalnik ali kakšna druga aplikacija relativno enostavno dekodira. Večina novejših brskalnikov ima vgrajeno funkcijo atob(), ki poskrbi za dekodiranje niza iz Base64. Inverzna funkcija btoa() pa poskrbi za kodiranje v Base64 niz.
Zadnji del, podpis, je šifriran. Za uspešno dešifriranje moramo poznati skrito geslo na strani strežnika, ki mora ostati skrito in nedostopno javnosti.
Za celoten proces generiranja JWT so na voljo knjižnice, ki nam to opravilo olajšajo. Najprej bomo poskrbeli za namestitev knjižnice jsonwebtoken.
Modul uvozimo v naš model api/models/users.js.
api/models/users.js
const mongoose = require("mongoose");
const crypto = require("crypto");
const jwt = require("jsonwebtoken");Sedaj bomo implementirali funkcijo generateJwt, ki bo poskrbela za generiranje žetona JWT. Pri generiranju JWT-ja bomo potrebovali koristno vsebino (t.j. podatki o uporabniku - _id, email in name) in skrito geslo. Med koristno vsebino bomo vključili tudi datum poteka JSON spletnega žetona, in sicer ga bomo nastavili na 7 dni. Za dejansko generiranje JWT uporabimo metodo sign.
api/models/users.js
usersSchema.methods.generateJwt = function () {
const expiry = new Date();
expiry.setDate(expiry.getDate() + 7);
return jwt.sign(
{
_id: this._id,
email: this.email,
name: this.name,
exp: parseInt(expiry.getTime() / 1000),
},
"superSecretPassword"
);
};8.2.5.1 Hramba skritega gesla v okoljski spremenljivki
Vključevanje skritega gesla v kodi na strani strežnika ni priporočljivo, saj v primeru posredovanja kode v oddaljene repozitorije posredujemo tudi skrito geslo, kar pa zagotovo ni namen. Zato je dobra praksa shranjevanje skritega gesla v okoljski spremenljivki.
Okoljske spremenljivke znotraj projekta najlažje obvladujemo z datoteko .env v korenski mapi. Na tem mestu že imamo shranjeno spremenljivko MONGODB_ATLAS_URI in sedaj dodajmo še JWT_SECRET.
.env
Sedaj lahko okoljsko spremenljivko JWT_SECRET vključimo v našo aplikacijo.
api/models/users.js
usersSchema.methods.generateJwt = function () {
const expiry = new Date();
expiry.setDate(expiry.getDate() + 7);
return jwt.sign(
{
_id: this._id,
email: this.email,
name: this.name,
exp: parseInt(expiry.getTime() / 1000),
},
process.env.JWT_SECRET"superSecretPassword"
);
};Prav tako moramo nastaviti tudi okoljsko spremenljivko v produkcijskem okolju.
8.3 Priprava API-ja za avtentikacijo
Za implementacijo API-ja za avtentikacijo bomo uporabili Node.js modul Passport. Ključna prednost omenjenega modula je možnost uporabe različnih metod avtentikacije, ki se imenujejo strategije:
S pomočjo Passporta lahko uporabite enega ali več načinov za prijavo vaših uporabnikov. V primeru naše aplikacije bomo uporabili lokalno strategijo, ker bomo uporabniška imena in zgoščene vrednosti gesel shranjevali v podatkovno bazo.
8.3.1 Namestitev in konfiguracija Passporta
Passport je zgrajen modularno, in sicer v obliki osrednjega modula ter dodatnih modulov za vsako strategijo. V našem primeru smo se odločili za uporabo lokalne strategije, zato bomo namestili osrednji modul passport in lokalno strategijo passport-local.
Za konfiguracijo ogrodja Passport bomo kreirali novo mapo config znotraj mape api.
Konfiguracija ogrodja Passport bo implementirana v datoteki api/config/passport.js, kjer najprej poskrbimo za uvoz vseh knjižnic, ki jih potrebujemo.
api/config/passport.js
const passport = require("passport");
const LocalStrategy = require("passport-local").Strategy;
const mongoose = require("mongoose");
const User = mongoose.model("User");Sledi konfiguracija lokalne strategije. Passport lokalna strategija pričakuje in uporablja privzeta atributa username in password, ki jih lahko ustrezno prilagodimo. Najprej bomo dostopali do modela User v podatkovni bazi in poiskali uporabnika s podanim elektronskim naslovom. Če uporabnika ne najdemo, vrnemo napako, sicer preverimo njegovo geslo. Če je geslo napačno, zopet vrnemo napako. Če je bila avtentikacija uporabnika uspešna, vrnemo objekt s podatki o uporabniku v podatkovni bazi.
api/config/passport.js
passport.use(
new LocalStrategy(
{
usernameField: "email",
passwordField: "password",
},
async (username, password, cbDone) => {
try {
let user = await User.findOne({ email: username });
if (!user)
return cbDone(null, false, { message: "Incorrect username." });
else if (!user.validPassword(password))
return cbDone(null, false, { message: "Incorrect password." });
else return cbDone(null, user);
} catch (err) {
return cbDone(err);
}
}
)
);V osrednjo aplikacijo moramo dodati odvisnost od Passporta, kjer je pomembno, da jo vključimo za shemo podatkovne baze.
server.js
require("dotenv").config();
const express = require("express");
const path = require("path");
const bodyParser = require("body-parser");
const passport = require("passport");
...
/**
* Database connection
*/
require("./api/models/db.js");
require("./api/config/passport");Passport moramo tudi inicializirati, in sicer po delu kode, kjer so opredeljene statične poti.
server.js
8.3.2 Kreiranje API dostopne točke in vračanje JSON spletnih žetonov (JWT)
Če želimo uporabnikom omogočiti prijavo in registracijo, bo treba dodati 2 API dostopni točki. Najprej bomo v api/routes/api.js opredelili usmerjevalnik, ki bo implementiran v datoteki app_api/controllers/authentication.js.
api/routes/api.js
const ctrlLocations = require("../controllers/locations");
const ctrlComments = require("../controllers/comments");
const ctrlAuthentication = require("../controllers/authentication");Kot že omenjeno, bomo ponudili 2 funkcionalnosti: registracijo na naslovu /api/register in prijavo na naslovu /api/login.
api/routes/api.js
/**
* Authentication
*/
router.post("/register", ctrlAuthentication.register);
router.post("/login", ctrlAuthentication.login);Ker krmilnika še nismo opredelili, kreirajmo datoteko api/controllers/authentication.js, kjer najprej poskrbimo za odvisnosti.
api/controllers/authentication.js
const passport = require("passport");
const mongoose = require("mongoose");
const User = mongoose.model("User");Sledi implementacija metode za registracijo uporabnika register, kjer najprej preverimo, ali je uporabnik posredoval vse zahtevane podatke. Nato kreiramo nov dokument in nastavimo posredovane podatke s strani uporabnika ter nastavimo geslo, ki poskrbi za generiranje naključne in zgoščene vrednosti. Na koncu uporabnika shranimo v podatkovno bazo in v odgovoru odjemalcu vrnemo JWT žeton.
api/controllers/authentication.js
const register = async (req, res) => {
if (!req.body.name || !req.body.email || !req.body.password)
return res.status(400).json({ message: "All fields required." });
const user = new User();
user.name = req.body.name;
user.email = req.body.email;
user.setPassword(req.body.password);
try {
await user.save();
res.status(200).json({ token: user.generateJwt() });
} catch (err) {
res.status(500).json({ message: err.message });
}
};
module.exports = {
register,
};Pri implementaciji metode za prijavo login dostopamo do passport avtentikacije, kjer se v ozadju ponovno izračuna zgoščena vrednost na podlagi naključne vrednosti in gesla ter se preveri ujemanje. Če ujemanje obstaja, potem uporabniku vrnemo JWT žeton. Pozorni moramo biti tudi na vrstico ...})(req, res), kjer je treba okolju passport omogočiti dostop do zahteve in odgovora.
api/controllers/authentication.js
const login = (req, res) => {
if (!req.body.email || !req.body.password)
return res.status(400).json({ message: "All fields required." });
else
passport.authenticate("local", (err, user, info) => {
if (err) return res.status(500).json({ message: err.message });
if (user) return res.status(200).json({ token: user.generateJwt() });
else return res.status(401).json({ message: info.message });
})(req, res);
};
module.exports = {
register,
login,
};Če želimo preveriti delovanje naših novih storitev, lahko za to uporabimo program curl. Za registracijo posredujemo POST zahtevo na URL naslov /api/register. Poskrbeti moramo, da so v telesu zahteve (x-www-form-urlencoded) podatki name, email in password. Kot rezultat nam storitev vrne JWT žeton.
$ curl -X POST \
-d "name=Dejan Lavbič&email=dejan@lavbic.net&password=test" \
-H "Content-Type: application/x-www-form-urlencoded" \
http://localhost:3000/api/register
{"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOiI2Mzc2MzY5YmY3ZTYxMzY0ZGRkZTQ0NDMiLCJlbWFpbCI6ImRlamFuQGxhdmJpYy5uZXQiLCJuYW1lIjoiRGVqYW4gTGF2YmnEjSIsImV4cCI6MTY2OTI5NjQxMSwiaWF0IjoxNjY4NjkxNjExfQ.c5_VjSWsQfyE7QS2-y01GF5ip3sF8yQ-ivS7WN6DZII"}Preverimo lahko tudi stanje v podatkovni bazi, kjer opazimo, da se v zbirki Users nahaja nov uporabnik z elektronskim naslovom dejan@lavbic.net.
$ docker exec -it web-dev-mongo-db bash -c "mongosh Demo --eval \
'db.Users.findOne({\"email\": \"dejan@lavbic.net\"})'"
{
_id: ObjectId("6376369bf7e61364ddde4443"),
name: 'Dejan Lavbič',
email: 'dejan@lavbic.net',
salt: '9b07b3e57ed9201ef02f35719c0f48f1',
hash: '1c98e0af3924f84a13e7be1c4d7628c8646c931967c8878f1f475b65602eaf86236a6df7940af7ad15618f97509876cf83bc2b68cab6d513c4298f7a18d047d3',
__v: 0
}Ravno tako lahko preverimo prijavo uporabnika, kjer posredujemo POST zahtevo na URL naslov /api/login. Poskrbeti moramo, da sta v telesu zahteve (x-www-form-urlencoded) podatka email in password.
$ curl -X POST \
-d "email=dejan@lavbic.net&password=test" \
-H "Content-Type: application/x-www-form-urlencoded" \
http://localhost:3000/api/login
{"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOiI2Mzc2MzY5YmY3ZTYxMzY0ZGRkZTQ0NDMiLCJlbWFpbCI6ImRlamFuQGxhdmJpYy5uZXQiLCJuYW1lIjoiRGVqYW4gTGF2YmnEjSIsImV4cCI6MTY2OTI5NjgxNywiaWF0IjoxNjY4NjkyMDE3fQ.OMrnlGp3ZE3LCZvdyPBcE6K5mQpAAlcxDv52oveufGo"}V primeru uspešne prijave na storitev nam vrne JWT žeton, medtem ko v primeru napake dobimo sporočilo z opisom napake.
8.4 Varnostna zaščita izbranih API dostopnih točk
V produkcijskem okolju je pogosta funkcionalna zahteva zaščita API dostopnih točk ter omogočanje dostopa zgolj prijavljenim uporabnikom. V primeru naše aplikacije želimo dodajanje komentarjev omogočiti zgolj registriranim uporabnikom, kar bomo dosegli z naslednjimi koraki:
- klic API-ja za dodajanje novega komentarja bomo dovolili zgolj uporabnikom, ki bodo poleg zahteve posredovali tudi veljaven žeton JWT,
- že v krmilniku bomo preverili, ali uporabnik obstaja in ima tako posledično pravico dodajanja novega komentarja.
8.4.1 Dodajanje vmesne avtentikacijske ravni pri Express usmerjanju
V Expressu lahko s pomočjo vmesnih ravni (angl. middleware) dosežemo obvladovanje uporabniških zahtev med usmerjevalnikom in krmilnikom. To pomeni, da se omenjena funkcionalnost izvede še pred izvajanjem kode v krmilniku in tako lahko onemogoči izvajanje določenih funkcij.
Vmesni sloj želimo v našem primeru uporabiti za preverjanje veljavnosti JWT žetona, pridobivanje podatkov iz žetona in dodajanje pridobljenih podatkov v objekt zahteve, da lahko do njih dostopa tudi krmilnik. Za ta namen bomo uporabili knjižnico express-jwt.
Knjižnico express-jwt vključimo v usmerjevalnik implementacije REST API-ja v datoteki api/routes/api.js. Prav tako poskrbimo za parametre avtentikacije, kjer opredelimo geslo, ki se nahaja v okoljski spremenljivki JWT_SECRET, in koristno vsebino payload ter algoritem, s katerim je bila vsebina JWT žetona podpisana.
api/routes/api.js
const express = require("express");
const router = express.Router();
const { expressjwt: jwt } = require("express-jwt");
const auth = jwt({
secret: process.env.JWT_SECRET,
userProperty: "payload",
algorithms: ["HS256"],
});
const ctrlLocations = require("../controllers/locations");
const ctrlComments = require("../controllers/comments");
const ctrlAuthentication = require("../controllers/authentication");Opredelitev avtentikacije vključimo kot dodatni parameter vmesnega sloja, ki se tudi pri vrstnem redu parametrov metode usmerjevalnika nahaja na sredini. Zaščitili bomo zgolj POST, PUT in DELETE metode.
api/routes/api.js
/**
* Comments
*/
router.post(
"/locations/:locationId/comments",
auth,
ctrlComments.commentsCreate
);
router.get(
"/locations/:locationId/comments/:commentId",
ctrlComments.commentsReadOne
);
router.put(
"/locations/:locationId/comments/:commentId",
auth,
ctrlComments.commentsUpdateOne
);
router.delete(
"/locations/:locationId/comments/:commentId",
auth,
ctrlComments.commentsDeleteOne
);Na tem mestu lahko tudi izpopolnimo usmerjevalno tabelo, da bo le-ta bolj pregledna, kjer so HTTP metode združene na posamezni poti.
Če je avtentikacija uspešna, bo express-jwt vmesni sloj poskrbel, da bo v parametru auth pri zahtevi req an voljo koristna vsebina (payload) žetona JWT.
api/routes/api.js
/**
* Comments
*/
router.post(
"/locations/:locationId/comments",
auth,
ctrlComments.commentsCreate
);
router
.route("/locations/:locationId/comments/:commentId")
.get(ctrlComments.commentsReadOne)
.put(auth, ctrlComments.commentsUpdateOne)
.delete(auth, ctrlComments.commentsDeleteOne);
router.get(
"/locations/:locationId/comments/:commentId",
ctrlComments.commentsReadOne
);
router.put(
"/locations/:locationId/comments/:commentId",
auth,
ctrlComments.commentsUpdateOne
);
router.delete(
"/locations/:locationId/comments/:commentId",
auth,
ctrlComments.commentsDeleteOne
);Prav tako moramo poskrbeti za obvladovanje napak, kjer detektiramo dogodek nepooblaščenega dostopa UnauthorizedError in v tem primeru vrnemo napako \(401\).
server.js
/**
* Authorization error handler
*/
app.use((err, req, res, next) => {
if (err.name === "UnauthorizedError")
res.status(401).json({ message: err.message });
});
/**
* Start server
*/
app.listen(port, () => {
console.log(
`Demo app started in '${
process.env.NODE_ENV || "development"
} mode' listening on port ${port}!`
);
});Če želimo sedaj s pomočjo POST zahteve dodati nov komentar, nam REST API vrne napako, saj za dostop potrebujemo JWT žeton.
$ LOCATION_ID=$(docker exec -it web-dev-mongo-db bash -c "mongosh Demo --eval \
'db.Locations.findOne(\
{\"name\": \"Ljubljana - Viško pokopališče\"}, { _id: 1 })'"\
| grep 'ObjectId' \
| sed 's/.*"\(.*\)".*/\1/')
$ curl -X POST \
"http://localhost:3000/api/locations/${LOCATION_ID}/comments" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "author=Dejan%20Lavbi%C4%8D&rating=3&comment=Srednja%20%C5%BEalost."
{"message":"No authorization token was found"}8.4.2 Uporaba informacij iz JWT v Express krmilniku
8.4.2.1 Prijavljen uporabnik kot avtor komentarja
Najprej se bomo posvetili metodi commentsCreate v krmilniku api/controllers/comments.js, kjer bomo dodali nov klic funkcije getAuthor. S tem želimo preveriti uporabnika (če sploh obstaja v podatkovni bazi) in kot odgovor v povratnem klicu vrnemo njegovo ime author, ki ga bomo posredovali naprej za uporabo v spletnem obrazcu.
api/controllers/comments.js
const commentsCreate = async (req, res) => {
getAuthor(req, res, async (req, res, author) => {
const locationId = req.params.locationId;
if (locationId) {
Location.findById(locationId)
.select("comments")
.exec((err, location) => {
if (err) res.status(500).json({ message: err.message });
else doAddComment(req, res, location, author.name);
});
} else {
res.status(400).json({
message: "Path parameter 'locationId' is required.",
});
}
});
};Pri validaciji uporabnika bomo morali dostopati do podatkovne baze s poizvedbo po njegovem elektronskem naslovu. Ob izvajanju povratnega klica zraven posredujemo še uporabnikovo ime, ki ga bomo uporabili na obrazcu.
api/controllers/comments.js
const mongoose = require("mongoose");
const Location = mongoose.model("Location");
const User = mongoose.model("User");
...
const getAuthor = async (req, res, cbResult) => {
if (req.auth?.email) {
try {
let user = await User.findOne({ email: req.auth.email }).exec();
if (!user) res.status(401).json({ message: "User not found." });
else cbResult(req, res, user);
} catch (err) {
res.status(500).json({ message: err.message });
}
}
};Na koncu moramo poskrbeti še za uporabo pridobljenega imena uporabnika pri shranjevanju komentarja v podatkovno bazo, saj bomo v nadaljevanju ime uporabnika iz spletnega obrazca odstranili.
api/controllers/comments.js
const doAddComment = (req, res, location, author) => {
if (!location) {
res.status(404).json({
message: "Location with id '" + req.params.locationId + "' not found.",
});
} else {
if (!req.body.author || !req.body.rating || !req.body.comment) {
res.status(400).json({
message:
"Body parameters 'author', 'rating' and 'comment' are required.",
});
} else {
location.comments.push({
author: author, req.body.author,
rating: req.body.rating,
comment: req.body.comment,
});
location.save((err, location) => {
if (err) res.status(500).json({ message: err.message });
else {
updateAverageRating(location._id);
res.status(201).json(location.comments.slice(-1).pop());
}
});
}
}
};8.4.3 Avtorizacija pri ažuriranju komentarja
Pri trenutni implementaciji vsakemu prijavljenemu uporabniku dovolimo spreminjanje in brisanje poljubnega komentarja. V nadaljevanju bi to funkcionalnost omogočili zgolj nad uporabniku lastnimi komentarji.
Najprej poskrbimo, da pri urejanju obstoječega komentarja v okviru metode commentsUpdateOne preverimo ali je uporabnik, ki želi izvesti spremembo dejanski avtor komentarja.
api/controllers/comments.js
const commentsUpdateOne = async (req, res) => {
if (!req.params.locationId || !req.params.commentId)
res.status(400).json({
message: "Path parameters 'locationId' and 'commentId' are required.",
});
else {
try {
let location = await Location.findById(req.params.locationId)
.select("comments")
.exec();
if (!location)
res.status(404).json({
message: `Location with id '${req.params.locationId}' not found.`,
});
else {
const comment = location.comments.id(req.params.commentId);
if (!comment)
res.status(404).json({
message:
"Comment with id '" + req.params.commentId + "' not found.",
});
else {
getAuthor(req, res, async (req, res, author) => {
if (comment.author != author.name) {
res.status(403).json({
message: "Not authorized to update this comment.",
});
} else if (!req.body.rating && !req.body.comment) {
res.status(400).json({
message:
"At least one of body parameters 'rating' or 'comment' is required.",
});
} else {
if (req.body.rating) comment.rating = req.body.rating;
if (req.body.comment) comment.comment = req.body.comment;
await location.save();
await updateAverageRating(location._id);
res.status(200).json(comment);
}
});
}
}
} catch (err) {
res.status(500).json({ message: err.message });
}
}
};Podobno v metodi commentsDeleteOne preverimo tudi ali je komentar, ki ga uporabnik želi izbrisati, res njegov.
api/controllers/comments.js
const commentsDeleteOne = async (req, res) => {
const { locationId, commentId } = req.params;
if (!locationId || !commentId)
res.status(400).json({
message: "Path parameters 'locationId' and 'commentId' are required.",
});
else {
try {
let location = await Location.findById(locationId)
.select("comments")
.exec();
if (!location)
res.status(404).json({
message: `Location with id '${locationId}' not found.`,
});
else if (location.comments && location.comments.length > 0) {
const comment = location.comments.id(commentId);
if (!comment)
res.status(404).json({
message: `Comment with id '${commentId}' not found.`,
});
else {
getAuthor(req, res, async (req, res, author) => {
if (comment.author != author.name) {
res.status(403).json({
message: "Not authorized to delete this comment.",
});
} else {
comment.deleteOne();
await location.save();
await updateAverageRating(location._id);
res.status(204).send();
}
});
}
} else res.status(404).json({ message: "No comments found." });
} catch (err) {
res.status(500).json({ message: err.message });
}
}
};8.4.3.1 Podpora avtentikaciji pri OpenAPI dokumentaciji
Ker bomo imeli sedaj dokumentacijo v večini datotek modela in krmilnika, to spremembo izvedemo v opredelitvi dokumenta Swagger v začetni datoteki strežnika.
server.js
const swaggerDocument = swaggerJsDoc({
...,
apis: ["./api/models/* locations.js", "./api/controllers/*.js"],
});Najprej poskrbimo za opredelitve sheme uporabnika User in v primeru uspešne avtentikacije še shemo Authentication.
api/models/users.js
/**
* @openapi
* components:
* schemas:
* User:
* type: object
* description: User of the application.
* properties:
* email:
* type: string
* format: email
* description: email of the user
* example: dejan@lavbic.net
* name:
* type: string
* description: name and surname of the user
* example: Dejan Lavbič
* writeOnly: true
* password:
* type: string
* description: password of the user
* example: test
* required:
* - email
* - name
* - password
* Authentication:
* type: object
* description: Authentication token of the user.
* properties:
* token:
* type: string
* description: JWT token
* example: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJfaWQiOiI1NTZiZWRmNDhmOTUzOTViMTlhNjc1ODgiLCJlbWFpbCI6InNpbW9uQGZ1bGxzdGFja3RyYWluaW5nLmNvbSIsIm5hbWUiOiJTaW1vbiBIb2xtZXMiLCJleHAiOjE0MzUwNDA0MTgsImlhdCI6MTQzNDQzNTYxOH0.GD7UrfnLk295rwvIrCikbkAKctFFoRCHotLYZwZpdlE
* required:
* - token
*/
const usersSchema = new mongoose.Schema({
email: { type: String, unique: true, required: [true, "Email is required!"] },
name: { type: String, required: [true, "Name is required!"] },
hash: { type: String, required: [true, "Hash is required!"] },
salt: { type: String, required: [true, "Salt is required!"] },
});
usersSchema.methods.setPassword = function (password) {
this.salt = crypto.randomBytes(16).toString("hex");
this.hash = crypto
.pbkdf2Sync(password, this.salt, 1000, 64, "sha512")
.toString("hex");
};Obvladovanje uporabnikov je tudi nova kategorija metod, zato dodamo novo oznako Authentication, kjer bosta na voljo metodi za prijavo in registracijo uporabnika.
server.js
const swaggerDocument = swaggerJsDoc({
definition: {
...,
tags: [
{
name: "Locations",
description: "Cultural heritage <b>locations</b> in Slovenia.",
},
{
name: "Comments",
description:
"<b>Comments</b> for cultural heritage locations in Slovenia.",
},
{
name: "Authentication",
description: "<b>User management</b> and authentication.",
}
],
...
},
...
});Razvijalcem, ki bodo pregledovali OpenAPI dokumentacijo, želimo že kar neposredno pri pregledu omogočiti prijavo in testiranje metod z vključenim žetonom JWT, zato dodamo varnostno shemo dostopa. V našem primeru bomo uporabili varnostno shemo tipa http, sheme bearer in formatom JWT.
server.js
const swaggerDocument = swaggerJsDoc({
definition: {
...,
components: {
securitySchemes: {
jwt: {
type: "http",
scheme: "bearer",
in: "header",
bearerFormat: "JWT",
},
},
...,
},
},
...
});Sedaj pa varnostno shemo vključimo v dokumentacijo POST, PUT in DELETE metod za ažuriranje komentarjev. Dopolnili bomo tudi dodatne tipe odgovorov, in sicer 401 Unauthorized (uporabnik ni prijavljen) ter 403 Forbidden (uporabnik nima pravice dostopa).
Najprej poskrbimo za opis metode commentsCreate, kjer dodajamo nov komentar. Funkcionalnost naj bo na voljo le za prijavljene uporabnike, med odgovori je tudi status 401 ter odstranimo zahtevani parameter author, saj se uporabi podatek prijavljenega uporabnika.
api/controllers/comments.js
/**
* @openapi
* /locations/{locationId}/comments:
* post:
* ...
* tags: [Comments]
* security:
* - jwt: []
* parameters:
* - name: locationId
* ...
* requestBody:
* ...
* content:
* application/x-www-form-urlencoded:
* schema:
* type: object
* properties:
* author:
* type: string
* description: <b>author</b> of comment
* example: Dejan Lavbič
* rating:
* ...
* comment:
* ...
* required:
* - author
* - rating
* - comment
* responses:
* '201':
* ...
* '400':
* ...
* '401':
* description: <b>Unauthorized</b>, with error message.
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/ErrorMessage'
* examples:
* no token provided:
* value:
* message: No authorization token was found.
* user not found:
* value:
* message: User not found.
* malformed token:
* value:
* message: jwt malformed
* invalid token signature:
* value:
* message: invalid signature
* '404':
* ...
*/
const commentsCreate = (req, res) => {
...
};Posodobimo tudi za opis metode commentsUpdateOne, kjer lahko prijavljen uporabnik spreminja svoj lastni komentar. Med odgovori sta sedaj na voljo statusa 401 in 403.
api/controllers/comments.js
/**
* @openapi
* /locations/{locationId}/comments/{commentId}:
* put:
* ...
* tags: [Comments]
* security:
* - jwt: []
* parameters:
* ...
* requestBody:
* ...
* responses:
* '200':
* ...
* '400':
* ...
* '401':
* description: <b>Unauthorized</b>, with error message.
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/ErrorMessage'
* examples:
* no token provided:
* value:
* message: No authorization token was found.
* user not found:
* value:
* message: User not found.
* malformed token:
* value:
* message: jwt malformed
* invalid token signature:
* value:
* message: invalid signature
* '403':
* description: <b>Forbidden</b>, with error message.
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/ErrorMessage'
* example:
* message: Not authorized to update this comment.
* '404':
* ...
* '500':
* ...
*/
const commentsUpdateOne = (req, res) => {
...
};S podobnimi funkcionalnimi zahtevami kot pri spreminjanju komentarja, dopolnimo še opis metode commentsDeleteOne, kjer lahko prijavljen uporabnik izbriše svoj lastni komentar.
api/controllers/comments.js
/**
* @openapi
* /locations/{locationId}/comments/{commentId}:
* delete:
* ...
* tags: [Comments]
* security:
* - jwt: []
* parameters:
* ...
* responses:
* '204':
* ...
* '400':
* ...
* '401':
* description: <b>Unauthorized</b>, with error message.
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/ErrorMessage'
* examples:
* no token provided:
* value:
* message: No authorization token was found.
* user not found:
* value:
* message: User not found.
* malformed token:
* value:
* message: jwt malformed
* invalid token signature:
* value:
* message: invalid signature
* '403':
* description: <b>Forbidden</b>, with error message.
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/ErrorMessage'
* example:
* message: Not authorized to delete this comment.
* '404':
* ...
* '500':
* ...
*/
const commentsDeleteOne = (req, res) => {
...
};Sedaj moramo v okviru oznake Authentication še dodati OpenAPI dokumentacijo metod za registracijo (register) in prijavo (login).
Pri registraciji novega uporabnika v okviru metode register mora uporabnik vnesti vse zahtevane podatke in v primeru uspešne registracije pridobi žeton JWT.
api/controllers/authentication.js
/**
* @openapi
* /register:
* post:
* summary: Register a new user
* description: <b>Register a new user</b> with name, email and password.
* tags: [Authentication]
* requestBody:
* description: User object
* required: true
* content:
* application/x-www-form-urlencoded:
* schema:
* $ref: '#/components/schemas/User'
* responses:
* '200':
* description: <b>OK</b>, with JWT token.
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Authentication'
* '400':
* description: <b>Bad Request</b>, with error message.
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/ErrorMessage'
* example:
* message: All fields required.
* '500':
* description: <b>Internal Server Error</b>, with error message.
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/ErrorMessage'
* example:
* message: Database not available.
*/
const register = (req, res) => {
...
};Podobno pri prijavi obstoječega uporabnika v okviru metode login uporabnik vnese elektronski naslov in geslo ter v primeru uspešne prijave prejme žeton JWT.
api/controllers/authentication.js
/**
* @openapi
* /login:
* post:
* summary: Login a user
* description: <b>Login an existing user</b> with email and password.
* tags: [Authentication]
* requestBody:
* description: User credentials
* required: true
* content:
* application/x-www-form-urlencoded:
* schema:
* type: object
* properties:
* email:
* type: string
* format: email
* description: email of the user
* example: dejan@lavbic.net
* password:
* type: string
* description: password of the user
* example: test
* required:
* - email
* - password
* responses:
* '200':
* description: <b>OK</b>, with JWT token.
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Authentication'
* '400':
* description: <b>Bad Request</b>, with error message.
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/ErrorMessage'
* example:
* message: All fields required.
* '401':
* description: <b>Unauthorized</b>, with error message.
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/ErrorMessage'
* examples:
* incorrect username:
* value:
* message: Incorrect username.
* incorrect password:
* value:
* message: Incorrect password.
* '500':
* description: <b>Internal Server Error</b>, with error message.
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/ErrorMessage'
* example:
* message: Database not available.
*/
const login = (req, res) => {
...
};8.4.3.2 Testiranje varnosti na ravni REST API metod
Najprej registrirajmo novega uporabnika Janez Kranjski.
$ curl -X POST \
"http://localhost:3000/api/register" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "email=janez%40kranjski.net&name=Janez%20Kranjski&password=test"
{ "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOiI2Mzc3NzMyMmNmYTVlNGFiZmRkYjIzYzIiLCJlbWFpbCI6ImphbmV6QGtyYW5qc2tpLm5ldCIsIm5hbWUiOiJKYW5leiBLcmFuanNraSIsImV4cCI6MTY2OTM3NzQ0MiwiaWF0IjoxNjY4NzcyNjQyfQ.2M6QUo_lYGUy3X9zWjpen-mAdSb9Lq6Uzgz4AJONbc4" }Po uspešni registraciji pridobimo žeton JWT, ki ga lahko uporabimo za ostale zaščitene funkcionalnosti sistema.
Če poskusimo še enkrat registrirati istega uporabnika, nam sistem vrne napako, saj uporabnik že obstaja, kjer smo v shemi opredelili, da je atribut email enoličen.
$ curl -i -X POST \
"http://localhost:3000/api/register" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "email=janez%40kranjski.net&name=Janez%20Kranjski&password=test"
HTTP/1.1 500 Internal Server Error
...
{ "message": "E11000 duplicate key error collection: Demo.Users index: email_1 dup key: { email: \"janez@kranjski.net\" }" }Sistem nam omogoča prijavo uporabnika, kjer sta zahtevana elektronski naslov in geslo, kot rezultat pa pridobimo žeton JWT.
$ JWT_TOKEN_JANEZ=$( \
curl -X POST \
"http://localhost:3000/api/login" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "email=janez%40kranjski.net&password=test" \
| sed 's/.*"\(.*\)".*/\1/')
$ echo $JWT_TOKEN_JANEZ
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOiI2Mzc3NzMyMmNmYTVlNGFiZmRkYjIzYzIiLCJlbWFpbCI6ImphbmV6QGtyYW5qc2tpLm5ldCIsIm5hbWUiOiJKYW5leiBLcmFuanNraSIsImV4cCI6MTY2OTM4MDU5OCwiaWF0IjoxNjY4Nzc1Nzk4fQ.bFagSjcf7R4GoeJJdU8lrTZ6zjQSx7tQCweDmLE9AWETestirali bomo na lokaciji Ljubljana - Viško pokopališče, zato pridobimo enolični identifikator lokacije in ga shranimo v okoljsko spremenljivko.
$ LOCATION_ID=$(docker exec -it web-dev-mongo-db bash -c "mongosh Demo --eval \
'db.Locations.findOne(\
{\"name\": \"Ljubljana - Viško pokopališče\"}, { _id: 1 })'"\
| grep 'ObjectId' \
| sed 's/.*"\(.*\)".*/\1/')Sedaj zahtevamo dodajanje novega komentarja obstoječi lokaciji, kjer pri zahtevi dodamo žeton JWT, ki nas na strani strežnika identificira kot prijavljenega uporabnika. V odgovoru opazimo, da se pri dodajanju novega komentarja samodejno doda naziv prijavljenega uporabnika, kot avtorja komentarja.
$ curl -X POST \
"http://localhost:3000/api/locations/${LOCATION_ID}/comments" \
-H "Authorization: Bearer ${JWT_TOKEN_JANEZ}" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "rating=3&comment=Srednja%20%C5%BEalost."
{
"author": "Janez Kranjski",
"rating": 3,
"comment": "Srednja žalost.",
"_id": "6378e948cca751abdf91c10f",
"createdOn": "2022-11-19T14:33:44.616Z"
}Če želimo dodati nov komentar, kjer sicer dodamo žeton JWT, vendar le-ta ni ustrezen, nam strežnik vrne napako 401 z obvestilo jwt mailformed.
$ curl -i -X POST \
"http://localhost:3000/api/locations/${LOCATION_ID}/comments" \
-H "Authorization: Bearer 123" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "rating=3&comment=Srednja%20%C5%BEalost."
HTTP/1.1 401 Unauthorized
...
{ "message": "jwt malformed" }Uporabnik lahko tudi zahteva spreminjanje lastnega komentarja. Najprej pridobimo enolični identifikator ravno dodanega komentarja, nato zahtevamo spremembo, kjer dodamo tudi žeton avtorja komentarja. Operacija je uspešna, saj nam strežnik vrne posodobljen komentar z zahtevanimi spremembami.
$ COMMENT_ID=$(docker exec -it web-dev-mongo-db bash -c "mongosh Demo --eval \
'db.Locations.findOne(\
{\"name\": \"Ljubljana - Viško pokopališče\" }, \
{ _id: 0, comments: { \$elemMatch: { \"author\": \"Janez Kranjski\" }} })'" \
| grep 'ObjectId' \
| sed 's/.*"\(.*\)".*/\1/')
$ curl -X PUT \
"http://localhost:3000/api/locations/${LOCATION_ID}/comments/${COMMENT_ID}" \
-H "Authorization: Bearer ${JWT_TOKEN_JANEZ}" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "rating=1&comment=Obup."
{
"author": "Janez Kranjski",
"rating": 1,
"comment": "Obup.",
"_id": "6378e948cca751abdf91c10f",
"createdOn": "2022-11-19T14:33:44.616Z"
}Sedaj poskusimo spremeniti omenjeni komentar, vendar z drugim uporabnikom, ki ni prvotni avtor. Iz odgovora vidima, da operacija ni dovoljena, saj nam strežnik vrne napako 403, kjer uporabnik nima pravice izvajati omenjene operacije.
$ JWT_TOKEN_DEJAN=$( \
curl -X POST \
"http://localhost:3000/api/login" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "email=dejan%40lavbic.net&password=test" \
| sed 's/.*"\(.*\)".*/\1/')
$ curl -i -X PUT \
"http://localhost:3000/api/locations/${LOCATION_ID}/comments/${COMMENT_ID}" \
-H "Authorization: Bearer ${JWT_TOKEN_DEJAN}" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "rating=5&comment=Super%20lokacija."
HTTP/1.1 403 Forbidden
...
{ "message": "Not authorized to update this comment." }Sedaj poskusimo še z istim uporabnikom, ki ni avtor komentarja, le-tega izbrisati. Podobno kot v prejšnjem primeru operacija ni dovoljena, kar strežnik tudi sporoči kot napako 403.
$ curl -i -X DELETE \
"http://localhost:3000/api/locations/${LOCATION_ID}/comments/${COMMENT_ID}" \
-H "Authorization: Bearer ${JWT_TOKEN_DEJAN}"
HTTP/1.1 403 Forbidden
...
{ "message": "Not authorized to delete this comment." }Ko pa brisanje zahteva avtor komentarja, je omenjena operacija dovoljena in vrne uspešno potrditev 204.
$ curl -i -X DELETE \
"http://localhost:3000/api/locations/${LOCATION_ID}/comments/${COMMENT_ID}" \
-H "Authorization: Bearer ${JWT_TOKEN_JANEZ}"
HTTP/1.1 204 No ContentDodatno gradivo
|
|
|
