Projekt Bilet #3 — Konfigurujemy Spring Security oraz OAuth

By 6 August 2017 Projekt Bilet

W tej lekcji skon­fig­u­ru­je­my auto­ryza­cję użytkown­ików naszej aplikacji z uży­ciem mech­a­niz­mu OAuth oraz bazy danych.

Najprost­szym pode­jś­ciem, jeśli chodzi o rejes­trację i uwierzytel­ni­an­ie użytkown­ików z punk­tu widzenia pro­gramisty jest po pros­tu prze­chowywanie loginu (lub adresu email) oraz hasła (a raczej jego skró­tu) w bazie danych. Cza­sem takie pode­jś­cie ma też uza­sad­nie­nie biz­ne­sowe (np. aplikac­ja bankowa raczej nie powin­na pozwalać na logowanie się za pomocą kon­ta Google), ale w więk­szoś­ci przy­pad­ków jest po pros­tu utrud­nie­niem dla naszych użytkown­ików i może negaty­wnie wpłynąć na ich wraże­nia i ilość odwiedzin.

Z tego powodu w pro­jek­cie bilet wyko­rzys­tamy tech­nologię OAuth, dzię­ki której nie będzie trze­ba wymyślać loginów i haseł oraz prze­chodz­ić przez pro­ces rejes­tracji — wystar­czy być zal­o­gowanym np. w Google i kliknąć ‘OK’ na odpowied­nim ekranie. Taki pro­ces wyma­ga nieco więcej kon­fig­u­racji po stron­ie aplikacji, ale dzię­ki temu nasza aplikac­ja będzie bez­pieczniejsza (nie musimy martwić się o bez­pieczeńst­wo haseł prze­chowywanych w bazie danych, robo­ty tworzące wiele kont z automatu itp)

Autoryzacja użytkowników z użyciem OAuth

Uwa­ga! Ta sekc­ja nie ma na celu szczegółowe omówie­nie specy­fikacji OAuth i w pewnych miejs­cach sto­su­je uproszczenia i skró­ty myślowe — chodzi o to, aby przekazać ‘kon­cept’ i ogól­ny mech­a­nizm dzi­ała­nia, szczegóły imple­men­tacji są na tym etapie mniej istotne. Zain­tere­sowanych szczegóła­mi tech­niczny­mi oraz dokład­nym opisem odsyłamy do doku­men­tacji źródłowej.

Co to jest?

W najprost­szym pode­jś­ciu pro­ces uwierzytel­ni­a­nia użytkown­i­ka opar­ty jest o parę login (lub adres email) oraz hasło. Założe­niem w tym wypad­ku jest to, że hasło znamy tylko my i trud­no jest je odgad­nąć. Nieste­ty, takie pode­jś­cie współcześnie nie jest wygodne dla użytkown­ików — biorąc pod uwagę ilość ser­wisów i aplikacji z których korzys­tamy na codzień lub okazyjnie, ilość unikalnych haseł do zapamię­ta­nia prz­eras­ta możli­woś­ci więk­szoś­ci osób. Jed­nocześnie więk­szość osób ma kon­to u jed­nego z ‘zau­fanych’ dostaw­ców — np. Google, Yahoo, GitHub, Microsoft itp. A gdy­by tak móc użyć tego kon­ta z ‘zau­fanego’ ser­wisu w innych aplikacjach?

Takie pode­jś­cie miało­by wiele korzyś­ci — użytkown­ik musi­ał­by pamię­tać tylko jed­no hasło, przez co mogło­by ono być bardziej skom­p­likowane lub wsparte dodatkowy­mi mech­a­niz­ma­mi (np. kodem SMS czy token­em sprzę­towym). Potenc­jal­nie moglibyśmy także korzys­tać z infor­ma­cji o użytkown­iku zgro­mad­zonych w tym cen­tral­nym miejs­cu — adres email, numer tele­fonu czy ‘avatar’ i strona inter­ne­towa, nie było­by potrze­by wpisy­wa­nia tych danych wile razy i aktu­al­i­zowa­nia ich wszędzie. Brz­mi kuszą­co i nieste­ty dość utopi­jnie :) Pow­stało wiele stan­dard­ów mniej lub bardziej związanych z tym kon­ceptem — OpenID, Oauth, SAML i inne. W przeszłoś­ci inte­grac­ja była dość prob­lematy­cz­na i nie było prostego rozwiąza­nia powszech­nie akcep­towanego przez różnych dostaw­ców. Na szczęś­cie współcześnie mamy OAuth, który jest imple­men­towany prak­ty­cznie przez każdą liczącą się organizację.

Jak to działa?

Jak wiele współczes­nych tech­nologii i stan­dard­ów, także OAuth bazu­je na pomysłach, które sprawdza­ją się od wielu lat. Mowa o sys­temie Ker­beros — opra­cow­anym na MIT sys­temie pozwala­ją­cym na cen­tralne uwierzytel­ni­an­ie użytkown­ików oraz wykony­wanie przez sys­te­my akcji w ich imie­niu. Co ciekawe, jest to jeden z niewielu stan­dard­ów, które Microsoft zaim­ple­men­tował w swoich sys­temach — opro­gramowanie ActiveDi­rec­to­ry, które jest uży­wane w prak­ty­cznie każdej fir­mie do zarządza­nia kon­ta­mi użytkown­ików opiera się o LDAP oraz właśnie pro­tokół Kerberos.

