Прикручиваем авторизацию на основе KeyCloak к веб-приложению / Хабр

Что документируется в разделе аутентификации

В документации API не нужно подробно объяснять внешним пользователям, как работает аутентификация. Отсутствие объяснений внутренних процессов аутентификации, является лучшей практикой, поскольку хакерам будет сложнее злоупотреблять API.

Тем не менее нужно объяснить необходимую информацию:

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

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

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

Hmac (код авторизации сообщений на основе хэша)

HMAC расшифровывается как Hash-based message authorization code – код авторизации сообщений на основе хэша и является более строгим типом аутентификации, более распространенным в финансовых API. В HMAC только отправитель, и получатель знают секретный ключ, который больше неизвестен никому.

Затем сообщение кодируется секретным ключом и проходит через алгоритм безопасного хеширования (SHA – secure hashing algorithm). (Хеш – это зашифрованная строка на основе алгоритма.) Результирующее значение, называемое сигнатурой, помещается в заголовок запроса.

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

Вот диаграмма, отображающая процесс авторизации HMAC:

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

Amazon web services

авторизация амазон

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

Api ключ

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

Ключи APK используют строку в свойстве заголовка для авторизации запросов

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

Auth-сервис

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

Пользователи будут храниться в БД. В идеале это должна быть отдельная БД, но в обучающих целях будем использовать ту же, что использовали в первой части. Опишем DAO пользователя:

@Data
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "clients")
public class ClientEntity {
    @Id
    @Column(name = "client_id")
    private String clientId;
    private String hash;
}

Репозиторий будет выглядеть совсем просто:

public interface ClientRepository
        extends CrudRepository<ClientEntity, String> {
}

Т.к. мы будем реализовывать Client Credentials Flow в терминах протокола OAuth, то здесь client — это и есть пользователь. Соответственно clientId, clientSecret — аутентификационные данные пользователя. В открытом виде пароль пользователя хранить нельзя, поэтому будем хранить некий хеш, о котором будет написано ниже.

Опишем сервис, который будет регистрировать нового клиента и проверять его аутентификационные данные:

public interface ClientService {
    void register(String clientId, String clientSecret);
    void checkCredentials(String clientId, String clientSecret);
}

Для правильного хранения паролей в БД будем использовать Bcrypt. Это криптографическая хеш-функция, основанная на шифре Blowfish. Воспользуемся реализацией в библиотеке jBCrypt, добавим зависимость в проект:

dependencies {
    ...
    implementation group: 'org.mindrot', name: 'jbcrypt', version: '0.4'
}

Реализуем интерфейс ClientService:

Basic auth

Другой тип авторизации называется Basic Auth. С помощью этого метода отправитель помещает пару имя пользователя:пароль в заголовок запроса. Имя пользователя и пароль кодируются с помощью Base64, который представляет собой метод кодирования, который преобразует имя пользователя и пароль в набор из 64 символов для обеспечения безопасной передачи. Вот пример Basic Auth в заголовке запроса:

Dropbox

Авторизация в Dropbox

Jwt (json web tokens)

JWT — это стандарт, основанный на JSON, для создания токенов доступа. Он состоит из трех частей: заголовка, тела и подписи:

В заголовке указывается тип токена и алгоритм подписи:

{
  "typ": "JWT",
  "alg": "HS256"
}

В тело записывается необходимая пользовательская информация (payload). Для аутентификации и авторизации это может быть id пользователя, его роль, время действия токена:

Похожее:  Авторизация на сайте через ВК с занесением пользователя в базу данных (PHP) »

Keycloak: настройки

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

В рамках статьи основные исходные настройки KeyCloak следующие:

realm – myproject (Зарегистрированное в KeyCloak имя проекта для которого разрабатывается приложение)

client_id – myproject-app (Зарегистрированное в KeyCloak имя приложения)

Более подробно о этих параметрах – ниже.

Oauth для полностью клиентских приложений

Стороннее приложение может представлять собой лишь графический интерфейс без серверной бизнес-логики и выполнять взаимодействие с сервисом API с помощью кода на клиенте — например, мобильное приложение календаря, заметок и т.д.

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

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

При таком подходе опускается обмен кода авторизации на токен доступа во время обмена запросами между серверами приложения и API. Вся авторизация, как было отмечено ранее, происходит на стороне клиента.

Oauth для приложений с серверной частью: рассмотрим по шагам

Последовательность шагов приведена на схеме ниже.

  1. Пользователь перенаправляется на страницу авторизации, где у него запрашиваются разрешения для приложения на работу с данными его аккаунта.
  2. После предоставления необходимых разрешений пользователь попадает на callback URL — адрес, указанный при регистрации приложения, предназначенный для завершения авторизации. При этом происходит подстановка кода авторизации в  GET-параметры адреса.
  3. Сервер клиентского приложения формирует POST-запрос к серверу авторизации API с кодом авторизации в качестве параметра.
  4. Сервер авторизации проверяет код и возвращает приложению токен доступа (access token).
  5. Используя токен, приложение авторизуется на сервере API и получает доступ к запрашиваемым пользовательским ресурсам.

Стоит отметить, что описываемый вариант авторизации является самым сложным, но только в рамках этой механики можно достоверно идентифицировать клиентское приложение (благодаря коммуникации между серверами на последнем шаге). Во всех остальных случаях авторизация проходит только на клиенте, что позволяет злоумышленникам маскировать одно приложение под другое. Данный фактор необходимо учитывать при внедрении механизма OAuth авторизации в своих сервисах.

Авторизация

Нам осталось добавить авторизацию в написанное приложение Bookstore. Откроем проект из предыдущей части и добавим в зависимости библиотеку для работы с JWT (как было показано выше). Также добавим две новых настройки в application.properties:

auth.enabled=true
auth.jwt.secret=30faa058f27f690c7e9a098d54ebcfb3d8725bcb85ee7907a2d84c69622229e2

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

Для проверки полученного токена опишем интерфейс TokenService и его реализацию:

public interface TokenService {
    boolean checkToken(String token);
}

@Service
@Slf4j
public class DefaultTokenService implements TokenService {
    @Value("${auth.jwt.secret}")
    private String secretKey;

    @Override
    public boolean checkToken(String token) {
        Algorithm algorithm = Algorithm.HMAC256(secretKey);
        JWTVerifier verifier = JWT.require(algorithm).build();

        try {
            DecodedJWT decodedJWT = verifier.verify(token);
            if (!decodedJWT.getIssuer().equals("auth-service")) {
                log.error("Issuer is incorrect");
                return false;
            }

            if (!decodedJWT.getAudience().contains("bookstore")) {
                log.error("Audience is incorrect");
                return false;
            }
        } catch (JWTVerificationException e) {
            log.error("Token is invalid: "   e.getMessage());
            return false;
        }

        return true;
    }
}

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

Закроем все эндпоинты и разрешим доступ только при наличии авторизационного токена. Значение авторизационного токена необходимо положить в заголовок следующим образом:

Authorization: Bearer <значение токена>

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

Авторизация пользователя при обращении к web сервису 1с

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

Взаимодействие фронта с микросервисами. axios

Для взаимодействия с микросервисами будем использовать библиотеку AXIOS.

Поскольку у нас будут 2 типа запросов:авторизированные (для работы с микросервисами) и неавторизированные (для самой авторизации и refresh токена) – для каждого типа запроса будем использовать отдельный инстанс axios.

Итак, создадим утилитарный класс с нашими axios – AxiosInstance.js и добавим в него экземпляр axios для авторизированных запросов:

export const axiosInstance = axios.create({
    headers: { Authorization: `Bearer ${getToken()}` }
});

Здесь мы добавили в заголовок параметр авторизации со значением – bearer-token. Он будет посылаться в каждом запросе.

Кроме того, перед каждым запросом с фронта нам придется проверять валидность токена и, в случае если он невалиден (истек срок действия например) – делать его refresh. После этого в запросе уйдет уже обновленный валидный token.

Итак, добавим вызов метода verifyToken в axios request interceptor:

axiosInstance.interceptors.request.use(request => {
    verifyToken().catch(() => refreshToken(getRefreshToken()));
    return request;
});

В случае, если токен невалиден, метод кидает исключение, в обработке которого вызываем обновление токена методом refreshToken, а в качестве его аргумента передаем refresh_token который хранится в нашем localStorage и доступный через утилитарный метод getRefreshToken.

Похожее:  Личный кабинет Система Город: официальный сайт

Теперь осталось только проверить статус ответа в авторизированных запросах. Сделаем это через axios response interceptors:

axiosInstance.interceptors.response.use(response => {
    if (response?.headers?.authorization) {
        setToken(response.headers.authorization);
    }
    return response;
}, error => {
    if (error?.response?.status === 401) {
        logout();
    }
    return Promise.reject(error);
});

Доступ из visual studio 2022

Как подробно описано в статье на хабре, чтобы добавить ссылку на опубликованный web сервис 1С в Visual Studio для разработки клиента необходимо создать приложение (например, консольное), кликнуть правой кнопкой мышки на solution и выбрать пункт «Add Service Reference…».

Далее разрабатываем обычный клиент для web сервиса.

Удачи!!!

Используемые роли в oauth 2

