Autentizace bez hesel

Je to už nějaká chvíle, co se Internetem šířila zpráva o chybě Heartbleed. Mimo jiné ukázala, že autentizace pomocí hesel není dokonalá. Pojďme si ukázat, jak implementovat autentizaci, která hesla nevyžaduje.

Stručný úvod

Chyba Heartbleed ukázala, že autentizace pomocí hesel má své slabiny. Jak nejspíše víte, díky nedostatku v knihovně OpenSSL mohl útočník získat vaše heslo s uživatelským jménem ze serveru vámi používané služby. Všichni zodpovědní provozovatelé webů, kterých se toto týkalo, začali posílat varovné emaily, které nás vyzývaly ke změně hesla.

Z vaší strany uživatele si stačí tedy vytvořit nové heslo, nejlépe u každé služby, kterou na Internetu používáte. Snadné? Pokud jste zaregistrováni na spoustě webů, zabere vám to nezanedbatelně dlouhou dobu. Dostatečně dlouhou na to, abyste si mohli postěžovat, že jste ji museli obětovat kvůli něčemu, co jste nezpůsobili.

Přitom by stačilo hesla z autentizačního procesu odstranit. Potom byste vy po další podobné chybě nemuseli dělat vůbec nic. Vše by obstaral provozovatel. Vás by pak jenom o útoku/chybě informoval, ale také ujistil, že jste zase v bezpečí. Ale jak na to?

Email a SMS

Světe div se, ale takový systém už dlouhou dobu používáme, a sice při resetování našeho hesla. Tento proces většinou sestává z těchto kroků:

  1. Kliknete na odkaz “Zapomenuté heslo”
  2. Služba vám na email pošle zprávu s dočasným odkazem pro jeho resetování
  3. Link vás přesměruje na stránku, kde si můžete nastavit heslo nové

Občas se vás ještě ptají na jméno vašeho domácího mazlíčka nebo příjmení babičky za svobodna, ale to je pouze doplněk. Stejný princip (pouze s SMS) je vlastně použit také u dvoukrokového přihlášení, kdy se vám po prvním kroku pošle na mobil dočasný kód, který musíte zadat do aplikace.

Na co tedy potřebujeme hesla? Tento anglický článek hezky popisuje, jak funkci “Zapomenuté heslo” využít tak, abyste si museli pamatovat jenom jedno – k vaší mailové schránce. Způsob to ale není dokonalý, vyžaduje krok navíc – zvolení nového hesla. Jak by tedy měla vypadat autentizace bez hesel?

  1. Při přihlašování nám od uživatele stačí pouze emailová adresa nebo telefonní číslo
  2. Naše aplikace vygeneruje dočasný token a uloží si ho do databáze
  3. Pošleme email nebo SMS s tímto kódem uživateli (mezitím mu můžeme nabídnout omezený přístup – například veřejné informace, které stejně vidí ostatní)
  4. Při kliknutí na odkaz (nebo zadání kódu) naše aplikace obdrží daný token a zkontroluje, zda je platný
  5. Pokud ano, vytvoří nový s dlouhou platností, uloží ho do databáze a na klientovi
  6. Uživatel je nyní přihlášen (tento proces není potřeba opakovat, dokud nevyprší token nebo se uživatel nechce přihlásit na novém zařízení)

Výhody

Jaké z toho plynou výhody? Představte si přihlašovací formulář pouze s jedním políčkem. Není to krásné? Nechme ale nyní stranou uživatelské rozhraní a zaměřme se na bezpečnost. Pokud by každý web implementoval tento systém, uživatelovi si stačí pamatovat pouze jediné heslo, a to ke svému emailu. Toto heslo může být velmi silné (jedno takové si uživatel zapamatuje), aby bylo odolné vůči brute-force útoku. Je taky možné, že provozovatel mailové schránky může nabídnout takovou podporu při bezpečnostním problému, kterou si my třeba nemůžeme dovolit.

A teď konečně – co když se objeví chyba podobná Heartbleedu? Potom si stačí změnit pouze jedno heslo (což takový problém není). O všechno ostatní by se zodpovědní provozovatelé služeb měli postarat sami. Na jejich straně stačí pouze zneplatnit všechny tokeny (a samozřejmě opravit danou chybu). Vy se budete muset pouze zase přihlásit. Tohle stačí a vaše účty jsou opět na nějakou dobu v bezpečí.

Dále je to ochrana pro líné a nezodpovědné uživatele, kteří volí hesla typu “password”, “123456”, “qwerty” nebo “000000”. Při klasickém přihlašování, tím, že jim taková hesla zakážete, je jen naštvete. Navíc pravidla typu minimální počet znaků a co musí heslo obsahovat vadí i ostatním uživatelům a navádí je do podobné situace.

