Strona główna » OAuth2 Authorization Code Flow w Springu

OAuth2 Authorization Code Flow w Springu

by Grzegorz Sowa

Każdy programista styka się na jakimś etapie swojej kariery z OAuth2. W artykule przyjrzymy się, na czym polega ten protokół, a właściwie framework. Szczególnie omówimy Authorization Code Flow i czym się różni od innych flow. Potem zobaczymy w praktyce, jak użyć go do pobrania listy znajomych na Facebooku w aplikacji opartej o Spring Boot. Enjoy! 🙂

Cały kod zawarty w tym artykule znajdziecie tutaj.

Co to jest OAuth2 Authorization Code Flow?

OAuth2 to framework autoryzacji, wymyślony na potrzeby delegacji uprawnień do zasobów. W ramach OAuth2 właściciel zasobu (Resource Owner ) udziela jakiejś aplikacji (Client Application) dostępu do swoich informacji w innej aplikacji, przechowywanych na serwerze zasobu (Resource Server). W tym procesie pośredniczy serwer autoryzacji (Authorization Server). Resource Server sprawdza uprawnienia Client Application na podstawie Access Tokenu, który musi ona otrzymać od Authorization Servera. Tutaj pojawia się pojęcie flow, czyli sposobu, w jaki Client Application otrzymuje Access Token. OAuth2 definiuje 4 podstawowe typy flow:

  • Authorization Code Flow – najczęściej używany standard, w którym Client Application otrzymuje Authorization Code, który wymienia na Access Token.
  • Client Credential Flow – używany do transmisji serwer-serwer, bez udziału Resource Ownera. Client Application wysyła do Authorization Servera wcześniej uzgodnione dane uwierzytelnienia, a w zamian otrzymuje Access Token.
  • Implicit Flow – cały odbywa się w przeglądarce, dlatego jest najmniej bezpieczny. Używany np. w aplikacjach mobilnych, zastępowany przez bezpieczniejszy Authorization Code Flow rozszerzony o PKCE.
  • Resource Owner Password Flow – Powstał na potrzeby aplikacji desktopowych, teraz jednak jest uznawany za protokół legacy.

Warto jeszcze wspomnieć, że OAuth2 nie służy do uwierzytelnienia, a tylko do autoryzacji. . Wszędzie tam, gdzie używamy Google czy Facebooka do zalogowania się, aplikacja korzysta z OpenID Connect, który jest nakładką na OAuth2, umożliwiającą uwierzytelnienie.

Czemu framework autoryzacji? Często możemy się spotkać z określeniem, że OAuth2 jest protokołem. Nie jest to jednak prawdą, bo OAuth2 nie opisuje ze szczegółami sposobu nawiązania autoryzacji, a tylko definiuje luźne reguły procesu. Implementację niektórych jego elementów (np. Access Token) pozostawia w gestii użytkownika.

Przebieg OAuth2 Authorization Code Flow

Zajmijmy się najczęściej używanym flow, z którego często korzystamy na co dzień. OAuth2 Authorization Code Flow składa się z kilku kroków:

OAuth2 Authorization Code Flow
  1.  Użytkownik odpytuje specjalny endpoint wystawiany przez Client Application, który służy do rozpoczęcia procesu autoryzacji.
  2. Client Application przekierowuje go na Authorization Server. W URL-u znajdują się dane mówiące o Client Application, zakresie uprawnień, jakich wymaga od Resource Ownera oraz redirect_uri na który należy przekierować użytkownika.
  3. Authorization Server generuje ekran zgody, na którym wylistowane są wszystkie uprawnienia wymagane przez Client Application oraz przycisk akceptacji.
  4. Gdy Resource Owner zgodzi się na podane warunki, zostaje przekierowany na redirect_uri, spowrotem do Client Application.
  5. Jako query parameter „doklejony” do URL-a jest Authorization Code, który Client Application odczytuje.
  6. Ze zdobytym kodem Client Application odpytuje Authorization Server i otrzymuje w odpowiedzi Access Token.
  7. Client Application może teraz odpytywać Resource Server w zakresie pokrywanym przez Access Token.

Każdy z tych kroków jest opisany w dokumentacji OAuth2.

 Client OAuth2 w Spring Boot

Spróbujmy stworzyć aplikację, która dla będzie w stanie pobrać i wyświetlić zalogowanemu użytkownikowi listę znajomych, którzy też z niej korzystają.

Konfiguracja

Zacznijmy od zaimportowania niezbędnych projektów w pom.xml:

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-security</artifactId>
</dependency>

Do wyświetlenia widoku z listą znajomych potrzebujemy jeszcze:

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

Teraz skonfigurujmy logowanie do aplikacji z jednym zarejestrowanym użytkownikiem w pamięci. Poza tym, musimy dodać odpowiednią konfigurację, aby Spring przygotował beany odpowiadające klientowi OAuth2.

@Configuration
@EnableWebSecurity
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(final AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
                .withUser("user")
                .password(passwordEncoder().encode("pass"))
                .roles("USER");
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .oauth2Client()
                .and()
                .formLogin()
                .and()
                .logout()
                .and()
                .httpBasic()
                .and()
                .csrf().disable();
    }

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

}

Musimy jeszcze dodać szczegółową konfigurację klienta OAuth2 dla Facebooka w application.yml:

spring:
  security:
    oauth2:
      client:
        registration:
          facebook:
            client-id: {client id of the Facebook app}
            client-secret: {client secret of the Facebook app}
            authorization-grant-type: authorization_code
            redirect-uri: "{baseUrl}/friends"
            scope: user_friends

Client-id i client-secret to parametry pozwalające Facebookowi zidentyfikować naszą aplikację. Nie wiesz skąd wziąć te parametry? Sprawdź ten tutorial.

Redirect-uri to url, na który Spring ma przekierować użytkownika po zakończonym procesie.

Parametr scope to zakres uprawnień, o jakie prosimy. Jego wartość tutaj jest wzięta z dokumentacji Facebooka i pozwala m. in. na pobranie listy znajomych.

Widok

Stwórzmy kontroler, który będzie zwracał widok znajomych:

@Controller
@RequestMapping("friends")
@RequiredArgsConstructor
public class FriendsController {

    private final FriendsProvider friendsProvider;

    @GetMapping
    public String friendsSite(Model model) {
        model.addAttribute("friends", friendsProvider.getFriends());
        return "friends";
    }

}

Sam widok znajomych możemy zdefiniować dzięki bibliotece Thymeleaf:

<table>
    <thead>
    <tr>
        <th> ID </th>
        <th> Name </th>
    </tr>
    </thead>
    <tbody>
    <tr th:if="${friends.empty}">
        <td colspan="2"> You have no friends </td>
    </tr>
    <tr th:each="friend : ${friends}">
        <td><span th:text="${friend.id}"> ID </span></td>
        <td><span th:text="${friend.name}"> Name </span></td>
    </tr>
    </tbody>
</table>

Facebook Client

Teraz możemy już zdefiniować klienta, który spełni kontrakt interfejsu FriendsProvider i będzie w stanie pobrać znajomych z Facebooka.

@Component
@RequiredArgsConstructor
public class FacebookClient implements FriendsProvider {

    private final RestTemplate restTemplate = new RestTemplate();

    private final String FRIENDS_URL = "https://graph.facebook.com/me/friends";
    private final String CLIENT_ID = "facebook";

    @Override
    public Set<FacebookFriend> getFriends() {
        HttpHeaders headers = new HttpHeaders();
        headers.set("Authorization", String.format("Bearer %s", accessToken()));
        return restTemplate.exchange(
                FRIENDS_URL,
                HttpMethod.GET,
                new HttpEntity<Void>(headers),
                FacebookUserFriendsResponse.class
        ).getBody().getData();
    }

    // …
}