В рамках описываемого протокола выделяются следующие типы ролей:

  • владелец (пользователь): авторизует клиентское приложение на доступ к данным своего аккаунта;
  • сервер ресурсов/API: здесь располагаются данные пользовательских аккаунтов, а также бизнес-логика авторизации, отвечающая за выдачу новых OAuth-токенов и проверку их подлинности при обращении клиентских приложений к ресурсам. Целесообразно объединять эти роли, так как физически это один сервис;
  • клиентское приложение: собственно сервис, которому пользователь делегирует права доступа к своим данным на сервере ресурсов. Пользователь должен авторизовать приложение, а со стороны сервера API оно должно получить подтверждение в виде ключа (токена) доступа. 

Как работает oauth 2: от запроса на авторизацию до генерации токена

Рассмотрим схему, описывающую принцип действия протокола.

Выделим следующие шаги:

  1. Приложение запрашивает у пользователя разрешение на доступ к серверу ресурсов.
  2. После получения разрешения приложение сообщает об этом авторизационному серверу, а также предоставляет ему сведения о себе.
  3. Сервер авторизации проверяет подлинность разрешения и предоставленных сведений о приложении. В случае успешной проверки генерируется токен доступа.
  4. Далее приложение обращается к серверу ресурсов, предоставляя токен в качестве подтверждения пройденной авторизации.
  5. Сервер ресурсов проверяет действительность токена и выдает приложению доступ к запрашиваемому ресурсу.

В зависимости от бизнес-логики клиентского приложения последовательность шагов может меняться. Далее рассмотрим наиболее распространенные примеры использования OAuth 2.

Клиент для auth server

Создадим класс AuthClient.js с методом аутентификации и refresh токена:

Клиент для keycloak

Клиент для KeyCloak будет содержать 2 метода:

Оба метода будут возвращать класс org.keycloak.representations.AccessTokenResponse.class из подключенной ранее библиотеки. Структура класса соответствует спецификации OpenId Connect для Successful Token Response.

Код клиента будет выглядеть так:

Контроллеры

Далее нам нужно создать котроллер с методом для аутентификации AuthenticateController.java:

@RestController
@RequiredArgsConstructor
@RequestMapping("/authenticate")
public class AuthenticateController {
    private final KeyCloakClient keyCloakClient;

    @PostMapping
    public ResponseEntity<AccessTokenResponse> authenticate(@RequestBody AuthRequestDto request) {
        return ResponseEntity.ok(keyCloakClient.authenticate(request));
    }
}

Конфигурация сервиса

Создадим проект auth server и добавим в него необходимые зависимости. Конфигурационный файл pom.xml будет выглядеть так:

Конфигурирование nginx

Поскольку фронт будет обращаться к нескольким микросервисам, нужна единая точка для взаимодействия фронта с ними. В качестве единой точки был выбран nginx.

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

Минусы

  • различия в подходах к реализации у разных сервисов, порождающие необходимость написания отдельного кода под каждый новый сервис;
  • если реализована интеграция с семейством приложений, работающих в одной экосистеме (например, Google), существует риск для всех интеграций при компрометации учетных данных либо сбое на стороне сервера API;
  • OAuth 2.0 является новым и динамично развивающимся протоколом, в связи с чем возможны некоторые логические противоречия в его спецификации;
  • основанная на SSL безопасность протокола может стать проблемой в высоконагруженных проектах.

Обновление доступа

Как можно было видеть в предыдущих примерах, вместе с токеном при успешной авторизации возвращается также и ключ для обновления доступа (refresh_token). Он может использоваться для обновления токена по окончании его срока жизни, а также принудительного обновления во избежание компрометации токена при его передаче по открытым каналам. В случае успешного обновления доступа сервер API перевыпустит не только access, но и refresh_token.

Образцы разделов авторизации

Ниже приведены несколько примеров разделов авторизации в документации API.

Определяем термины

Во-первых, давайте определимся с некоторыми ключевыми терминами:

  • Аутентификация: подтверждение правильности личности
  • Авторизация: разрешение определенного действия

API может аутентифицировать, но не разрешит делать определенный запрос.

аутентификация и авторизация

Плюсы

  • использование SSL для защиты данных,
  • ограничение прав доступа стороннего приложения к пользовательским ресурсам областями видимости,
  • обилие библиотек для реализации использования протокола во всех языках общего назначения.

После чего рабочий web.config для опубликованного web сервиса 1с будет выглядеть следующим образом:

Эквивалент последней выполненной операции (настройка <authentication>) — публикация сервиса с включенной галкой в чекбоксе «Использовать аутентификацию операционной системы на веб-сервере». 1С при публикации меняет эту настройку не в web.config, а в настройках IIS через API. В любом случае изменения должны быть видны в консоли управления IIS:

Последствия нехватки безопасности api

Почему даже API-интерфейсы нуждаются в аутентификации? Для API, которые предназначены только для чтения, иногда пользователям не нужны ключи. Но большинство коммерческих API требуют авторизации в виде ключей API или других методов. Если нет никакой защиты API, пользователи могут совершать неограниченное количество запросов API без какой-либо регистрации. Разрешение неограниченных запросов усложнит модель дохода для вашего API.

