Технология имеет способ обновления быстрее, чем стандарты безопасности. OAuth 2.0 — это новейший и лучший стандарт для современных приложений, но ему уже восемь лет! Его участники работают над следующей версией, пока мы говорим, и тем временем периодически выпускают «руководство», чтобы помочь разработчикам использовать OAuth 2.0 с новыми технологиями.
В прошлом году разработчики представили два проекта важных руководств для OAuth 2.0. В рекомендациях по безопасности OAuth 2.0 для защиты современных приложений содержатся рекомендации по обеспечению безопасности современных приложений с помощью OAuth 2.0, а в OAuth 2.0 для приложений на основе браузера особое внимание уделяется рекомендациям по веб-приложениям.
В этом руководстве основное внимание будет уделено наилучшим текущим методам обеспечения безопасности OAuth 2.0, а также будут рассмотрены практические последствия, которые имеет руководство для Spring Boot с приложениями Spring Security.
Сначала давайте выясним отношения между OAuth 2.0 и OpenID Connect. OAuth 2.0 для делегированной авторизации, а OpenID Connect для идентификации и находится поверх OAuth 2.0. Давайте посмотрим на эти два стандарта и почему они важны.
Трехминутный обзор OpenID Connect и OAuth 2.0
В начале были разрозненные сайты, которые не общались друг с другом, и все были грустными.
Такие сайты, как Yelp, начали получать доступ к контактной информации, которая была у вас в контактах Google. Итак, Yelp, естественно, собрал ваше имя пользователя и пароль Google, чтобы он мог получить доступ к вашим контактам. Вы дали Yelp свое разрешение, так что все было хорошо, да? Нет! С вашим именем пользователя и паролем Yelp может получить доступ к вашей электронной почте, вашим документам — всему, что у вас было в Google — не только вашим контактам. И что еще хуже, Yelp пришлось хранить ваш пароль таким образом, чтобы он мог использовать его в виде открытого текста, и не было стандартного способа отозвать ваше согласие на Yelp для доступа к вашей учетной записи Google.
Нам нужна была структура авторизации, которая позволяла бы вам предоставлять доступ к определенной информации без вашего пароля. Кий OAuth.
Используйте OAuth 2.0 для делегированной авторизации
Три ревизии спустя, мы находимся в OAuth 2.0 (до него было 1.0 и 1.0a) и все в порядке с миром. Теперь приложение, такое как Yelp (клиентское приложение), может запросить токен доступа у такой службы, как Google (сервер авторизации). Вы (владелец ресурса) входите в Google со своими учетными данными и даете свое согласие на Yelp для доступа к вашим контактам (и только вашим контактам). Получив токен доступа, Yelp запрашивает API контактов Google (сервер ресурсов) и получает ваши контакты. Yelp никогда не видит ваш пароль и никогда не имеет доступа ни к чему большему, чем вы согласились. И вы можете отозвать свое согласие в любое время.
Используйте OpenID Connect для идентификации
В этом новом мире согласия и авторизации не хватало только одного: идентичности. Cue OpenID Connect. OIDC — это тонкий слой поверх OAuth 2.0, который представляет токен нового типа: токен идентификации. В этих криптографически подписанных токенах в формате JWT содержится информация об аутентифицированном пользователе. Это открыло дверь на новый уровень взаимодействия и единого входа.
OAuth (и, соответственно, OIDC) используют несколько определенных потоков для управления взаимодействиями между клиентским приложением, сервером авторизации и сервером ресурсов. В этом посте я остановлюсь на потоке кода авторизации. Этот поток предназначен для запуска из вашего браузера и выглядит так:
- Yelp хочет получить доступ к вашим контактам. Он представляет собой кнопку, чтобы связать ваши контакты Google.
- Когда вы нажимаете кнопку, вы перенаправляетесь в Google, где вы входите под своим именем пользователя и паролем (если вы еще не вошли в систему).
- Google показывает вам экран, сообщающий, что Yelp хотел бы получить доступ только для чтения к вашим контактам.
- Как только вы дадите свое согласие, Google перенаправит обратно в Yelp через ваш браузер временный код (называемый кодом авторизации).
- Используя этот код, Yelp связывается с Google, чтобы обменять его на токен доступа
- Google проверяет код и, если все проверено, выдает токен доступа с ограниченными возможностями (доступ только для чтения к вашим контактам) для Yelp
- Затем Yelp представляет токен доступа в API контактов Google.
- API контактов Google проверяет токен и, если запрос соответствует возможностям, указанным токеном, возвращает ваш список контактов в Yelp
Использование конфиденциальных клиентов против публичных клиентов
Конфиденциальные клиенты работают на сервере и находятся под полным контролем компании, создавшей приложение. Spring Boot, .NET и Node.js являются примерами конфиденциальных приложений клиентского типа. Поскольку они запускаются на серверах и, как правило, находятся за брандмауэром с другими средствами защиты, можно безопасно настроить секретный клиент с секретом. В OAuth 2.0 это называется Client Secret
. Сервер авторизации выдает и приложение, Client Id
и a Client Secret
для использования в приложении, и именно так приложение аутентифицируется на сервере авторизации.
Общедоступные клиенты работают в средах, которые не могут контролироваться такими организациями, как компания. Существуют такие приложения, как одностраничные приложения (SPA) или мобильные или нативные приложения. Если в компании много пользователей, вполне вероятно, что какой-то процент этих пользователей скомпрометировал компьютеры или браузеры. У компании нет возможности это контролировать. Хранить секреты в этих типах приложений небезопасно, поскольку их можно проверять и декомпилировать. Чтобы воспользоваться преимуществами потока кода авторизации в общедоступном клиенте, используется расширение, называемое ключом проверки для обмена кодами (PKCE).
Изначально PKCE был разработан для повышения безопасности мобильных и нативных приложений, использующих OAuth 2.0. Недавно его использование было распространено на основанные на браузере приложения Singe-Page. Теперь PKCE рекомендуется даже для конфиденциальных клиентов.
Изучая раздел « Предоставление кода авторизации » в документе с рекомендациями по обеспечению безопасности, в нем говорится:
Клиенты, использующие тип разрешения авторизации, ДОЛЖНЫ использовать PKCE [RFC7636], чтобы (с помощью сервера авторизации) обнаруживать и предотвращать попытки внедрения (повторного воспроизведения) кодов авторизации в ответ авторизации.
Таким образом, использование PKCE имеет преимущество в безопасности даже с конфиденциальными клиентами, которые уже аутентифицируют себя с помощью секрета клиента.
Хотя это и не завершено, похоже, что следующая версия OAuth упростит стандарт и потребует использования PKCE во всех потоках, в которых участвует конечный пользователь (за пределами потока устройств ).
Последняя версия Spring Security (на момент написания статьи 5.2.1) изначально поддерживает OAuth 2.0 и OpenID Connect. Он поддерживает PKCE для публичных клиентов. Он пока не поддерживает PKCE для конфиденциальных клиентов. Есть запрос на удаление (написанный моим коллегой Брайаном Демерсом), который, как ожидается, будет включен в следующий выпуск. Тем не менее, Spring Security настолько хорошо написан и модульный, что сегодня легко подключиться к PKCE с конфиденциальным клиентом, чтобы воспользоваться преимуществами рекомендуемых лучших методов обеспечения безопасности.
Spring Star Boot Starter от Okta позволяет легко начать работу.
Начните с весеннего загрузчика Okta Starter
Вы можете найти полный исходный код этого поста здесь или перейти к start.spring.io, чтобы быстро создать приложение Spring Boot со всем необходимым для конфиденциального клиента. Единственные стартеры, которые вам нужны: Spring Web, Okta и Thymeleaf . Okta автоматически добавляет Spring Security. Thymeleaf используется для HTML-шаблонов. Ядро вашего pom.xml
должно выглядеть примерно так:
XML
1
<dependencies>
2
<dependency>
3
<groupId>org.springframework.boot</groupId>
4
<artifactId>spring-boot-starter-thymeleaf</artifactId>
5
</dependency>
6
<dependency>
7
<groupId>org.springframework.boot</groupId>
8
<artifactId>spring-boot-starter-web</artifactId>
9
</dependency>
10
<dependency>
11
<groupId>com.okta.spring</groupId>
12
<artifactId>okta-spring-boot-starter</artifactId>
13
<version>1.3.0</version>
14
</dependency>
15
</dependencies>
ПРИМЕЧАНИЕ. В примере кода используется Java 11.
Запустите приложение на Heroku с помощью дополнения Okta
Стартер Okta Spring Boot требует только трех свойств:
okta.oauth2.issuer
okta.oauth2.client-id
okta.oauth2.client-secret
В этом приложении используется дополнительное свойство для контроля того, будет ли использоваться PKCE:
okta.oauth2.pkce-always
В целях демонстрации приложения вы также можете установить уровень ведения журнала для веб-клиента, чтобы DEBUG
вы могли видеть, как выглядит POST при обмене кодом авторизации для токенов. Это выглядит так:
logging.level.org.springframework.web.client=DEBUG
Используя бесплатную учетную запись Heroku , вы можете легко развернуть приложение, нажав эту кнопку:
ПРИМЕЧАНИЕ. При нажатии кнопки развертывания в Heroku для вас будет выделена организация Okta, создайте веб-приложение Okta OpenID Connect, разверните пример приложения и задайте все переменные среды для запуска приложения. Посмотрите это видео, чтобы узнать больше о дополнении Okta Heroku .
ПРИМЕЧАНИЕ. Также полезно установить инструмент командной строки Heroku , если у вас его еще нет.
По умолчанию приложение настроено на работу без использования PKCE. Давайте посмотрим, что в действии в первую очередь. Пользователь был создан как часть процесса создания организации Okta. Чтобы увидеть имя пользователя и пароль, которые были автоматически сгенерированы для вас, используйте следующую команду:
Оболочка
xxxxxxxxxx
1
heroku config --app peaceful-citadel-41978
(Замените peaceful-citadel-41978
на то, что вы назвали приложение)
Вы увидите вывод так:
Простой текст
xxxxxxxxxx
1
=== peaceful-citadel-41978 Config Vars
2
LOGGING_LEVEL_ORG_SPRINGFRAMEWORK_WEB_CLIENT: DEBUG
3
OKTA_ADMIN_EMAIL: 109cd922-ab20-468e-a41d-01f986553087 .okta.com
4
OKTA_ADMIN_PASSWORD: A$1169b533-cfe9-466c-9355-07d7a922981a
5
OKTA_CLIENT_ORGURL: https://dev-155005.okta.com
6
OKTA_CLIENT_TOKEN: 007pm-Knwql_WrDDJIEFa74tt5WuuXh564kdPZsgni
7
OKTA_OAUTH2_CLIENT_ID_SPA: 0oaztv4iSIaEcJRxS4x5
8
OKTA_OAUTH2_CLIENT_ID_WEB: 0oazsmh9j6TegkxYD4x5
9
OKTA_OAUTH2_CLIENT_SECRET_WEB: 70nx3iG-mxzufsjYeb1-Gn5CUeo_n3slTpzBuliI
10
OKTA_OAUTH2_ISSUER: https://dev-155005.okta.com/oauth2/default
11
OKTA_OAUTH2_PKCE_ALWAYS: true
Смотрите PKCE в действии
Теперь вы можете перейти к приложению (рекомендуется окно в режиме инкогнито): https://peaceful-citadel-41978.herokuapp.com
. Когда вы нажмете кнопку « Профиль» , вы будете перенаправлены на вновь созданную организацию Okta. Введите имя пользователя и пароль , используя OKTA_ADMIN_EMAIL
и OKTA_ADMIN_PASSWORD
значение , которые вы видели с выхода конфигурации выше вход. На этом этапе вам необходимо задать секретный вопрос для учетной записи пользователя, так как это первый вход в систему. Это разовая операция.
После входа вы будете перенаправлены обратно в приложение и увидите информацию своего профиля.
Чтобы увидеть, что происходило внутри, взгляните на журналы приложений с помощью этой команды:
Оболочка
xxxxxxxxxx
1
heroku logs --app peaceful-citadel-41978
В нижней части вывода вы должны увидеть информацию о регистрации POST
запроса к конечной точке токена:
Простой текст
xxxxxxxxxx
1
o.s.web.client.RestTemplate : HTTP POST https://dev-155005.okta.com/oauth2/default/v1/token
2
o.s.web.client.RestTemplate : Accept=[application/json, application/*+json]
3
o.s.web.client.RestTemplate : Writing [{grant_type=[authorization_code], code=[qNkdyHI1spPxPjDicBUE], redirect_uri=[https://peaceful-citadel-41978.herokuapp.com/login/oauth2/code/okta]}] as "application/x-www-form-urlencoded;charset=UTF-8"
4
o.s.web.client.RestTemplate : Response 200 OK
Обратите внимание , что у него есть grant_type
, code
и redirect_uri
параметры. Это часть обычного потока кода авторизации.
Как только вы дошли до этого места, приложение в основном работает, как и ожидалось. Затем вы обновите переменную среды, чтобы приложение использовало PKCE. Запустите следующее:
Оболочка
xxxxxxxxxx
1
heroku config:set OKTA_OAUTH2_PKCE_ALWAYS=true --app peaceful-citadel-41978
Приложение будет перезапущено. Вы можете повторно просмотреть журналы и продолжить работу, выполнив следующую команду:
Оболочка
xxxxxxxxxx
1
heroku logs -t --app peaceful-citadel-41978
В новом окне инкогнито вернитесь к приложению и войдите как прежде. На этот раз вывод журнала для обмена токенами выглядит следующим образом:
Простой текст
xxxxxxxxxx
1
o.s.web.client.RestTemplate : HTTP POST https://dev-155005.okta.com/oauth2/default/v1/token
2
o.s.web.client.RestTemplate : Accept=[application/json, application/*+json]
3
o.s.web.client.RestTemplate : Writing [{grant_type=[authorization_code], code=[8z1EyS94F0WxWzI8Fx5h], redirect_uri=[https://peaceful-citadel-41978.herokuapp.com/login/oauth2/code/okta], code_verifier=[ugQLbLiF-IzJctR6TZkJBpgC6P38HrOpsr8vmYTYD7NAQLVIeMjQshst43S1NQtpaxL69pBRqEx-tpxixi1D4z7FOHiOctV6Gjn6DBN3CFmeMv-lvf_xMH4qzsvDZFmJ]}] as "application/x-www-form-urlencoded;charset=UTF-8"
4
o.s.web.client.RestTemplate : Response 200 OK
Обратите внимание, что на этот раз на выходе есть code_verifier
параметр. Это указывает на то, что PKCE использовался на начальном этапе авторизации и также используется на этапе токена.
Далее мы рассмотрим код, который заставляет все это работать.
Заставить PKCE работать для конфиденциальных клиентов в Spring Security
Хорошей новостью для нас является то, что большая часть того, что нам нужно для поддержки PKCE, уже встроена в Spring Security. Единственная проблема заключается в том, что Spring Security в настоящее время не поддерживает PKCE для конфиденциальных клиентов. Мы можем исправить это, используя существующую архитектуру, чтобы изменить поведение по умолчанию. Во-первых, нам нужен собственный обработчик запросов авторизации.
Джава
xxxxxxxxxx
1
public class CustomAuthorizationRequestResolver implements OAuth2AuthorizationRequestResolver {
2
private OAuth2AuthorizationRequestResolver defaultResolver;
4
private final StringKeyGenerator secureKeyGenerator =
5
new Base64StringKeyGenerator(Base64.getUrlEncoder().withoutPadding(), 96);
6
public CustomAuthorizationRequestResolver(ClientRegistrationRepository repo, String authorizationRequestBaseUri) {
8
defaultResolver = new DefaultOAuth2AuthorizationRequestResolver(repo, authorizationRequestBaseUri);
9
}
10
12
public OAuth2AuthorizationRequest resolve(HttpServletRequest servletRequest) {
13
OAuth2AuthorizationRequest req = defaultResolver.resolve(servletRequest);
14
return customizeAuthorizationRequest(req);
15
}
16
18
public OAuth2AuthorizationRequest resolve(HttpServletRequest servletRequest, String clientRegistrationId) {
19
OAuth2AuthorizationRequest req = defaultResolver.resolve(servletRequest, clientRegistrationId);
20
return customizeAuthorizationRequest(req);
21
}
22
private OAuth2AuthorizationRequest customizeAuthorizationRequest(OAuth2AuthorizationRequest req) {
24
if (req == null) { return null; }
25
Map<String, Object> attributes = new HashMap<>(req.getAttributes());
27
Map<String, Object> additionalParameters = new HashMap<>(req.getAdditionalParameters());
28
addPkceParameters(attributes, additionalParameters);
29
return OAuth2AuthorizationRequest.from(req)
30
.attributes(attributes)
31
.additionalParameters(additionalParameters)
32
.build();
33
}
34
private void addPkceParameters(Map<String, Object> attributes, Map<String, Object> additionalParameters) {
36
String codeVerifier = this.secureKeyGenerator.generateKey();
37
attributes.put(PkceParameterNames.CODE_VERIFIER, codeVerifier);
38
try {
39
String codeChallenge = createHash(codeVerifier);
40
additionalParameters.put(PkceParameterNames.CODE_CHALLENGE, codeChallenge);
41
additionalParameters.put(PkceParameterNames.CODE_CHALLENGE_METHOD, "S256");
42
} catch (NoSuchAlgorithmException e) {
43
additionalParameters.put(PkceParameterNames.CODE_CHALLENGE, codeVerifier);
44
}
45
}
46
private static String createHash(String value) throws NoSuchAlgorithmException {
48
MessageDigest md = MessageDigest.getInstance("SHA-256");
49
byte[] digest = md.digest(value.getBytes(StandardCharsets.US_ASCII));
50
return Base64.getUrlEncoder().withoutPadding().encodeToString(digest);
51
}
52
}
Примечание : addPkceParameters
и createHash
методы заимствованы из существующих , org.springframework.security.oauth2.client.web.DefaultOAuth2AuthorizationRequestResolver
находящихся в текущей версии исходного кода Spring Security.
customAuthorizationRequest
Метод , где действие. Предполагая, что OAuth2AuthorizationRequest
параметр не является нулевым, код:
- захватывает любые существующие атрибуты и дополнительные карты параметров из запроса
- добавляет необходимые атрибуты pkce и дополнительные параметры к существующим картам для каждого
- создает и возвращает новый,
OAuth2AuthorizationRequest
который включает атрибуты pkce и дополнительные карты параметров.
По сути, в четырех строках кода мы изменяем исходный запрос на авторизацию, чтобы включить параметры PKCE.
Вы можете отслеживать запрос авторизации в инструментах разработчика вашего браузера. Запрос будет выглядеть примерно так (добавлены новые строки для удобства чтения):
Файлы свойств
xxxxxxxxxx
1
https://dev-155005.okta.com/oauth2/default/v1/authorize?
2
response_type=code&
3
client_id=0oazsmh9j6TegkxYD4x5&
4
scope=openid%20profile%20email%20address%20phone%20offline_access&
5
state=ZQaSXDRv-GBuBPkB4DMkbmgthkMGmkImT49iCV5Wvyg%3D&
6
redirect_uri=https://peaceful-citadel-41978.herokuapp.com/login/oauth2/code/okta&
7
nonce=cJMgCAlCt_RpVuxb-p1dZ3TEOem1m7JR_NIXot_WM9s&
8
code_challenge=Hu9YyH7gPbfpK650J7H_cYHIrPNad6UE_DupSUV2mGE&
9
code_challenge_method=S256
Параметры code_challenge
и code_challenge_method
являются параметрами строки запроса, добавленными нашим addPkceParameters
методом выше. Обычный поток кода авторизации не включает эти дополнительные параметры.
Приятно то, что после добавления параметров PKCE на шаге авторизации Spring Security автоматически включает code_verifier
шаг токена без необходимости в дополнительном коде.
Есть одна небольшая уборка, которая должна быть сделана, чтобы связать все это вместе. Мы должны сказать Spring Security, чтобы использовать CustomAuthorizationRequestResolver
. Мы делаем это с помощью конфигурации безопасности.
Сообщите Spring Security об использовании настраиваемого средства разрешения запросов на авторизацию
Посмотрите на SecurityConfig.java
:
Джава
xxxxxxxxxx
1
2
public class SecurityConfig extends WebSecurityConfigurerAdapter {
3
private ClientRegistrationRepository clientRegistrationRepository;
5
private Environment env;
6
public SecurityConfig(ClientRegistrationRepository clientRegistrationRepository, Environment env) {
8
this.clientRegistrationRepository = clientRegistrationRepository;
9
this.env = env;
10
}
11
13
protected void configure(HttpSecurity http) throws Exception {
14
http
15
.authorizeRequests()
16
.antMatchers("/", "/img/**")
17
.permitAll()
18
.anyRequest()
19
.fullyAuthenticated();
20
if (Boolean.valueOf(env.getProperty("okta.oauth2.pkce-always"))) {
22
http
23
.oauth2Login()
24
.authorizationEndpoint()
25
.authorizationRequestResolver(new CustomAuthorizationRequestResolver(
26
clientRegistrationRepository, DEFAULT_AUTHORIZATION_REQUEST_BASE_URI
27
));
28
}
29
}
30
}
Первая часть configure
метода - это стандартная конфигурация Spring Security. Здесь мы выражаем, что домашняя страница ( /
) и все, что находится в статической img
папке, не требует аутентификации. Любой другой путь потребует аутентификации.
Вторая часть configure
метода проверяет okta.oauth2.pkce-always
переменную среды и, если она установлена, настраивает Spring Security authorizationRequestResolver
с помощью нашего CustomAuthorizationRequestResolver
.
Имея конфигурацию безопасности, мы можем теперь гарантировать, что приложение использует PKCE для OAuth 2.0, что повышает общую безопасность приложения.
Хорошие новости всем!
В самом ближайшем будущем, после объединения запроса на загрузку Spring-Security # 7804 и выпуска новой версии Spring Security (а также новой версии Spring Boot Spring Security Starter) вам не нужно будет использовать пользовательский преобразователь запроса авторизации и конфигурация безопасности, как показано выше. PKCE с конфиденциальными клиентами будет поведением по умолчанию.
Это соответствует текущим рекомендациям по безопасности, изложенным в разделе « Предоставление кода авторизации ».
Чтобы увидеть две спецификации руководств по рекомендациям OAuth 2.0, упомянутые в этом посте, используйте следующие ссылки
Чтобы узнать больше об OAuth 2.0 и OpenID Connect, я рекомендую следующие записи в блогах и видео:
- Руководство по Java OAuth 2.0: защитите свое приложение за 5 минут
- Используйте Okta Token Hooks для перезарядки OpenID Connect
- Иллюстрированное руководство по OAuth и OpenID Connect
- Что происходит с неявным потоком OAuth 2.0?
- Объяснение токенов доступа OAuth 2.0
- OAuth 2.0 и OpenID Connect (простым английским языком)
Для получения дополнительной информации о PKCE я рекомендую предыдущую статью PKCE, которую я написал, и нашу документацию:
- Реализация кода авторизации OAuth 2.0 с помощью потока PKCE
- Используйте поток кода авторизации с PKCE
Если вам нравится этот пост в блоге и вы хотите видеть его более похожим, следите за @oktadev в Twitter , подписывайтесь на наш канал на YouTube или подписывайтесь на нас в LinkedIn . Как всегда, пожалуйста, оставьте комментарий ниже, если у вас есть какие-либо вопросы.