Ker­beros bazu­je na tzw. grantach — grant może zostać wydany na pod­staw­ie np. loginu i hasła użytkown­i­ka lub innego grantu (o takich samych lub mniejszych uprawnieni­ach). Sys­tem, który taki grant posi­a­da, może wykony­wać oper­ac­je w imie­niu danego użytkown­i­ka. Granty wygasają po określonym cza­sie o ile nie zostaną odnowione, moż­na je także anulować.

OAuth to właś­ci­wie uproszcze­nie i prze­niesie­nie tej idei do real­iów inter­ne­tu — głównym kon­ceptem są toke­ny. Token jest gen­erowany za każdym razem, kiedy dana aplikac­ja otrzy­mu­je dostęp do określonych zasobów. Toke­ny mogą być wys­taw­ione na określony czas, mogą być przedłużane oraz uży­wane do wykony­wa­nia oper­acji ‘w imie­niu’ użytkown­i­ka. OAuth określa także jak należy przekazy­wać token pomiędzy sys­tema­mi z uży­ciem pro­tokołu HTTP (w nagłówku Authen­ti­ca­tion z pre­fix­em Bear­er), jakie uprawnienia są z nim pow­iązane (np. pozwala poz­nać tylko adres email lub pozwala na wysyłanie/czytanie wiado­moś­ci w GMail) a także sposo­by uzyska­nia takiego tokenu (nie będziemy się na tym sku­pi­ać, ponieważ leży to w kom­pe­tenc­jach dostaw­cy usług — jeśli chci­ałabyś poz­nać szczegóły poszukaj np. ‘OAuth flow types’).

W najprost­szym przy­pad­ku korzys­tanie z OAuth przez naszą aplikację może prze­b­ie­gać następująco:

  1. Użytkown­ik aplikacji kli­ka w link ‘zaloguj z Goolge’ i jest przekierowany na stronę Google
  2. Użytkown­ik potwierdza udzie­le­nie uprawnień dla aplikacji
  3. Użytkown­ik jest przekierowany z powrotem do aplikacji, wraz z token­em przekazanym w parametrach
  4. Przeglą­dar­ka użytkown­i­ka zapamię­tu­je token np. w ciasteczku i dołącza go do każdego zapy­ta­nia do naszej aplikacji
  5. Aplikac­ja po otrzy­ma­niu zapy­ta­nia może zapy­tać dostaw­cę (np. Google) prosząc o potwierdze­nie, czy token jest nadal ważny oraz zwróce­nie jakichś infor­ma­cji (np. adresu email). Korzys­ta­jąc z pozyskanego w ten sposób adresu email, aplikac­ja iden­ty­fiku­je użytkowników.

Praw­da, że proste? A co najważniejsze, wygodne dla naszych użytkowników :)

Przys­tęp­ny opis jak dzi­ała OAuth zna­jdziesz np. na stron­ie Aarona Pareck­iego.

Mały haczyk, czyli o nadużywaniu standardów słów kilka

Wiesz już czym jest OAuth i mniej więcej jak dzi­ała. Zan­im prze­jdziemy do wyko­rzysty­wa­nia go do uwierzytel­ni­a­nia użytkown­ików w naszej aplikacji, powin­naś wiedzieć że OAuth to skrót od Open Autho­riza­tion, a więc służy do auto­ryza­cji, a nie uwierzytel­ni­a­nia (uwierzytel­ni­an­ie to potwierdzanie tożsamoś­ci, a auto­ryza­c­ja to udzie­le­nie uprawnień do czegoś, np. jakichś danych)! Jak to dzi­ała w takim razie? Otóż korzys­ta­jąc z OAuth do uwierzytel­ni­a­nia więk­szość ser­wisów w rzeczy­wis­toś­ci prosi o dostęp (auto­ryza­cję) do adresu email. Dostaw­cy usług (np. Google) wyma­ga­ją od użytkown­i­ka zal­o­gowa­nia się, aby móc takiego dostępu udzielić naszej aplikacji. W prak­tyce więc, otrzy­manie auto­ryza­cji (dostępu) do adresu email aplikac­je utożsami­a­ją z poprawnym uwierzytel­nie­niem (co my także wykorzystamy).

Niem­niej trze­ba wiedzieć, że takie pode­jś­cie może nas narażać na prob­le­my z bez­pieczeńst­wem — dostaw­cy usług nie muszą (wg pro­tokołu) uwierzytel­ni­ać użytkown­ików, mogą nam udostęp­nić np. pusty adres email dla nieuwierzytel­nionych użytkown­ików (jed­nocześnie zwraca­jąc ‘sukces’ jako wynik naszego zapy­ta­nia). Jeśli inte­gru­jesz się ze znany­mi dostaw­ca­mi usług (jak np. Google czy GitHub), ryzyko jest min­i­malne, jeśli jed­nak planu­jesz inte­grację z ‘dowol­nym’ dostaw­cą, musisz mieć powyższe na uwadze (i o ile to możli­we zrezyg­nować z OAuth na rzecz OpenID — opartego o OAuth pro­tokołu uwierzytel­ni­a­nia). Ciekawy artykuł w tym tema­cie zna­jdziesz na ofic­jal­nej stron­ie OAuth. Krót­ki opis różnic zna­jdziesz też na anglo­języ­cznej wikipedii.