Похожее:  Basic Authentication for json-server - DZone Security

Вдобавок, без аутентификации не было бы простого способа связать запросы с конкретными данными пользователя. И не было бы способа защиты от запросов от злонамеренных пользователей, которые могут удалить данные другого пользователя (например, путем удаления запросов DELETE для учетной записи другого пользователя).

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

В целом, аутентификация и авторизация с помощью API служат следующим целям:

Преимущества и недостатки oauth 2

Рассмотрим подробнее плюсы и минусы протокола авторизации.

Пример реализации протокола на php

Рассмотрим живой пример проекта клиент-серверного стороннего приложения, взаимодействующего по API с сервисом Google Таблицы.

Публикация web сервиса 1с на iis 7.5

Как уже упоминал выше, с публикацией web сервиса на IIS 7.5 с первого раза у меня не задалось, хотя тонкий клиент запускается без проблем. Поскольку пароль в конфигурационном файле по соображениям безопасности меня не устраивал, вернулся к вопросу настройки IIS.

Работа с токенами

Полученные от KeyCloak access_token и refresh_token будем хранить в localStorage. Создадим для работы с токенами и localStorage утилитарный класс TokenUtils.js:

Разграничение интерфейса по ролям

Для того чтобы предоставить на фронте доступ к интерфейсу/функционалу соответствующий каждой роли, нам остается вызвать в соответствующих местах ранее созданный метод TokenUtils.hasRole(‘role_name’).

Например, отрисовка элементов Tab будет выглядеть следующим образом:

Различия протоколов openid и oauth 2

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

Для верификации пользователя OpenID использует ID учетной записи у провайдера, а OAuth — авторизационные ключи (токены) с предопределенным сроком действия и правами доступа.

Разные виды авторизации

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

Сервис авторизации

Итак, первое что нужно для нашей задачи – сервис авторизации (далее auth server) осуществляющий авторизацию пользователей (предоставление access_token), продление токена (refresh_token) и валидацию токена. Сам сервис будет максимально простым и содержать минимум boilerplate кода, потому что большая его частьбудет переложена на “плечи” KeyCloak.

Поскольку сам auth server будет написан на Spring Boot, для его интеграции с KeyCloak будем использовать Spring Boot KeyCloak Adapter. Данное решение рекомендуют сами разработчики KeyCloak.

Страница авторизации

На странице авторизации создадим метод login и повесим его в качестве обработчика на кнопку «Войти».

AuthPage.js:

const login = async () => {
    await authenticate(credentials.login, credentials.password)
        .then(() => {
            window.location.href = '/';
        }).catch(() => setError(true));
};

Здесь мы вызываем созданный ранее метод authenticate, а в качестве обработчика успешного выполнения метода передаем редирект на корневой маппинг приложения (‘/’). В качестве обработчика ошибки выполнения метода передаем вызов функции setError, в моем случае – это просто установка хука error, означающего, что надо показать сообщение об ошибке.

👨‍💻 практическое занятие: авторизация

В своем найденном опен-сорс проекте найдем информацию об авторизации для запросов к API. Ответьте на следующие вопросы:

  • Какого рода авторизация требуется для отправки запросов к API?
  • Существуют ли разные уровни доступа в рамках авторизации (например, платные и бесплатные), которые определяют, сколько запросов можно сделать или какие типы информации можно получить?
  • Можно ли вы получить ключ API или какой-либо другой метод авторизации, необходимый для выполнения тестовых вызовов API?
  • Как информация об авторизации интегрируется в раздел “Начало работы”?

Go next ➡

Sendgrid

SendGrid
API ключ SendGrid

SendGrid предлагает подробное объяснение ключей API, начиная с основ, поясняя: «Что такое ключи API?». Контекстно раздел ключей API появляется вместе с другими разделами по управлению учетными записями.

Заключение

На текущий момент протокол широко распространен и активно развивается — в сервисах соцсетей, Google, IoT-сервисах для устройств умного дома и других. Наиболее полно его возможности раскрываются при интеграции с серией сервисов одного поставщика (например, Google), где вашему приложению можно с легкостью добавлять новые интеграции, используя те же учетные данные.

Автор текста: Роман Андреев.

В заключение

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

Стоит отметить, что в случае взаимодействия фронта только с  одним сервисом, стоит пересмотреть решение, исключив nginx, как инструмент для предварительной проверки авторизованности запроса (метод /validate вызываемый каждый раз перед вызовом сервиса). В таком случае весь механизм работы с spring-keycloak-adapter переносится в единственный сервис (back).

Также, возможно, полезно будут следующие материалы:

1 Звезда2 Звезды3 Звезды4 Звезды5 Звезд (1 оценок, среднее: 4,00 из 5)
Загрузка...

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *