Přejít k navigační liště

Zdroják » JavaScript » Autentizace v single-page aplikacích

Autentizace v single-page aplikacích

Články JavaScript

Implementace přihlašování do aplikací, které jsou postaveny jako single-page, je obvykle řešeno jinak než u klasických server-side aplikací. Jak dosáhnout bezstavovosti serverové části a jak vytvořit chytré přihlašování do administrace v prostředí AngularJS je téma dalšího dílu.

Nálepky:

Úvod

Článek bude rozdělen do dvou dílů. V tom dnešním bude řešena autentizace na straně prohlížeče a ve frameworku AngularJS. V tom příštím se podíváme na serverovou část, na implemenetaci v Node.js a na knihovnu Passport. Všechny zdrojové kódy dnešního dílu jsou jako obvykle dostupné na Githubu, můžete si je také stáhnout příkazem git checkout -f eshop014.

V tomto díle se budeme věnovat jen autentizaci, což je proces ověřování proklamované identity uživatele (wikipedia). Uživatel nám svěří své přihlašovací údaje a my se podíváme do databáze, zda někdo takový existuje. Pokud takový uživatel existuje, proběhla autentizace úspěšně.

Tradiční způsob řešení autentizace

Nejčastěji se vše řeší tak, že pokud uživatel potřebuje přistupovat do části, ve které již musí být přihlášen, přejde na přihlašovací formulář. Zde zadá svůj e-mail a heslo a formulář odešle. Na straně serveru se dotazem do databáze ověří, zda jsou přihlašovací údaje správné. Pokud ano, načtou se všechny potřebné údaje o uživateli a vytvoří se relace, což znamená, že na straně serveru se vygeneruje unikátní řetězec, který se jednak vloží do cookie a pošle se uživateli zpět, ale také se společně s vybranými údaji o uživateli někam na serveru uloží. Při příštím požadavku si server z cookie přečte onen unikátní řetězec, přes který načte dříve uložená data, takže uživatel již znova nemusí svojí identitu ověřovat a obejde se tím bezstavovost HTTP.

Autentizace u single-page aplikací

V single-page aplikacích využívajících REST API se tento způsob přihlašování nepoužívá. Jedním z principů REST architektury je bezstavovost, což přináší u SPA řadu výhod (třeba nemusíme řešit session management, expiraci session na serveru, atd.). Chceme-li však dosáhnout bezstavovosti, musí být v každém požadavku zaslány informace pro ověření identity. Jak toho dosáhnout? Možností je několik, dále popisuji metodu, kterou používám já.

Nejjednodušší způsob je použít basic HTTP autentizaci. Ta využívá hlavičku Authorization, jejíž hodnotou je uživatelské jméno (email) a heslo, které jsou uloženy ve formátu base64. Na straně serveru zjišťujeme, zda nějaký HTTP požadavek vyžaduje ověření identity, a pokud ano, stačí vzít data z hlavičky Authorization a ověřit, že uživatel má k danému zdroji přístup. Pokud nemá, zastavíme zpracování a vrátíme HTTP kód 401.

Základní HTTP autentizace má nevýhodu v tom, že posílá heslo v otevřeném nezašifrovaném formátu. Pokud by tedy někdo odposlouchával HTTP požadavky uživatele, snadno by heslo uživatele zjistil. Proto je lepší používat všude HTTPS, čímž je komunikace šifrovaná a s posíláním citlivých údajů v hlavičce pak problém není. Z pohledu výkonu serveru už dnes nepředstavuje použití HTTPS problém (odkaz vede na online verzi knihy High Performance Browser Networking od Ilya Grigorika, který je specialistou na výkon aplikací v Google a rozhodně ji vřele doporučuji k přečtení).

Ještě je potřeba vyřešit jeden problém. Co situace, kdy uživatel načte stránku znova? Předtím se přihlásil a přihlašovací údaje jsme drželi v paměti, ale pokud provede reload, vše se ztratí. Bude tedy potřeba někde uložit přihlašovací údaje, odkud je pak načteme do hlavičky Authorization při dotazech na API. Museli bychom však ukládat někam heslo v otevřené podobě, a to rozhodně nepatří mezi nejlepší praktiky.