Pozosta­je kwes­t­ia dlaczego tak robimy, sko­ro jest to niepraw­idłowe pode­jś­cie? Pro­jek­tu­jąc aplikację wiele decyzji, które będziesz musi­ała pod­jąć jest z kat­e­gorii ‘zro­bić coś dobrze, czy wystar­cza­ją­co dobrze i szy­bko?’. Ponieważ wiemy, z jaki­mi dostaw­ca­mi usług się inte­gru­je­my, jakie są tego ryzy­ka oraz co (potenc­jal­nie) tracimy, może­my wybrać prost­szą drogę (która jed­nak nie jest w 100% tą praw­idłową). Imple­men­tac­ja zgod­na z OpenID jest nietry­wial­na (krót­ka not­ka na Stack­Over­flow z opisem doświad­czeń z tym związanych) i przy pewnych założe­ni­ach rozwiązanie oparte o OAuth jest ‘wystar­cza­ją­co dobre’ (i znacznie szyb­sze w implementacji).

Integrujemy OAuth z naszą aplikacją

Sko­ro znamy już teorię, czas na prak­tykę, a dokład­niej zin­te­growanie naszej aplikacji z OAuth2. Zan­im jed­nak zaczniemy, skon­fig­u­ru­jmy pod­sta­wowe zabez­pieczenia naszej aplikacji.

Podstawowa konfiguracja Spring Security

Ponieważ pod­czas tworzenia pro­jek­tu wybral­iśmy także mod­uł Spring Secu­ri­ty, wystar­cza­ją­cym będzie dodanie najprost­szej kon­fig­u­racji. Spring Boot ponown­ie wprowadza znaczące uproszczenia w sto­sunku do ‘stan­dar­d­owego’ Springa MVC (dla porów­na­nia oraz przy­pom­nienia, możesz prześledz­ić lekcję 16 naszego kur­su Javy, w której kon­fig­urowal­iśmy Spring Secu­ri­ty) — ter­az wystar­czy rozsz­erzyć bazową kon­fig­u­rację i zmody­fikować to, czego potrze­bu­je­my. Tworzymy więc nową klasę kon­fig­u­racji, która dziedz­iczy po Web­Se­cu­ri­ty­Con­fig­u­ra­tionAdapter. Doda­je­my także adno­tac­je @Configuration (ponieważ jest to bean kon­fig­u­racji) oraz @EnableWebSecurity (o tej adno­tacji za chwilę), a także imple­men­tu­je­my metodę configure(AuthenticationManagerBuilder auth) tak jak na przykładzie poniżej:

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

  @Override
  protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth.inMemoryAuthentication()
      .withUser("[email protected]")
      .password("admin")
      .roles("USER");
  }
}

W tym krótkim frag­men­cie kodu zro­bil­iśmy następu­jące rzeczy:

  • Uru­chomil­iśmy mech­a­nizmy Spring Secu­ri­ty i zin­te­growal­iśmy je ze Spring MVC (to wszys­tko dzię­ki adno­tacji @EnableWebSecurity)
  • Zas­tosowal­iśmy domyślne ustaw­ienia (m.in. umożli­wiliśmy logowanie za pośred­nictwem for­mu­la­rza oraz zabez­pieczyliśmy wszys­tkie zapy­ta­nia  — dostęp do jakiejkol­wiek częś­ci naszej aplikacji będzie wyma­gał uwierzytel­nienia) dzię­ki rozsz­erze­niu Web­Se­cu­ri­ty­Con­fig­ur­erAdapter
  • Skon­fig­urowal­iśmy użytkown­i­ka, którym moż­na się logować w aplikacji (przesła­ni­a­jąc metodę pro­tect­ed void configure(AuthenticationManagerBuilder auth) )

Uruchami­a­jąc aplikację (pamię­taj o uru­chomie­niu jej z uży­ciem pro­filu devel­op­ment!) w tym momen­cie i wpisu­jąc w przeglą­darce adres http://localhost:8080/greet/Jakub zosta­niesz przekierowana na stronę logowa­nia — możesz tam użyć danych z powyższego frag­men­tu (login to [email protected]; hasło to admin). Po zal­o­gowa­niu się aplikac­ja będzie dzi­ałała normalnie.

Jak na pewno pamię­tasz, w poprzed­niej lekcji utworzyliśmy już tabelę w bazie danych do prze­chowywa­nia danych użytkown­ików, a także testowego użytkown­i­ka. Pod­mieńmy więc naszego użytkown­i­ka zdefin­iowanego na szty­wno na kon­fig­u­rację z uży­ciem bazy danych. Jest to na tyle częsty przy­padek, że Spring Secu­ri­ty posi­a­da wbu­dowane mech­a­nizmy, które mogą nam pomóc — wystar­czy podać zapy­ta­nia SQL dopa­sowane do naszego mod­elu. Wystar­czy pod­mienić metodę con­fig­ure(…) na następu­jącą (oraz dodać pole typu Data­Source z adno­tacją @Autowired):

