Рубрики
Без рубрики

JWS + JWK в приложении Spring Security OAuth2

Узнайте больше о веб-подписи JSON и о том, как ее можно реализовать с помощью спецификации веб-ключа JSON в приложениях, настроенных с помощью Spring Security OAuth2.

Автор оригинала: Ger Roza.

1. Обзор

В этом уроке мы узнаем о JSON Web Signature (JWS) и о том, как ее можно реализовать с помощью спецификации JSON Web Key (JWK) в приложениях, настроенных с помощью Spring Security OAuth2.

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

Во-первых, мы попытаемся понять основные понятия; например, что такое JWS и JWK, их назначение и как мы можем легко настроить сервер ресурсов для использования этого решения OAuth.

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

2. Понимание общей картины JWS и JWK

Прежде чем начать, важно правильно понять некоторые основные понятия. Желательно сначала ознакомиться с нашим OAuth и нашими СОБСТВЕННЫМИ статьями, поскольку эти темы не входят в объем данного руководства.

JWS – это спецификация , созданная IETF, которая описывает различные криптографические механизмы для проверки целостности данных , а именно данных в веб-токене JSON (JWT) . Он определяет структуру JSON, которая содержит необходимую для этого информацию.

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

В первом случае JWT представляется как JWS. В то время как если он зашифрован, то JWT будет закодирован в структуре JSON Web Encryption (JWE).

Наиболее распространенным сценарием при работе с OAuth является только что подписанный JWTs. Это происходит потому, что нам обычно не нужно “скрывать” информацию, а просто проверять целостность данных.

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

Это цель JWK , структуры JSON, которая представляет собой криптографический ключ, определенный также IETF.

Многие поставщики аутентификации предлагают конечную точку” JWK Set”, также определенную в спецификациях. С его помощью другие приложения могут найти информацию об открытых ключах для обработки JWTS.

Например, Сервер ресурсов использует поле kid (Key Id), присутствующее в JWT, чтобы найти правильный ключ в наборе JWK.

2.1. Реализация решения с использованием JWK

Обычно, если мы хотим, чтобы наше приложение обслуживало ресурс безопасным образом, например, используя стандартный протокол безопасности, такой как OAuth 2.0, нам нужно будет выполнить следующие шаги:

  1. Регистрируйте Клиентов на Сервере авторизации – либо в нашем собственном сервисе, либо в известном провайдере, таком как Okta, Facebook или Github
  2. Эти клиенты запросят маркер доступа с Сервера авторизации, следуя любой из стратегий OAuth, которые мы могли бы настроить
  3. Затем они попытаются получить доступ к ресурсу, представляющему токен (в данном случае в виде JWT) на сервер ресурсов
  4. Сервер ресурсов должен проверить, что токеном не манипулировали, проверив его подпись , а также проверить его утверждения
  5. И, наконец, наш Сервер ресурсов извлекает ресурс, теперь будучи уверенным, что Клиент имеет правильные разрешения

3. JWK и конфигурация сервера ресурсов

Позже мы увидим, как настроить свой собственный сервер авторизации, который обслуживает JWTS и конечную точку “JWK Set”.

Однако на этом этапе мы сосредоточимся на самом простом – и, вероятно, наиболее распространенном – сценарии, когда мы указываем на существующий сервер авторизации.

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

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

3.1. Зависимость Maven

Нам нужно будет добавить зависимость автоматической настройки OAuth2 в pom-файл нашего приложения Spring:


    org.springframework.security.oauth.boot
    spring-security-oauth2-autoconfigure
    2.1.6.RELEASE

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

Обратите внимание, что эта зависимость не управляется Spring Boot, и поэтому нам нужно указать ее версию.

В любом случае он должен соответствовать версии Spring Boot, которую мы используем.

3.2. Настройка Сервера ресурсов

Далее мы должны включить функции сервера ресурсов в нашем приложении с помощью аннотации @EnableResourceServer :

@SpringBootApplication
@EnableResourceServer
public class ResourceServerApplication {

    public static void main(String[] args) {
        SpringApplication.run(ResourceServerApplication.class, args);
    }
}

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

OAuth2 Boot предлагает различные стратегии проверки токена.

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

Мы настроим конечную точку JWK Set локального сервера авторизации, над которым будем работать дальше.

Давайте добавим следующее в наш application.properties :

security.oauth2.resource.jwk.key-set-uri=
  http://localhost:8081/sso-auth-server/.well-known/jwks.json

Мы рассмотрим другие стратегии по мере детального анализа этой темы.

Примечание : новый сервер ресурсов Spring Security 5.1 поддерживает только JWK-подписанные JWTS в качестве авторизации, а Spring Boot также предлагает очень похожее свойство для настройки конечной точки набора JWK:

spring.security.oauth2.resourceserver.jwk-set-uri=
  http://localhost:8081/sso-auth-server/.well-known/jwks.json

3.3. Конфигурации пружин под капотом

Свойство, которое мы добавили ранее, переводится в создание пары весенних бобов.

Точнее, OAuth2 Boot создаст:

  • a JwkTokenStore с единственной возможностью декодировать JWT и проверять его подпись
  • a DefaultTokenServices экземпляр для использования первого TokenStore

4. Конечная точка набора JWK на Сервере авторизации

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

Обратите внимание, что поскольку Spring Security еще не предлагает функций для настройки сервера авторизации, создание его с использованием возможностей Spring Security OAuth является единственным вариантом на данном этапе. Он будет совместим с Spring Security Resource Server, через.

4.1. Включение Функций Сервера Авторизации

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

Мы также добавим зависимость spring-security-oauth2-auto configure , как и в случае с Resource Server.

Во-первых, мы будем использовать аннотацию @EnableAuthorizationServer для настройки механизмов сервера авторизации OAuth2:

@Configuration
@EnableAuthorizationServer
public class JwkAuthorizationServerConfiguration {

    // ...

}

И мы зарегистрируем клиент OAuth 2.0 с помощью свойств:

security.oauth2.client.client-id=bael-client
security.oauth2.client.client-secret=bael-secret

При этом наше приложение будет извлекать случайные токены по запросу с соответствующими учетными данными:

curl bael-client:bael-secret\
  @localhost:8081/sso-auth-server/oauth/token \
  -d grant_type=client_credentials \
  -d scope=any

Как мы видим, Spring Security OAuth по умолчанию извлекает случайное строковое значение, а не JWT-кодировку:

"access_token": "af611028-643f-4477-9319-b5aa8dc9408f"

4.2. Выдача JWTS

Мы можем легко изменить это, создав JwtAccessTokenConverter bean в контексте:

@Bean
public JwtAccessTokenConverter accessTokenConverter() {
    return new JwtAccessTokenConverter();
}

и использовать его в экземпляре JwtTokenStore :

@Bean
public TokenStore tokenStore() {
    return new JwtTokenStore(accessTokenConverter());
}

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

Мы можем легко идентифицировать JWSS; их структура состоит из трех полей (заголовок, полезная нагрузка и подпись), разделенных точкой:

"access_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
  .
  eyJzY29wZSI6WyJhbnkiXSwiZXhwIjoxNTYxOTcy...
  .
  XKH70VUHeafHLaUPVXZI9E9pbFxrJ35PqBvrymxtvGI"

По умолчанию Spring подписывает заголовок и полезную нагрузку с помощью метода кода аутентификации сообщений (MAC).

Мы можем проверить это, проанализировав JWT в одном из многих онлайн-инструментов JWT decoder/verifier мы можем найти там.

Если мы расшифруем полученный JWT, то увидим , что значение атрибута align равно HS256 , что указывает на то, что для подписи токена использовался алгоритм HMAC-SHA256 .

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

4.3. Симметричная Подпись По Умолчанию

Хэширование MAC использует тот же ключ для подписи сообщения и проверки его целостности; это симметричная функция хэширования.

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

Только по академическим причинам мы опубликуем конечную точку Spring Security OAuth /oauth/token_key :

security.oauth2.authorization.token-key-access=permitAll()

И мы настроим значение ключа подписи при настройке JwtAccessTokenConverter bean:

converter.setSigningKey("bael");

Чтобы точно знать, какой симметричный ключ используется.

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

Как только мы узнаем ключ подписи, мы можем вручную проверить целостность токена с помощью онлайн-инструмента, о котором мы упоминали ранее.

Библиотека Spring Security OAuth также настраивает конечную точку /oauth/check_token , которая проверяет и извлекает декодированный JWT.

Эта конечная точка также настроена с помощью правила доступа deny All() и должна быть сознательно защищена. Для этой цели мы могли бы использовать свойство security.oauth2.authorization.check-token-access , как мы делали это раньше для ключа токена.

4.4. Альтернативы конфигурации Сервера ресурсов

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

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

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

security.oauth2.client.client-id=bael-client
security.oauth2.client.client-secret=bael-secret

Затем мы можем использовать конечную точку /oauth/check_token (она же конечная точка интроспекции) или получить один ключ из /oauth/token_key :

## Single key URI:
security.oauth2.resource.jwt.key-uri=
  http://localhost:8081/sso-auth-server/oauth/token_key
## Introspection endpoint:
security.oauth2.resource.token-info-uri=
  http://localhost:8081/sso-auth-server/oauth/check_token

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

## Verifier Key
security.oauth2.resource.jwt.key-value=bael

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

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

4.5. Создание файла хранилища ключей

Давайте не будем забывать о нашей конечной цели. Мы хотим предоставить конечную точку JWK Set, как это делают самые известные поставщики.

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

Первый шаг к этому-создание файла хранилища ключей.

Один простой способ достичь этого:

  1. откройте командную строку в каталоге /bin любого JDK или JRE, который у вас есть под рукой:
cd $JAVA_HOME/bin
  1. выполните команду keytool с соответствующими параметрами:
./keytool -genkeypair \
  -alias bael-oauth-jwt \
  -keyalg RSA \
  -keypass bael-pass \
  -keystore bael-jwt.jks \
  -storepass bael-pass

Обратите внимание, что здесь мы использовали алгоритм RSA, который является асимметричным.

  1. ответьте на интерактивные вопросы и создайте файл хранилища ключей

4.6. Добавление файла хранилища ключей в Ваше приложение

Мы должны добавить хранилище ключей к ресурсам нашего проекта.

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

Если мы используем Maven, то одна из альтернатив-поместить текстовые файлы в отдельную папку и настроить pom.xml соответственно:


    
        
            src/main/resources
            false
        
        
            src/main/resources/filtered
            true
        
    

4.7. Настройка хранилища токенов

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

Мы создадим экземпляр KeyPair , используя файл хранилища ключей в пути к классу и параметры, которые мы использовали при создании файла .jks :

ClassPathResource ksFile =
  new ClassPathResource("bael-jwt.jks");
KeyStoreKeyFactory ksFactory =
  new KeyStoreKeyFactory(ksFile, "bael-pass".toCharArray());
KeyPair keyPair = ksFactory.getKeyPair("bael-oauth-jwt");

И мы настроим его в нашем JwtAccessTokenConverter bean, удалив любую другую конфигурацию:

converter.setKeyPair(keyPair);

Мы можем снова запросить и декодировать JWT, чтобы проверить измененный параметр algorithm .

Если мы посмотрим на конечную точку ключа токена, то увидим открытый ключ, полученный из хранилища ключей.

Его легко идентифицировать по заголовку PEM “Encapsulation Boundary”; строка, начинающаяся с ” –BEGIN PUBLIC KEY–.

4.8. JWK Устанавливает Зависимости Конечных точек

Библиотека Spring Security OAuth не поддерживает JWK из коробки.

