Tento článek píšu částečně proto, že jsem se sám chtěl důkladněji podívat na to, co se děje při cookie authentikaci, a částečně pod vlivem prezentace Authentication Is Hard, Let's Ride Bikes, kterou vám tímto doporučuji, ač je o něčem jiném.
Co se tedy děje. Při přihlášení pošle browser nějaké to jméno a heslo, server jej ověří, řekne browseru: V dalších požadavcích mi posílej toto cookie. Při odhlášení server řekne browseru: Zruš cookie. Teď si napíšeme malou aplikaci, která tohle dělá. Udělejte si už klasicky, virtualenv, do kterého nainstalujete pyramid a waitress. Pak si do něj naklonujte tricycles. Při spouštění jednotlivých kroků pamatujte, že máme malou samostatně stojící aplikaci bez pasteru, takže když změníte kód, aplikace se vám nerestartuje.
A teď už ke kroku 1, který dělá to, co jsem popsal nahoře.
@view_config(route_name="login") def login_view(self): userid = self.request.params.get("userid") response = self.response(["LOGGED IN", userid ]) response.set_cookie("userid", str(userid)) #response.set_cookie("userid", str(userid), httponly=1) #response.set_cookie("userid", str(userid), max_age=600) # 600 seconds - survives browser restart #response.set_cookie("userid", str(userid), secure=1) # secure flag return response
View login bychom samozřejmně dělali ověření uživatelova hesla. Vyzkoušejte si variantu a max_age, při které ta cookie přežije restart browseru (ovšem máme nastaveno 5 minut, takže rychle). Taky si všimněte příznaku secure. Nenechte se mýlit, ten neudělá tu cookie nějak zázračně bezpečnou, ale povolí browseru její posílání jen po zabezpečeném spojení. Nejdůležitějsí je ale příznak httponly. Ten si v praxi určitě zapneme, protože znemožní sáhnout na cookie ze skriptu, přes document.cookies. V logoutu naopak řekneme browseru, ať cookie zapomene.
@view_config(route_name="logout") def logout_view(self): response = self.response(["LOGGED OUT" ]) response.delete_cookie("userid") return response
Teď v chrome console napište
document.cookie="userid=frank"a klikněte na HOME. Jednoduché, ale naprosto očekávané. Proto v kroku 2 cookie podepíšeme.
Nejprve varování: Tento kód je ukázka. Nepoužívejte ho pro žádné seriózní účely. Raději použijte váš framework, který to dělá rychleji, lépe, a konzistentně. A teď už k tomu podepisování. Probíhá to tak, že vezmeme nějaká data, k nim přidáme nějaké tajemství, které zná jen naše aplikace, a uděláme hash. Do cookie dáme tento hash, naše userid, a ještě časové razítko. Když přijde cookie aplikaci, tak z něj vyextrahuje userid a časové razítko, spočítá hash, a porovná s hashem, který přišel v cookie. Pokud jsou různé, něco je špatně.
def _decode_cookie(self): cookie = self.request.cookies.get("userid", None) if not cookie: return None # try to extract a userid and timestamp from the cookie try: (digest, ts, userid) = cookie.split("-", 2) logging.info("cookie splitted up:%s-%s-%s" % (digest, ts, userid, )) except: logging.error("BAD COOKIE FORMAT:%s|" % cookie) return None ip = "" d2 = calculate_digest(self.SECRET, userid, ts, ip) if d2==digest: return userid logging.error("bad digest") return None
A v loginu (po úspěšném ověření hesla) naopak vytvoříme novou cookie:
def _encode_cookie(self, userid): ip = "" ts = int(time.time()) digest = calculate_digest(self.SECRET, userid, ts, ip) return "%s-%s-%s" % (digest, ts, userid)Vlastní počítání hashe v mém případě jen použije funkci sha1 na všechno, co do ní přijde, a jak jsem psal, takhle to nedělejte, použijte Váš framework.
Samozřejmně použijeme opět cookie s onlyhttp, ale pro tu legraci si to na chvíli vypněte, a zkuste změnit obsah cookie.
V kroku 3 v rámci aspoň trochy politické korektnosti v okamžiku detekce špatné cookie nebudeme prostě říkat, že nikdo není přihlášen, ale vrátíme HTTP Bad Request. Tady si všiměte pěkné věci. V Pyramid jsou httpexceptions normální Response, takže já si ji vytvořím, pak nastavím cookie, a pak ji vyvolám. Kdybych nenastavil cookie, tak by mi klient ji pořád posílal, a já bych pořád vracel HTTP Bad Request.
def _decode_cookie(self): # [...] response = HTTPBadRequest() response.delete_cookie("userid") raise response
Na závěr krok 4 jako bonus: podle návodu ve zmíněném článku na zdrojáku uděláme to, aby nás tento systém automaticky odhlásil na všech ostatních zařízeních při změně hesla. V tom článku se heslo použije k výpočtu hashe, který se pak posílá zpět v cookie. Mě je posílání hesla (ač zahashovaného) proti srsti, proto raději při změně hesla vygeneruju náhodný řetězec, který použiju jako per-user sůl při vytváření hashe. Zase platí, že tohle je ukázka a že v praxi použijeme rozumný zdroj náhodného řetězce a vygenerujeme rozumnou délku (a použijeme rozumnou hashovací funkci).
@view_config(route_name="passwd") def passwd_view(self): # When the user changes the password, we generate a new random string # and "store it into the database with the password". del self.USER_SALT[self.identity] # Then we re-generate the cookie, otherwise we will be logged out. response = self.response(["PASSWORD CHANGED", self.identity ]) response.set_cookie("userid", self._encode_cookie(self.identity), httponly=1) return response
Pyramid Cookie auth například ještě podporuje Cookie Reissue. To funguje tak, že si řeknete, že ticket v cookie platí třeba 20 minut (timeout), a že pokud je v příchozím requestu aspoň 2 minuty starý (reissue_time), tak se má vygenerovat nový. Takže bude fungovat odhlášení po dvaceti minutách neaktivity.
Druhá věc, kterou by bylo dobré zmínit, je ochrana proti CSRF. K tomu se asi dostanu až v dalších dílech dvojité alchymie.
Jmenuju se Petr Blahoš. Programuju něco přes 20 let. Tady se snažím psát hlavně o Pythonu, webovém frameworku Pyramid, a občas i o něčem úplně jiném.
Přečteno 19 309×
Přečteno 11 880×
Přečteno 9 408×
Přečteno 8 866×
Přečteno 8 651×