@Autowired
DataSource dataSource;

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
  auth.jdbcAuthentication()
    .dataSource(dataSource)
    .usersByUsernameQuery("SELECT `email`,`passwordHash`,`active` FROM `users` WHERE `email`=?")
    .authoritiesByUsernameQuery("SELECT 'USER' FROM `users` WHERE `email`=?")
    .passwordEncoder(passwordEncoder());
}

@Bean
public PasswordEncoder passwordEncoder() {
  return new BCryptPasswordEncoder();
}

W powyższym frag­men­cie kon­fig­u­ru­je­my także domyśl­ny Pass­wor­dEn­coder — czyli sposób, w jaki hasła będą hashowane aby zapo­biec ich kradzieży. W tym wypad­ku uży­je­my algo­ryt­mu BCrypt, który jest jed­ną z bez­pieczniejszych opcji.
Jeśli czy­tałaś nasz kurs Javy, być może sko­jarzysz zapy­ta­nia SQL — było­by to sko­jarze­nie bard­zo słuszne, ponieważ kon­fig­u­ru­je­my dokład­nie te same mech­a­nizmy, tyle tylko, że w bardziej przys­tęp­ny sposób. Po ponownym uru­chomie­niu aplikacji będzie ona zachowywać się dokład­nie tak samo jak poprzed­nio, ale użyte przez nas login i hasło zostaną porów­nane z tymi zna­j­du­ją­cy­mi się w bazie danych.

Konfiguracja serwera autoryzacji OAuth2

Ser­w­er auto­ryza­cji to jeden z dwóch ‘skład­owych’ aplikacji OAuth2 — jest to ta część, która odpowia­da za auto­ryza­cję użytkown­ików. Inny­mi słowy, odpowia­da za uwierzytel­ni­an­ie (sprawdze­nie loginu oraz hasła), gen­erowanie i odświeżanie (wydłużanie cza­su ważnoś­ci) tokenów. Sam stan­dard (jak i imple­men­tac­ja Spring’owa) pozwala korzys­tać z niego jako mod­ułu aplikacji ale także jako zewnętrznego sys­te­mu (dzię­ki temu nasza aplikac­ja może wyko­rzysty­wać auto­ryza­cję np z Face­book­iem — tym zajmiemy się w przyszłości ;) ).

Aby skon­fig­urować nasz ser­w­er auto­ryza­cji, potrze­bu­je­my kilku rzeczy:

  • sposób na prze­chowywanie tokenów — tzw. Token Store (my sko­rzys­tamy z bazy danych)
  • infor­ma­cji dla Springa żeby wykon­ał za nas więk­szość pracy ;)
  • kon­fig­u­rac­ja klien­ta (cli­en­tId oraz secret), z którego będzie korzys­tał nasz frontend

Ale wszys­tko po kolei, zaczni­jmy więc od …

Konfiguracja Token Store

Podob­nie jak w każdej innej sytu­acji i tutaj Spring daje nam wiele możli­woś­ci i opcji kon­fig­u­ra­cyjnych — może­my np. wyko­rzys­tać gotowy mech­a­nizm prze­chowywa­nia tokenów w pamię­ci, ale może­my też skon­fig­urować do tego bazę danych. Ta dru­ga opc­ja jest zde­cy­dowanie wygod­niejsza — w przy­pad­ku aktu­al­iza­cji aplikacji na ser­w­erze czy koniecznoś­ci doda­nia kilku dodatkowych ser­w­erów, użytkown­i­cy nie zostaną wyl­o­gowani. Jed­nocześnie jest to na tyle proste, że nie przys­porzy nam sporo pracy.

Nieste­ty kon­fig­u­rac­ja bazy danych pod Token Store jest jed­nym z niewielu przykładów, kiedy doku­men­tac­ja Springa jest niezbyt pomoc­na (wyni­ka to z fak­tu, że Spring pozwala nam zdefin­iować bazę danych samodziel­nie i sko­rzys­tać z włas­nych zapy­tań SQL — my jed­nak zdamy się na ‘domyślne’ ustaw­ienia). Nie oznacza to jed­nak, że jesteśmy zupełnie pozbaw­ieni infor­ma­cji — bazu­jąc na domyśl­nych zapy­ta­ni­ach SQL oraz schemat­ach uży­wanych do testów może­my łat­wo napisać odpowied­ni skrypt:

CREATE TABLE oauth_access_token (
  token_id VARCHAR(255),
  token LONG VARBINARY,
  authentication_id VARCHAR(255) PRIMARY KEY,
  user_name VARCHAR(255),
  client_id VARCHAR(255),
  authentication LONG VARBINARY,
  refresh_token VARCHAR(255)
);

CREATE TABLE oauth_refresh_token (
  token_id VARCHAR(255),
  token LONG VARBINARY,
  authentication LONG VARBINARY
);

