#29 Spring Boot – Single Sign-On

Podczas tworzenia aplikacji prędzej czy później pojawi się temat bezpieczeństwa. Musimy zastanowić się w jaki sposób chcemy chronić nasze zasoby oraz jaki typ uwierzytelniania wybierzemy. W dobie mikroserwisów nie jest to już takie proste jak w monolicie. Dziś wpis dotyczący podejścia Single Sign-On, zapraszamy!

Single Sign-On

Single Sign-On jest odpowiedzią na powstające coraz to nowsze aplikacje, w których używamy danych do logowania. Powstawanie nowych aplikacji wiąże się z zapamiętaniem kolejnych haseł. Ponadto, jeśli nasza aplikacja składa się z kilku modułów, w każdym z nich musimy zaimplementować mechanizm uwierzytelniania, jednocześnie wymuszając na użytkowniku zapamiętanie kolejnych danych. Aby zredukować te niedogodności powstał Single Sign-On, czyli jeden centralny punkt logowania. Od teraz użytkownik logując się w jednym miejscu, ma dostęp do wszystkich usług, które korzystają z Single Sign-On. Ponadto daje to takie korzyści jak:

  • lepsze bezpieczeństwo – jedno miejsce z danymi
  • mniejszy narzut – nie ma potrzeby tworzenia mechanizmów uwierzytelniania we wszystkich modułach
  • ułatwione korzystanie – jeden punkt logowania dla użytkowników
  • kończenie sesji – w momencie wylogowania się użytkownika działa to dla wszystkich modułów
  • łatwiejsza obsług – jeden centralny punkt

Maven

Zaczynamy od dodania zależności do Mavena:

<dependency>
   <groupId>org.springframework.cloud</groupId>
   <artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>

Włączamy Single Sign-On

Aby włączyć Single Sign-On korzystając ze Spring Boot’a wystarczy użyć @EnableOAuth2Sso w naszej klasie konfiguracyjnej:

@Configuration
@EnableOAuth2Sso
class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/for-all")
                .permitAll()
                .anyRequest()
                .authenticated();
    }
}

Ponadto ustawiliśmy, iż zasób /for-all dostępny będzie dla wszystkich, natomiast każdy inny adres wymaga uwierzytelnienia.

Controller

Dodajemy prosty kontroler:

@RestController
class CodeCoupleController {

    @GetMapping("/for-all")
    String showCodeCouple(){
        return "Code Couple!";
    }

    @GetMapping("/not-for-all")
    String showCodeCoupleAwesome(){
        return "Code Couple is awesome!";
    }

}

Pierwszy z zasobów będzie dostępny dla wszystkich, natomiast drugi wymaga logowania.

Properties

Jak napisałem we wstępie, w modelu Single Sign-On stosujemy jeden serwis odpowiedzialny za uwierzytelnianie. Dotychczas wskazaliśmy, które zasoby mają być chronione oraz to, że będziemy korzystać z SSO. Teraz musimy wskazać serwis uwierzytelniający. Możemy utworzyć własny serwis, jednakże w tym wpisie wykorzystamy GitHub’a. Aby skorzystać z API GitHub’a należy w pliku application.properites dodać wpisy:

#Client id from GitHub
security.oauth2.client.client-id=generated-id
#Client secret from GitHub
security.oauth2.client.client-secret=generated-secret
#URI with access token
security.oauth2.client.access-token-uri=https://github.com/login/oauth/access_token
#URI for authorization
security.oauth2.client.user-authorization-uri=https://github.com/login/oauth/authorize
#Scheme of authentication
security.oauth2.client.client-authentication-scheme=form

#User info endpoint
security.oauth2.resource.user-info-uri=https://api.github.com/user
#Prefer user info
security.oauth2.resource.prefer-token-info=false

Pod wpisami security.oauth2.client.client-id oraz security.oauth2.client.client-secret ustawiamy dane wygenerowane w serwisie GitHub. Opis jak wygenerować te dane:

Flow

Po wejściu na “chroniony” adres, czyli w naszym przypadku /not-for-all sprawdzane jest to, czy jesteśmy zalogowani. Jeśli nie, następuje przekierowanie na stronę /login. Następnie sprawdzane jest, jaki sposób uwierzytelniania wybraliśmy. Wybraliśmy SSO, więc następuje przekierowanie na adres security.oauth2.client.user-authorization-uri wraz z przygotowanymi parametrami:

  • client_id – wygenerowane w serwisie unikalne ID (to które ustawiliśmy w security.oauth2.client.client-id)
  • redirect_uri – adres, na który zostanie przekierowany użytkownik po poprawnym zalogowaniu
  • response_type – typ odpowiedzi, w naszym przypadku oczekujemy typu code
  • state – unikalny ciąg znaków dla bezpieczeństwa

Przykładowy adres:

https://github.com/login/oauth/authorize?client_id=generated-id&redirect_uri=http://localhost:9191/login&response_type=code&state=yz1Bhx

Jeśli użytkownik wpisał poprawne dane, jesteśmy z powrotem przekierowywani na naszą stronę, tym razem z wygenerowanym parametrem code (tym samym, który ustawialiśmy w response_type):

http://localhost:9191/login?code=404e60f9ab4208bde000&state=yz1Bhx

Teraz wysyłany jest POST na adres z security.oauth2.client.access-token-uri, aby zdobyć token. W tym żądaniu zawarte są takie informacje jak:

  • client_id – wygenerowane w serwisie unikalne ID (to które ustawiliśmy w security.oauth2.client.client-id)
  • client_secret – wygenerowany w serwisie kod secret (ten który ustawiliśmy w security.oauth2.client.client-secret)
  • code – wygenerowany kod w poprzednim kroku
  • grant_type – typ uprawnień, w naszym przypadku authorization_code
  • redirect_uri – adres, na który zostanie przekierowany użytkownik po poprawnym zalogowaniu

W odpowiedzi otrzymujemy access_token typu Bearer. Od teraz wszystkie nasze requesty będą podpisane tym tokenem. Następnie wracamy do naszego flow i w odpowiedzi w nagłówku Location otrzymujemy Location: http://localhost:9191/not-for-all i jesteśmy przekierowywani na nasz “chroniony” zasób.

Uruchamiamy

Możemy teraz uruchomić aplikację i udać się pod adres /for-all. Powinien pokazać się napis “Code Couple!“. Natomiast dla /not-for-all powinna pojawić się strona do logowania dla GitHub’a, po poprawnym zalogowaniu przeglądarka przenosi nas na nasz zasób z napisem “Code Couple is awesome!

GitHub

Całość jak zawsze na GitHubie.