V neposlední řadě můžete s trochou snahy implementovat to, aby se bylo možné odkudkoliv odhlásit z určitého (nebo všech) zařízení. Protože je jedno, zda na straně klienta budete stav o jeho přihlášení uchovávat pomocí cookies nebo třeba local storage, vždy byste měli kontrolovat, zda je daný token platný i na serveru. Pokud není, uživatel není přihlášen.

Implementace

Nyní si ukážeme způsob, jak by se dal tento systém přihlašování implementovat v té nejjednodušší podobě. Ukázky budou psané v pseudo-kódu, abyste mohli použít váš nejoblíbenější jazyk, framework či databázi. Budu se zabývat pouze emailem, ale myslím, že s trochou snahy zvládnete SMS variantu vytvořit sami.

Model

V našem modelu uživatele budeme potřebovat pouze následující atributy: email a tokens. Políčko email je jasné.

Pojďme si vysvětlit tokens. Pokud chceme, aby se mohl uživatel přihlásit na více zařízeních, potřebujeme aby tento atribut mohl uchovávat více hodnot. V relačních databázích tohoto docílíme novou tabulkou Tokens, kterou s uživatelem spojíme pomocí one-to-many relace. Zato ve většině NoSQL databázích můžeme ukládat pole hodnot. To se nám bude hodit.

Záznam tokenu by měl obsahovat minimálně hodnotu tokenu a čas expirace. Token musí být náhodný řetězec. Snad každý jazyk nabízí možnost, jak generovat kryptograficky bezpečnou sekvenci znaků, nebo by k němu měla alespoň existovat knihovna. A co délka tokenu? To záleží na vaší bezpečnostní politice, ale mělo by stačit 16 znaků.

Dále budeme mít model s názvem třeba LoginRequest. Když se uživatel bude chtít přihlásit, vezmeme jeho ID a vygenerujeme dočasný token. Obě hodnoty vložíme do tohoto modelu opět spolu s časem expirace. Při ověřování uživatele přes odkaz budeme hledat v LoginRequest. Pokud ověření proběhne úspěšně, smažeme daný záznam v LoginRequest a přihlásíme uživatele vytvořením tokenu s dlouhou expirační dobou v User modelu.

Aplikace

Náš controller/presenter/cokoliv Authenticator bude obsahovat tyto metody:

  • Login(email) – vloží záznam do LoginRequest a pošle autentizační email
  • Authenticate(userid, token) – projde LoginRequest a zkontroluje, jestli jsou údaje platné. Pokud ano, vytvoří token u příslušného uživatele v modelu User
  • EnsureAuth(userid, authtoken) – umožňuje klientské aplikaci zjistit, zda je token, který má uložený, stále platný (tato metoda je užitečná spíše v SPA aplikacích)
  • Logout(userid, authtoken) – Vymaže uživatelův token patřící zařízení

Registraci zde nebudu řešit. Stejně se nijak zvlášť neliší od té klasické, pouze chybí pole “heslo” a “heslo znovu”. Pojďme se podívat na metodu Login:

user = User::FindByEmail(Request::email)

if user exists
    token = Token::Generate()
    //uživatelské ID, náhodný token, datum a čas za půl hodiny
    LoginRequest::Add(user::id, token, DateTime::NowPlusMinutes(30))
    //pošleme uživatelovi přihlašovací email s danými údaji
    Email::Template("login")::To(user::email)::Send(user::id, token)
    //všechno v pořádku
    Response::Send(200, "Byl vám odeslán autentizační email")
    
else
    //uživatel s danou adresou neexistuje
    Response::Send(404, "Uživatel s tímto emailem neexistuje")

Nejdříve ověříme, zda uživatel existuje. Pokud ano, vložíme záznam do modelu LoginRequest. Daný token platí půl hodiny. Pro přihlašovací token by bylo víc času na škodu. Třicet minut je více než dost. Pošleme email, ve kterém bude odkaz ve tvaru třeba takovémhle: mujweb.cz/authenticate?userid=<user::id>&token=<token>. Zajímavější je samotná autentizace (Authenticate):

userid = Request::userid
token = Request::token

record = LoginRequest::FindByUserId(userid)

//je token správný
if token is record::token
    //a je ještě platný?
    if record::expiration is still valid
        //autentizační token s dlouhou platností
        authtoken = Token::Generate()
        user = User::FindById(userid)
        user::tokens::Add(authtoken, DateTime::NowPlusDays(14))
        //uložíme si uživatele do nějakého našeho session objektu
        Session::Set("user", user)
        //pošleme token zpět, aby si ho mohl klient uložit
        //také vrátíme objekt uživatele
        Response::Send(200, authtoken, user)
        
    else
        //smažeme požadavek o přihlášení z modelu
        LoginRequest::Remove(record)
        Response::Send(401, "Platnost tokenu vypršela")
        
