«Обратите внимание, что эта статья была обновлена ​​до нового стека Spring Security OAuth 2.0. Тем не менее, учебник с использованием устаревшего стека все еще доступен.

1. Обзор

В этом кратком руководстве мы сосредоточимся на настройке OpenID Connect (OIDC) с Spring Security.

Мы представим различные аспекты этой спецификации, а затем увидим поддержку, которую предлагает Spring Security для ее реализации на клиенте OAuth 2.0.

2. Краткое введение в OpenID Connect

OpenID Connect — это уровень идентификации, построенный поверх протокола OAuth 2.0.

Таким образом, очень важно знать OAuth 2.0, прежде чем углубляться в OIDC, особенно в поток кода авторизации.

Набор спецификаций OIDC обширен; он включает в себя основные функции и несколько других дополнительных возможностей, представленных в разных группах. Основные из них:

    Ядро: аутентификация и использование утверждений для передачи информации о конечном пользователе. Обнаружение: определяет, как клиент может динамически получать информацию о провайдерах OpenID. Динамическая регистрация: определяет, как клиент может зарегистрироваться у провайдера. Управление сеансом: определяет. как управлять сеансами OIDC

Помимо этого, в документах различаются серверы аутентификации OAuth 2.0, которые предлагают поддержку этой спецификации, и они называются «поставщиками OpenID» (OP), а клиенты OAuth 2.0, использующие OIDC, — как полагающиеся Стороны (РП). Мы будем придерживаться этой терминологии в этой статье.

Стоит также знать, что клиент может запросить использование этого расширения, добавив область openid в свой запрос на авторизацию.

Наконец, еще один аспект, который полезно понять для этого руководства, заключается в том, что OP выдают информацию о конечном пользователе в виде JWT, называемого «ID Token».

Теперь да, мы готовы погрузиться глубже в мир OIDC.

3. Настройка проекта

Прежде чем сосредоточиться на фактической разработке, нам необходимо зарегистрировать клиент OAuth 2.o у нашего поставщика OpenID.

В этом случае мы будем использовать Google в качестве провайдера OpenID. Мы можем следовать этим инструкциям, чтобы зарегистрировать наше клиентское приложение на их платформе. Обратите внимание, что область openid присутствует по умолчанию.

URI перенаправления, который мы настроили в этом процессе, является конечной точкой нашего сервиса: http://localhost:8081/login/oauth2/code/google.

Мы должны получить Client Id и Client Secret из этого процесса.

3.1. Конфигурация Maven

Мы начнем с добавления этих зависимостей в файл pom нашего проекта:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-oauth2-client</artifactId>
    <version>2.2.6.RELEASE</version>
</dependency>

Стартовый артефакт объединяет все зависимости, связанные с клиентом Spring Security, включая:

    the spring-security-oauth2-client зависимость для входа в систему OAuth 2.0 и функциональности клиента библиотека JOSE для поддержки JWT

Как обычно, мы можем найти последнюю версию этого артефакта с помощью поисковой системы Maven Central.

4. Базовая настройка с использованием Spring Boot

Во-первых, мы начнем с настройки нашего приложения для использования регистрации клиента, которую мы только что создали в Google.

Использование Spring Boot делает это очень простым, так как все, что нам нужно сделать, это определить два свойства приложения:

spring:
  security:
    oauth2:
      client:
        registration: 
          google: 
            client-id: <client-id>
            client-secret: <secret>

Давайте запустим наше приложение и попробуем получить доступ к конечной точке. Мы увидим, что нас перенаправляют на страницу входа в Google для нашего клиента OAuth 2.0.

Выглядит очень просто, но здесь довольно много всего происходит под капотом. Далее мы рассмотрим, как Spring Security справляется с этим.

Ранее, в нашем сообщении о поддержке WebClient и OAuth 2, мы анализировали внутренности того, как Spring Security обрабатывает серверы и клиенты авторизации OAuth 2.0.

Там мы увидели, что мы должны предоставить дополнительные данные, помимо идентификатора клиента и секрета клиента, для успешной настройки экземпляра ClientRegistration. Итак, как это работает?

Ответ таков: Google — известный провайдер, поэтому фреймворк предлагает некоторые предопределенные свойства, чтобы упростить задачу.

Мы можем взглянуть на эти конфигурации в перечислении CommonOAuth2Provider.

Для Google перечисляемый тип определяет такие свойства, как:

    «области действия по умолчанию, которые будут использоваться Конечная точка авторизации Конечная точка токена Конечная точка UserInfo, которая также является частью основной спецификации OIDC

4.1. Доступ к информации о пользователе

Spring Security предлагает полезное представление принципала пользователя, зарегистрированного у поставщика OIDC, объекта OidcUser.

Помимо основных методов OAuth2AuthenticatedPrincipal, этот объект предлагает некоторые полезные функции:

    получение значения маркера идентификатора и содержащихся в нем утверждений получение утверждений, предоставленных конечной точкой UserInfo, создание совокупности двух наборов

Мы можем легко получить доступ к этому объекту в контроллере:

@GetMapping("/oidc-principal")
public OidcUser getOidcUserPrincipal(
  @AuthenticationPrincipal OidcUser principal) {
    return principal;
}

Или с помощью SecurityContextHolder в bean-компоненте:

Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication.getPrincipal() instanceof OidcUser) {
    OidcUser principal = ((OidcUser) authentication.getPrincipal());
    
    // ...
}

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

Кроме того, важно отметить, что Spring добавляет полномочия к принципалу на основе областей, полученных от провайдера, с префиксом «SCOPE_». Например, область действия openid становится предоставленным полномочием SCOPE_openid.

Эти полномочия можно использовать для ограничения доступа к определенным ресурсам, например:

@EnableWebSecurity
public class MappedAuthorities extends WebSecurityConfigurerAdapter {
    protected void configure(HttpSecurity http) {
        http
          .authorizeRequests(authorizeRequests -> authorizeRequests
            .mvcMatchers("/my-endpoint")
              .hasAuthority("SCOPE_openid")
            .anyRequest().authenticated()
          );
    }
}

5. OIDC в ​​действии

До сих пор мы узнали, как легко реализовать решение для входа в OIDC с помощью Spring. Безопасность

Мы увидели преимущества, которые дает делегирование процесса идентификации пользователя поставщику OpenID, который, в свою очередь, предоставляет подробную полезную информацию даже в масштабируемой форме.

Но правда в том, что до сих пор нам не приходилось иметь дело с каким-либо аспектом, специфичным для ODDC. Это означает, что Spring делает большую часть работы за нас.

Следовательно, мы посмотрим, что происходит за кулисами, чтобы лучше понять, как эта спецификация применяется в действии, и получить от нее максимальную отдачу.

5.1. Процесс входа в систему

Чтобы увидеть это ясно, давайте включим журналы RestTemplate для просмотра запросов, которые выполняет служба:

logging:
  level:
    org.springframework.web.client.RestTemplate: DEBUG

Если мы вызовем защищенную конечную точку сейчас, мы увидим, что служба выполняет обычный поток кода авторизации OAuth 2.0. Это потому, что, как мы уже говорили, эта спецификация построена на основе OAuth 2.0. Во всяком случае, некоторые отличия есть.

Во-первых, в зависимости от используемого нами провайдера и настроенных нами областей действия мы можем увидеть, что служба выполняет вызов конечной точки UserInfo, о которой мы упоминали в начале.

А именно, если ответ авторизации извлекает хотя бы один из профилей, адресов электронной почты, адресов или телефонов, инфраструктура вызовет конечную точку UserInfo для получения дополнительной информации.

Несмотря на то, что все указывает на то, что Google должен получить профиль и область электронной почты — поскольку мы используем их в запросе авторизации — OP вместо этого извлекает их пользовательские аналоги, https://www.googleapis. com/auth/userinfo.email и https://www.googleapis.com/auth/userinfo.profile, поэтому Spring не вызывает конечную точку.

Это означает, что вся информация, которую мы получаем, является частью идентификатора токена.

Мы можем адаптироваться к такому поведению, создав и предоставив собственный экземпляр OidcUserService:

@Configuration
public class OAuth2LoginSecurityConfig
  extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        Set<String> googleScopes = new HashSet<>();
        googleScopes.add(
          "https://www.googleapis.com/auth/userinfo.email");
        googleScopes.add(
          "https://www.googleapis.com/auth/userinfo.profile");

        OidcUserService googleUserService = new OidcUserService();
        googleUserService.setAccessibleScopes(googleScopes);

        http
          .authorizeRequests(authorizeRequests -> authorizeRequests
            .anyRequest().authenticated())
          .oauth2Login(oauthLogin -> oauthLogin
            .userInfoEndpoint()
              .oidcUserService(googleUserService));
    }
}

