Работа SOWA с OAUTH2#

Platform V SOWA может быть установлена перед защищенным сервером и настроена на проверку пользовательских JWT токенов.

Ниже приведен алгоритм проверки:

  1. Пользователь самостоятельно получает токен из IDP.

  2. Пользователь обращается через SOWA на защищенный сервер, отправляя токен в заголовке apikey. Пример запроса:

    curl -vk https://localhost:12345/back -X POST -d „request“ -H «apikey: eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJaLWFLR0l1QU9JdWdpY09hYW1DMEMzQkRjNmNEekFJZThzN2VoVjFxLWFVIn0.eyJleHAiOjE3MjEwNTI5NTEsImlhdCI6MTcyMTA1MjY1MSwianRpIjoiMDNjYjBmZWEtY2VjOS00MTZjLThjZGUtYzMxZDJkOWI0MjVlIiwiaXNzIjoiaHR0cHM6Ly9wbGF0Zm9ybWF1dGgtaWZ0My5zYy5kZXYuc2J0L2F1dGgvcmVhbG1zL1BsYXRmb3JtQXV0aCIsImF1ZCI6WyJQbGF0Zm9ybUF1dGhaIiwiUGxhdGZvcm1VRlMiXSwic3ViIjoiYjNjY2Y5OTUtMTU3NS00MTQxLThmYzQtYmIwMTA5NTJlYmU4IiwidHlwIjoiQmVhcmVyIiwiYXpwIjoiU293YUF1dGgiLCJzZXNzaW9uX3N0YXRlIjoiZWQwOWVkMTItMTZhYS00MmYwLWIxYzItMzM1ZDVkMDUyYmIxIiwiYWNyIjoiMSIsInJlYWxtX2FjY2VzcyI6eyJyb2xlcyI6WyJwbGF0Zm9ybWF1dGhfdXNlciIsInVtYV9hdXRob3JpemF0aW9uIiwiZGVmYXVsdC1yb2xlcy1wbGF0Zm9ybWF1dGgiLCJzb3dhLWRldi10ZXN0LXJvbGUiXX0sInJlc291cmNlX2FjY2VzcyI6eyJhY2NvdW50Ijp7InJvbGVzIjpbIm1hbmFnZS1hY2NvdW50IiwibWFuYWdlLWFjY291bnQtbGlua3MiLCJ2aWV3LXByb2ZpbGUiXX19LCJzY29wZSI6ImxvZ2luIHByb2ZpbGUgdWZzX3Njb3BlIGVtYWlsIGVtcGxveWVlIiwic2lkIjoiZWQwOWVkMTItMTZhYS00MmYwLWIxYzItMzM1ZDVkMDUyYmIxIiwiZW1haWxfdmVyaWZpZWQiOmZhbHNlLCJuYW1lIjoiU293YSBUZXN0LVVzZXIiLCJwcmVmZXJyZWRfdXNlcm5hbWUiOiJzb3dhLWRldi10ZXN0LXVzZXIiLCJnaXZlbl9uYW1lIjoiU293YSIsImZhbWlseV9uYW1lIjoiVGVzdC1Vc2VyIiwiZW1haWwiOiJzb3dhLWRldi10ZXN0LXVzZXJAbWFpbC5sb2NhbCJ9.HXUcu4sUTJy32d2JDjK7_iXkUzi0rBBdCR42xC7otHJxRLSKEhyLM116KKr9CB5MFTOi9x4ogoAazcF4dr4XuL7lOWOAfVYf_w3fjCiq6ZyKny1wej8LPRJzEcv92mwTtjsHW3Aah5NffGgOL_yb0q1i17CJbRw7qeJc9jg5v7xeDPx3uRYFhfgqT3uAuMRR2VwbCnKAczp8LDWf7hLnR76EMb4hD6uMrUdDilv0XxswI034cxOSbx2UpTNwNxO1-oOiZfYDpY9OIJrVJLNOi9N2muS9EWlVrz686AbhvGlGgQ3KeyMXCACsrM0KfcM32anZK74xgMvX-jr8o6L1ZBK9dFnidX6MlbBZg_ZN1GnUwaWqyzvQhX4WzMjvh1qTzj_IphuJIm2n7IWXkuRmpQ9fhYCgrELvghWzZN1zw7urdFAaKIrvnrzsoGaJc8Et1nl9gHaC0Uou9KEuDWktvfuuBSHIwbNNZsVcKWU8ez–WzUa6H5MBlt0BvxMobuLbSD4sAst6FPw8ImtIGj01LxdkDSw9-no_TBmqS5cArL7HtNmLALTvPndBBkvndWgOTXpFaJJeQO-A3gybhWmBbdy8vVRqxxaQCKy-uP7BkHxL9iMo1bovfCh37gkWPRmkWnB86zWX1opVQjxbI0JzusyAnepFbFgwlMb6yjgCUQ»

  3. Далее на SOWA с помощью действия FileToVars подгружаются clientId & clientSecret из файла oauth (который берется из vault) и происходит генерация Basic заголовка с помощью действия Base64Encode.

  4. С помощью действия HttpSender отправляется запрос с заголовком авторизации на сервис кэширования, который перенаправит запрос на IDP сервер для token introspection и закэширует полученный результат для конкретного токена на указанное время. Также при обращении на сервис кэширования предоставляется токен в виде заголовка с целью использования этого параметра в виде ключа для кэширования запросов. Результат будет закэширован на одну минуту в случае успешного ответа.

  5. С помощью действий JsonPathExtractToVars и ConditionChecker проверяется поле active из json ответа. Если значение true - запрос идет на backend, если false - запрос блокируется.

  6. Дополнительно проверяется наличие роли «sowa-dev-test-role» у токена.

d6

Пример профиля с проверкой токена через introspect endpoint приведен в разделе.

Выпуск токена для пользователя посредством обращения на OAUTH2 сервер#

Благодаря гибкости цепочек также можно создать конфигурацию, которая будет самостоятельно обращаться в IDP и выпускать JWT токен за пользователя, в случае, если пользователь прислал свои Basic Authorization данные.

# case when apikey token empty and auth header exists
      - actions:
        # load client authentication data from file
        - action: FileToVars
          params:
            rules:
                # Имя переменной, в которую поместить изъятое значение
              - var: $clj_client_id
                # Ключ элемента
                key: clientId
                # Путь к файлу, их которого должно быть взято значение переменной. Переменные должны храниться в виде key=value (java properties)
                filePath: /sowa/profile_storage/custom/oauth2/oauth
                setwhenempty: true
              - var: $clj_client_secret
                # Ключ элемента
                key: clientSecret
                # Путь к файлу, их которого должно быть взято значение переменной. Переменные должны храниться в виде key=value (java properties)
                filePath: /sowa/profile_storage/custom/oauth2/oauth
                setwhenempty: true
          conditions:
            - {var: $http_apikey, operator: "=", val: ""}
            - {var: $http_authorization, operator: "!=", val: ""}
        # check that client authentication data was loaded
        - action: ConditionChecker
          params:
            rules:
            - var: $clj_client_id
              operator: "!="
              val: ""
            - var: $clj_client_secret
              operator: "!="
              val: ""
        - action: SetMessageFromVar
          params:
            var: http_authorization
          conditions:
            - {var: $http_apikey, operator: "=", val: ""}
            - {var: $http_authorization, operator: "!=", val: ""}
          actions:
            # parse authorization header, split by 'space'
            - action: DelimiterSplitter
              params:
                delimiter: " "
              actions:
                # for second part after splitting decode base64 and split by ':'
              - action: Base64Decode
                conditions:
                  - {var: $clj_message_index, operator: "=", val: "1"}
                actions:
                - action: DelimiterSplitter
                  params:
                    delimiter: ":"
                  actions:
                    # and first part will be login, second - password
                    - action: SetVarFromMessage
                      params:
                        variable: $clj_user_login
                      conditions:
                        - {var: $clj_message_index, operator: "=", val: "0"}
                    - action: SetVarFromMessage
                      params:
                        variable: $clj_user_password
                      conditions:
                        - {var: $clj_message_index, operator: "=", val: "1"}
                      actions:
                      - action: ConditionChecker
                        params:
                          rules:
                          - var: $clj_user_login
                            operator: "!="
                            val: ""
                          - var: $clj_user_password
                            operator: "!="
                            val: ""
                        # construct request to IDP to get token by user login/user password
                      - action: SetVar
                        params:
                          variables:
                            - name: clj_body
                              value: "client_id=$clj_client_id&client_secret=$clj_client_secret&grant_type=password&username=$clj_user_login&password=$clj_user_password"
                      - action: SetMessageFromVar
                        params:
                          var: clj_body
                        conditions:
                        - {var: $clj_body, operator: "!=", val: ""}
                        actions:
                            # send this generated request to IDP though service cache(we do not provide apikey, so our request will not be cached)
                          - action: HttpSender
                            params:
                              external_url: /auth/realms/PlatformAuth/protocol/openid-connect/token
                              service_id: request_to_oauth_with_cache
                              service_type: service_http_proxy
                              connect_timeout: 200s
                              headers:
                                - name: Content-Type
                                  value: application/x-www-form-urlencoded
                            conditions:
                            - {var: $clj_message_size, operator: ">", val: "0"}
                            actions:
                            - action: JsonPathExtractToVars
                              params:
                                expressions:
                                - path: "$.access_token"
                                  allowNotFound: false
                                  var: clj_access_token_result

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

Также в SOWA есть возможность проверять сигнатуры и срок действия токенов через действие JwtVerifier.

Работа SOWA с OpenID#

Функциональность цепочек позволяет настроить на SOWA OpenID Authorization code flow.

d7

Пример конфигурации приведен по ссылке.

Ниже рассмотрены отдельные этапы текущей диаграммы:

Шаг 1. Пользователь пытается вызвать защищенное API (или получить доступ к ресурсу) через SOWA без авторизационных данных (токена).

Шаг 2. SOWA генерирует необходимые параметры для запуска процесса аутентификации и перенаправляет пользователя постпроцессором на окно с аутентификационной формой с необходимостью ввода логина/пароля.

Осуществляется с помощью цепочек действий:

# если токен не найден, пользователь перенапрявляется при помощи постпроцессора на IDP для аутентификации
- actions:
  - action: SetVar
    params:
      variables:
        - name: clj_token_not_found
          value: "true"
    conditions:
    # Здесь и далее - auth_token - сессионная кука SOWA
      - {var: $cookie_auth_token, operator: "=", val: ""}
    actions:
    # генерируем nonce и проставляем результат в переменную clj_hmac_result
    - action: HMac
      params:
        key: key
        result_var: $clj_hmac_result
        data: $request_id
        base64_url_encode: true

Правило постпроцессора, отвечающее за редирект пользователя:

- condition:
    - var: clj_token_not_found
      operator: "="
      val: "true"
    - var: clj_processing_phase
      operator: "="
      val: "REQUEST"
  redirectUrl: "https://platformauth-ift3.sc.dev.sbt/auth/realms/PlatformAuth/protocol/openid-connect/auth?client_id=$clj_client_id&response_type=code&redirect_uri=https%3A%2F%2F10.20.28.11%3A12345%2F_codexch&scope=openid&nonce=$clj_hmac_result"
  priority: 2
  status: 302

Шаг 3, 4, 5, 6. Пользователь аутентифицируется на стороне IDP и IDP после аутентификации пользователя предоставляет SOWA authorization code.

Шаг 7, 8. Используя полученный authorization code SOWA запрашивает у IDP пользовательские access/id/refresh токены:

  # генерация тела запроса для получения access/id/refresh токенов
- action: SetVar
  params:
    variables:
      - name: clj_body_to_exchange_keys
        value: "grant_type=authorization_code&client_id=$clj_client_id&code=$arg_code&redirect_uri=https%3A%2F%2F10.20.28.11%3A12345%2F_codexch"
- action: SetMessageFromVar
  params:
    var: clj_body_to_exchange_keys
  actions:
  - action: HttpSender
    params:
      external_url: /auth/realms/PlatformAuth/protocol/openid-connect/token
      service_id: request_to_openid_without_cache
      service_type: service_http_proxy
      connect_timeout: 200s
      headers:
        - name: Content-Type
          value: "application/x-www-form-urlencoded"
        - name: Authorization
          value: "Basic $clj_client_basic_encoded"
    actions:
    # получаем ответ от IDP и сохраняем из ответа токена в память(ID токен обязательный, refresh и access токены - не обязательные)
    - action: JsonPathExtractToVars
      params:
        expressions:
          - path: $.id_token
            allowNotFound: false
            var: $clj_id_token
      actions:
      - action: VarsToSharedMap
        params:
          shared_store_name: id_token_by_auth_token
          operation: REPLACE
          rules:
            - key: $request_id
              var: $clj_id_token
      ...

Шаг 9, 10. Используя полученные токены пользователю проставляется session cookie ($request_id), по значению которой можно получать id/access/refresh токены из памяти и с данными токенами обращаться к API, или к IDP для обновления токенов в случае истечения их срока действия.

Шаг 11. Пользователь запрашивает оригинальную страницу уже с сессионной cookie.

Шаг 12. Получаем сессионную cookie, по ней получаем ID токен, валидируем.

Шаг 13. Проксируем запрос.

Шаг 14. Пользователь получает защищенный ресурс.

В дальнейшем, при обращении пользователя с session cookie, алгоритм действий SOWA следующий:

  1. По session cookie берется из памяти ID токен. В случае, если ID токен не найден - запрос блокируется.

  2. ID токен найден. Его актуальность проверяется при помощи вызова introspection endpoint IDP через service_cache (который будет кэшировать ответ от IDP).

    2.1) ID токен активный - запрос пропускается.

    2.2) ID токен неактивный - попытка обновить его при помощи refresh токена в IDP.

    2.2.1) Обновить токен удалось - новые значения id/access/refresh токена сохраняются в памяти, запрос пропускается.

    2.2.2) Обновить токен не удалось - запрос блокируется.