OAuth 2 Authorization Code Flow. Пример на Spring Boot

В этой статье рассмотрим OAuth 2 Authorization Code Flow на примере из официального репозитория сервера авторизации (от октября 2020). Пример переделан на Maven, кроме того, включено логирование запросов в консоль.
Пример состоит из трех компонентов:

  • сервера авторизации Spring Authorization Server (он в процессе разработки, но основное уже есть)
  • сервера ресурсов
  • клиента

Запущенный пример поддерживает как Authorization Code Flow, так и Client Credentials flow и выглядит так:

Запущенный пример

Client Credentials Flow уже описан, так как он проще (короче способ получения токена). Там же кратко пояснен код. В этой статье рассмотрим OAuth 2 Authorization Code Flow.

Если запустить пример (а надо запустить все три компонента) и авторизоваться (с помощью Authorization Code), то запросы от «сервера к серверу» будут выводиться в консоли каждого из трех запущенных приложений. Но браузерные запросы будут видны только в браузере. Это осложняет понимание.

Поэтому будем использовать Postman для обращения за Access Token-ом — он может симулировать браузер и отображает все запросы в своей консоли: как браузерные, так и от сервера к серверу. Впрочем, ничто не мешает отследить их же в логах приложений и в браузере.

Истоки OAuth 2

В 2000-ых годах стали появляться сайты, которые хотят воспользоваться вашими ресурсами, находящимися на другом сайте. Например, сервис печати фотографий хочет получить доступ к вашим фото на Google. Или сайт Yelp (это сайт с рейтингами услуг) хочет получить список ваших контактов в Google, чтобы выслать им приглашение. Все бы ничего, но эти «желающие» требовали дать их сервису ваш пароль от Google, MSN и т.п., чтобы осуществить свои услуги.

А иначе они не могли, потому что протокола OAuth2 не было. Вот доказательство:

Какой-то стартап Yelp хотел пароль от вашего Google
Какой-то стартап Yelp хотел пароль от вашего Google

Какое-то время это безобразие существовало, но люди придумывали, как его прекратить.

А именно: как сохранить возможность доступа одного сайта-клиента (нашего борзого Yelp или сервиса печати фотографий) к ресурсам пользователя на другом сайте (на Google), чтобы при этом пользователь не передавал малоизвестному Yelp имя и пароль от Google.

В итоге появился протокол — сначала OAuth 1, а в 2012 — современный OAuth 2.

OAuth 2 Authorization Code Flow

Договор и Client Credentials

Схема такая. Сайт-клиент и сайт-сервер между собой договариваются о «сотрудничестве». Клиент регистрируется на сайте-сервере — то есть Yelp  регистрируется на Google, или сервис печати фотографий регистрируется на Google. Сайт-клиент получает от Google (сервера авторизации) имя и пароль. А точнее, client credentials (client-id и client-secret).

Ввод имени и пароля на сервере авторизации

И пользователь сайта-клиента больше не вводит имя пароль от Google непосредственно на сайте-клиенте, а перенаправляется для этого на сервер Google. А Google в ответ дает клиенту Access Token для доступа к ресурсам для данного конкретного пользователя (как именно дает будет ниже).

Google тут выполняет роль сервера авторизации.

Как это выглядит для пользователя. Authorization Code в URL
Наверно каждый уже сталкивался с OAuth 2 в интернете как пользователь,  но можно еще раз попробовать на том же yelp.com — теперь он поддерживает OAuth 2.

После успешного ввода имени и пароля сервер авторизации Google говорит пользователю: такой-то сайт хочет получить доступ к вашим фото, списку контактов или т.д., готовы ли вы дать разрешение? В ответ на «да» Google перенаправляет пользователя обратно на клиент с кодом в url-адресе — с так называемым Authorization Code.

Правда, по поводу разрешений: конкретно на yelp разрешения запрашиваются  не сразу, а попозже в настройках.

Access Token в обмен на Authorization Code и обращение за ресурсами

Сайт-клиент перехватывает Authorization Code из url, прикладывает к нему свой client-id и client-secret и снова обращается на сервер авторизации (к Google)  — на этот раз за Access Token-ом.