else
    Response:Send(401, "Neplatný token")

Pokud tokenu již vypršela platnost, smažeme ho. Jinak ho ale necháváme uložený. Proč? Uživatel se tak může na jedno přihlášení autentizovat na všech svých domácích zařízeních. Stačí pouze potvrdit odkaz na všech přístrojích a je hotovo. Ani nemusí chodit na přihlašovací obrazovku vaší aplikace. Problém může být obrana proti brute-force útoku. Půl hodiny je však podle mého názoru dostatečně málo na prolomení.

Jinak je, myslím, vše jasné. Doba platnosti tokenu je opět na vás – 14 dní je taková zlatá střední cesta. V reálné aplikaci by bylo asi vhodné nabídnout uživateli klasický “zapamatovat” check box. Pokud nechce, abychom si ho zapamatovali, žádný token ani ukládat nemusíme. Postačí nám objekt v naší Session.

Na řadu přichází metoda EnsureAuth, která je vhodná především pro SPA aplikace, které mají svůj token uložený například v local storage, kam se ze serveru nedostaneme. Pokud však máte klasickou serverovou aplikaci a token uložený například v cookies, hned na serveru víme, zda je uživatel přihlášen či ne.

user = Session::Get("user")

//pokud náhodou ještě nemáme uživatele uloženého v Session
if user exists
    Response::Send(200, user)
    
else
    userid = Request::userid
    authtoken = Request::authtoken
    
    user = User::FindById(userid)
    record = user::tokens::FindByToken(authtoken)
    
    if record exists
        if record::expiration is still valid
            //můžeme obnovit čas expirace
            record::expiration = DateTime::NowPlusDays(14)
            record::Save()
            
            Session::Set("user", user)
            Response::Send(200, user)
            
        else
            //vypršelý token už nebudeme potřebovat
            user::tokens::Remove(record)
            Response::Send(401, "Byl jste již odhlášen")
        
    else
        Response::Send(401, "Nejste přihlášen")

Myslím si, že kód mluví sám za sebe. Nejdříve zkoušíme, zda nemáme uživatele uloženého v našem Session objektu. To se může stát, pokud je třeba otevřen nový panel s naší aplikací a nově načtený klientský kód si bude chtít ověřit, zda je uživatel přihlášen. Pokud záznam v Session neexistuje, ověřujeme platnost posílaného tokenu. Pokud tento proces proběhne úspěšně, uložíme si objekt uživatele pro pozdější použití a pošleme ho také klientovi. Uživatel je přihlášen. Případně můžeme prodloužit platnost tokenu na dalších 14 dní. Pokud jakýkoliv krok v našem ověřování selhal, pošleme klientovi chybovou zprávu.

Jak se v této metodě bránit proti brute-force útoku? Útočník, pokud by náhodou věděl ID uživatele, může zkoušet uhodnout autentizační token. Pomineme-li fakt, že trefit náhodnou kombinaci 16 znaků je přinejmenším velmi obtížné, můžeme si zaznamenávat počet neúspěšných pokusů. Naše správně napsaná klientská část aplikace by se totiž měla zeptat neúspěšně nanejvýš jednou. A to jen, když má u sebe uložený token, který již není platný. Když server odpoví záporně, token se ze zařízení klienta musí smazat.

Jako poslední nám zbývá implementovat odhlášení pomocí Logout:

userid = Request::userid
authtoken = Request::authtoken

user = User::FindById(userid)
user::tokens::RemoveWhereToken(authtoken)
Session::Delete("user")

Response::Send(200, "Byl jste úspěšně odhlášen")

Stačí pouze odstranit daný token z databáze a vymazat objekt uživatele ze Session.

A to je vše. Ukázali jsme si naivní implementaci (neošetřujeme mnoho možných chyb, určitě by šlo přidat další bezpečnostní prvky, atd.) bezheslové autentizace. Tento základ se dá samozřejmě dále rozšiřovat. Prostoru k vylepšování je mnoho. Tak to zkuste. Důležité je také říct, že by vaší databázi prospělo občasné vyčištění od již neplatných tokenů. Záleží na počtu uživatelů, ale jednou za čas byste mohli skriptem projít všechny záznamy v User i LoginRequest a zkontrolovat jejich platnost. Ty s vypršelou expirační dobou totiž v databázi nepotřebujeme.

Námitky

K tomuto systému může mít spousta lidí určité výhrady. To je pochopitelné, nic není dokonalé a nic se nedokáže zavděčit všem. Zkusil jsem vymyslet nebo sesbírat několik námitek, které by mohl uživatel mít:

Nezajímá mě bezpečnost, ale pohodlnost!

Tak to je vaše rozhodnutí. Ale pokud zůstáváte přihlášený na vašem zařízení delší dobu (neustále), zas takové komplikace vám tento systém nepřinese.

Jsem zvyklý na dosavadní systém. Nechci nic měnit!

Na každou změnu se dá zvyknout. Pokud v tomto řešení nevidíte zvýšení bezpečnosti, ale pouze jen znesnadnění procesu přihlašování, moc mě to mrzí.

Nezahltí mi přihlašovací emaily mou schránku?

Zprávy můžete ihned po přihlášení mazat nebo si schránku nastavit tak, aby takové emaily mazala třeba den po doručení. Také je třeba připomenout, že pokud na svém zařízení zůstáváte přihlášeni, zas tolik zpráv vám chodit nebude.

Co když získá někdo přístup k mému emailu? Dostane tak možnost přihlásit se na každý můj účet.

To je opravdu nemilé, ale stejné nebezpečí hrozí i při současném systému, kdy útočník může zresetovat všechna vaše hesla, když má k vašemu emailu přístup. Tato situace může být vyřešena například pomocí sekundárního emailu, na který si zresetujete heslo vašeho primárního. Nebo použití mobilu. Případně kontaktovat provozovatele vaší schránky a dohodnout řešení s ním.

Kamarád si může vzít můj mobil a přihlásit se, když mu na něj dorazí autentizační kód.

Když dnes získá kamarád přístup k vašemu mobilu, může resetovat všechna vaše hesla pomocí emailu, který máte s mobilem synchronizovaný. Takový člověk ale není kamarád.

Co když se vám někdo nabourá do databáze? Pak bude mít přístup ke všem mým tokenům a pomocí jednoho se bude moci přihlásit.

To je pravda a při situaci, kdy se někdo dostane do databáze, je tento systém mnohem zranitelnější než řešení s hesly. Když se toto stane, je průšvih. Provozovateli však stačí smazat všechny platné tokeny, hned jak se o útoku dozví. Kdežto heslo vám jen tak nezmění (přestože se to může stát). Pak už se útočník nepříhlásí. Ale možnost, že se dostane na váš účet tu je, nicméně se ho nemůže plně zmocnit. Závažnější změny účtu by měly být potvrzovány opět emailem, který máte vy. A hned jak provozovatel provede daná opatření, znovu přihlásit se můžete zase jenom vy.

Závěr

Toto řešení není vhodné pro aplikace, kde je bezpečnost opravdu na nejvyšším místě. Mám na mysli banky, platební sytémy a podobně. Zde je potřeba zajistit neprůstřelné přihlašování. Taková řešení ovšem nebývají tak pohodlná, a proto je zbytečné je mít všude. A právě tam, kde doteď stačila pouhá hesla, by se mohla objevit bezheslová autentizace. Pokud máte otázky, námitky proti tomuto systému či návrhy na vylepšení implementace, podělte se v diskuzi.

Hotová řešení

NoPassword pro

Další čtení

Klidný, nekonfliktní a skromný člověk, vždy s úsměvem na tváři. Vývojář, co se snaží prorazit do velkého světa vývoje softwaru. V tuto chvíli se nejraději topí v bažinách JavaScriptu a dalších webových technologií. Miluje čerstvé nápady, liberální řešení a minimalismus.

Věděli jste, že nám můžete zasílat zprávičky? (Jen pro přihlášené.)

Komentáře: 24

Přehled komentářů

pepca
Petr Nevyhoštěný Re:
pepak Bezpečnost??
Petr Nevyhoštěný Re: Bezpečnost??
Petr Dobrý nápad
Petr Nevyhoštěný Re: Dobrý nápad
krab Re: Dobrý nápad
Tomáš Myšík Hashování tokenů
Petr Nevyhoštěný Re: Hashování tokenů
Stanislav Nechutný
Petr Nevyhoštěný Re:
Jakub Pekny clanek
Bubla Dost nepohodlné řešení
Petr Nevyhoštěný Re: Dost nepohodlné řešení
danaketh
Petr Nevyhoštěný Re:
LH
Pavel D. A co když nechci (kvůli bezpečnosti), aby uživatel zůstával do mé služby přihlášený?
Petr Nevyhoštěný Re: A co když nechci (kvůli bezpečnosti), aby uživatel zůstával do mé služby přihlášený?
zlatkofedor1 JSON web token
Petr Nevyhoštěný Re: JSON web token
kominár Dalsia diera...
Petr Nevyhoštěný Re: Dalsia diera...
filip.jirsak OpenID, pomalé doručení e-mailu
Zdroj: https://www.zdrojak.cz/?p=12189