To wystar­czy, aby prze­chowywać nasze toke­ny w bazie danych. W tym momen­cie zas­tanaw­iasz się zapewne po co dwie tabele i czym się różni token od refresh_token. Otóż głównym ele­mentem, który wyko­rzys­tu­je­my i przesyłamy w każdym zapy­ta­niu jest token. Token ma określony czas ważnoś­ci, lic­zony od cza­su jego wygen­erowa­nia — np. poprzez logowanie (prze­ważnie wyrażony w min­u­tach, do godziny). Po upły­wie cza­su ważnoś­ci tokenu jest on unieważ­ni­any, z punk­tu widzenia użytkown­i­ka następu­je wyl­o­gowanie. W tym miejs­cu pojaw­ia się pewnego rodza­ju kon­flikt interesów — z jed­nej strony wygod­nym dla użytkown­i­ka było­by utrzy­manie tokenu ‘ważnym’ jak najdłużej, aby nie iry­tować kogoś koniecznoś­cią ponownego logowa­nia się; z drugiej strony mamy bez­pieczeńst­wo — token może zostać wykradziony, ktoś może prze­jąć kom­put­er i pozostać ‘zal­o­gowanym’ itp — w tym wypad­ku token powinien żyć jak najkrócej. Pewnego rodza­ju kom­pro­misem jest wprowadze­nie dodatkowego spec­jal­nego tokenu (refresh_token), który musimy użyć aby przedłużyć ważność naszego tokenu. Ponieważ jest on uży­wany tylko do tej jed­nej czyn­noś­ci (czyli nie jest przesyłany z każdym zapy­taniem), jest mniejsze praw­dopodobieńst­wo jego kradzieży czy przech­wyce­nia; w przy­pad­ku nieak­ty­wnoś­ci (np. zamknię­cia kom­put­era) stan­dar­d­owy token się po pros­tu względ­nie szy­bko przeter­min­u­je, więc male­je też ryzyko niepowołanej oso­by będącej zal­o­gowanej na naszym kon­cie. Z tego powodu też czas życia refresh_token jest zwyk­le znacznie dłuższy (mier­zony w dni­ach, np. tydzień czy miesiąc). Oba toke­ny otrzy­mu­je­my w momen­cie wygen­erowa­nia tokenu (zal­o­gowa­nia się), co zobaczymy już za chwilę :)

Konfiguracja Springa

Aby skon­fig­urować ser­w­er auto­ryza­cji, doda­jmy klasę Oauth2AuthServerConfig:

@Configuration
@EnableAuthorizationServer
public class Oauth2AuthServerConfig extends AuthorizationServerConfigurerAdapter {

  @Autowired
  DataSource dataSource;

  @Autowired
  AuthenticationManager authenticationManager;

  @Override
  public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
    oauthServer
      .tokenKeyAccess("permitAll()")
      .checkTokenAccess("isAuthenticated()");
  }

  @Override
  public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
    clients
      .inMemory()
        .withClient("frontendClientId")
          .secret("frontendClientSecret")
          .authorizedGrantTypes("password","authorization_code", "refresh_token")
          .accessTokenValiditySeconds(3600)
          .refreshTokenValiditySeconds(28*24*3600)
          .scopes("read");
  }

  @Override
  public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
    endpoints
      .tokenStore(tokenStore())
      .authenticationManager(authenticationManager);
  }

  @Bean
  public TokenStore tokenStore() {
    return new JdbcTokenStore(dataSource);
  }
}

Voilà! W tym momen­cie jesteś już autorką dzi­ała­jącej aplikacji wyko­rzys­tu­jącej OAuth! Ale zna­jąc Two­ją dociek­li­wość, chci­ałabyś wiedzieć co tam się dzieje, przyjrzyjmy się więc nieco bliżej ;)

Adno­tac­ja @EnableAuthorizationServer — ta adno­tac­ja infor­mu­je Springa, że ma włączyć obsługę OAuth oraz że stosowną kon­fig­u­rac­je dostar­czymy z beanem typu Autho­riza­tion­Server­Con­fig­ur­erAdapter. W naszym przy­pad­ku adno­tację tą umieś­cil­iśmy dokład­nie nad definicją tego beana, aby zachować pow­iązane rzeczy w jed­nym miejscu.

Rozsz­erze­nie klasy Autho­riza­tion­Server­Con­fig­ur­erAdapter — w ten sposób może­my skon­fig­urować poszczególne ele­men­ty ser­w­era auto­ryza­cji, dopa­sowu­jąc je do naszej aplikacji.

Kon­fig­u­rac­ja klien­ta (meto­da o syg­naturze pub­lic void configure(ClientDetailsServiceConfigurer clients) throws Excep­tion) — w tej metodzie może­my skon­fig­urować klien­ta (lub klien­tów) OAuth. W uproszcze­niu, klient to aplikac­ja korzys­ta­ją­ca z naszego API, a więc taka, która może gen­erować toke­ny dla użytkown­ików, prosić o ich dane czy sprawdzać ważność tokenów (oczy­wiś­cie wszys­tkie te uprawnienia aplikacji moż­na kon­fig­urować). W naszej aplikacji będzie tylko jeden klient — czyli fron­tend (choć będzie on w jed­nym repozy­to­ri­um, to uruchamia się on jed­nak w przeglą­darce użytkown­i­ka, trak­tu­je­my go więc jako odd­ziel­ną aplikację). Dzię­ki temu może­my skon­fig­urować go w kodzie, nie ma potrze­by tworzenia bazy danych (ta była­by potrzeb­na, gdy­byś chci­ała umożli­wić innym inte­grację z Two­ją aplikacją — tak jak możli­we jest to np. w przy­pad­ku Google, Face­booka czy GitHu­ba). Dwa ważne para­me­try w tym wypad­ku to cli­en­tId oraz secret — każde zapy­tanie związane z OAuth (np. gen­erowanie tokenu, sprawdze­nie jego ważnoś­ci czy odswieże­nie) będzie musi­ało być uwierzytel­nione właśnie tą parą infor­ma­cji. W naszym przy­pad­ku infor­ma­c­je te będą względ­nie jawne (muszą się one zna­j­dować gdzieś w kodzie Fron­tEndu), dlat­ego ważne jest jakie uprawnienia nada­je­my temu klien­towi — dobrą prak­tyką jest nadawanie ich jak najm­niej (w naszym przy­pad­ku będzie to pass­word oraz refresh_token, jeśli ser­w­er auto­ryza­cji dzi­ałał­by jako odd­ziel­na aplikac­ja, potrzeb­ny mógł­by być także authorization_code — przy­jazne wyjaśnie­nie różnic moż­na znaleźć np na stron­ie oauth2.thephpleague.com)

Token­Store — czym jest Token­Store omówiliśmy sobie już powyżej, tutaj jedynie skon­fig­u­ru­je­my go i ‘udostęp­n­imy’ Springowi jako beana. Wystar­czy utworzyć obiekt typu Jdbc­To­ken­Store — ponieważ naszą bazę danych mod­e­lowal­iśmy na pod­staw­ie domyśl­nych zapy­tań Springa, nie musimy kon­fig­urować specy­ficznych zapytań.

Warta uwa­gi jest także meto­da pub­lic void configure(AuthorizationServerSecurityConfigurer oauth­Serv­er) throws Excep­tion — służy ona kon­fig­u­racji uprawnień do funkcjon­al­noś­ci OAuth2. Zapewne pytasz w tym momen­cie — ale jak to, czy nie kon­fig­urowal­iśmy zabez­pieczeń w innej klasie? I będziesz miała rację, jed­nak wszys­tkie kwest­ie związane z OAuth2 są wykony­wane ‘przed’ trady­cyjną kon­fig­u­racją Spring Secu­ri­ty (dzię­ki temu w jed­nej aplikacji może­my obsługi­wać zarówno OAuth2, jak i ‘trady­cyjne’ logowanie za pomocą sesji).

Testujemy nasze API

Do testowa­nia sko­rzys­tamy z klien­ta Rest­let — jest on bezpłat­ny i dostęp­ny jako plu­g­in do przeglą­dar­ki, przez co jest wygod­ny w uży­ciu. Dodatkowo może­my zapisy­wać wywoły­wane zapy­ta­nia i grupować je w sce­nar­iusze, co pozwala szy­bko zwery­fikować rzeczy pod­czas rozwi­ja­nia aplikacji.

Przede wszys­tkim musimy wygen­erować token. W tym celu wysyłamy zapy­tanie POST na domyśl­ny end­point OAuth dla Springa (czyli /oauth/token), jako para­me­try poda­jąc grant_type (wartość: pass­word), user­name (nasza nazwa użytkown­i­ka) oraz pass­word (nasze hasło) tak jak na poniższym obrazku:

Zapy­tanie generu­jące token

Do tego zapy­ta­nia musimy także ustaw­ić auto­ryza­cję (czyli podać dane klien­ta) — w tym celu klikamy na ‘Add autho­riza­tion’ po lewej stron­ie i uzu­peł­ni­amy for­mu­la­rz naszy­mi danymi:

Ter­az już może­my wykon­ać zapy­tanie (przy­cisk ‘SEND’), w odpowiedzi powin­naś zobaczyć coś podobnego:

Oczy­wiś­cie toke­ny będą inne, ale struk­tu­ra obiek­tu pozostanie taka sama. Ter­az może­my prze­jść do naszego API — zmody­fikuj zapy­tanie aby wyglą­dało tak jak na poniższym ekranie, w nagłówkach wpisz jedynie Autho­riza­tion jako nazwę oraz ‘Bear­er {token}’ jako wartość (bez apos­trofów, pod­mieni­a­jąc za {token} to, co otrzy­małaś w odpowiedzi na poprzed­nie zapytanie).

Zapy­tanie do naszego API

Po wyko­na­niu tego zapy­ta­nia nieste­ty nie otrzy­mamy oczeki­wanej odpowiedzi:

Wyni­ka to z tego, że stan­dar­d­owa kon­fig­u­rac­ja Spring Secu­ri­ty nie ‘rozu­mie’ tokenów OAuth2 i nie potrafi zwery­fikować naszej tożsamoś­ci. Zaradz­imy temu jed­nak w kole­jnym kroku :)

Zwróć uwagę, że jeżeli jesteś już zal­o­gowana, zwró­cony zostanie dokład­nie ten sam token (wraz z odpowied­nim cza­sem życia) — aby przedłużyć ważność toke­na, musisz użyć end­pointu do odświeża­nia. Pamię­taj także, że token będzie inny za każdym razem kiedy uru­chomisz aplikację / poprzed­ni wygaśnie! Dlat­ego tes­tu­jąc musisz sko­pi­ować odpowiedź z pier­wszego zapy­ta­nia do nagłówka z drugiego zapy­ta­nia przy każdej zmi­an­ie w aplikacji.

Jako że jesteśmy dobry­mi pro­gramis­ta­mi, a więc naszą wartoś­cią wyższą jest lenist­wo, powyższy pro­ces może­my także skon­fig­urować automaty­cznie w klien­cie Rest­let (dawniej DHC) — szczegóły zna­jdziesz opisane na blogu restlet.com. Gotowy sce­nar­iusz do pobra­nia dla naszej aplikacji (uru­chomionej na Twoim kom­put­erze na por­cie 8080) możesz pobrać tutaj.

Konfiguracja serwera zasobów (Resource Server) OAuth2

Ser­w­er zasobów w nomen­klaturze OAuth2 to nic innego jak nasze API (‘zasób’ bierze się stąd, że w założe­niu roz­maw­iamy o RESTowych API). W prak­tyce tą część już mamy (czy raczej będziemy mieli w przyszłoś­ci), a jedyne co musimy zro­bić to poin­for­mować Springa, które zaso­by będą chro­nione przez OAuth. Dzię­ki temu, że nasz ser­w­er auto­ryza­cji dzi­ała w tej samej aplikacji, zadzieje się automa­gia — gdy­by tak nie było, musielibyśmy podać kil­ka dodatkowych infor­ma­cji (a dokład­niej adresy URL do ser­w­era auto­ryza­cji, id oraz sekret klien­ta). W przyszłoś­ci połączymy nasz wewnętrzny ser­w­er z zewnętrznym (np. Face­book­iem czy GitHubem), póki co uprościmy sobie sprawę i sko­rzys­tamy z wbu­dowanych mechanizmów.

Aby skon­fig­urować nasz ser­w­er zasobów, podob­nie jak poprzed­nio potrze­bu­je­my tylko kilku rzeczy:

  • sposób na wery­fikację tokenów — to już mamy za darmo!
  • infor­ma­cji dla Springa żeby wykon­ał za nas więk­szość pracy ;)

Bez przedłuża­nia, prze­jdźmy do naszego kodu, a dokład­niej nowej klasy konfiguracji:

@Configuration
@EnableResourceServer
public class Oauth2ResourceServerConfig extends ResourceServerConfigurerAdapter {

  @Override
  public void configure(HttpSecurity http) throws Exception {
    http
      .requestMatchers()
        .antMatchers("/greet/*")
        .and()
      .authorizeRequests()
        .anyRequest()
          .authenticated();
  }
}

Nie za wiele, praw­da? Podob­nie jak ostat­nio — Spring przyjmie sen­sowne wartoś­ci domyślne i wykona całą ciężką pracę za nas. Zerkni­jmy jed­nak nieco między wier­sze, co tak naprawdę napisal­iśmy powyżej:

Adno­tac­ja @EnableResourceServer — podob­nie jak w przy­pad­ku ser­w­era auto­ryza­cji, ta adno­tac­ja mówi Springowi aby uru­chomił mech­a­nizmy związane z ser­w­erem zasobów oraz poszukał beana typu Resource­Server­Con­fig­ur­er.

Rozsz­erze­nie klasy Resource­Server­Con­fig­ur­erAdapter — w ten sposób zapew­ni­amy beana, który imple­men­tu­je Resource­Server­Con­fig­ur­er; klasa Resource­Server­Con­fig­ur­erAdapter pozwala nam użyć Springowych wartoś­ci domyśl­nych i nie imple­men­tować wszys­t­kich metod interfejsu.

Także tutaj mamy dodatkową metodę pub­lic void configure(HttpSecurity http), która związana jest z kon­fig­u­racją zabez­pieczeń. Kon­fig­u­rac­ja Spring Secu­ri­ty, którą zdefin­iowal­iśmy na początku lekcji, nadal będzie obow­iązy­wała, ale tylko dla ‘nor­mal­nych’ sesji — tj takich, które pow­stały poprzez zal­o­gowanie się za pomocą for­mu­la­rza (jak ze wszys­tkim innym, Spring pozwala nam łączyć wiele różnych mech­a­nizmów — taki podzi­ał był­by użyteczny jeśli mielibyśmy część aplikacji ‘stan­dar­d­owej’ (np. aplikac­ja webowa z widoka­mi JSP czy Tiles tak, jak robil­iśmy to w naszym kur­sie Javy) i chcielibyśmy do niej powoli dołączać kole­jne mod­uły z auto­ryza­cją OAuth2). Ta meto­da pozwala nam określić zabez­pieczenia dla sesji OAuth2 — wskazać, które ścież­ki mają być obsługi­wane oraz jakie ewen­tu­al­nie dodatkowe warun­ki musi speł­ni­ać użytkown­ik, aby uzyskać dostęp do zasobu (trochę szerzej poroz­maw­iamy o tym przy okazji inte­gracji z inny­mi dostaw­ca­mi OAuth — na ten moment wystar­czy nam, ze użytkown­ik jest uwierzytelniony).

Testujemy!

Testować może­my dokład­nie w ten sam sposób jak poprzed­nim razem — z tym wyjątkiem, że ter­az zadziała ;)

Sprzątamy konfiguracje Spring Security

W teorii moglibyśmy całkowicie usunąć naszą pier­wot­ną kon­fig­u­rację Spring Secu­ri­ty i man­u­al­nie stworzyć beany wyma­gane przez OAuth2 (Authen­ti­ca­tion­Man­ag­er itp). W prak­tyce, dużo wygod­niej będzie po pros­tu wyłączyć ‘stan­dar­d­owe’ mech­a­nizmy Spring Secu­ri­ty (takie jak for­mu­la­rz logowa­nia czy ogól­nie mech­a­nizm logowa­nia w aplikacji webowej) oraz domyśl­nie odrzu­cać wszys­tkie zapy­ta­nia. Do naszej klasy doda­jmy więc metodę pro­tect­ed void configure(HttpSecurity http) throws Excep­tion :

@Override
protected void configure(HttpSecurity http) throws Exception {
  http
    .authorizeRequests()
      .anyRequest()
        .denyAll()
      .and()
    .formLogin()
      .disable();
}

w ten sposób wyłączyliśmy stan­dar­d­owe mech­a­nizmy logowa­nia (poprzez for­mu­la­rz) oraz bloku­je­my dostęp do wszys­t­kich zapy­tań (pamię­taj, że przed tą kon­fig­u­racją uru­cho­mi się kon­fig­u­rac­ja OAuth2 — jeśli zapy­tanie HTTP będzie miało odpowied­nie nagłów­ki i zostanie pozy­ty­wnie uwierzytel­nione za pomocą OAuth2, ta kon­fig­u­rac­ja zostanie w efek­cie pominię­ta — nie zostanie uruchomiona).

Podsumowanie

Uff, w dzisiejszej lekcji było niemało mate­ri­ału, ale mamy nadzieję, że udało nam się przed­staw­ić go w przys­tęp­ny sposób :) To oczy­wiś­cie tylko przysłowiowy ‘wierz­chołek góry lodowej’, jed­nak zupełnie wystar­cza­ją­cy do posługi­wa­nia się OAuth2 na codzień w swo­jej aplikacji. Poniżej poda­je­my także lin­ki do zasobów, gdzie moż­na zapoz­nać się z samym stan­dar­d­em OAuth2, wyjaśnie­niem różnych typów grantów i przykład­owy­mi flow. Do kon­fig­u­racji OAuth2 powrócimy jeszcze w przyszłoś­ci, kiedy będziemy umożli­wiali logowanie się za pomocą kon­ta Google czy Facebook.

Jak pewnie zauważyłaś, granice pomiędzy ‘uwierzytel­ni­an­iem’ i ‘auto­ryza­cją’ czy ‘logowaniem’ i ‘rejes­tracją’ zaczy­na­ją się zacier­ać — i fak­ty­cznie w tym kierunku zmierza­ją stan­dardy. Wiąże się to przede wszys­tkim z wygodą użytkown­i­ka — po co wyma­gać rejes­tracji (i pamię­ta­nia haseł) do kole­jnego ser­wisu, sko­ro na 99% użytkown­ik ma już kon­to w Google / Face­book / GitHub itp. Jest to także bez­pieczniejsze pode­jś­cie — nie musimy się martwić o luki w naszym sys­temie rejes­tracji czy prze­chowywa­nia haseł, nie stracimy masy cza­su na poprawną imple­men­tację 2FA; zami­ast tego wystar­czy nam inte­grac­ja z ist­nieją­cym dostaw­cą i może­my poświę­cić naszą energię na imple­men­tację funkcjon­al­noś­ci. Użytkown­i­cy także docenią takie pode­jś­cie — dzię­ki temu zami­ast wpisy­wać po raz kole­jny swo­je dane i pamię­tać kole­jne hasło, wystar­czy że klikną jeden przy­cisk i potwierdzą uprawnienia drugim kliknię­ciem. Ot, postęp!

Kod źródłowy

Przeglądaj kodPobierz ZIP

Kody źródłowe są dostęp­ne w ser­wisie GitHub — użyj przy­cisków po prawej aby pobrać lub prze­jrzeć kod do tego mod­ułu. Jeśli masz wąt­pli­woś­ci, jak posługi­wać się Git’em, instrukc­je i lin­ki zna­jdziesz w naszym wpisie na tem­at Git’a.

Licencja Creative Commons

Jeśli uważasz powyższą lekcję za przy­dat­ną, mamy małą prośbę: pol­ub nasz fan­page. Dzię­ki temu będziesz zawsze na bieżą­co z nowy­mi treś­ci­a­mi na blogu ( i oczy­wiś­cie, z nowy­mi częś­ci­a­mi kur­su Javy). Dzięki!