И вот с ним, Access Token-ом, клиент уже обращается за ресурсами на Google — за фото, списком контактов и остальным. Тут Google выступает уже сервером ресурсов.  То есть в OAuth 2 роли три — клиент, сервер авторизации и сервер ресурсов. Просто в данном случае и сервер авторизации, и сервер ресурсов — Google. Но в протоколе (и в примере ниже) это могут быть разные сервера.

Конечная цель любого из четырех видов flow в OAuth 2 — получить Access Token. Его достаточно, чтобы получить ресурс. В нем все есть, включая срок действия и разрешения. Обычно он имеет формат JWT.

Зачем нужен Authorization Code

Вышеприведенная пляска с перенаправлением и получением кода Authorization Code  — это Authorization Code Flow. А вообще типов flow, как уже упомянуто, четыре. Два из них уже depricated (Implicit flow и еще один).

Возникает вопрос — почему бы сразу не отдать клиенту вожделенный Access Token, с которым можно обращаться к ресурсам? Зачем сначала получать Authorization Code, а потом его обменивать на Access Token? Вроде бы лишний шаг.

Back Channel и Front Channel

Но нет, не лишний — так безопаснее. Дело в том, что есть Back Channel — обращение от сервера к серверу (так клиент получает Access Token). Этот канал безопасный.

И есть Front Channel —  это канал запросов, проходящих через браузер. И Authorization Code проходит через браузер: он возвращается в url при перенаправлении обратно на клиент. Злоумышленник его может даже просто увидеть, стоя за спиной. Поэтому для обращения к ресурсам прошедший через браузер Authorization Code не очень подходит. Так что клиент его меняет на Access Token — но делает это через back channel, приложив Client Credentials. Без client-secret его не обменять. А client-secret хранится, естественно, не в браузере, а на backend сайта-клиента. (В Spring он хранится в application.properties клиента).  На backend сохраняется и полученный Access Token — но он уже хранится в сессии пользователя, у каждого пользователя он свой.

Implicit flow

Implicit flow как раз потому и depricated, что получал Access Token сразу в браузер и хранил его там. Этот flow предназначался для приложений без своего backend, у которых просто нет безопасного места хранения Access Token  — например, это чисто браузерное JavaScript приложение.

Client Credentials Flow

В Client Credentials Grant Flow выпадает не Backend, а Frontend. Браузер в нем не фигурирует — Client Credentials Grant Flow предназначен не для людей, а для микросервисов. Поэтому он безопасный.

Смысл Access Token-а

Для любого типа flow годится следующая метафора. Клиент (клиент) регистрируется на reception гостиницы (сервер авторизации), чтобы получить ключ (Acсess Token) от комнаты (сервера ресурсов). Регистрируется клиент один раз, а потом многократно заходит в комнату (на сервер ресурсов) c одним ключом без документов.

Пояснение к схеме из OAuth 2 спецификации для нашего случая

Еще раз про Back Channel, Front Channel, клиент и сервер авторизации — где они на схеме:

Authorization Code Grant Flow
Authorization Code Grant Flow

На схеме показано, как клиент получает Access Token. С которым он потом обращается на сервер ресурсов. Но самого сервера ресурсов на схеме нет.

Запросы в Postman

Наконец, опустимся еще на уровень ниже.  Проследим запросы в Postman.

Но сначала запустим Spring OAuth2 Authorization сервер из примера, который рассматривался в связи с Client Credentials Flow (пример един на обе статьи, код тут).

Перед запуском примера в файл C:\Windows\System32\drivers\etc\hosts нужно добавить строку 127.0.0.1 auth-server

Составим в Postman запрос для получения Access Token-а.

Запрос для получения Access Token-а

Если выбрать тип запроса OAuth 2, то появится возможность ввести все настройки клиента (они же есть на сервере авторизации, поскольку клиент на нем зарегистрирован):

Составление OAuth2 запроса в Postman

Подробнее о настройках ниже.

Код из сервера авторизации на Spring