Второе отличие, которое мы заметим, это вызов JWK Set URI. Как мы объяснили в нашем сообщении JWS и JWK, это используется для проверки подписи токена идентификатора в формате JWT.

Далее мы подробно проанализируем ID Token.

5.2. Токен ID

Естественно, спецификация OIDC охватывает множество различных сценариев и адаптируется к ним. В этом случае мы используем поток кода авторизации, и протокол указывает, что и токен доступа, и токен идентификатора будут получены как часть ответа конечной точки токена.

Как мы уже говорили ранее, объект OidcUser содержит утверждения, содержащиеся в токене идентификатора, и фактический токен в формате JWT, который можно проверить с помощью jwt.io.

Кроме того, Spring предлагает множество удобных геттеров для простого получения стандартных утверждений, определенных спецификацией.

Мы видим, что ID Token включает в себя несколько обязательных утверждений:

    «идентификатор эмитента в формате URL-адреса (например, «https://accounts.google.com») идентификатор субъекта, который является ссылкой на конечного пользователя, содержащейся эмитентом время истечения срока действия маркера время, в которое Токен был выдан аудитории, которая будет содержать идентификатор клиента OAuth 2.0, который мы настроили

А также множество стандартных утверждений OIDC, подобных тем, которые мы упоминали ранее (имя, локаль, изображение, адрес электронной почты).

Поскольку они являются стандартными, мы можем ожидать, что многие провайдеры извлекут хотя бы некоторые из этих полей, что облегчит разработку более простых решений.

5.3. Заявки и области действия

Как мы можем себе представить, утверждения, извлекаемые OP, соответствуют областям, которые мы (или Spring Security) настроили.

OIDC определяет некоторые области, которые можно использовать для запроса утверждений, определенных OIDC:

    профиль, который можно использовать для запроса утверждений профиля по умолчанию (например, имя, предпочитаемое_имя_пользователя, изображение и т. д.), электронную почту для доступа к электронной почте. и email_verified Claims address phone, чтобы запрашивать phone_number и phone_number_verified Claims

Несмотря на то, что Spring еще не поддерживает его, спецификация позволяет запрашивать отдельные претензии, указывая их в запросе авторизации.

6. Поддержка Spring для обнаружения OIDC

Как мы объяснили во введении, OIDC включает в себя множество различных функций, помимо своей основной цели.

Возможности, которые мы собираемся проанализировать в этом и следующем разделах, являются необязательными в OIDC. Следовательно, важно понимать, что могут быть OP, которые их не поддерживают.

Спецификация определяет механизм обнаружения для RP для обнаружения OP и получения информации, необходимой для взаимодействия с ним.

В двух словах, OP предоставляют документ JSON со стандартными метаданными. Информация должна передаваться известной конечной точкой расположения эмитента, /.well-known/openid-configuration.

Spring выигрывает от этого, позволяя нам настроить ClientRegistration только с одним простым свойством, местоположением эмитента.

Но давайте сразу перейдем к примеру, чтобы ясно это увидеть.

Мы определим собственный экземпляр ClientRegistration:

spring:
  security:
    oauth2:
      client:
        registration: 
          custom-google: 
            client-id: <client-id>
            client-secret: <secret>
        provider:
          custom-google:
            issuer-uri: https://accounts.google.com

Теперь мы можем перезапустить наше приложение и проверить журналы, чтобы убедиться, что приложение вызывает конечную точку конфигурации openid в процессе запуска.

Мы даже можем просмотреть эту конечную точку, чтобы просмотреть информацию, предоставленную Google:

https://accounts.google.com/.well-known/openid-configuration

Мы можем увидеть, например , авторизация, токен и конечные точки UserInfo, которые должна использовать служба, а также поддерживаемые области.

Здесь особенно важно отметить тот факт, что если конечная точка обнаружения недоступна во время запуска службы, наше приложение не сможет успешно завершить процесс запуска.

7. Управление сеансом OpenID Connect

Эта спецификация дополняет основные функции, определяя:

    различные способы постоянного мониторинга состояния входа конечного пользователя в OP, чтобы RP мог выйти из конечного -Пользователь, который вышел из провайдера OpenID, возможность регистрации URI выхода RP с OP как часть регистрации клиента, чтобы получать уведомления, когда конечный пользователь выходит из OP, механизм для уведомления OP о том, что Конечный пользователь вышел с сайта и может захотеть выйти и из OP

Естественно, не все OP поддерживают все эти элементы, и некоторые из этих решений могут быть реализованы только во внешней реализации через Пользовательский агент.

В этом руководстве мы сосредоточимся на возможностях, предлагаемых Spring для последнего элемента списка, выхода из системы, инициированного RP.

На этом этапе, если мы войдем в наше приложение, мы обычно сможем получить доступ к любой конечной точке.

Если мы выйдем из системы (вызвав конечную точку /logout), а затем сделаем запрос к защищенному ресурсу, мы увидим, что можем получить ответ без повторного входа в систему.

«Однако на самом деле это не так; если мы проверим вкладку «Сеть» в консоли отладки браузера, мы увидим, что когда мы попадаем в защищенную конечную точку во второй раз, мы перенаправляемся в конечную точку авторизации OP, и, поскольку мы все еще вошли в систему, поток завершается прозрачно , почти мгновенно попадая в защищенную конечную точку.

Конечно, в некоторых случаях это может быть нежелательным. Давайте посмотрим, как мы можем реализовать этот механизм OIDC, чтобы справиться с этим.

7.1. Конфигурация провайдера OpenID

В этом случае мы будем настраивать и использовать экземпляр Okta в качестве нашего провайдера OpenID. Мы не будем вдаваться в подробности того, как создать экземпляр, но мы можем выполнить шаги, описанные в этом руководстве, и помнить, что конечной точкой обратного вызова Spring Security по умолчанию будет /login/oauth2/code/okta.

В нашем приложении мы можем определить регистрационные данные клиента со свойствами:

spring:
  security:
    oauth2:
      client:
        registration: 
          okta: 
            client-id: <client-id>
            client-secret: <secret>
        provider:
          okta:
            issuer-uri: https://dev-123.okta.com

OIDC указывает, что конечная точка выхода из системы OP может быть указана в документе Discovery как элемент end_session_endpoint.

7.2. Конфигурация LogoutSuccessHandler

Далее нам нужно настроить логику выхода HttpSecurity, предоставив настраиваемый экземпляр LogoutSuccessHandler:

@Override
protected void configure(HttpSecurity http) throws Exception {
    http
      .authorizeRequests(authorizeRequests -> authorizeRequests
        .mvcMatchers("/home").permitAll()
        .anyRequest().authenticated())
      .oauth2Login(oauthLogin -> oauthLogin.permitAll())
      .logout(logout -> logout
        .logoutSuccessHandler(oidcLogoutSuccessHandler()));
}

Теперь давайте посмотрим, как мы можем создать LogoutSuccessHandler для этой цели, используя специальный класс, предоставляемый Spring. Безопасность, OidcClientInitiatedLogoutSuccessHandler:

@Autowired
private ClientRegistrationRepository clientRegistrationRepository;

private LogoutSuccessHandler oidcLogoutSuccessHandler() {
    OidcClientInitiatedLogoutSuccessHandler oidcLogoutSuccessHandler =
      new OidcClientInitiatedLogoutSuccessHandler(
        this.clientRegistrationRepository);

    oidcLogoutSuccessHandler.setPostLogoutRedirectUri(
      URI.create("http://localhost:8081/home"));

    return oidcLogoutSuccessHandler;
}

Следовательно, нам нужно настроить этот URI как допустимый URI перенаправления выхода в панели конфигурации клиента OP.

Очевидно, что конфигурация выхода из системы OP содержится в настройке регистрации клиента, поскольку все, что мы используем для настройки обработчика, — это bean-компонент ClientRegistrationRepository, присутствующий в контексте.

Итак, что теперь будет?

После входа в наше приложение мы можем отправить запрос на конечную точку /logout, предоставленную Spring Security.

Если мы проверим сетевые журналы в консоли отладки браузера, мы увидим, что нас перенаправили на конечную точку выхода из системы OP, прежде чем, наконец, мы получили доступ к настроенному нами URI перенаправления.

В следующий раз, когда мы получим доступ к конечной точке в нашем приложении, требующей аутентификации, нам в обязательном порядке потребуется снова войти в нашу платформу OP, чтобы получить разрешения.

8. Заключение

Подводя итог, в этом руководстве мы многое узнали о решениях, предлагаемых OpenID Connect, и о том, как мы можем реализовать некоторые из них с помощью Spring Security.

Как всегда, все полные примеры можно найти в нашем репозитории GitHub.