Jakieś trzy godziny zajęło mi napisanie prostego systemu rejestracji i logowania. Możecie sobie pomyśleć, że to całkiem sporo. I słusznie. Szczególnie dla ludzi pochodzących ze środowiska railsowego, przyzwyczajonych że rails generate devise:install i rake db:migrate praktycznie załatwi im sprawę, może to być sporo. Czy jednak powinno to dziwić?

Logowanie, uwierzytelnianie, autentykacja... Jakkolwiek by tego nie nazwać, jest to jedna z podstawowych funkcji prawie każdej aplikacji webowej. Tego typu kwestie (wraz z np. uploadem plików) nazywam funkcjami podstawowymi – a więc czymś, co robi się w prawie każdym projekcie i naprawdę nie chce się człowiekowi za każdym razem spędzać kilku godzin tylko po to, żeby kolejny raz zrobić praktycznie to samo. W dodatku nie ma to nic wspólnego (zwykle) z merytoryczną zawartością projektu ani z logiką biznesową. Miejmy więc to jak najszybciej za sobą...

Właśnie ten sposób myślenia przyczynił się do stworzenia takich one-click-authentication jak wspomniany Devise. Nie mówię, że jest to narzędzie złe. Używałem go wiele razy i za każdym razem pozwalał mi szybko wystartować z projektem, móc projektować moje piękne modele, minimalistyczne kontrolery itd., bez zaprzątania sobie głowy wyborem pomiędzy bcryptem a scryptem. Ale... Nic nie przychodzi bez dodatkowych kosztów.

Tak naprawdę napisanie własnego systemu autentykacji jest proste, szczególnie kiedy wie się, jak to zrobić. Pisałem taki nie tak dawno w Hanami. Zajął mi 15 minut i pod względem bezpieczeństwa zasadniczo niczym nie ustępował sprawdzonym gotowym rozwiązaniom. Ot, baza danych, tabela users, kolumna email oraz crypted_password, jeden z wymienionych wyżej algorytmów lub SHA-512 (byle nie MD5) i gotowe. Diabeł, jak zwykle, tkwi w szczegółach.

A może OAuth?

Jeśli chcecie wiedzieć więcej na temat tego, czym OAuth jest, współtowarzysz niedoli Daj Się Poznać przedstawił temat całkiem nieźle. Natomiast w skrócie telegraficznym, chodzi o logowanie przy pomocy Githuba, Twittera, Google czy jednego z setek innych providerów. Widzimy to często i nie ma co ukrywać, że jest to wygodne. Dlatego kiedykolwiek rozpoczynacie komercyjny projekt, oczekujcie że w ciągu trzech miesięcy klient przyjdzie i rzuci: "A może by tak logowanie Facebookiem?". Gwarantuję, przyjdzie, choćby wcześniej zarzekał się, że na pewno logowanie przy pomocy emaila i hasła wystarczy.

Co wtedy? Cóż, skoro napisaliśmy nasz system tak, jak powyżej, musimy go rozszerzyć. Klasyczne podejście każe w tym momencie dodać kolumnę oid, w której przechowywany będzie identyfikator zwracany przez providera OAuth (w naszym wypadku Facebooka). Działa to tak, że po kliknięciu przyciski "Zaloguj przez Facebooka" zostaniemy na niech na chwilę przekierowani, po czym nasza aplikacja na wybrany przedtem adres zwróci nam garść danych o użytkowniku, w tym wspomniany unikalny identyfikator. Stąd droga jest już prosta – sprawdzamy w bazie czy użytkownik z takim identyfikatorem istnieje i logujemy go lub tworzymy nowego.

Proste, klient zadowolony i wydaje się, że wszystko jest OK, ale nie dajcie się zmylić. Już za kilka tygodni ktoś zażyczy sobie, że on konta na FB nie ma, więc chce się logować Googlem. Co wtedy? Być może identyfikatory Facebooka i Google'a różnią się znacząco, ale nie możemy sobie tutaj pozwolić na błąd. Dodajemy kolejną kolumnę oauth_provider z informacją o tym, skąd przychodzą dane uwierzytelniające. Myślimy że wszystko teraz będzie już okej, ale gdzie tam. Za chwilę okaże się, że użytkownicy chcą sobie podpiąć i Google, i Facebooka i jeszcze VKontakte (bo czemu nie?) do jednego konta. Da się? Nie da się1. Musimy więc stworzyć dodatkową tabelę (co powiecie na oauth_credentials?) i powiązać ją z tabelą users...

Gratuluję. Niniejszym inkrementalnie właśnie osiągnęliśmy poziom złożoności Devise'a!

Do czego on dąży?

Mam szczerą nadzieję, że właśnie zadajecie sobie to pytanie. Ale jeśli nie, to i tak na nie odpowiem.

Celem całego wywodu jest pokazanie, że wydający się czymś naprawdę prostym system uwierzytelniania ma potencjał rozrośnięcia się do całkiem niezłego skomplikowania. Dlatego być może czasem warto na chwilę usiąść i zastanowić się czego do niego użyjemy, aby nie obudzić się za jakiś czas z rozwiązaniem, którego nasza biblioteka, z którą jesteśmy bardzo mocno powiązani i ciężko będzie to odkręcić, nie obsługuje.

Co zrobiłem ja? Użyłem komponentu Überauth. Wybrałem go świadomie, ponieważ bardzo przypomina mi rubiowe Omniauth. I choć nadal logowanie u mnie jest możliwe wyłącznie przy pomocy maila i hasła, "zachowuje się ono" w pewnym sensie jak gdyby było OAuthem. W tym wypadku rolę providera spełnia aplikacja, którą właśnie piszę, i podpięta do niej baza danych. Dlatego nie będę miał problemów w przyszłości z dodaniem logowania przez Githuba albo cokolwiek innego. Zachęcam do podobnego podejścia. Nie jestem niestety w stanie wskazać podobnych rozwiązań dla innych języków, jeśli jednak takie znajdziecie to możecie mi podesłać – postaram się stworzyć z tego jakąś listę.

Lekcja na dziś

Nawet w wypadku bardzo prostych funkcji aplikacji webowej czasami warto się zastanowić nad rozwiązaniem, które właśnie chcemy zastosować. Czy nie zwiąże mnie ono zbyt bardzo z konkretną bazą danych? Albo z konkretnym przepływem zachowań użytkownika, których będę chciał w przyszłości uniknąć2? Czasem warto poświęcić nieco więcej czasu na rozwiązanie, które da mi większą elastyczność w przyszłości. Szczególnie w środowisku, w którym spodziewam się zmieniających się wymagań co do produktu.


  1. Nawiasem mówiąc ten błąd popełnili kiedyś twórcy GitLaba (teraz już chyba można), więc jeśli o tym nie pomyśleliście wcześniej to jesteście w godnym gronie. 

  2. Niektóre serwisy wysyłając mailing zawierają w nim link ze specjalnym tokenem, który pozwala zalogować się bez podawania hasła. Pomijając wiążące się z tym kwestie bezpieczeństwa, życzę powodzenia w implementacji czegoś takiego przy pomocy tightly-coupled rozwiązań do uwierzytelniania.