Ниже приведен класс настроек запущенного Spring сервера авторизации. Тут мы регистрируем клиент messaging-client и добавляем пользователя user1/password:

@EnableWebSecurity
@Import(OAuth2AuthorizationServerConfiguration.class)
public class AuthorizationServerConfig {

    // @formatter:off
    @Bean
    public RegisteredClientRepository registeredClientRepository() {
        RegisteredClient registeredClient = RegisteredClient.withId(UUID.randomUUID().toString())
                .clientId("messaging-client") // пункт 6
                .clientSecret("secret")       //пункт 7
                .clientAuthenticationMethod(ClientAuthenticationMethod.BASIC)
                .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)// пункт 2
                .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
                .redirectUri("http://localhost:8080/authorized") //пункт 3
                .scope("message.read") //пункт 8
                .scope("message.write") //пункт 8
                .clientSettings(clientSettings -> clientSettings.requireUserConsent(true))
                .build();
        return new InMemoryRegisteredClientRepository(registeredClient);
    }
    // @formatter:on

    @Bean
    public KeyManager keyManager() {
        return new StaticKeyGeneratingKeyManager();
    }

    // @formatter:off
    @Bean
    public UserDetailsService users() {
        UserDetails user = User.withDefaultPasswordEncoder()
                .username("user1")
                .password("password")
                .roles("USER")
                .build();
        return new InMemoryUserDetailsManager(user);
    }
    // @formatter:on
}

В коде обратите внимание на комментарии про пункты — они со скриншота выше.

Ниже дано пояснение некоторых пунктов.

Auth URL и Access Token URL

Что касается пунктов 4 и 5 со скриншота выше (Auth URL и Access Token URL), то это настройки Spring Authorization Server, по которым авторизуется пользователь и берется Access Token.  Spring Authorization Server запущен по адресу:

http://auth-server:9000/

Эти настройки прописываются и на клиенте, поскольку клиент должен знать, куда обращаться.

По первому URL (Auth URL) идет редирект пользователя для ввода имени и пароля (по Front Channel). А по второму (Access Token URL) клиент обращается для получения Access Token (это уже Back Channel). :

authorization-uri: http://auth-server:9000/oauth2/authorize
token-uri: http://auth-server:9000/oauth2/token

То есть второй URL мы не увидим в браузере в Dev Tools, его можно отследить только в консоли запущенного Spring Boot клиента (если запустить еще клиент и сервер ресурсов и посмотреть как работают все три части вместе).

Вообще все Front Channel запросы видны только в браузере, а Back Channel — запросы — только в консоли запущенного клиента.

В консоли же Postman можно отследить все запросы.

Callback URL

Настройка Callback URL в Postman напрямую не используется — это адрес клиента, куда идет обратное перенаправление после логина на сервере авторизации.  Здесь клиент должен подхватить Authorization Code из url и обменять его. У нас клиент не запущен, все дело проворачивает Postman (автоматически). Но этот адрес должен быть в списке разрешенных в настройках сервера авторизации (см. строку из кода выше — это тот адрес):

.redirectUri("http://localhost:8080/authorized")
Получение Access Token

Итак, щелкнем Get New Access Token в POSTMAN:

Сначала вводим имя и пароль зарегистрированного на сервере авторизации пользователя (user1/password), затем разрешаем клиенту (в смысле его симулятору Postman) получить права:

В ответ придет JSON с Access Token-ом. С ним уже можно обращаться к серверу ресурсов.

Заглянем в консоль Postman, здесь будут запросы c обоих каналов. Сначала идут все браузерные (Front Channel), а последний POST-запрос — это запрос Access-токена от сервера к серверу (Back Channel):

Этот Access-токен и есть ключ к получению ресурса.

Задействуем его для получения ресурса.

Запрос ресурса с сервера ресурсов

Запрос ресурсов тоже идет по Back Channel — от клиента к серверу ресурсов. К нему должен быть приложен Access Token в заголовке Authorization.

Запустим дополнительно сервер ресурсов.

Серверу ресурсов не нужно ничего знать ни про клиент, ни про пользователя. Ему нужен только адрес конечной точки на сервере авторизации, по которой проверяется получаемый с каждым запросом от клиента Access Token.