Следовательно, нам нужно будет добавить еще одну зависимость к нашему проекту, nimbus-jose-jwt которая предоставляет некоторые базовые реализации JWK:


    com.nimbusds
    nimbus-jose-jwt
    7.3

Помните, что мы можем проверить последнюю версию библиотеки с помощью поисковой системы Maven Central Repository .

4.9. Создание конечной точки набора JWK

Давайте начнем с создания JWK Set bean с использованием экземпляра KeyPair , который мы настроили ранее:

@Bean
public JWKSet jwkSet() {
    RSAKey.Builder builder = new RSAKey.Builder((RSAPublicKey) keyPair().getPublic())
      .keyUse(KeyUse.SIGNATURE)
      .algorithm(JWSAlgorithm.RS256)
      .keyID("bael-key-id");
    return new JWKSet(builder.build());
}

Теперь создать конечную точку довольно просто:

@RestController
public class JwkSetRestController {

    @Autowired
    private JWKSet jwkSet;

    @GetMapping("/.well-known/jwks.json")
    public Map keys() {
        return this.jwkSet.toJSONObject();
    }
}

Поле идентификатора ключа, настроенное в экземпляре JWK Set , преобразуется в параметр kind .

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

Теперь мы сталкиваемся с новой проблемой: поскольку Spring Security OAuth не поддерживает JWK, выпущенные JWT не будут включать kid Заголовок.

Давайте найдем обходной путь, чтобы решить эту проблему.

4.10. Добавление значения kid в заголовок JWT

Мы создадим новый класс , расширяющий JwtAccessTokenConverter , который мы использовали, и который позволяет добавлять записи заголовка в JWTS:

public class JwtCustomHeadersAccessTokenConverter
  extends JwtAccessTokenConverter {

    // ...

}

Прежде всего, нам нужно:

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

Давайте настроим конструктор на основе этого:

private Map customHeaders = new HashMap<>();
final RsaSigner signer;

public JwtCustomHeadersAccessTokenConverter(
  Map customHeaders,
  KeyPair keyPair) {
    super();
    super.setKeyPair(keyPair);
    this.signer = new RsaSigner((RSAPrivateKey) keyPair.getPrivate());
    this.customHeaders = customHeaders;
}

Теперь мы переопределим метод encode . Наша реализация будет такой же, как и родительская, с той лишь разницей, что мы также передадим пользовательские заголовки при создании токена String :

private JsonParser objectMapper = JsonParserFactory.create();

@Override
protected String encode(OAuth2AccessToken accessToken,
  OAuth2Authentication authentication) {
    String content;
    try {
        content = this.objectMapper
          .formatMap(getAccessTokenConverter()
          .convertAccessToken(accessToken, authentication));
    } catch (Exception ex) {
        throw new IllegalStateException(
          "Cannot convert access token to JSON", ex);
    }
    String token = JwtHelper.encode(
      content,
      this.signer,
      this.customHeaders).getEncoded();
    return token;
}

Давайте теперь использовать этот класс при создании JwtAccessTokenConverter bean:

@Bean
public JwtAccessTokenConverter accessTokenConverter() {
    Map customHeaders =
      Collections.singletonMap("kid", "bael-key-id");
    return new  JwtCustomHeadersAccessTokenConverter(
      customHeaders,
      keyPair());
}

Мы готовы идти. Не забудьте изменить свойства сервера ресурсов обратно. Нам нужно использовать только свойство key-set-url , которое мы установили в начале урока.

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

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

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

Мы узнали довольно много в этом всеобъемлющем руководстве о JWT, JWS и JWK. Не только конкретные конфигурации Spring, но и общие концепции безопасности, видя их в действии на практическом примере.

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

Наконец, мы расширили основные функции Spring Security OAuth, настроив сервер авторизации, эффективно предоставляющий конечную точку набора JWK.

Мы можем найти оба сервиса в нашем репо OAuth Github , как всегда.