Oczywiście, ponieważ potrzebujemy do nagłówka autoryzacji Access Token, musimy go skądś wziąć. W tym celu pobieramy beana typu OAuth2AuthorizedClientService, z którego będziemy mogli wyciągnąć obiekt OAuth2AuthorizedClient, zawierający Access Token użytkownika. Potrzebujemy jeszcze kontekstu zalogowanego usera, jednak ten wyciągniemy w łatwy sposób z użyciem statycznej metody SecurityContextHolder.getContext().

    private final OAuth2AuthorizedClientService clientService;

    private String accessToken() {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        OAuth2AuthorizedClient client = clientService.loadAuthorizedClient(CLIENT_ID, authentication.getName());
        if (client == null) {
            throw new UserNotAuthorizedException(CLIENT_ID);
        }
        return client.getAccessToken().getTokenValue();
    }

Jeśli nie znajdziemy OAuth2AuthorizedClient rzucamy customowy exception, który możemy wyłapać w ErrorController. Chcemy tutaj zwrócić użytkownikowi redirect na endpoint w aplikacji, który zainicjuje OAuth2 Authorization Code Flow z Facebookiem.

@ControllerAdvice
public class ErrorController {

    @ExceptionHandler(UserNotAuthorizedException.class)
    public String handleUserNotAuthorizedException(UserNotAuthorizedException exception) {
        return String.format("redirect:oauth2/authorization/%s", exception.getClient());
    }

}

OAuth2 Authorization Code Flow

Gdy użytkownik trafia na endpoint /oauth2/authorization/facebook Spring konstruuje URL z danych z konfiguracji i przekierowuje tam użytkownika. Dzięki temu Facebook wie, jaka aplikacja przekierowała użytkownika, jakie zgody mu wyświetlić i gdzie przekierować spowrotem. Request wygląda mniej-więcej tak:

GET /v2.8/dialog/oauth?response_type=code&client_id={client id}&scope=user_friends&state=ZT--DzCsz-neIZ4kjSUiEVmBMyJzOBtS3opw9SOYe1E%3D&redirect_uri=http://localhost:8080/friends HTTP/2
Host: www.facebook.com

Parametr state, który aplikacja dodała do URL-a to losowo wygenerowany string, którego aplikacja będzie musiała potem użyć w zapytaniu o Access Token. Serwer Autoryzacji sprawdzi zgodność parametru state pomiędzy requestami, co pozwala na uniknięcie ataku CSRF. W odpowiedzi Authorization Server Facebooka zwraca Consent Screen, na którym wg OAuth2 musi się znaleźć m.in. nazwa aplikacj i lista uprawnień, jakich jej udzielamy:

OAuth2 Authorization Code Flow Consent

Jeśli zgodzimy się na nadanie aplikacji uprawnień, serwer autoryzacji przekierowuje nas z powrotem na adres naszej aplikacji, w URL-u zawierając Authorization Code:

HTTP/2 302 Found
Location: http://localhost:8080/friends?code=AQCXt29Kwz81qmAVM-[...]hSvvnc&state=ZT--DzCsz-neIZ4kjSUiEVmBMyJzOBtS3opw9SOYe1E%3D

Teraz nasza aplikacja przekierowuje użytkownika spowrotem na skonfigurowany adres url, w tym przypadku /friends. W międzyczasie odpytuje ona serwer autoryzacji o Access Token, posługując się otrzymanym wsześniej Authorization Code:

GET /v11.0/oauth/access_token?client_id={client_id}&client_secret={client_secret}&redirect_uri=http://localhost:8080/friends&code=AQCXt29Kwz81qmAVM-[...]hSvvnc HTTP/2
Host: graph.facebook.com

Następnie aplikacja, mając już Access Token, pobiera listę znajomych z Resource Servera Facebooka, aby zasilić nimi widok:

GET /me/friends HTTP/2
Host: graph.facebook.com
Authorization: Bearer EAAOfs9ZCz[…]de10cwi81slujv

Podsumowanie

OAuth2 to zdecydowanie temat, który warto znać. Oczywiście, w jednym artykule nie da się wyczerpać tematu OAuth2 Authorization Code Flow, a co dopiero innych typów flow. Dlatego w kolejnych artykułach postaram się napisać coś o innych typach flow, zagrożeniach związanych z OAuth2 i być może coś o OpenID Connect. Bardzo dobrym źródłem nauki jest też dokumentacja OAuth2 i spróbowanie tego w praktyce, na własnej testowej apce 🙂

You may also like

Leave a Comment