В настройках сервера ресурсов есть адрес этой точки:

spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          jwk-set-uri: http://auth-server:9000/oauth2/jwks

Получив запрос с приложенным токеном, сервер ресурсов обращается по этому адресу. И если все ок, выдает ресурс.

Получать мы будем сообщения:

@RestController
public class MessagesController {

    @GetMapping("/messages")
    public String[] getMessages() {
        return new String[] {"Message 1", "Message 2", "Message 3"};
    }
}

В настройках сервера ресурсов прописано, что они доступны для всех с разрешениями:

message.read

А именно, это прописано в классе ResourceServerConfig сервера ресурсов:

@EnableWebSecurity
public class ResourceServerConfig extends WebSecurityConfigurerAdapter {

    // @formatter:off
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .mvcMatcher("/messages/**")
                .authorizeRequests()
                    .mvcMatchers("/messages/**").access("hasAuthority('SCOPE_message.read')")
                    .and()
                .oauth2ResourceServer()
                    .jwt();
    }

    // @formatter:on
}

Когда сервер авторизации выдавал токен, он включил для нашего клиента это разрешение. Потому что при регистрации клиента мы его указывали (см. выше бин RegisteredClientRepository сервера авторизации).

Если расшифровать полученный Access-токен на сайте jwt.io, то мы увидим нужное разрешение:

Access Token со всей информацией
Access Token со всей информацией

Теперь можно составить на POSTMAN запрос. Для этого выберем заголовок Authorization, тип Bearer Token и вставим токен.  Это GET-запрос по адресу сервера ресурсов localhost:8090:

Запрос ресурса с приложенным Access Token
Срок годности Access Token по умолчанию несколько минут, так что с проверкой надо поторопиться либо запросить новый токен.

Итоги

Мы рассмотрели OAuth 2 Authorization Code Flow и как посмотреть в Postman все участвующие в нем запросы.
Код примера есть на GitHub.

 

 

OAuth 2 Authorization Code Flow. Пример на Spring Boot: 4 комментария

  1. Здравствуйте, а сервер авторизации и сервер ресурсов — это типа микросервисы, их надо отдельно запускать? Был бы какой-то законченный простейший пример, типа бложек c моделями Articles и User, вход по паролю, может впоследствии через Гугл. Вот как тут https://www.bezkoder.com/spring-boot-jpa-crud-rest-api/ но минимуму

    1. Да, это отдельные приложения. Если входить через Google, то сервер авторизации Google, там же — сервер ресурсов (который мы используем только для получения инфы о пользователе), а наше приложение — клиент. Все это называется SSO (Single Sign On). Пример есть. (Но он без локальных пользователей, вход только через Google, Vk, Github).

  2. Почитал эту статью https://sysout.ru/oauth-2-slient-credentials-flow-primer-na-spring-boot и понял, что да, микросервисы, нельзя ли их интересно совместить… И чем интересно этот вариант лучше/хуже этого https://www.youtube.com/watch?v=mYKf4pufQWA , без отдельных серверов, если нужна просто REST API авторизация

    1. это совсем разные вещи — в статье OAuth2, в видео — (как понимаю) просто как на фронтенд передавать JWT-токен (вместо сессий). Но в OAuth2 не обязательно фигурирует JWT (хотя обычно да).

      Если нужна просто REST API авторизация, можно обычные сессии использовать (в том числе если фронтенд — SPA). Советую осилить гайд https://spring.io/guides/tutorials/spring-security-and-angular-js/ — он от Spring. Там говорится (см. часть «Help, How is My Application Going to Scale?»), что stateless не совсем безопасен, поэтому предпочтение statefull, и показаны несколько архитектур, как делать приложение с Angular. Что касается JWT-токена, который все стремятся использовать вместо сессий — такой пример у Spring тоже выложен в готовом виде https://github.com/spring-projects/spring-security-samples/tree/main/servlet/spring-boot/java/jwt/login. Он сделан не на кастомных фильтрах (как во многих примерах в интернете), а проще, это готовое решение.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *