Tech Recipe Book
My Services
  • Book
    • About the author
    • Architect
      • Algorithms
        • DB index algorithms
          • How does database indexing work
        • Neural network optimization
          • Neural Network Optimization
        • Route search
          • Road network in a database to build a route
          • Traveling Salesman Problem (TSP)
      • Architecture Frameworks
        • DODAF
        • TOGAF
        • Enterprise Architecture (EA) Tools Reviews 2023 | Gartner
      • Zero Trust
      • Billing
        • SHM billing system
      • Bots
        • Discord
        • Telegram
          • Chat GPT Telegram bot
          • Получаем статистику Telegram-канала при помощи api и python или свой tgstat с регистрацией и смс
          • Как хостить телеграм-бота (и другие скрипты на Python) на Repl.it бесплатно 24/7
          • Создание Telegram бота на PHP #1: основные понятия для работы с API
          • Создание Telegram бота на PHP #2: создание первого бота для Telegram
          • Создание Telegram бота на PHP #3: примеры отправки сообщений с кнопками в Telegram
          • Создание Telegram бота на PHP #4: отправка файлов и изображений в Telegram
          • Создание Telegram бота на PHP #5: работа с хуками
      • Business intelligence
      • Cloud Storage
        • Ceph
        • Virtual Distributed File System
      • Cryptography
        • Open Source PKI Software
        • OpenPGP
          • Email Encryption
          • Kleopatra
          • Miscellaneous Tools
          • Server side applications
      • Message broker
        • Kafka
          • Kafka UI-tools
          • Kafka streams ksqlDb
        • RabbitMQ
      • DB
        • MySQL
          • Auto sharding
          • MariaDB Zabbix monitoring
          • MySQL and MariaDB replication with Zabbix monitoring
        • Postgres
          • HA PostgreSQL with Patroni, Haproxy, Keepalived
          • Mass parallel requests - Greenplum
          • PostgreSQL cluster for development and testing
        • Vitess - Scalable. Reliable. MySQL-compatible. Cloud-native. Database.
      • Identity and Access Management (IDM)
        • FreeIPA - Identity, Policy, Audit
        • FreeIPA as an Enterprise solution
        • Keycloak
          • Keycloak HA cluster
        • Open Identity Platform
        • SSO
          • Keycloak for Java app
          • OpenAM
          • OpenIG
      • Firewall
        • nftables
      • Infrastructure As a Code
        • Ansible
        • IaC Packer Ansible Teraform
        • Installing Jenkins using terraform in Kubernetes in Yandex Cloud with letsencypt
        • Teraform Crosplan Pulumi
        • Yandex IaC solutions
      • Kubernetes
        • Installation
          • Install Kubernetes cluster
          • Deploying a Kubespray cluster to OpenStack using Terraform
          • Kube deploy in Yandex cloud
        • Frameworks
          • Deckhouse
            • LDAP authentification
            • On premise Install
            • Yandex Cloud Install
          • K3S
          • OpenShift OKD
          • RKE2
          • Rancher
            • Rancher Install
        • Auth
          • Keycloak in k8s
          • LDAP
        • GUI management Lens
        • Monitoring
          • Monitoring with Falco
          • Network monitoring
          • Nginx ingress
          • Prometheus Graphana for sample Nodejs app
          • Rsource monitoring Avito
        • Exposing services
          • Exposing Kubernetes Services
          • Cilium BGP
        • CNCF
        • Helm
          • Repositories
            • Artifact Hub | official
            • Bitnami | vmware
          • Awesome helm charts and resources
          • Essential Services for Modern Organizations
          • Security and Compliance
          • Additional charts
        • Isolation
          • vcluster - Virtual Kubernetes Clusters
          • Kiosk
          • KubeArmor
          • Control Plane Hardening
          • Hierarchical namespaces
        • Security Center
          • Minesweeper
          • NeuVector by SUSE
          • SOAR in Kubernetes
          • Security Сenter for Kubernetes
        • Terraform CI security
          • Terraform plan analysis with Checkov and Bridgecrew
          • Yandex Terraform scan
        • Vulnerability management
          • Aqua
          • Sysdig
          • Kyverno
          • GitLab
          • NeuVector by SUSE
        • Image scanning
          • Snyk
          • Sysdig
          • Harbor
          • Trivy
        • Signature verification
          • Sigstore
        • Control plane security
          • Gatekeeper
            • Applying OPA Gatekeeper
          • Kyverno
            • Policy as a code. Kyverno
        • Runtime Security
          • Osquery
          • Falco
          • ClamAV
        • Network security
          • Cilium
          • Control Plane Hardening (API restriction)
          • Network policy recipes
          • Service mesh
            • Istio HA, LoadBalance, Rate limit
          • mTLS Autocert
        • Honeypot
          • Building honeypot using vcluster and Falco
        • Backup
          • Kasten K10
        • Secrets
          • Vault CSI Driver
      • Load Balance
        • Nginx
        • HAProxy
          • Proxy methods
          • HAProxy for RDP
          • Payment gateway A/B test with HAProxy
          • HAPRoxy for Percona or Galera
      • Monitoring
        • Zabbix
          • Apache Zabbix
          • Disc Quota
          • Nginx Zabbix
          • SSL certificates Zabix
          • Zabbix notifications
        • Nagios
          • Datacenter monitoring
        • Prometheus and Grafana
      • Windows
        • Sysmon enhanced Windows audit
        • Sysmon to Block Unwanted File
      • Linux
        • Rsync
        • Debian based
          • Apt-Cacher NG
          • Unattended Upgrades in Debian / Ubuntu
        • RedHat basede
          • RPM Server
        • Logs analysis
        • Build armhf qemu
      • NGFW
      • CI/CD
        • DevSecOps
          • DAST
            • Burp
              • Dastardly
            • StackHawk
            • ZAP and GitHub Actions
          • SAST
            • Checkmarx
            • OSV by Google
            • Snyk
            • SonarQube
        • GitLab Runner in Yandex Cloud
        • Dynamic Gitlab Runners in Yandex Cloud
        • GitLab runner in Kubernetes with Werf
        • Kubernetes deploy strategies
        • Kubernetes highload deploy. part 1
        • Kubernetes highload deploy. part 2
        • Kubernetes Argo Rollouts
        • Jenkins in Kubernetes
        • Ansible Semaphore
        • Image storage, scaning and signing
        • Install WireGuard with Gitlab and Terraform
        • CI/CD example fror small web app
        • Threat matrix for CI CD Pipeline
      • SIEM / SOC
        • Datadog
        • Splunk
          • Splunk — general description
        • MaxPatrol
          • MaxPatrol 8 and RedCheck Enterprise
        • QRadar IBM
        • Cloud Native Security Platform (CNAPP) - Aqua
        • OSSIM | AT&T
          • AlienVault (OSSIM) install
        • Wazuh
        • EDR
          • Cortex XDR | Palo Alto Networks
          • Cynet
          • FortiEDR | Fortinet
          • Elastic
        • Elastic
          • Install Elasticsearch, Logstash, and Kibana (Elastic Stack) on Ubuntu 22.04
          • Setting Up Elastic 8 with Kibana, Fleet, Endpoint Security, and Windows Log Collection
        • Threat Intelligence
          • MISP
          • msticpy Microsoft
          • X-Force | IBM
          • Elastic
      • VPN
        • Full-Mesh VPN fastd, tinc, VpnCloud
        • Wireguard
          • WireGuard for Internet access
          • WireGuard on MikroTik and Keenetic
          • WireGuard site to site
        • SoftEther VPN Project
        • Cisco AnyConnect client
        • OpenConnect
        • SSTP python server
      • OS hardening
        • CIS Benchmarks
      • Cloud Providers
      • OpenNebula
        • OpenNebula Edge Cloud - Open Source Cloud & Edge Computing
        • Discover OpenNebula – Open Source Cloud & Edge Computing Platform
        • OpenNebula Multi-Cloud
        • Kubernetes on OpenNebula
        • The Open Source Alternative to Nutanix
        • The Simple Alternative to OpenStack
        • OpenNebula Partner Ecosystem
      • OpenStack
        • Install manual
        • Install with DevStack
      • VM
        • Create a VHD file from a Linux disk
        • Backup / Migration
          • Coriolis
          • Proxmox Backup Server
        • oVirt
        • VMware vCenter
        • Proxmox
      • Docker
        • Container optimization
        • Ubuntu RDP container
      • LXC
        • LXD on Ubuntu 18.04
        • Install, Create and Manage LXC in Ubuntu/Debian
    • Big Data
      • OLAP data qubes
      • Storage and autoscale in Lerua
    • Machine Learning
      • Yandex YaLM 100B. GPT model
      • Kaggle Community Datasts Models
      • AI in video production
      • Image search
      • Chat bots
        • You.com
        • Chat GPT
          • Implementing GPT in NumPy
        • Jailbreak Chat
      • Coding plugins CodeWhisperer
    • Malware
      • Isiaon/Pitraix: Modern Cross-Platform Peer-to-Peer Botnet over TOR
      • theZoo A repository of LIVE malwares
    • Pentest
      • Red Team
        • MITRE ATT&CK matrix
        • C2 Frameworks
          • Brute Ratel C4
          • Cobalt Strike
          • Covenant
          • Havoc Framework
          • Merlin
          • Metasploit
          • Sillenttrinity
          • Sliver
        • Manage and report
          • Dradis Framework
          • Hexway
        • Underground
      • Social engineering
        • Social Engineer Toolkit setoolkit
      • OSINT
        • OSINT for comapny
        • Instagram fishing
      • Forensics
        • Forensics tools
      • Pentesting Methodology
      • Web
      • CI/CD Methodology
      • Cloud Methodology
        • Hacking The Cloud
      • Kubernetes Pentesting
      • Android
        • SSL Unpinning for Android applications
      • iOS
        • SSL unpinning iOS and macOS applications
      • HackBar tool
      • CyberChef Tools
      • Python virtualenv
      • IppSec - YouTube
      • Hacktricks.xyz
    • Compliance
      • 152 ФЗ. Personal data
      • PCI DSS and ГОСТ Р 57580.1-2017
      • Cloud compliance
      • ГОСТ Р 57580.1-2017 для Kubernetes
      • Kubernets as DevSecOps and NIST compliance
      • NIST SP 800-61 cyberincidece control
      • CIS Kubernetes Benchmark v1.6 - RKE2 v1.20
      • CIS Kubernetes Benchmark v1.23 - RKE2
      • Requirements for Russian Banks
      • Tools
        • Chef InSpec
        • Elastic SIEM
    • Asset management
      • CMDBuild
    • Project management
    • Incident management SRE
    • Risk management
      • IT risk management
      • BSI-Standard 200-3
    • Web Dev
      • Cookie security
      • OWASP Top 10 2021
      • Docker nginx php mysql
      • Docker tor hiddenservice nginx
      • Docker Compose wp nginx php mariadb
      • Dependency Checking
        • Nexus Analyzer
        • OWASP dependency-check
      • Yii skeeks cms
      • YiiStudio
    • Art
      • GTK Themes
      • Themes for Xfce Desktop
      • XFCE / Xubuntu Windows 95
      • Moscow events
      • Photo goods
      • Russian style gifts
    • Cryptocurrency
      • News
      • Arbitrage
      • Stocks
      • Exchange aggregators
      • Where to use
      • Prepaid cards
        • BitFree
        • Pyypl Your Money at Your Fingertips
    • IT magazines
      • WIKI and Writeups tools
        • BookStack
        • GitBook
        • MkDocs
        • Wiki.js
        • DokuWiki
    • Languages
    • Learning
      • (ISC)2
        • CISSP
      • Offensive Security
        • OSCP
        • OSEP
        • OSED
      • DevSecOps
        • Certified DevSecOps Professional (CDP)
        • Certified DevSecOps Expert (CDE)
      • Web Security Academy: PortSwigger
    • Relocation
      • London experience
      • IT visas in 2022
      • Remote work
      • Running business in UAE
    • Freenet
      • Independent online services: the philosophy of a free Internet
      • Tor Project Anonymity Online
      • I2P Anonymous Network
    • Services
      • SMS Registration
        • Registering ChatGPT in Russia
      • Local and regional eSIMs for travellers - Airalo
      • Digital busines cards
      • No KYC services and exchanges
Powered by GitBook
On this page
  • Keycloak for Java app
  • Содержание
  • О чем речь?
  • 1. Подготовка стенда для разработки
  • 2. Настройка realm
  • 3. Подготовка приложения к внедрению
  • 4. Настройка приложения для взаимодействия с Keycloak
  • 5. Реализация Attribute based access control в Keycloak
  • - META-INF
  • - keycloak-scripts.json
  • - policy.js
  • - …

Was this helpful?

  1. Book
  2. Architect
  3. Identity and Access Management (IDM)
  4. SSO

Keycloak for Java app

Last updated 1 year ago

Was this helpful?

Keycloak for Java app

Содержание

  1. Содержание

  2. О чем речь?

  3. Подготовка стенда для разработки

  4. Настройка realm

  5. Подготовка приложения к внедрению

  6. Настройка приложения для взаимодействия с Keycloak

  7. Реализация Attribute based access control в Keycloak

О чем речь?

Это первая часть серии статей о переходе на Keycloak в качестве SSO в условиях кровавого enterprise.

Целью данной серии постов является демонстрация одного из возможных вариантов внедрения Keycloak в большой enterprise проект в качестве SSO. Еще в ней будут приведены мои размышления на этот счет, а также описаны сложности, с которыми пришлось столкнуться и решения, которые при этом были найдены.

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

Проект написан на spring-framework и как следствие использует компоненты spring-security для реализации системы аутентификации / авторизации пользователей.

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

  • Basic (по логину паролю)

  • По логину паролю с ассеss и refresh-token

  • OAUTH

  • SAML

  • LDAP

  • JWT (где наш бэкенд выступает просто в качестве resource-server, а аутентификация происходит на стороне заказчика)

На сегодняшний день реализованные нами компоненты аутентификации и авторизации уже очень сильно устарели и техническое руководство приняло, на мой взгляд, очень верное решение избавиться от них полностью и внедрить SSO (Single sign-on). Выбор пал на Keycloak, как на явного фаворита в среде систем данного рода с открытым исходным кодом. (В данной последующих постах не будут подниматься вопросы delivery нашего продукта заказчику вкупе с Keycloak и специфики комбинации проприетарного и open-source кода).

Когда я только начал заниматься этой задачей, я весьма оптимистично оценил ее в 100 чч по трудозатратам, потому что буквально недавно внедрил Keycloak на другом проекте за неделю, однако я не учел, что мой нынешний проект в десятки раз больше и запутаннее чем предыдущий, который только-только стартовал.

1. Подготовка стенда для разработки

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

Ниже я приведу compose файл, который у меня получился и дам ряд комментариев:

version: "3.9"
services:
  keycloak-postgres:
    image: library/postgres:${KC_POSTGRES_IMAGE_TAG:-14}
    container_name: ${POSTGRES_CONTAINER_NAME:-postgres}
    restart: on-failure
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: postgres
      POSTGRES_DB: postgres
    healthcheck:
      test: pg_isready -d postgres
      interval: 10s
      timeout: 5s
      retries: 3
      start_period: 5s
    ports:
      - ${KC_POSTGRES_PORT_MAPPING:-5435}:5432
    deploy:
      resources:
        limits:
          memory: 256M

  keycloak:
    image: quay.io/keycloak/keycloak:20.0.2
    container_name: keycloak
    command:
      - start --auto-build --db postgres --hostname-strict-https false --hostname-strict false --proxy edge --http-enabled true --import-realm --spi-user-profile-legacy-user-profile-read-only-attributes *_RES_ACCESS_MODE
    environment:
      KC_DB_URL: jdbc:postgresql://keycloak-postgres:5432/postgres
      KC_DB_USERNAME: postgres
      KC_DB_PASSWORD: postgres
      KC_DB_SCHEMA: public
      KC_FEATURES: preview
      KEYCLOAK_ADMIN: admin
      KEYCLOAK_ADMIN_PASSWORD: admin
    ports:
      - 8282:8080
    depends_on:
      keycloak-postgres:
        condition: service_healthy
    healthcheck:
      test: ["CMD", "curl", "-f", "http://0.0.0.0:8080/realms/master"]
      start_period: 10s
      interval: 30s
      retries: 3
      timeout: 5s

Данный compose-файл позволяет запустить keycloak в продуктивном режиме с реляционной базой данных.

Так как SSO это как правило отдельный микросервис, то под него имеет смысл выделять собственную БД.

Данный файл содержит ряд place-holder’ов для более гибкой конфигурации через .env файлы (в дальнейшем нам это пригодится для CI).

Пройдемся подробнее по конфигурации сервиса keycloak:

Все начинается с команды запуска:

start --auto-build --db postgres --hostname-strict-https false --hostname-strict false --proxy edge --http-enabled true --import-realm

Теперь пройдемся по опциям:

  • auto-build – собирает наш экземпляр со всеми зависимостями и кастомизациями

  • db – указывает, какую БД мы будем использовать, чтобы выбрать соответствующий драйвер

  • hostname-strict-https разрешаем/запрещаем фронту и бэку keycloak общаться по HTTP

  • proxy устанавливает режим reverse-proxy

  • hostname-strict вкл/выкл динамического имени хоста из заголовков запросов

  • http-enabled разрешаем взаимодействие по http

  • import-realm включаем импортирование realms из файлов конфигураций

Войдя по admin:admin мы окажемся на странице master realm:

2. Настройка realm

Темой данного раздела является базовая настройка realm Keycloak для разработки.

В прошлом разделе мы создали docker-compose файл, который позволяет нам развернуть стенд Keycloak, теперь нам необходимо его настроить, для этого:

Создадим собственный realm:

Теперь в рамках созданного realm создадим клиента:

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

Теперь у нас есть клиент, с возможностью логиниться из под него в Keycloak, давайте попробуем получить access-token с помощью сurl, для этого нам понадобится client_secret, который был сгенерирован в момент создания клиента, найти его можно в разделе credentials вашего клиента, тут важно подчеркнуть, что в продуктивном контуре этот секрет всегда должен генерироваться keycloak, однако в тестовой среде и среде для разработки допускается его определять заранее, ниже будет описано как именно:

curl --location --request POST 'http://localhost:8282/realms/demo/protocol/openid-connect/token' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'client_id=backend' \
--data-urlencode 'client_secret=v2iL3uK1uZxQqpYfw634SOEdAvT0UFSb' \
--data-urlencode 'grant_type=client_credentials'
{
  "access_token":"eyJhbGciOiJSUzI1NiIsInR5c...",
  "expires_in":300,
  "refresh_expires_in":0,
  "token_type":"Bearer",
  "not-before-policy":0,
  "scope":"profile email"
}

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

Для того чтобы экспортировать realm, переходим на страницу настроек и выбираем соответствующее действие:

В итоге мы получим большой JSON файл, содержащий конфигурацию нашего realm.

Теперь мы можем усовершенствовать наш docker‑compose файл из прошлого поста, мы прокинем наш файл внутрь контейнера keycloak, чтобы наш realm импортировался автоматически при старте приложения:

version: "3.9"
services:
  keycloak-postgres:
    image: library/postgres:${KC_POSTGRES_IMAGE_TAG:-14}
    container_name: ${POSTGRES_CONTAINER_NAME:-postgres}
    restart: on-failure
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: postgres
      POSTGRES_DB: postgres
    healthcheck:
      test: pg_isready -d postgres
      interval: 10s
      timeout: 5s
      retries: 3
      start_period: 5s
    ports:
      - ${KC_POSTGRES_PORT_MAPPING:-5435}:5432
    deploy:
      resources:
        limits:
          memory: 256M

  keycloak:
    image: quay.io/keycloak/keycloak:20.0.2
    container_name: keycloak
    command:
      - start --auto-build --db postgres --hostname-strict-https false --hostname-strict false --proxy edge --http-enabled true --import-realm --spi-user-profile-legacy-user-profile-read-only-attributes *_RES_ACCESS_MODE
    environment:
      KC_DB_URL: jdbc:postgresql://keycloak-postgres:5432/postgres
      KC_DB_USERNAME: postgres
      KC_DB_PASSWORD: postgres
      KC_DB_SCHEMA: public
      KC_FEATURES: preview
      KEYCLOAK_ADMIN: admin
      KEYCLOAK_ADMIN_PASSWORD: admin
    volumes:
      - type: bind
        source: ./src/main/resources/keycloak/import/realm-export.json
        target: /opt/keycloak/data/import/realm-export.json
        read_only: true
    ports:
      - 8282:8080
    depends_on:
      keycloak-postgres:
        condition: service_healthy
    healthcheck:
      test: ["CMD", "curl", "-f", "http://0.0.0.0:8080/realms/master"]
      start_period: 10s
      interval: 30s
      retries: 3
      timeout: 5s

Теперь при запуске наших контейнеров, в Keycloak сразу будет realm demo и client backend.

Для удобства локальной разработки и тестирования (ЭТО ВАЖНО, В ПРОДЕ НИКОГДА ТАК НЕ ДЕЛАЙТЕ !!!), мы можем прописать secret нашего клиента в файл импорта, и тогда он не будет генерироваться заново каждый раз. Для этого в нашем JSON файле находим следующее поле: clients[YOUR CLIENT INDEX.secret и пишем туда что хотим.

3. Подготовка приложения к внедрению

Темой данного раздела является проблема конфликтов конфигураций, с которой я столкнулся в процессе внедрения Keycloak.

Когда мы используем Spring очень многое настраивается за нас, в частности, простое добавление зависимости spring-security уже тянет за собой пачку авто-конфигураций, которые добавляют бин SecurityFilterChain, и много других разностей, которые и отвечают за то, чтобы наше приложение было защищено от неправомерного доступа.

Различные конфигурации иногда вполне могут сосуществовать совместно, а иногда нет. В моем случае я столкнулся с тем, что один из фильтров, которые тянет конфигурация, использующая устаревшую зависимость, конфликтует с фильтрами, которые нужны мне для внедрения Keycloak.

Например, мы внедряем SSO, следовательно если мы ходим в приложение через SSO остальные варианты входа нам не нужны, но и выпилить их из проекта мы тоже не можем, потому что их используют другие заказчики, что нам остается? - правильно - исключить их из контекста приложения завязавшись, например, на настройку приложения.

keykloak.enabled=true

Тогда старая конфигурация, которая нам мешает, примет следующий вид:

@Configuration
@ConditionalOnProperty(name = "keycloak.enabled", havingValue = "false")
@EnableResourceServer
public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {
// Some cofiguration code
}

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

Если ваш проект достаточно большой, то вам придется потратить довольно много времени на то, чтобы найти и "выключить" все, что вам мешает. Тут могу посоветовать, вооружившись отладчиком, при старте приложения смотреть содержание бина SecurityFilterChain, чтобы удостовериться, что оно соответствует вашим потребностям.

4. Настройка приложения для взаимодействия с Keycloak

Темой данного раздела является самая простая часть внедрения Keycloak, а именно настройка spring-security для проверки токенов через Keycloak.

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

Поэтому на стороне бэкенда от нас требуется только валидировать JWT-токены, которые присылаются в запросах.

Для того чтобы подружить наше приложение с Keycloak нужно не так много.

Для начала подключим зависимости, в данном примере используется gradle:

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-oauth2-resource-server'
    implementation 'org.springframework.security:spring-security-oauth2-jose'
    implementation 'org.springframework.boot:spring-boot-starter-security'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testImplementation 'org.springframework.security:spring-security-test'
    // Your other dependecies
    // ...
}

В application.yml надо прописать URL откуда наше приложение будет брать информацию о том, как именно ему коммуницировать с Keycloak:

spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          jwk-set-uri: {YOUR_KEYCLOAK_URL}/realms/{YOUR_REALM_NAME}/protocol/openid-connect/certs

Теперь надо добавить конфигурацию spring-security, которая будет проверять JWT из запроса через Keycloak:

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http.authorizeRequests(authorize -> authorize.anyRequest().authenticated())
            .oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt);
        return http.build();
    }
}

Voi la, наше приложение подружилось с keycloak и теперь пропускает запросы, которые содержат токены авторизации, полученные в Keycloak. Давайте добавим контроллер и убедимся в этом:

@RestController
@RequestMapping("/api")
public class UserController {

    @GetMapping("/me")
    public Authentication whoAmI() {
        SecurityContext context = SecurityContextHolder.getContext();
        return context.getAuthentication();
    }

}

В данном примере мы просто возвращаем информацию из JWT-токена, которую наше приложение распарсило с помощью данных полученных от Keycloak через URL, который мы указали в настройках:

curl --location --request GET 'http://localhost:8080/api/me' \
--header 'Authorization: Bearer {TOKEN}'
{
   "authorities": [
       {
           "authority": "SCOPE_profile"
       },
       {
           "authority": "SCOPE_email"
       }
   ],
   "details": {
       "remoteAddress": "0:0:0:0:0:0:0:1",
       "sessionId": "DBB88DFABD56C8E057F0B5D15D501841"
   },
   "authenticated": true,
   "principal": {
       "tokenValue": "{TOKEN}",
       "issuedAt": "2023-01-06T12:11:07Z",
       "expiresAt": "2023-01-06T12:16:07Z",
       "headers": {
           "kid": "3aBPuufejG63gx8kZLvc57dfz48zwro2HAkU1yLsgj4",
           "typ": "JWT",
           "alg": "RS256"
       },
       "claims": {
           "sub": "6e70f6cf-e1cb-4c78-8b81-082fd5c18d36",
           "resource_access": {
               "backend": {
                   "roles": [
                       "uma_protection"
                   ]
               },
               "account": {
                   "roles": [
                       "manage-account",
                       "manage-account-links",
                       "view-profile"
                   ]
               }
           },
           "email_verified": false,
           "clientHost": "172.25.0.1",
           "clientId": "backend",
           "iss": "http://localhost:8282/realms/demo",
           "typ": "Bearer",
           "preferred_username": "service-account-backend",
           "clientAddress": "172.25.0.1",
           "aud": [
               "account"
           ],
           "acr": "1",
           "realm_access": {
               "roles": [
                   "offline_access",
                   "uma_authorization",
                   "default-roles-demo"
               ]
           },
           "azp": "backend",
           "scope": "profile email",
           "exp": "2023-01-06T12:16:07Z",
           "iat": "2023-01-06T12:11:07Z",
           "jti": "c4bf73ae-ab84-484f-afb1-0d14b4f7efe7"
       },
       "subject": "6e70f6cf-e1cb-4c78-8b81-082fd5c18d36",
       "notBefore": null,
       "id": "c4bf73ae-ab84-484f-afb1-0d14b4f7efe7",
       "issuer": "http://localhost:8282/realms/demo",
       "audience": [
           "account"
       ]
   },
   "credentials": {
       "tokenValue": "{TOKEN}",
       "issuedAt": "2023-01-06T12:11:07Z",
       "expiresAt": "2023-01-06T12:16:07Z",
       "headers": {
           "kid": "3aBPuufejG63gx8kZLvc57dfz48zwro2HAkU1yLsgj4",
           "typ": "JWT",
           "alg": "RS256"
       },
       "claims": {
           "sub": "6e70f6cf-e1cb-4c78-8b81-082fd5c18d36",
           "resource_access": {
               "backend": {
                   "roles": [
                       "uma_protection"
                   ]
               },
               "account": {
                   "roles": [
                       "manage-account",
                       "manage-account-links",
                       "view-profile"
                   ]
               }
           },
           "email_verified": false,
           "clientHost": "172.25.0.1",
           "clientId": "backend",
           "iss": "http://localhost:8282/realms/demo",
           "typ": "Bearer",
           "preferred_username": "service-account-backend",
           "clientAddress": "172.25.0.1",
           "aud": [
               "account"
           ],
           "acr": "1",
           "realm_access": {
               "roles": [
                   "offline_access",
                   "uma_authorization",
                   "default-roles-demo"
               ]
           },
           "azp": "backend",
           "scope": "profile email",
           "exp": "2023-01-06T12:16:07Z",
           "iat": "2023-01-06T12:11:07Z",
           "jti": "c4bf73ae-ab84-484f-afb1-0d14b4f7efe7"
       },
       "subject": "6e70f6cf-e1cb-4c78-8b81-082fd5c18d36",
       "notBefore": null,
       "id": "c4bf73ae-ab84-484f-afb1-0d14b4f7efe7",
       "issuer": "http://localhost:8282/realms/demo",
       "audience": [
           "account"
       ]
   },
   "token": {
       "tokenValue": "{TOKEN}",
       "issuedAt": "2023-01-06T12:11:07Z",
       "expiresAt": "2023-01-06T12:16:07Z",
       "headers": {
           "kid": "3aBPuufejG63gx8kZLvc57dfz48zwro2HAkU1yLsgj4",
           "typ": "JWT",
           "alg": "RS256"
       },
       "claims": {
           "sub": "6e70f6cf-e1cb-4c78-8b81-082fd5c18d36",
           "resource_access": {
               "backend": {
                   "roles": [
                       "uma_protection"
                   ]
               },
               "account": {
                   "roles": [
                       "manage-account",
                       "manage-account-links",
                       "view-profile"
                   ]
               }
           },
           "email_verified": false,
           "clientHost": "172.25.0.1",
           "clientId": "backend",
           "iss": "http://localhost:8282/realms/demo",
           "typ": "Bearer",
           "preferred_username": "service-account-backend",
           "clientAddress": "172.25.0.1",
           "aud": [
               "account"
           ],
           "acr": "1",
           "realm_access": {
               "roles": [
                   "offline_access",
                   "uma_authorization",
                   "default-roles-demo"
               ]
           },
           "azp": "backend",
           "scope": "profile email",
           "exp": "2023-01-06T12:16:07Z",
           "iat": "2023-01-06T12:11:07Z",
           "jti": "c4bf73ae-ab84-484f-afb1-0d14b4f7efe7"
       },
       "subject": "6e70f6cf-e1cb-4c78-8b81-082fd5c18d36",
       "notBefore": null,
       "id": "c4bf73ae-ab84-484f-afb1-0d14b4f7efe7",
       "issuer": "http://localhost:8282/realms/demo",
       "audience": [
           "account"
       ]
   },
   "name": "6e70f6cf-e1cb-4c78-8b81-082fd5c18d36",
   "tokenAttributes": {
       "sub": "6e70f6cf-e1cb-4c78-8b81-082fd5c18d36",
       "resource_access": {
           "backend": {
               "roles": [
                   "uma_protection"
               ]
           },
           "account": {
               "roles": [
                   "manage-account",
                   "manage-account-links",
                   "view-profile"
               ]
           }
       },
       "email_verified": false,
       "clientHost": "172.25.0.1",
       "clientId": "backend",
       "iss": "http://localhost:8282/realms/demo",
       "typ": "Bearer",
       "preferred_username": "service-account-backend",
       "clientAddress": "172.25.0.1",
       "aud": [
           "account"
       ],
       "acr": "1",
       "realm_access": {
           "roles": [
               "offline_access",
               "uma_authorization",
               "default-roles-demo"
           ]
       },
       "azp": "backend",
       "scope": "profile email",
       "exp": "2023-01-06T12:16:07Z",
       "iat": "2023-01-06T12:11:07Z",
       "jti": "c4bf73ae-ab84-484f-afb1-0d14b4f7efe7"
   }
}

5. Реализация Attribute based access control в Keycloak

Темой данного раздела является реализация ABAC-политик безопасности в Keycloak.

ABAC или Attribute based access control - весьма популярный способ разграничения прав в крупных системах с большим числом пользователей и множеством ролей. ABAC позволяет разграничивать доступ между пользователями одной роли или группы на основе атрибутов этих пользователей. И так уж вышло, что и в нашей системе присутствует ABAC, и следовательно его логику нужно было переносить в Keycloak, и все было бы здорово, но Keycloak не поддерживает его из коробки.

Для реализации кастомных политик безопасности в Keycloak есть возможность писать их на JavaScript и упаковав в JAR со специальным дескриптором подложить в Keycloak.

Рассмотрим это на примере.

В парадигме Keycloak есть основные сущности, когда речь идет о разграничении доступа:

  • Ресурс - например оконечная точка API

  • Политика - правило на основе которого предоставляется доступ

  • Разрешение - связующий элемент, который объединяет ресурс и политику

Таким образом если мы имеем ресурс RESOURCE_1, политику, которая гласит, что разрешение выдается только пользователям с атрибутом RESOURCE_1_ACCESS_ATTRIBUTE, то чтобы это все объединить, нам нужно еще разрешение PERMISSION_1, что доступ к ресурсу RESOURCE_1 могут получить только пользователи с атрибутом RESOURCE_1_ACCESS_ATTRIBUTE.

P.S. с точки зрения Keycloak разрешение - тоже политика, но об этом позже.

Для того чтобы использовать атрибут как средство разграничения доступа у рядового пользователя не должно быть возможности его самостоятельно редактировать, потому что иначе любой джуниор пропишет себе, что он CTO и пойдет крушить-ломать. Тут на помощь приходит механизм Keycloak, который позволяет задать шаблон, соответствие которому автоматически защищает атрибут от редактирования. Вспомним наш compose файл, в команде запуска сервера у нас есть следующая опция:

  • -spi-user-profile-legacy-user-profile-read-only-attributes *_RES_ACCESS_MODE