To by šlo vyřešit tak, že bude u každého uživatele v databázi uveden ještě náhodný vygenerovaný hash, který po přihlášení uživatel dostane, a ten bude uložen na straně klienta a bude zasílán místo hesla. Jenže je tady jiný problém. Co se stane, když nějaký útočník získá kopii databáze? Heslo se sice nedozví, ale může nastavit sám hlavičku Authorization se zjištěným hashem a přihlásit se jako jakýkoliv jiný uživatel.

To lze vyřešit tím, že budeme generovat hash z hesla, a to dvakrát. Při registraci se z hesla vygeneruje první hash (označme ho pro účely článku proměnnou x) pomocí nějaké funkce (pro jednoduchost třeba SHA1 + nějaký salt)  a tento hash se použije pak jako vstup do další funkce (třeba opět SHA1 + nějaký jiný salt), čímž se vygeneruje druhý hash (který označíme pro účely článku proměnnou y):

//zaslane heslo od uživatele
var password = 'abc123456';

//sůl pro ztížení získání hesla z hashe
var salt1 = 'xyz1';
var salt2 = 'xyz2';

//generování prvního hashe
var x = sha1(password + salt1);

//generování druhého hashe - ten bude uložen v databázi
var y = sha1(x + salt2);

Jak pak bude probíhat přihlašování?

  1. Uživatel přistupuje do zabezpečené oblasti, bude vyzván k tomu, aby zadal své uživ. jméno (email) a heslo.

  2. Uživatel odešle formulář, prohlížeč zašle dotaz na API, zda existuje uživatel s daným uživ. jménem a heslem.

  3. Server vytvoří dvakrát hash podle dříve uvedeného postupu a podívá se, zda existuje záznam v databázi pro dané uživ. jméno a získaný hash z hesla (proměnná y).

  4. Pokud záznam existuje, vrátí uživateli odpověď, ve které odešle první vygenerovaný hash (promměná x).

  5. Prohlížeč si vrácený hash uloží (může to být cookie, sessionStorage atd.), aby se uživatel nemusel přihlašovat znova po reloadu.

Jak pak bude vypadat dotaz na API, kde je vyžadováno přihlašování?

  1. Framework nastaví HTTP hlavička Authorization s uživ. jménem a uloženým hashem (proměnná x) a odešle požadavek na API.

  2. Server zjišťuje, že se uživatel ptá na zabezpečené údaje. Podívá se proto, zda je zaslána HTTP hlavička Authorization, pokud ano, vybere z ní uživ. jméno a hash (proměnná x).

  3. Server vygeneruje druhý hash (proměnná y) z předaného hashe (proměnná x) v hlavičce Authorization a podívá se do databáze, zda daný uživatel existuje.

  4. Pokud ano, předá požadavek dále, pokud ne, vrátí chybu, po které bude uživatel vyzván k přihlášení.

Tím je zajištěno, že je komunikace bezstavová (tedy serverová část) a zároveň bezpečná.

Stav udržuje prohlížeč, takže třeba pro odhlášení není potřeba zasílat dotaz na server, ale stačí jen smazat získaný hash.

Vše budeme řešit na straně serveru přes framework Passport, který je pro Node.js nejlepší. Dříve byl populárnější Everyauth, doporučuji ales spíše použití Passportu kvůli snadnější rozšiřitelnosti. Kromě toho podporuje obrovské množství provideru (mj. Facebook, Twitter, Google atd.) a různých strategií pro přihlašování.

V našem případě uděláme ještě jednu malou změnu. Server nebude vracet jen daný hash (proměnnou x), ale vrátí rovnou kombinaci e-mailu a prvního hashe (proměnná x) v base64, a tento řetězec budeme dále v článku označovat jako autorizační token (authToken).

Autentizace u AngularJS aplikací

Použijeme upravené řešení, které popsal na svém blogu Witold Szczerba. V následujícím textu budeme používat vše, co již bylo v rámci seriálu popsáno, pokud jste však některé díly nečetli, pak je potřeba znát:

  • jak funguje zasílání zpráv v hierarichi scope (popsáno v 8. díle),

  • návrhový vzor Promise, response interceptors (popsáno ve 12. díle).

Jak bude tedy celý postup přihlašování na klientské straně vypadat?

  1. Nepřihlášený uživatel přijde na stránku, za které se dotazujeme na zabezpečená data. Prohlížeč zašle dotaz na API bez ohledu na to, zda je uživatel přihlášen či nikoliv.

  2. Server zjistí, že se uživatel ptá na data, která jsou k dispozici jen přihlášeným uživatelům. Podívá se tedy do hlavičky Authorization (popsáno dříve) a zjišťuje, že je uživatel nepřihlášen. Proto odešle HTTP kód 401.

  3. Aplikace zaregistruje, že byla vrácena tato chyba. Uchová informace o všech dotazech, které byly vráceny s HTTP kódem 401 a vyšle zprávu všem registrovaným posluchačům uvnitř aplikace, že je vyžadováno přihlášení.

  4. Tuto zprávu zachytí direktiva login, který zobrazí formulář pro přihlášení, a controller LoginCtrl, který bude formulář obsluhovat. Uživatel vyplní přihlašovací údaje a odešle formulář.

  5. Server vrátí authToken, který framework uloží do sessionStorage (nebo cookie) a nastaví hlavičku Authorization pro příští požadavky. Dále bude vyslána zpráva do aplikace, že přihlášení proběhlo úspěšně, což opět zaregistruje direktiva login, která přihlašovací formulář skryje.

  6. Protože jsme dříve všechny informace o neúspěšných požadavcích uložili, máme je k dispozici a po úspěšném přihlášení se pokusíme o jejich odeslání znova, takže uživatel nemusí provádět žádnou další akci a vše probíhá dále, jako kdyby vůbec přihlašování neproběhlo.

Řešení je tedy stejné jak pro situace, kdy poprvé přistupujeme jako nezaregistrovaní do nějaké zabezpečené sekce, tak pro situaci, kdy už jsme byli přihlášeni a s aplikací normálně pracujeme ale třeba aplikaci používáme na dvou místech zároveň (na tabletu a na mobilu) a třeba v tabletu změníme heslo. Pak se změní i authToken a objeví se na telefonu formulář pro přihlášení a uživatel je vyznán k zadání nového hesla i zde. Tím se řeší další bezpečnostní problém mnoha různých aplikací, kdy po změně hesla nejsou uživatelé okamžitě odhlášeni ze všech ostatních používání aplikace, takže když dojde třeba ke krádeži telefonu, přestože uživatel změní heslo, útočník si může pak s aplikací dělat co chce a uživatel je proti tomu zcela bezmocný.

Mimochodem bezstavovost na straně serveru + SPA architektura řeší ještě jeden velmi vážný bezpečnostní problém velkého množství aplikací, a to je obrana proti útoku CSRF. Zabezpečit se proti němu samozřejmě lze i v klasických aplikacích, ale vyžaduje to trochu nepříjemné práce navíc, což často vede k tomu, že je značná část webů k tomuto druhu útoku náchylná. Stačí v podstatě jen zaslat vybrané oběti mail s odkazem, který odklikne a dostane se na útočníkův web, kde už může útočník pod účtem uživatele provádět dotazy kamkoliv se mu zachce. Co třeba dostat majitelku konkurečního e-shopu na stránku, kde pod jeho účtem zvýšíme ceny zboží o 10%, takže u něj nikdo nebude chtít nakupovat? Stačí zaslat jeden mail ve stylu “Dobrý den, chci vás upozornit, že váš partner umístil na web fotografie z vašeho intimního života, víte o tom? Podívejte se.” Kolik lidí na něj asi odklikne?

Implementace v AngularJS

Vše bude vytvořeno tak, abychom řešení naprogramovali jen jednou a použili ho i na ostatních aplikacích. Budeme potřebovat několik částí:

  • službu error401, která detekuje, že přišla odpověď s HTTP kódem 401, uloží informace o odeslaném požadavku a vyšle notifikaci, že je potřeba přihlásit se;

  • službu authNotifier, která se bude starat o všechny notifikace, které se autentizace týkají (aby vše bylo na jednom místě);

  • službu requestStorage, která bude sloužit jako uložiště pro odeslané požadavky, která vyžadovaly autentizaci;

  • službu auth, která bude vše řídit, bude ukládat či mazat authToken, bude pracovat s HTTP hlavičkou Authorization, přepošle znova požadavky a zpracuje také pokus o přihlášení uživatele;

  • direktivu login, která se bude starat o zobrazení formuláře pro přihlášení, když to bude potřeba;

  • šablonu pro přihlašovací formulář;

  • controller LoginCtrl, který bude přihlašování obsluhovat.

