Blitz IDP: внедряем OAuth 2.0 в Java-приложении

от автора

Что может быть общего у разработчика из крупной московской ИТ-компании и пенсионерки из Вологодской области? Ну, например, они оба регулярно пользуются SSO — технологией единого входа. Разработчик входит под одной учеткой во все корпоративные рабочие системы, а пенсионерка авторизуется через «Госуслуги» — чтобы записаться к врачу, проверить пенсию или оплатить коммунальные услуги. Об этом и поговорим, в смысле, об SSO, а не о «Госуслугах».

Привет, Хабр! Я Денис Радостев, старший  backend-разработчик в IBS. В этой статье расскажу о Blitz Identity Provider, российской платформе управления цифровой идентичностью, которая обеспечивает единый вход — SSO, многофакторную аутентификацию и централизованное управление пользователями, и как ее можно интегрировать с приложениями Java по протоколу OAuth 2.0. Покажу ключевые настройки Blitz IDP и моменты, которые важно учитывать на стороне Java-приложения.

А вместо заключения расскажу про наш собственный кейс, связанный с синхронизацией пользователей между Blitz IDP и нашей старой БД для самописной авторизации.

Особенности Blitz Identity Provider

Ключевая фишка Blitz IDP — это поддержка современных протоколов: OAuth 2.0, OpenID Connect, SAML 2.0/WS-Federation и даже Radius. Это делает его универсальным мостом между корпоративными приложениями, написанными на любых языках и технологиях. 

В этой статье я сфокусируюсь на связке Blitz IDP версии 5.11.3 и Java Spring Boot через стандарт OAuth 2.0.

OAuth 2.0 в Blitz IDP

Прежде чем писать код, нужно познакомить Java-приложение с Blitz IDP. Это можно сделать с помощью четырех шагов:

  • регистрации приложения в консоли администрирования Blitz IDP;

  • генерации секретного ключа Secret ID; 

  • настройки редиректа;

  • выдачи разрешения на приложение.

В консоли администрирования Blitz IDP надо перейти в раздел «Приложения» и нажать «Добавить приложение». После чего в базовых настройках указываем Client ID.

На скриншоте ниже есть уже добавленные приложения. Их названия чаще всего указываются на русском, а чуть ниже указывается Client ID.

В добавленном приложении мы выбираем протокол OAuth 2.0, и система генерирует секретный ключ client_secret. Это первый параметр на скриншоте ниже:

Критически важно для безопасности указать redirect_url — адрес, куда Blitz будет перенаправлять пользователя после логина. Без него авторизация работать не будет. На скриншоте редирект указан предпоследним параметром, это префиксы ссылок возврата (как с https, так и без).

Последним шагом мы определяем, к каким именно данным пользователя приложение получит доступ. Список допустимых разрешений внизу скриншота. Здесь указаны usr_grps, blitz_groups, openid и profile.

После настройки приложения можно переходить к самому интересному — к коду.

Интеграция с Java (на примере Spring Boot)

Покажу интеграцию на примере Spring Boot.

Мы должны добавить стандартный стартер для OAuth-клиента.

А в application.yml указываем настройки для подключения Blitz — CLIENT_ID, тот же, что и при регистрации приложения, и сгенерированный BLITZ_CLIENT_SECRET — секретный ключ. Также обязательно указываем redirect-uri и scope.  

В самом низу указаны стандартные адреса Blitz, с помощью которых будет работать авторизация.

Настройка Spring Security

Настройка Spring Security довольно простая. Достаточно подключить OAuth 2.0, а все остальные параметры подтянутся из application.yml.

Получение данных пользователя

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

Мы можем получить стандартные данные: ФИО, почту, номер телефона и список групп, в которых пользователь состоит. 

Обратите внимание на поле sub. Это внутренний ключ нашего пользователя в системе Blitz.

Валидация токенов и безопасность

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

Самый первый, он же самый простой, — проверка подписи (JWT). Sprint Security с помощью application.yml сам подгружает публичные ключи с JWKS URI Blitz IDP и верифицирует подпись каждого access token. 

Второй способ также работает через Spring Security, но с дополнительными настройками. Это валидация claims. Для ее выполнения необходимо настроить introspection (интроспекцию). Метод выполняется дольше, но он способен автоматически отзывать истекшие токены (refresh выполняется через refresh token).

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

Logout-логика

Для выхода из  системы опять же требуется настройка Blitz. Требуется опять зайти в консоль администрирования и указать редирект. На скриншоте ниже это второй параметр.

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

Со стороны Java в Spring Security необходимо просто настроить редирект.

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

Синхронизация пользователей из Java-приложения

В качестве примера из собственного опыта расскажу историю, как мы настраивали синхронизацию пользователей между Blitz IDP и нашей старой БД для самописной авторизации

Авторизацию переписывали несколько раз. Сначала она была самописной, потом перешли на ЕСИА, а сейчас сделали все через Blitz. В такой схеме изначально данные пользователей, коих было очень много — больше миллиона, хранились в нашей же БД. Требовалось перенести все это в Blitz, так как без данных ничего бы не заработало, и попутно разослать всем новые логины и пароли — у Blitz были настроены более строгие правила для паролей.

Зарегистрировать такое количество пользователей вручную нереально. Поэтому мы написали кастомную синхронизацию пользователей между нашей БД и Blitz.

Здесь видно, как мы берем пользователя из базы, проверяем, есть ли он в Blitz, и при необходимости регистрируем.

//Берем всех пользователей из БД        List<UserBlitz> userBlitzs = userBlitzRepository.findAll();        for (UserBlitz user : userBlitzs) {            //Проверка, если не нашли пользователя в системе Blitz            if (!getUser(user.getId())) {                //Регистрируем пользователя в системе                createUser(user);                //Если у пользователя есть группы                if (user.getRegion() != null) {                    String groupId = String.format(FORMAT_REGION_ID, user.getRegion().getRegionCode());                    GroupDto groupDto = getRegion(groupId);                    //Если такой группы не существует в системе Blitz                    if (groupDto == null) {                        //Создаем группу                        createRegion(groupId, user.getRegion().getSubjectName());                    }                    //Добавляем пользователя в группу                    addUserInGroup(groupId, user.getId());                }            }        }

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

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

private Boolean getUser(String sub) {        HttpHeaders headers = new HttpHeaders();        headers.set("Authorization", "Basic " + toBase64(config.getClientId(), config.getClientSecret()));        ResponseEntity<String> response = restTemplate.exchange(config.getFindUserURL(sub), HttpMethod.GET, new HttpEntity<>(headers), String.class);        return !response.getBody().equals("[]");    }
    @SneakyThrows    private void createUser(UserBlitz user) {        HttpHeaders headers = new HttpHeaders();        headers.setContentType(MediaType.APPLICATION_JSON);        headers.set("Authorization", "Basic " + toBase64(config.getClientId(), config.getClientSecret()));        HttpEntity<String> entity = new HttpEntity<>(new ObjectMapper().writeValueAsString(parseUser(user)), headers);        restTemplate.exchange(config.getCreateUserURL(), HttpMethod.PUT, entity, String.class);    }

Обратите внимание на header, где прописана авторизация. Blitz просит авторизацию Basic, которая состоит из ClientId и ClientSecret-приложения.

@SneakyThrows    private void createRegion(String groupId, String groupName) {        GroupDto groupDto = new GroupDto();        groupDto.setId(groupId);        groupDto.setName(groupName);        groupDto.setProfile("orgs");        ObjectMapper mapper = new ObjectMapper();        restTemplate.exchange(config.getCreateGroupUrl(), HttpMethod.POST, getHeaderWithBearer(mapper.writeValueAsString(groupDto)), GroupDto.class);    }

А вот код для создания группы. Здесь мы используем хедеры с авторизацией Bearer, где мы также получаем CredentialsToken, это отдельный метод.

private HttpEntity<String> getHeaderWithBearer(String body) {        HttpHeaders headers = new HttpHeaders();        headers.setContentType(MediaType.APPLICATION_JSON);        headers.set("Authorization", "Bearer " + getCredentialsToken());        return new HttpEntity<>(body, headers);    }

Вот он:

public String getCredentialsToken() {        // Заголовки        HttpHeaders headers = new HttpHeaders();        headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);        headers.set("Authorization", "Basic " + toBase64(config.getClientId(), config.getClientSecret()));        // Параметры запроса        String body = "grant_type=client_credentials&scope=blitz_groups";        // Создаем сущность запроса        HttpEntity<String> entity = new HttpEntity<>(body, headers);        // Выполняем запрос        ResponseEntity<TokenDto> response = restTemplate.exchange(config.getBlitzTokenEndpoint(), HttpMethod.POST, entity, TokenDto.class);        if (response.getBody() != null) {            return response.getBody().getAccessToken();        } else {            throw new AuthException("Произошла ошибка при авторизации");        }    }

Этот токен можно получить из самого Blitz, но для этого надо указать авторизацию Basic, снова передать ClientId, ClientSecret и разрешение.

Мне показалось интересным, что для создания и регистрации пользователя достаточно basic, но для создания и получения группы нужна авторизация bearer, которая также получается через basic. То есть для работы с группами происходит двойная авторизация, а для работы с самим пользователем — одинарная. Почему так сделано — для меня загадка. В документации это не расписано. 

Еще один интересный момент. В Blitz есть генерация паролей и рассылка писем. Но они работают не так, как хотелось бы. 

Мы можем создать пользователя и не придумать ему пароль самостоятельно, а просто нажать на кнопку «генерация пароля». Но работает она только из консоли Blitz. Пароль скопируется в буфер обмена и его можно куда-нибудь отослать либо где-то сохранить. Однако когда мы обращаемся к Blitz из Java, генерация не работает. В итоге нам обязательно нужно сгенерировать пароль на стороне Java.

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

Личные впечатления от опыта работы с Blitz IDP

Нам удалось легко и быстро настроить интеграцию через Spring Security. Также Blitz позволяет сделать кастомную реализацию, чем мы и воспользовались. Это помогло быстрее перенастроить работу со старой самописной авторизации на Blitz.

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

В самом Blitz есть генерация пароля и отправка писем, но работает этот функционал с нюансами, о которых я рассказал.

И последнее: Blitz — живой, активно развивающийся продукт. Его все еще разрабатывают. При этом у нас на проектах используется сертифицированная версия. Она одна и не является самой свежей. В интернете чаще всего есть информация о более новых версиях, поэтому нам приходится постоянно обращаться к документации (благо она на русском).

ссылка на оригинал статьи https://habr.com/ru/articles/1043914/