Данная опция означает, что все пользовательские атрибуты с постфиксом _RES_ACCESS_MODE доступны для редактирования только администратору realm.

Давайте добавим в наше приложение новый ресурс:

@RestController
@RequestMapping("/api")
public class UserController {

    @GetMapping("/me")
    public Authentication whoAmI() {
        SecurityContext context = SecurityContextHolder.getContext();
        return context.getAuthentication();
    }

    @GetMapping("/resource")
    public ResponseEntity<String> administratorReadRes(){
        return ResponseEntity.of(Optional.of("Resource for users with fancy access attribute"));
    }

}

Мы хотим, чтобы доступ к этому ресурсу был только у пользователя с атрибутом FANCY_RES_ACCESS_ATTRIBUTE в значении PRETTY_FANCY, каков наш алгоритм действий?

Нам нужно создать ресурс в Keycloak, мы это можем сделать руками, для этого переходим во вкладку authorization -> resources и создаем его:

либо это можно сделать через код, для этого в наш JSON, в список ресурсов нашего клиента (clients[YOUR CLIENT INDEX].authorizationSettings.resources) добавляем следующее:

{
   "name": "fancy resource",
   "ownerManagedAccess": false,
   "attributes": {},
   "_id": "55a4bc23-ae83-4f0c-a7c0-c26118f18b10",
   "uris": [
     "/api/resource"
   ],
   "icon_uri": ""
}

теперь если мы пересоздадим контейнеры, этот ресурс сразу будет в нашем клиенте.

Создаем политику

Тут все будет несколько сложнее) Для начала где-нибудь в ресурсах создадим структуру каталогов вида:

custom-scripts

- META-INF

- keycloak-scripts.json

- policy.js

- …

из этого каталога мы впоследствии будем собирать JAR-файл и деплоить его в Keycloak.

keycloak-scripts.json это дескриптор следующего вида:

{
   "authenticators": [],
   "policies": [],
   "mappers": [],
   "saml-mappers": []
}

теперь давайте создадим политику, как уже говорилось ранее, мы хотим, чтобы доступ к ресурсу был только у пользователя с атрибутом FANCY_RES_ACCESS_ATTRIBUTE в значении PRETTY_FANCY

для этого мы реализуем следующую логику:

var context = $evaluation.getContext().getIdentity().getAttributes();
var identity = context.getIdentity();
var attributes = identity.getAttributes();

if (attributes.containsValue(' FANCY_RES_ACCESS_ATTRIBUTE', 'PRETTY_FANCY')) {
   $evaluation.grant();
} else {
   $evaluation.deny();
}

сохраним это в файле pretty-fancy-policy.js в подкаталоге policies и опишем в дескрипторе в соответствии с документацией:

{
 "authenticators": [],
 "policies": [
   {
     "name": "Pretty fancy policy",
     "fileName": "policies/pretty-fancy-policy.js",
     "description": "Gives access only to pretty fancy users."
   }
 ],
 "mappers": [],
 "saml-mappers": []
}

Когда все готово нам нужно собрать JAR файл, для этого вызываем следующую команду:

jar --create --file "/path/to/your/custom-scripts.jar" --no-manifest -C "/path/to/your/custom-scripts" .

Теперь нам надо прокинуть этот JAR в контейнер Keycloak, это можно сделать командой докера, потом залезть в контейнер и перезагрузить сервер, но так как мы вынесли нашу конфигурацию в код, мы смело можем просто пересоздать контейнер дописав volume в compose файле:

version: "3.9"
services:
  keycloak-postgres:
    image: library/postgres:${KC_POSTGRES_IMAGE_TAG:-14}
    container_name: ${POSTGRES_CONTAINER_NAME:-postgres}
    restart: on-failure
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: postgres
      POSTGRES_DB: postgres
    healthcheck:
      test: pg_isready -d postgres
      interval: 10s
      timeout: 5s
      retries: 3
      start_period: 5s
    ports:
      - ${KC_POSTGRES_PORT_MAPPING:-5435}:5432
    deploy:
      resources:
        limits:
          memory: 256M

  keycloak:
    image: quay.io/keycloak/keycloak:20.0.2
    container_name: keycloak
    command:
      - start --auto-build --db postgres --hostname-strict-https false --hostname-strict false --proxy edge --http-enabled true --import-realm --spi-user-profile-legacy-user-profile-read-only-attributes *_RES_ACCESS_MODE
    environment:
      KC_DB_URL: jdbc:postgresql://keycloak-postgres:5432/postgres
      KC_DB_USERNAME: postgres
      KC_DB_PASSWORD: postgres
      KC_DB_SCHEMA: public
      KC_FEATURES: preview
      KEYCLOAK_ADMIN: admin
      KEYCLOAK_ADMIN_PASSWORD: admin
    volumes:
      - type: bind
        source: ./src/main/resources/keycloak/import/realm-export.json
        target: /opt/keycloak/data/import/realm-export.json
        read_only: true
      - type: bind
        source: ./src/main/resources/keycloak/scripts/custom-scripts.jar
        target: /opt/keycloak/providers/custom-scripts.jar
        read_only: true
    ports:
      - 8282:8080
    depends_on:
      keycloak-postgres:
        condition: service_healthy
    healthcheck:
      test: ["CMD", "curl", "-f", "http://0.0.0.0:8080/realms/master"]
      start_period: 10s
      interval: 30s
      retries: 3
      timeout: 5s

после запуска, наша политика станет доступна как тип в разделе политик нашего клиента your realm -> your client -> authorization -> policies -> create new:

теперь мы можем создать политику с нашим кастомным типом, это можно сделать через интерфейс, но я не буду отдельно останавливаться на этом, так как наша основная цель - содержать конфигурацию keycloak в коде. Поэтому мы добавим следующий код в секцию политик нашего клиента (clients[YOUR CLIENT INDEX].authorizationSettings.policies):

// Политика
{
 "name": "Pretty fancy policy",
 "description": "Gives access only to pretty fancy users",
 "type": "script-policies/pretty-fancy-policy.js",
 "logic": "POSITIVE",
 "decisionStrategy": "UNANIMOUS",
 "config": {}
},
// Разрешение
{
 "name": "Pretty fancy permission",
 "description": "Fancy permission for fancy resource",
 "type": "resource",
 "logic": "POSITIVE",
 "decisionStrategy": "UNANIMOUS",
 "config": {
   "resources": "[\"fancy resource\"]",
   "applyPolicies": "[\"Pretty fancy policy\"]"
 }
}

Политика:

  • В описании политики поле type формируется следующим образом: script-[Путь до JS файла от корня Jar]

  • logic в значении POSITIVE означает, что логика работы нашей политики - прямая, т. е. если политика принимает положительное решение оно таковым и остается, с помощью значения NEGATIVE можно сделать обратную логику

  • decisionStrategy в значении UNANIMOUS условно означает логическое И, если бы наша политика состояла из комбинации нескольких других, то все они должны были бы прийти к положительному решению, чтобы наша также дала положительный ответ, помимо UNANIMOUS есть еще AFFIRMATIVE - логическое ИЛИ, а также CONSENSUS, в последнем случае решение принимается в зависимости от того каких решений было больше.

Разрешение:

  • Типом данного разрешения является resource, это означает, что данное разрешение выдается на конкретный ресурс, безотносительно к Scope и другим атрибутам ресурса.

  • Поля logic и decisionStrategy имеют такую же семантику, как и в обычной политике

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

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

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

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

Для начала создадим пользователя:

Теперь идем во вкладку evaluate секции authorization, там выбираем нашего пользователя и ресурс и жмем evaluate:

Иииии….

Получаем вполне закономерный результат, потому что у нашего пользователя нет требуемого атрибута. Добавим его:

И проверим еще раз:

Хмммм… мы что-то явно упустили.

А именно, то что заботливые разработчики клоаки решили, что просто добавить пользовательский атрибут это слишком просто, и чтобы он был доступен во время проверки прав, нужно еще добавить маппер, поэтому идем в секцию clients[YOUR CLIENT INDEX].protocolMappers и добавляем туда маппер для нашего атрибута:

{
 "name": "FANCY_RES_ACCESS_ATTRIBUTE_MAPPER",
 "protocol": "openid-connect",
 "protocolMapper": "oidc-usermodel-attribute-mapper",
 "consentRequired": false,
 "config": {
   "aggregate.attrs": "false",
   "userinfo.token.claim": "true",
   "multivalued": "false",
   "user.attribute": "FANCY_RES_ACCESS_ATTRIBUTE",
   "id.token.claim": "true",
   "access.token.claim": "true",
   "claim.name": "FANCY_RES_ACCESS_ATTRIBUTE"
 }
}

Перезапускаем все и проверяем еще раз:

Наконец разрешение получено.

У нас получилось реализовать полноценный ABAC в Keycloak, более того, вся конфигурация у нас хранится в коде, тут небольшое лирическое отступление, когда сами будете это все разрабатывать - проще всего набить все настройки в интерфейсе, а потом экспортировать в файл средствами Keycloak ;)

И так у нас есть контроль доступа, который работает на основе пользовательских атрибутов и вроде все прекрасно …, НО наше spring-приложение про это ничего не знает и все также пропускает любого авторизованного в keycloak пользователя к своим ресурсам, как это поправить поговорим в следующих постах.

Если мы посмотрим DockerFile [ ] из которого собирается наш образ Keycloak, то увидим, что entrypoint там стоит ENTRYPOINT ["/opt/keycloak/bin/kc.sh"] (скрипт запуска Keycloak в standalone режиме). Команда start запускает приложение в production режиме.

После запуска данной конфигурации командой docker-compose up -d у вас будет готовый стенд Keycloak, который можно использовать для разработки и прогона тестов, Главная страница будет доступна по адресу .

Есть один интересный баг, который пока никто не исправил - при создании клиента создается политика по умолчанию, которая имеет тип JS (подробнее о политиках и разрешениях можно прочитать здесь ). В более ранних версиях сложные кастомные политики можно было загружать прямо через интерфейс в виде JS кода, позже эту возможность убрали (потому что не секьюрно). Однако эта политика по умолчанию осталась, и при экспорте, она тоже выгрузится, но когда вы захотите потом импортировать ваш realm, вы получите ошибку, так как в актуальной версии JS политики можно загружать только через специальный JAR файл, однако об этом позже. Сейчас, чтобы магия случилась нам нужно удалить эту политику и связанное с ней разрешение:

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

Про то как писать политики на JS и какие данные при этом в вашем распоряжении можно почитать тут ().

как его составлять, можно прочитать здесь ()

Подробнее про ресурсы, политики и разрешения можно прочитать здесь ()

https://habr.com/ru/articles/716232/
https://www.keycloak.org/server/containers
http://localhost:8282
https://www.keycloak.org/docs/latest/authorization_services/index.html
@ConditionalOn
JavaScript-based policy
JavaScript providers
https://www.keycloak.org/docs/latest/authorization_services/index.html