Аутентификация через OpenID Connect
опубликовано: ⏰ 5Небольшой пример аутентификации через OpenID Connect для Spring Boot приложения с UI. Готового и полного примера (frontend + backend) как-то не получилось найти, поэтому и решил добавить эту статью и себе как заметку. Возможно, позже дополню статью.
Вводная
В чем суть? Нужно создать приложение, которое будет использовать OpenID Connect для аутентификации пользователей. При этом в приложении есть UI (ReactJS), и нужно сделать так, чтобы пользователь мог авторизоваться через OIDC провайдера. Провайдер любой, без привязки к определённому вендору.
Из чего будет состоять пример:
- Spring Boot приложение с Web MVC.
- Библиотека для работы OIDC на клиентской стороне.
- OAuth2 авторизация через OIDC с использованием Spring Security.
- Keycloak в качестве OIDC провайдера, но можно использовать и любой другой.
Серверная часть
Серверная часть – это Spring Boot приложение, которое будет использовать Spring Security для аутентификации пользователей через OIDC.
С серверной частью всё довольно просто. Проще всего создать нужное Spring Boot приложение с помощью Spring Initializr. В качестве зависимостей выбрать:
org.springframework.boot:spring-boot-starter-web.org.springframework.boot:spring-boot-starter-security.org.springframework.boot:spring-boot-starter-oauth2-resource-server– для работы с JWT токенами.
Далее необходимо настроить Spring Security.
public class SecurityConfig implements WebMvcConfigurer {
private final OidcJwtConverter jwtConverter;
@Bean
SecurityFilterChain demoSecurityFilterChain(final HttpSecurity http) throws Exception {
return http.oauth2ResourceServer(o -> o.jwt(jwt
-> jwt.jwtAuthenticationConverter(jwtConverter)))
.build();
}
}Класс OidcJwtConverter отвечает за преобразование JWT токена в токен аутентификации Spring Security –
org.springframework.security.core.Authentication.
@Component
public class OidcJwtConverter implements Converter<Jwt, AbstractAuthenticationToken> {
private final JwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter
= new JwtGrantedAuthoritiesConverter();
@Override
public AbstractAuthenticationToken convert(final Jwt jwt) {
final var expiresAt = jwt.getExpiresAt() == null ? 0 : jwt.getExpiresAt().toEpochMilli();
if (System.currentTimeMillis() >= expiresAt) {
SecurityContextHolder.getContext().setAuthentication(null);
return null;
}
final var authorities = jwtGrantedAuthoritiesConverter.convert(jwt);
final var identity = (String) jwt.getClaim(JwtClaimNames.SUB);
// {extra logic}
return new JwtAuthenticationToken(jwt, authorities, identity);
}
}Здесь {extra logic} – это дополнительная логика, которая может быть реализована в рамках конкретного проекта:
- Проверка на блокировку пользователя.
- Автоматическая регистрация пользователя или его профиля. Например, для случая, когда роли и разрешения назначаются не на стороне IdP, а в приложении.
- Получение списка ролей и разрешений пользователя, например, из БД.
Дополнительно стоит сделать кеширование токенов аутентификации, так как в случае stateless сервиса они будут проверяться и заполняться при каждом запросе. Что при выгрузке из БД может быть довольно накладно. Можно кешировать либо в памяти (если развёртывание без кластера), либо во внешнем кеше (Hazelcast, Redis, Infinispan и прочее).
Клиентская часть
Со стороны клиентской части есть несколько готовых реализаций OIDC клиентов (тут только часть из них):
- Не привязанные к определённому вендору, например:
- Для конкретных провайдеров, например:
К определённому провайдеру не хотелось привязываться, поэтому взят oidc-client-ts. На основе библиотеки сделана
обёртка react-oidc-context под ReactJS. Можно использовать её, если нет времени и желания разбираться с основной
библиотекой.
Основной класс управления авторизацией - UserManager. Он отвечает за:
- Получение токенов (access, refresh).
- Обновление токенов.
- Получение информации об авторизованном пользователе.
- Сброс авторизации (logout).
Пример конфигурации ниже:
import {UserManager} from 'oidc-client-ts';
const settings = {
authority: 'http://localhost:9080/realms/my-realm',
client_id: 'my-client-id',
redirect_uri: 'http://localhost:5173/callback',
response_type: 'code',
scope: 'openid profile email api',
post_logout_redirect_uri: 'http://localhost:5173/login',
};
const userManager = new UserManager(settings);authority– URL сервера авторизации (тут это Keycloak).client_id– идентификатор клиента.redirect_uri– URI, на который будет перенаправлен пользователь после успешной авторизации.response_type– тип ответа от сервера авторизации. В данном случае используетсяcode(Authorization Code Flow).scope– список запрашиваемых областей доступа.post_logout_redirect_uri– URI, на который будет перенаправлен пользователь после выхода из системы.
Необходимые методы:
userManager.getUser()– получение информации о текущем пользователе.userManager.signinRedirect()– перенаправление пользователя на страницу авторизации.userManager.signinCallback()– обработка результата авторизации. Возвращает информацию о пользователе.userManager.signoutRedirect()– завершение сеанса пользователя и перенаправление на страницу выхода.
URI для возврата от сервера авторизации можно сделать в виде отдельной страницы (callback), чтобы вызвать на ней метод
signinCallback и сохранить полученного пользователя локально. Этот метод можно вызвать только один раз после возврата,
так как он удаляет информацию о запросе из localStorage. После успешной авторизации, пользователя можно будет
перенаправить с неё на главную страницу.
Использование токенов доступа
Для использования токенов при обращении к серверному API (REST), необходимо добавлять заголовок Authorization в
формате Bearer <token> в каждом запросе.
Если используется axios, то можно добавить в стандартные заголовки
axios.defaults.headers.common['Authorization'] = 'Bearer ' + token;.
Как альтернатива, можно использовать
interceptors для динамического добавления заголовка к каждому запросу с учётом определённой логики.
axios.create({...}).interceptors.request.use(
(config) => {
if (oidc.isAuthenticated()) {
config.headers.Authorization = `Bearer ${oidcProtocol.getToken()}`;
}
return config;
},
(error: AxiosError) => {
return Promise.reject(error);
},
);Identity Provider (IdP)
В примере используется Keycloak. Настройку приводить не будут, так как там всё относительно просто. Необходимо:
- Создать клиент с
Standard flow. Все настройки по умолчанию, только в поляхValid *указать корректные URI. - Создать пользователя.
Заключение
Понятно, что это только базовая реализация, но с ней можно пройти процесс аутентификации через OpenID Connect и получить токен для дальнейшего использования в запросах к API.
PS: Вместо oidc-client-ts можно взять react-oidc-context, чтобы не писать заново логику под ReactJS.
Так как мне нужна своя реализация под tekBlueprint.frame, то использовал oidc-client-ts.
ℹ️ Примечание
Примера проекта пока нет, возможно будет попозже.