Služba error401

angular.module('zdrojak.service').factory('error401', ['$q', 'authNotifier', 'requestStorage', function($q, authNotifier, requestStorage){
  return function(promise)  {
    return promise.then(null, function(res){
      if (res.status !== 401) return promise;

      var deferred = $q.defer();
      var req = {
        config: res.config,
        deferred: deferred
      };
      requestStorage.add(req);
      authNotifier.notifyRequired();
      return deferred.promise;
    });
  };
}]);

Pokud má odpověď HTTP kód 401, vytvoří se nová promise, která se jako odpověď přepošle dále, což znamená, že aplikace bude čekat na další zpracování, dokud nebude promise vyřešena. My si ji uložíme společně s konfigurací požadavku (req.config) do requestStorage a vyšleme notifikaci aplikaci, že je potřeba vyřídit přihlášení.

Služba se zaregistruje jako interceptor v app.js podobně, jako jsme to dělali se službou error4xx:

module.config(['$httpProvider', function($httpProvider){
  $httpProvider.responseInterceptors.push('error401');
  $httpProvider.responseInterceptors.push('error4xx');
}]);

Služba authNotifier a requestStorage

angular.module('zdrojak.service').factory('authNotifier', ['$rootScope', function($rootScope){
  function AuthNotifier() {}
  AuthNotifier.prototype.onRequired = function(scope, cb) {
    scope.$on('auth:loginRequired', cb);
  };
  AuthNotifier.prototype.onConfirmed = function(scope, cb) {
    scope.$on('auth:loginConfirmed', cb);
  };
  AuthNotifier.prototype.notifyRequired = function() {
    $rootScope.$broadcast('auth:loginRequired');
  };
  AuthNotifier.prototype.notifyConfirmed = function() {
    $rootScope.$broadcast('auth:loginConfirmed');
  };
  return new AuthNotifier();
}]);
angular.module('zdrojak.service').factory('requestStorage', function(){
  function RequestStorage() {
    this.requests = [];
  }
  RequestStorage.prototype.clear = function() {
    this.requests = [];
  };
  RequestStorage.prototype.getAll = function() {
    return this.requests;
  };
  RequestStorage.prototype.add = function(req) {
    this.requests.push(req);
  };
  return new RequestStorage();
});

Služba authNotifier pouze shlukuje práci s notifikacemi do jednoho místa, requestStorage je uložiště pro všechny neúspěšně poslané requesty, abychom je mohli znova přeposlat.

Šablona pro login, direktiva login a controller LoginCtrl

<form ng-show="mode" class="form-signin" ng-submit="login()">
  <h2>Přihlašování</h2>
  <messages></messages>
  <input ng-model="email" type="text" class="input-block-level" placeholder="E-mail" autofocus required>
  <input ng-model="password" type="password" class="input-block-level" placeholder="Heslo" required>
  <input class="btn btn-large btn-primary" value="Přihlásit se!" type="submit">
</form>
angular.module('zdrojak.directive').directive('login', ['authNotifier', function(authNotifier){
  var config = {
    restrict: 'E',
    templateUrl: '/partials/admin/login.html',
    replace: true,
    scope: {},
    controller: 'LoginCtrl',
    link: function(scope, element) {
      authNotifier.onRequired(scope, function(){
        scope.mode = true;
      });
      authNotifier.onConfirmed(scope, function(){
        scope.mode = false;
      });
    }
  };
  return config;
}]);
angular.module('zdrojak.controller').controller('LoginCtrl', ['$scope', 'auth', 'flash', function($scope, auth, flash) {
  $scope.login = function() {
    auth.login($scope.email, $scope.password, null, function(){
      //oznamit uzivateli chybu...
    });
  };
}]);

Pokud direktiva zaregistruje notifikaci o potřebě zobrazit přihlašovací formulář, tak to udělá. Když uživatel odešle formulář, vyvolá se metoda $scope.login() v LoginCtrl.

V konfiguraci direktivy si všimněte vlastnosti templateUrl, která umožňuje načíst šablonu odjinud a nedefinovat HTML přímo uvnitř direktivy. Také nově specifikujeme controller, který se má pro direktivu načíst. Oboje je výhodné oddělit, protože tím zůstane direktiva univerzální pro všechny aplikace, zatímco šablona i controller se může pro různé aplikace měnit.

Služba auth

angular.module('zdrojak.service').factory('auth', ['$http', '$window', 'authNotifier', 'requestStorage', 'api', function($http, $window, authNotifier, requestStorage, api){
  return new Auth($http, $window.sessionStorage, requestStorage, authNotifier, api);
}]);
function Auth($http, tokenStorage, requestStorage, notifier, api) {
  this.$http = $http;
  this.tokenStorage = tokenStorage;
  this.requestStorage = requestStorage;
  this.notifier = notifier;
  this.api = api;
}

Auth.TOKEN = 'authToken';

Auth.prototype.getToken = function() {
  return this.tokenStorage.getItem(Auth.TOKEN);
};

Auth.prototype.setToken = function(token) {
  this.tokenStorage.setItem(Auth.TOKEN, token);
};

Auth.prototype.initHeaders = function() {
  var token = this.getToken();
  if (!token) return false;
  this.setHeader(token);
};

Auth.prototype.setHeader = function(token) {
  this.$http.defaults.headers.common.Authorization = 'Basic ' + token;
};

Auth.prototype.clearHeader = function() {
  delete this.$http.defaults.headers.common.Authorization;
};

Auth.prototype.retry = function(req) {
  this.$http(req.config).then(function(response) {
    req.deferred.resolve(response);
  });
};

Auth.prototype.resendRequests = function() {
  var requests = this.requestStorage.getAll();
  for (var i = 0; i < requests.length; i++) {
    this.retry(requests[i]);
  }
  this.requestStorage.clear();
};

Auth.prototype.login = function(email, password, successCb, errorCb) {
  successCb = successCb || function() {};
  errorCb = errorCb || function() {};

  this.clearHeader();

  var credentials = {
    email: email,
    password: password
  };

  var auth = this;

  this.api.user.auth(credentials, function(res){
    if (res.authToken) {
      auth.setToken(res.authToken);
      auth.setHeader(res.authToken);
      auth.resendRequests();
      auth.notifier.notifyConfirmed();
      successCb(res);
    } else {
      errorCb(res);
    }
  });
};

Služba auth volá třídu Auth, které předává všechny důležité parametry. Parametr tokenStorage je uložiště pro authToken. Může to být třída obsluhující cookies, v našem případě pro jednoduchost používáme sessionStorage (rozdíl mezi localStorage a sessionStorage je v tom, že sessionStorage se vymaže, jakmile se zavře prohlížeč).

Metoda setToken(), getToken() buď nastavuje nebo vrací authToken z tokenStorage. Metody setHeader() a clearHeader() nastavují či odstraňují hlavičku, kterou bude AngularJS automaticky zasílat se všemi požadavky. Všechny takové hlavičky jsou uloženy v poli $http.defaults.headers.common. Metoda initHeaders() se zavolá při prvním spuštění aplikace, aby byla hlavička nastavena, pokud už uživatel je přihlášen (třeba po reloadu aplikace). Metoda initHeaders() se volá v bloku run() při spuštění aplikace (viz soubor app.js):

module.run(['auth', function(auth){
  auth.initHeaders();
}]);

Metoda login() zavolá především metodu auth() na $response objektu api.user. Ta zajistí zaslání požadavku pro ověření, zda existuje uživatel pro daný e-mail a heslo. Pokud ano, nastaví authToken do tokenStorage, nastaví hlavičku pro další požadavky, přes metodu resendRequests() přepošle znova požadavky, které dříve neprošly kvůli chybějícímu přihlášení a nakonec vyšle notifikaci, že byl uživatel úspěšně přihlášen.

Co dále

Příště se můžete těšit na druhou část, která bude zaměřena na serverovou část, podíváme se na implementaci v Node.js. Kromě toho se také budeme zabývat tím, aby se uživateli nezobrazovaly odkazy, které vedou tam, kam nemá přístup a podíváme se na to, co třeba  znamená tajemná zkratka CORS.

Na tvorbě tohoto článku se svými připomínkami podílel také Pavel Lang. Díky!

Komentáře

Subscribe
Upozornit na
guest
31 Komentářů
Nejstarší
Nejnovější Most Voted
Inline Feedbacks
View all comments
jbub

v nadpise by to asi malo byt „single“ nie „singe“

Srigi

Me: „Do you know much about AJAX?“

Potential employee: „CORS I do“

Me: „Hired“

Vdaka za pekny serial. Paci sa mi ako si to rozbil na niekolko komponentov. Musi to byt radost testovat.

jakubvrana

„Bezpečnostní protokol“ použitý v článku je z několika důvodů špatně. V první řadě je hashovací funkce SHA-1 příliš rychlá a pro ukládání nepříliš složitých hesel nevhodná. Kvůli tomu jde z Y odvodit X a z X heslo, pokud není příliš složité. Lepší by bylo použít třeba Scrypt.

V druhé řadě – pokud útočník získá X, tak už heslo získávat nepotřebuje, protože X mu stačí pro jakoukoliv práci s aplikací. Takže argumentace tím, že ukládat heslo je příliš nebezpečné, proto uložíme něco, co se dá použít prakticky stejně jako heslo, je špatná. Řeší se to pomocí náhodného session identifikátoru, jehož hash na serveru uložíme a z klienta ho posíláme. Ten jednak nemá nic společného s heslem (protože není jediný důvod, aby měl) a jednak má jen omezenou platnost. Takže když ho útočník získá, tak ho může použít jen nějakou dobu. Vymlouvat se na bezstavovost serveru není potřeba, protože stejně používáme databázi jako prvek dostupný odevšud, takže stav (session ID) klidně můžeme uložit tam.

Jakub Vrána

1) Příklad je nešťastný. Stejně tak by v něm mohlo být uvedeno, že pro jednoduchost uložíme heslo v plaintextu a na obhajobu by se dal použít stejný argument. Je potřeba si uvědomit, že použití nevhodné hashovací funkce je srovnatelné s uložením hesla v plaintextu (pokud nemáme stanovené požadavky na dlouhé a složité heslo, což v článku samozřejmě není).

2) Uvedl jsem konkrétní argument, co je na postupu popsaném v článku špatně. Uvedl jsem, jak se s tím vypořádat. Mnou popsaný standardní postup nemá žádnou nevýhodu a není jediný důvod se zuby nehty držet zranitelnějšího řešení popsaného v článku.

Aplikace bezstavová není – stav je uložen v databázi, s kterou aplikace komunikuje. Když už tam ukládáme informace o hesle uživatele, tak se tam dá stejně dobře uložit session identifikátor.

Břetislav Wajtr

2) Musím se pana Vrány v tomto případě zastat. Neříkám, že řešení přes session je všespásné, ale přesto mi sedí více než to vaše. Napadá mě například jak si ve vašem řešení vynutíte znovupříhášení (znovuověření) uživatele. Např v situaci, kdy počítač (notebook, mobil) někdo ukradne a cookie už obsahuje validní token. Nic útočníkovi nezabrání prodloužit si token do nekonečna (stav přeci drží klient) a tím pádem používat aplikaci neomezeně. Dá se to jistě řešit tím, že token bude mít server-side expiraci, ale to už jsme v podstatě u řešení pana Vrány. Píšete, že „Nicméně stejně tak se můžu dostat k session ID a úplně stejně si můžu s aplikací dělat co potřebuji.“, ale rozdíl je v tom, že server nechá session po čase vyexpirovat. Vaše řešení ne.

Nevím, asi mi něco uniká, ale nějakou zásadní výhodu ve vašem řešení (oproti standartnímu) moc nevidím…

Jakub Vrána

Řešení rozhodně standardní není. Žádné rozumné řešení neukládá na klientu heslo ani žádnou jeho odvozeninu. Rozdíl proti ručnímu zadání hesla při každém požadavku je právě v tom, že při ručním zadání není heslo na klientu uloženo.

Heslo a session identifikátor jsou dvě různé věci. Odvozovat druhé z prvního jen proto, abychom nemuseli programovat nějakou funkci, je ledabylost. Navíc funkčnost, kterou tímto dostaneme, je hloupá – abychom se odhlásili z ostatních zařízení, tak si musíme změnit heslo. Proč? Naopak když si změním heslo, tak nemusím být chtít jinde odhlášen. Já si občas měním hesla na citlivých službách, ale rozhodně kvůli tomu nechci být odhlášen ze všech zařízení, kde je používám.

Lepší je tyto funkce rozdělit – změna hesla pouze změní heslo. Odhlášení z ostatních zařízení je triviální operace – v případě session jen smažu session identifikátory daného uživatele, v bezstavovém režimu jen změním centrální session identifikátor (v článku je nešťastně pojmenován X).

Bezpečnostní protokol popsaný v článku zkrátka zbytečně riskuje data uživatele jen proto, aby přinesl funkci, jejíž chování je hloupé. Neexistuje jediný důvod, proč session identifikátor odvozovat z hesla a článek by v této části měl být opraven, aby čtenáře nesváděl na scestí.

Jirka Kosek

HTTP autentizaci snad nikdo soudný nebude používat, ne? (Teď se nabavíme o tom, že spousta aplikací ji používá.)

Jakub Vrána

Řešení popsané v článku se standardními řešeními pokulhává i v bezpečnosti dat uložených v prohlížeči. Při HTTP autentizaci zajišťované prohlížečem např. neexistuje API, které by mi dovolilo získat heslo, pod kterým je uživatel k dané doméně přihlášen. Při session identifikátoru se tento obvykle posílá v HTTP-only cookie, takže je z JS také nedostupný. Řešení popsané v článku data ukládá do normálně přístupného úložiště, takže jediný XSS v aplikaci znamená trvalou kompromitaci potenciálně všech uživatelů. Jediný způsob, jak se z tohoto průšvihu po opravení případného XSS vyhrabat, je změnit algoritmus, který na serveru používáme pro vytvoření proměnné v článku označené jako x. Tím taky všechny uživatele odhlásíme (a postavíme na hlavu bezstavovost aplikace).

Jakub Vrána

Heslo v plaintextu na nic nepotřebuji. Pokud se mi podaří získat X (které je dostupné z JavaScriptu), tak uživatele v aplikaci plně ovládám bez časového omezení. To je fatální rozdíl oproti standardním způsobům přihlašování, kde takovouto hodnotu získat nelze a dveře do aplikace jsou otevřené, jen dokud XSS nezalepím.

Věř tomu nebo ne, ale i ve velkých aplikacích s miliardou a více uživatelů se XSS opravuje velmi často. Pokud by se s každou opravou měli všichni uživatelé odhlásit, tak by za chvíli žádný nezbyl. U malých aplikací bude chyba statisticky nejspíš ještě častější, i když kódu a uživatelů tolik není, takže není tolik vidět. Řešení je to každopádně nepoužitelné.

Správně napsaná aplikace není průstřelná ani při získání databáze s hashi. Stačí, aby byla použita správná metoda uložení hesel (např. scrypt) a aby stejnou metodou byly ošetřeny i náhodně vygenerované session identifikátory. Říká se tomu Security by Design.

Další nápad zmíněný v této diskusi, totiž „bude velmi nepravděpodobné, že dokážeš rozlousknout způsob, jakým byla vytvořena proměnná x“ je ryzí ukázkou přístupu Security through Obscurity. Jeho důsledkem je mimo jiné to, že nikdy nemůžeme propustit žádného zaměstnance a místo toho je musíme zastřelit. A to už je poněkud nepříjemné i z právního hlediska.

Jakub Vrána

Odhlášení po změně hesla není „pokud chci“, ale nevyhnutelné.

Použití standardního mechanismu prohlížeče (HTTP-only cookie) tomu pomohlo, ale bezpečnost ve srovnání s náhodně generovaným session identifikátorem platným omezenou dobu je pořád o řád horší.

Jakub Vrána

Vygenerovaný hash, který server posílá klientovi (v článku označený jako x), lze chápat jako session identifikátor, jen má nekonečnou platnost. Neexistuje jediný důvod, proč by neměl být náhodný a proč by měl být odvozen z hesla. Důvod v článku označený jako výhoda je ve skutečnosti nevýhoda – spojuje dvě funkce, které spolu nijak nesouvisí. Např. Facebook mě dovoluje odhlásit z jiných zařízení, aniž bych si kvůli tomu musel měnit heslo a je to správné chování. Stejně tak si můžu chtít změnit heslo, aniž bych se musel chtít odhlásit z ostatních zařízení. Návrh popsaný v článku obě tyto funkce znemožňuje. Navíc zbytečně vystavuje heslo riziku prolomení.

Břetislav Wajtr

Koukám že jste to řešili celou noc, to jste teda nadšenci :).
Jen krátce:

„když mi někdo ukradne notebook a já si hned změním heslo, ten authToken, který je uložen v prohlížeči, se stane nevalidním a útočníkovi je už k ničemu“

Ano, to jistě. Tzn. že váš bezpečnostní protokol předpokládá akci uživatele, aby došlo k odhlášení na odcizeném zařízení. To mi nepřijde šťastné. Musím se přiznat, že u každé aplikace, kam se přihlašuji, předpokládám, že „session“ (ať už je to cokoliv) po čase vyprší – u vás se to neděje (je to tak?) a to mě trochu děsí

„U klasického řešení přes session…“

Musím se přiznat že já sám k těmto účelům session také nepoužívám, zato po ověření uživatele generuji náhodný authToken (zdůrazňuji náhodný, na hesle nezávislý), který posílám zpět klientovi a zároveň ho ukládám na serveru do distribuované cache (přístupné všem serverům v clusteru) a na serverové straně omezuji čas, kdy je tento token validní. Po vypršení se uživatel musí přihlásit znovu.

„Bezstavovost podle principů REST má řadu výhod a příštím článku se o nich rozepíšu.“

Na článek se upřímě těším. Bezestavovost má opravdu spoustu výhod, v tomto bodě se s vámi přít nebudu. Nicméně „tlačit“ bezestavovost i za cenu snížení bezpečnosti uživatele? To určitě ne… A zatím jste mě nepřesvědčil, že váš bezpečnostní protokol je steně bezpečný jako ty, které využívají stav na serveru…

Jakub Vrána

Autor aplikace má právo si rozhodnout, jak se jeho aplikace bude chovat. Mně u většiny aplikací vyhovuje chování, že pokud si změním heslo, tak kvůli tomu nechci být odhlášen ze všech zařízení, kde aplikaci používám. To s postupem popsaným v článku nejde. Naopak chci mít možnost se odhlásit, aniž bych si kvůli tomu musel měnit heslo. Tuto funkci bych tedy měl naprogramovat tak jako tak.

Požadavek Břetislava je zcela oprávněný. Změna hesla je přesně ta akce uživatele, o které Břetislav píše. Pokud si neuvědomím, že k zařízení, kde jsem přihlášen, má přístup někdo cizí, tak očekávám, že mě z něj aplikace sama časem odhlásí, aniž bych musel cokoliv dělat. Délkou této doby samozřejmě vyvažujeme pohodlnost a bezpečnost, ale je chyba dobu nijak neomezit (a tím na bezpečnost rezignovat).

ivoszz

… s křížkem po funuse, ale přijde mi poněkud nešťastné v článku zmiňovat SHA1 v době, kdy je součástí standardní knihovny crypto metoda [crypto.pbkdf2(password, salt, iterations, keylen, callback)](http://nodejs.org/api/crypto.html#crypto_crypto_pbkdf2_password_salt_iterations_keylen_callback).

Enum a statická analýza kódu

Mám jednu univerzální radu pro začínající programátorty. V učení sice neexistují rychlé zkratky, ovšem tuhle radu můžete snadno začít používat a zrychlit tak tempo učení. Tou tajemnou ingrediencí je statická analýza kódu. Ukážeme si to na příkladu enum.