Skip to main content
Version: 6.2

Setting Up the Keycloak Provider

This is the first of three setup stages. The overall sequence is described in Setting Up Two-Factor Authentication over the OIDC Protocol.

Notation​

Note!

Paths containing $OS_HOME refer not only to files but also to the server where they reside. If the services run on different servers, transfer the required files between them in advance.

The Keycloak base address depends on the publishing scheme:

TokenDescriptionDirect publishingBehind a proxy
<KEYCLOAK_BASE>Keycloak base addresshttps://<KEYCLOAK_HOST>:8443https://<KEYCLOAK_HOST>/keycloak

The remaining tokens do not depend on the publishing scheme:

TokenDescription
<KEYCLOAK_HOST>external hostname of the Keycloak machine
<KEYCLOAK_CA>name of the CA file that signed the certificate of the side terminating the connection: the proxy if there is one, otherwise Keycloak itself. A copy of the file is placed in the configuration directory of each service separately
<REALM_ID>technical realm name
<REALM_NAME>human-readable realm name (Display name)
<LDAP_DOMAIN>AD domain controller
<POSTGRESQL_ADDRESS>PostgreSQL server address
<POSTGRESQL_PORT>PostgreSQL port
$DB_PASSpassword of the keycloak database user
$OS_HOMESmart Monitor Data Storage home directory

Preparing Keycloak​

User​

Create the keycloak user first; it is needed for the Java check.

sudo useradd -r -d /opt/keycloak -s /sbin/nologin keycloak

Java​

Keycloak 26.7 runs on OpenJDK 17, 21, or 25. Smart Monitor ships with Java 21, so copying the bundled one is recommended:

sudo cp -a $OS_HOME/jdk /opt/jdk-21
sudo chown -R root:root /opt/jdk-21
sudo chmod -R a+rX /opt/jdk-21

sudo -u keycloak /opt/jdk-21/bin/java -version

The system Java is suitable if the repositories are available or Keycloak is deployed on a separate server:

sudo apt install -y openjdk-21-jre-headless # Debian/Ubuntu
sudo dnf install -y java-21-openjdk-headless # RHEL/Rocky

Distribution​

cd /tmp
curl -LO https://github.com/keycloak/keycloak/releases/download/26.7.1/keycloak-26.7.1.tar.gz
sudo tar -xzf keycloak-26.7.1.tar.gz -C /opt
sudo mv /opt/keycloak-26.7.1 /opt/keycloak

sudo chown -R keycloak:keycloak /opt/keycloak

The directory is renamed so that paths in the service unit file and this guide do not depend on the Keycloak version.

Database​

This guide uses PostgreSQL. Any database from the list of supported ones is acceptable. The embedded H2 database should not be used in production: it is not on the list, and schema migrations during upgrades are not guaranteed on it. Tables do not need to be created, Liquibase sets up the schema itself.

CREATE USER keycloak WITH PASSWORD '$DB_PASS';
CREATE DATABASE keycloak OWNER keycloak ENCODING 'UTF8';

For PostgreSQL 15 and newer, additionally:

\c keycloak
GRANT ALL ON SCHEMA public TO keycloak;

Certificate for Keycloak​

Direct publishing only

When a proxy is used, this step is skipped entirely: the connection from the browser is terminated by the proxy with its own certificate, while Keycloak listens on plain HTTP on the loopback address.

Choosing a CA​

The Keycloak certificate is signed by a certificate authority (CA). There are two options:

  1. Cluster CA — ca-cert.pem contains the certificate of a private CA that is not tied to Smart Monitor Data Storage or a particular server. This CA can sign the Keycloak certificate. In this case, <KEYCLOAK_CA> is ca-cert.pem: the file is already in the Smart Monitor Data Storage configuration directory; copy it to the Smart Monitor Web directory if needed

  2. Separate CA — a corporate PKI, an internal ACME, or a self-signed CA created for Keycloak. In this case, <KEYCLOAK_CA> is the separate CA file; a copy is required in both the Smart Monitor Data Storage and Smart Monitor Web configuration directories

Issuing on a shared host​

If Keycloak runs on the same server as Smart Monitor Data Storage, the CA key is available locally and all steps can be performed on that server.

cd /opt/keycloak/conf

sudo openssl req -new -nodes -newkey rsa:2048 \
-keyout tls.key -out tls.csr \
-subj "/CN=<KEYCLOAK_HOST>"

printf 'subjectAltName=DNS:<KEYCLOAK_HOST>\n' | sudo tee tls.ext >/dev/null

sudo openssl x509 -req -in tls.csr -days 825 \
-CA $OS_HOME/config/ca-cert.pem \
-CAkey $OS_HOME/config/ca-key.pem -CAcreateserial \
-extfile tls.ext \
-out tls.crt

sudo chown keycloak:keycloak tls.key tls.crt
sudo chmod 640 tls.key
sudo chmod 644 tls.crt

Issuing on a separate server​

If Keycloak runs on a separate server, the CA key is not available there. The Keycloak private key stays on its server, and the CA key stays on the CA server. Only the request and the issued certificate are transferred between servers.

On the Keycloak host, create the key and the request:

cd /opt/keycloak/conf

sudo openssl req -new -nodes -newkey rsa:2048 \
-keyout tls.key -out tls.csr \
-subj "/CN=<KEYCLOAK_HOST>"

Copy the tls.csr file to the machine where the CA key resides.

On the CA host, sign the request:

printf 'subjectAltName=DNS:<KEYCLOAK_HOST>\n' > tls.ext

sudo openssl x509 -req -in tls.csr -days 825 \
-CA $OS_HOME/config/ca-cert.pem \
-CAkey $OS_HOME/config/ca-key.pem -CAcreateserial \
-extfile tls.ext \
-out tls.crt

Transfer only tls.crt to the Keycloak server. If the CA is an intermediate CA, append the chain up to the root certificate to tls.crt.

sudo chown keycloak:keycloak tls.key tls.crt
sudo chmod 640 tls.key
sudo chmod 644 tls.crt

Configuration​

File: /opt/keycloak/conf/keycloak.conf

Direct publishing​

hostname=<KEYCLOAK_BASE>

https-certificate-file=/opt/keycloak/conf/tls.crt
https-certificate-key-file=/opt/keycloak/conf/tls.key

db=postgres
db-url=jdbc:postgresql://<POSTGRESQL_ADDRESS>:<POSTGRESQL_PORT>/keycloak
db-username=keycloak
db-password=$DB_PASS

Port 8443 is the default value for HTTPS and does not need to be set separately; it is changed with the https-port parameter. Keycloak occupies its port entirely, so it needs neither a path prefix nor the acceptance of X-Forwarded-* headers.

Behind a proxy​

hostname=<KEYCLOAK_BASE>
http-relative-path=/keycloak

http-enabled=true
http-host=127.0.0.1
http-port=8080
proxy-headers=xforwarded

db=postgres
db-url=jdbc:postgresql://<POSTGRESQL_ADDRESS>:<POSTGRESQL_PORT>/keycloak
db-username=keycloak
db-password=$DB_PASS

http-relative-path is required only if Keycloak is published under a subpath rather than at the root of its own hostname. If it has its own hostname, the parameter is not set, and <KEYCLOAK_BASE> looks like https://<KEYCLOAK_HOST> without a prefix.

If Keycloak shares a hostname with Smart Monitor Web, the /auth prefix will not work: on that host the /auth/openid/login and /auth/openid/logout paths are already taken by Smart Monitor Web.

http-host=127.0.0.1 blocks direct access to Keycloak bypassing the proxy.

The file contains the database password, so restrict access to it:

sudo chown keycloak:keycloak /opt/keycloak/conf/keycloak.conf
sudo chmod 600 /opt/keycloak/conf/keycloak.conf

Proxy in front of Keycloak​

Only when working behind a proxy

The step is performed only in the scheme with a proxy. With direct publishing it is skipped entirely.

Example nginx configuration on the Keycloak server:

location /keycloak/ {
proxy_pass http://127.0.0.1:8080;

proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Port $server_port;

proxy_buffer_size 16k;
proxy_buffers 4 16k;
}
nginx -t && systemctl reload nginx

Startup​

sudo tee /etc/systemd/system/keycloak.service > /dev/null <<'EOF'
[Unit]
Description=Keycloak Identity Provider
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=keycloak
Group=keycloak
Environment=JAVA_HOME=/opt/jdk-21
Environment=JAVA_OPTS_KC_HEAP=-Xms256m -Xmx512m
ExecStart=/opt/keycloak/bin/kc.sh start
Restart=on-failure
RestartSec=10
TimeoutStartSec=180
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=full
ProtectHome=true

[Install]
WantedBy=multi-user.target
EOF

sudo -u keycloak JAVA_HOME=/opt/jdk-21 /opt/keycloak/bin/kc.sh build
sudo systemctl daemon-reload

The administrator is created while the service is stopped. The command is interactive: it prompts for a username and a password.

sudo -u keycloak JAVA_HOME=/opt/jdk-21 /opt/keycloak/bin/kc.sh bootstrap-admin user

After the administrator is created, enable and start the service:

sudo systemctl enable --now keycloak

Check all three addresses, not just issuer:

curl -sS --cacert $OS_HOME/config/<KEYCLOAK_CA> \
<KEYCLOAK_BASE>/realms/master/.well-known/openid-configuration \
| jq '.issuer, .authorization_endpoint, .token_endpoint'

All three addresses must contain the full <KEYCLOAK_BASE>. Run the request without -k.

Setting up the realm​

Creating a realm​

Note!

OTP secrets are bound to a realm: when a new realm is created, users must bind their authenticator again. Create a separate realm only for a new cluster.

Go to Create realm - Realm name and enter <REALM_ID>.

Before proceeding, make sure the console has switched to the new realm: in Manage realms, the Current realm label is shown to the right of <REALM_ID>.

Set the following in Realm settings - General:

  • setting Display name — <REALM_NAME>
  • setting Require SSL — External requests

Display name appears in the sign-in page title and in the entry name in the authenticator app. If it is empty, the technical <REALM_ID> ends up there, and entries from different systems may be hard to tell apart. The Require SSL value does not need to be changed: HTTP is allowed only from private addresses.

Set the following in Realm settings - Login:

  • setting Verify email — Off

Otherwise, users without an email address will not be able to sign in.

Login Page Localization​

By default, the Keycloak login page is displayed in English. To configure localization, go to Realm Settings - Localization - Locales and set:

  • setting Internationalization — Enabled
  • setting Supported locales — add Russian
  • setting Default locale — Russian

Restart Keycloak:

systemctl restart keycloak

Creating the Smart Monitor Theme​

Create the theme directories and files:

sudo mkdir -p /opt/keycloak/themes/smartmonitor/login/{messages,resources}
sudo mkdir -p /opt/keycloak/themes/smartmonitor/login/resources/css

sudo touch /opt/keycloak/themes/smartmonitor/login/resources/css/smartmonitor.css
sudo touch /opt/keycloak/themes/smartmonitor/login/theme.properties
sudo touch /opt/keycloak/themes/smartmonitor/login/messages/{messages_en.properties,messages_ru.properties}

Add the following content to the corresponding files:

/opt/keycloak/themes/smartmonitor/login/resources/css/smartmonitor.css
:root {
--sm-green: #3B8646;
--sm-green-dark: #2F6D38;
--sm-bg: #F0F1F2;
--sm-border: #E1E3E6;
--sm-text: #1A1A1A;
}

body,
body.login-pf,
.login-pf-page,
.pf-v5-c-background-image,
#kc-container,
#kc-container-wrapper {
background: var(--sm-bg) !important;
background-image: none !important;
font-family: 'Source Sans 3', 'Source Sans Pro', -apple-system,
'Segoe UI', Roboto, Arial, sans-serif !important;
color: var(--sm-text);
}

.card-pf,
.pf-v5-c-card,
.pf-v5-c-login__main,
.pf-v5-c-login__main-header,
.pf-v5-c-login__main-body,
#kc-header,
#kc-header-wrapper,
#kc-content,
#kc-content-wrapper,
#kc-form,
#kc-form-wrapper {
background: transparent !important;
box-shadow: none !important;
border: none !important;
border-top: none !important;
border-bottom: none !important;
}

hr,
.pf-v5-c-divider,
.pf-v5-c-login__main-header::after,
.pf-v5-c-login__main-header::before {
display: none !important;
border: none !important;
content: none !important;
}

.login-pf-page,
.pf-v5-c-login,
.pf-v5-c-login__container,
.pf-v5-c-login__main,
#kc-container,
#kc-container-wrapper {
display: block !important;
justify-content: flex-start !important;
align-items: stretch !important;
min-height: 0 !important;
padding-top: 0 !important;
margin-top: 0 !important;
}

#kc-header,
#kc-header-wrapper,
.pf-v5-c-login__main-header,
.login-pf-header {
padding: 0 !important;
margin: 0 !important;
}

#kc-header,
#kc-header-wrapper {
font-size: 0 !important;
line-height: 0 !important;
padding: 0 !important;
margin: 0 !important;
text-align: center;
}

#kc-header-wrapper::before {
content: "";
display: block;
width: 340px;
max-width: 80vw;
height: 48px;
margin: 10% auto 16px auto;
background: url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTQyMSIgaGVpZ2h0PSIxOTkiIHZpZXdCb3g9IjAgMCAxNDIxIDE5OSIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTUwMC42OTIgMTI4LjI2OUM1MDAuNjkyIDEzNC40NDcgNDk5LjE0OCAxMzkuNzIyIDQ5Ni4wNTkgMTQ0LjA5NUM0OTMuMDE3IDE0OC40MiA0ODguNzQgMTUxLjcyMyA0ODMuMjI3IDE1NC4wMDRDNDc3LjcxNCAxNTYuMjg1IDQ3MS4yMjYgMTU3LjQyNiA0NjMuNzY1IDE1Ny40MjZDNDU5LjgyIDE1Ny40MjYgNDU2LjA4OSAxNTcuMjM2IDQ1Mi41NzIgMTU2Ljg1NUM0NDkuMDU1IDE1Ni40NzUgNDQ1LjgyNCAxNTUuOTI5IDQ0Mi44NzcgMTU1LjIxNkM0MzkuOTMgMTU0LjUwMyA0MzcuMzQgMTUzLjYyNCA0MzUuMTA2IDE1Mi41NzhWMTQwLjk1OEM0MzguNjcxIDE0Mi40MzEgNDQzLjAyIDE0My44MSA0NDguMTUyIDE0NS4wOTNDNDUzLjI4NSAxNDYuMzI4IDQ1OC42NTYgMTQ2Ljk0NiA0NjQuMjY0IDE0Ni45NDZDNDY5LjQ5MiAxNDYuOTQ2IDQ3My45MTEgMTQ2LjI1NyA0NzcuNTIzIDE0NC44NzlDNDgxLjEzNSAxNDMuNDUzIDQ4My44NjggMTQxLjQzMyA0ODUuNzIyIDEzOC44MTlDNDg3LjYyMyAxMzYuMTU4IDQ4OC41NzMgMTMyLjk3NCA0ODguNTczIDEyOS4yNjdDNDg4LjU3MyAxMjUuNzAyIDQ4Ny43ODkgMTIyLjczMiA0ODYuMjIxIDEyMC4zNTVDNDg0LjY1MiAxMTcuOTMyIDQ4Mi4wMzggMTE1Ljc0NSA0NzguMzc5IDExMy43OTdDNDc0Ljc2NyAxMTEuODAxIDQ2OS44MjQgMTA5LjY4NiA0NjMuNTUxIDEwNy40NTJDNDU5LjEzMSAxMDUuODg0IDQ1NS4yMzQgMTA0LjE3MyA0NTEuODU5IDEwMi4zMTlDNDQ4LjQ4NSAxMDAuNDE4IDQ0NS42NTcgOTguMjc5NiA0NDMuMzc2IDk1LjkwMzNDNDQxLjA5NSA5My41MjcgNDM5LjM2IDkwLjc3MDUgNDM4LjE3MiA4Ny42MzM4QzQzNy4wMzEgODQuNDk3MSA0MzYuNDYxIDgwLjkwODkgNDM2LjQ2MSA3Ni44NjkxQzQzNi40NjEgNzEuMzA4NiA0MzcuODYzIDY2LjU1NiA0NDAuNjY3IDYyLjYxMTNDNDQzLjUxOSA1OC42MTkxIDQ0Ny40MzkgNTUuNTc3NSA0NTIuNDMgNTMuNDg2M0M0NTcuNDY3IDUxLjM0NzcgNDYzLjI0MiA1MC4yNzgzIDQ2OS43NTMgNTAuMjc4M0M0NzUuMzEzIDUwLjI3ODMgNDgwLjQ0NiA1MC44MDExIDQ4NS4xNTEgNTEuODQ2N0M0ODkuOTA0IDUyLjg5MjMgNDk0LjI1MyA1NC4yOTQzIDQ5OC4xOTcgNTYuMDUyN0w0OTQuNDE5IDY2LjQ2MDlDNDkwLjY2NCA2NC44OTI2IDQ4Ni42NzIgNjMuNTg1NiA0ODIuNDQyIDYyLjU0QzQ3OC4yNiA2MS40OTQ1IDQ3My45MzUgNjAuOTcxNyA0NjkuNDY4IDYwLjk3MTdDNDY1IDYwLjk3MTcgNDYxLjIyMiA2MS42MzcgNDU4LjEzMyA2Mi45Njc4QzQ1NS4wOTEgNjQuMjUxIDQ1Mi43NjIgNjYuMDgwNyA0NTEuMTQ2IDY4LjQ1N0M0NDkuNTMxIDcwLjgzMzMgNDQ4LjcyMyA3My42NjExIDQ0OC43MjMgNzYuOTQwNEM0NDguNzIzIDgwLjU5OTkgNDQ5LjQ4MyA4My42NDE2IDQ1MS4wMDQgODYuMDY1NEM0NTIuNTcyIDg4LjQ4OTMgNDU1LjA0NCA5MC42NTE3IDQ1OC40MTggOTIuNTUyN0M0NjEuODQgOTQuNDA2MiA0NjYuMzU1IDk2LjM1NDggNDcxLjk2MyA5OC4zOTg0QzQ3OC4wOTQgMTAwLjYzMiA0ODMuMjk4IDEwMy4wMDggNDg3LjU3NSAxMDUuNTI3QzQ5MS44NTMgMTA3Ljk5OSA0OTUuMTA4IDExMS4wNCA0OTcuMzQyIDExNC42NTJDNDk5LjU3NiAxMTguMjE3IDUwMC42OTIgMTIyLjc1NiA1MDAuNjkyIDEyOC4yNjlaTTYwNS4wNiA3Ni40NDE0QzYxMy42NjIgNzYuNDQxNCA2MjAuMTQ5IDc4LjY5ODkgNjI0LjUyMSA4My4yMTM5QzYyOC44OTQgODcuNjgxMyA2MzEuMDggOTQuODgxNSA2MzEuMDggMTA0LjgxNFYxNTZINjE5LjM4OVYxMDUuMzg1QzYxOS4zODkgOTkuMDYzOCA2MTguMDEgOTQuMzM1IDYxNS4yNTQgOTEuMTk4MkM2MTIuNTQ1IDg4LjAxNCA2MDguNTA1IDg2LjQyMTkgNjAzLjEzNSA4Ni40MjE5QzU5NS41NzggODYuNDIxOSA1OTAuMDg5IDg4LjYwODEgNTg2LjY2NyA5Mi45ODA1QzU4My4yNDUgOTcuMzUyOSA1ODEuNTM0IDEwMy43OTMgNTgxLjUzNCAxMTIuM1YxNTZINTY5Ljc3MVYxMDUuMzg1QzU2OS43NzEgMTAxLjE1NSA1NjkuMTU0IDk3LjYzOCA1NjcuOTE4IDk0LjgzNEM1NjYuNzMgOTIuMDI5OSA1NjQuOTI0IDg5LjkzODggNTYyLjUgODguNTYwNUM1NjAuMTI0IDg3LjEzNDggNTU3LjEwNiA4Ni40MjE5IDU1My40NDYgODYuNDIxOUM1NDguMjY2IDg2LjQyMTkgNTQ0LjEwNyA4Ny40OTEyIDU0MC45NzEgODkuNjI5OUM1MzcuODM0IDkxLjc2ODYgNTM1LjU1MyA5NC45NTI4IDUzNC4xMjcgOTkuMTgyNkM1MzIuNzQ5IDEwMy4zNjUgNTMyLjA2IDEwOC41MjEgNTMyLjA2IDExNC42NTJWMTU2SDUyMC4yMjZWNzcuODY3Mkg1MjkuNzc4TDUzMS41NjEgODguOTE3SDUzMi4yMDJDNTMzLjc3MSA4Ni4yMDggNTM1Ljc0MyA4My45MjY4IDUzOC4xMTkgODIuMDczMkM1NDAuNDk1IDgwLjIxOTcgNTQzLjE1NyA3OC44MTc3IDU0Ni4xMDQgNzcuODY3MkM1NDkuMDUgNzYuOTE2NyA1NTIuMTg3IDc2LjQ0MTQgNTU1LjUxNCA3Ni40NDE0QzU2MS40NTQgNzYuNDQxNCA1NjYuNDIxIDc3LjU1ODMgNTcwLjQxMyA3OS43OTJDNTc0LjQ1MyA4MS45NzgyIDU3Ny4zNzYgODUuMzI4OCA1NzkuMTgyIDg5Ljg0MzhINTc5LjgyM0M1ODIuMzkgODUuMzI4OCA1ODUuOTA3IDgxLjk3ODIgNTkwLjM3NCA3OS43OTJDNTk0Ljg0MSA3Ny41NTgzIDU5OS43MzcgNzYuNDQxNCA2MDUuMDYgNzYuNDQxNFpNNjg0LjYxOCA3Ni41ODRDNjkzLjkzMyA3Ni41ODQgNzAwLjg0OCA3OC42NzUxIDcwNS4zNjMgODIuODU3NEM3MDkuODc4IDg3LjAzOTcgNzEyLjEzNiA5My43MTcxIDcxMi4xMzYgMTAyLjg5VjE1Nkg3MDMuNTFMNzAxLjIyOSAxNDQuNDUxSDcwMC42NThDNjk4LjQ3MiAxNDcuMzAzIDY5Ni4xOTEgMTQ5LjcwMyA2OTMuODE0IDE1MS42NTFDNjkxLjQzOCAxNTMuNTUyIDY4OC42ODIgMTU1LjAwMiA2ODUuNTQ1IDE1NkM2ODIuNDU2IDE1Ni45NTEgNjc4LjY1NCAxNTcuNDI2IDY3NC4xMzkgMTU3LjQyNkM2NjkuMzg2IDE1Ny40MjYgNjY1LjE1NiAxNTYuNTk0IDY2MS40NDkgMTU0LjkzMUM2NTcuNzkgMTUzLjI2NyA2NTQuODkxIDE1MC43NDggNjUyLjc1MiAxNDcuMzc0QzY1MC42NjEgMTQ0IDY0OS42MTUgMTM5LjcyMiA2NDkuNjE1IDEzNC41NDJDNjQ5LjYxNSAxMjYuNzQ4IDY1Mi43MDQgMTIwLjc1OSA2NTguODgzIDExNi41NzdDNjY1LjA2MSAxMTIuMzk1IDY3NC40NzEgMTEwLjExNCA2ODcuMTEzIDEwOS43MzNMNzAwLjU4NyAxMDkuMTYzVjEwNC4zODdDNzAwLjU4NyA5Ny42MzggNjk5LjEzNyA5Mi45MDkyIDY5Ni4yMzggOTAuMjAwMkM2OTMuMzM5IDg3LjQ5MTIgNjg5LjI1MiA4Ni4xMzY3IDY4My45NzcgODYuMTM2N0M2NzkuODg5IDg2LjEzNjcgNjc1Ljk5MiA4Ni43MzA4IDY3Mi4yODUgODcuOTE4OUM2NjguNTc4IDg5LjEwNzEgNjY1LjA2MSA5MC41MDkxIDY2MS43MzQgOTIuMTI1TDY1OC4wOTkgODMuMTQyNkM2NjEuNjE2IDgxLjMzNjYgNjY1LjY1NSA3OS43OTIgNjcwLjIxOCA3OC41MDg4QzY3NC43OCA3Ny4yMjU2IDY3OS41OCA3Ni41ODQgNjg0LjYxOCA3Ni41ODRaTTcwMC40NDQgMTE3LjUwNEw2ODguNTM5IDExOC4wMDNDNjc4Ljc5NiAxMTguMzgzIDY3MS45MjkgMTE5Ljk3NSA2NjcuOTM3IDEyMi43NzlDNjYzLjk0NCAxMjUuNTgzIDY2MS45NDggMTI5LjU1MiA2NjEuOTQ4IDEzNC42ODVDNjYxLjk0OCAxMzkuMTUyIDY2My4zMDMgMTQyLjQ1NSA2NjYuMDEyIDE0NC41OTRDNjY4LjcyMSAxNDYuNzMyIDY3Mi4zMDkgMTQ3LjgwMiA2NzYuNzc2IDE0Ny44MDJDNjgzLjcxNSAxNDcuODAyIDY4OS4zOTUgMTQ1Ljg3NyA2OTMuODE0IDE0Mi4wMjdDNjk4LjIzNCAxMzguMTc4IDcwMC40NDQgMTMyLjQwMyA3MDAuNDQ0IDEyNC43MDRWMTE3LjUwNFpNNzcyLjAxOSA3Ni40NDE0Qzc3My41ODcgNzYuNDQxNCA3NzUuMjI3IDc2LjUzNjUgNzc2LjkzOCA3Ni43MjY2Qzc3OC42NDggNzYuODY5MSA3ODAuMTY5IDc3LjA4MyA3ODEuNSA3Ny4zNjgyTDc4MC4wMDMgODguMzQ2N0M3NzguNzIgODguMDE0IDc3Ny4yOTQgODcuNzUyNiA3NzUuNzI2IDg3LjU2MjVDNzc0LjE1NyA4Ny4zNzI0IDc3Mi42ODQgODcuMjc3MyA3NzEuMzA2IDg3LjI3NzNDNzY4LjE2OSA4Ny4yNzczIDc2NS4xOTkgODcuOTE4OSA3NjIuMzk1IDg5LjIwMjFDNzU5LjYzOCA5MC40Mzc4IDc1Ny4yMTQgOTIuMjQzOCA3NTUuMTIzIDk0LjYyMDFDNzUzLjAzMiA5Ni45NDg5IDc1MS4zOTIgOTkuNzc2NyA3NTAuMjA0IDEwMy4xMDRDNzQ5LjAxNiAxMDYuMzgzIDc0OC40MjIgMTEwLjA0MiA3NDguNDIyIDExNC4wODJWMTU2SDczNi41MTdWNzcuODY3Mkg3NDYuMzU0TDc0Ny42MzggOTIuMjY3Nkg3NDguMTM3Qzc0OS43NTMgODkuMzY4NSA3NTEuNzAxIDg2LjczMDggNzUzLjk4MiA4NC4zNTQ1Qzc1Ni4yNjQgODEuOTMwNyA3NTguOTAxIDgwLjAwNTkgNzYxLjg5NiA3OC41ODAxQzc2NC45MzcgNzcuMTU0MyA3NjguMzEyIDc2LjQ0MTQgNzcyLjAxOSA3Ni40NDE0Wk04MjEuNDkzIDE0Ny43M0M4MjMuNDQyIDE0Ny43MyA4MjUuNDM4IDE0Ny41NjQgODI3LjQ4MSAxNDcuMjMxQzgyOS41MjUgMTQ2Ljg5OSA4MzEuMTg4IDE0Ni40OTUgODMyLjQ3MiAxNDYuMDJWMTU1LjIxNkM4MzEuMDkzIDE1NS44MzQgODI5LjE2OSAxNTYuMzU2IDgyNi42OTcgMTU2Ljc4NEM4MjQuMjczIDE1Ny4yMTIgODIxLjg5NyAxNTcuNDI2IDgxOS41NjggMTU3LjQyNkM4MTUuNDM0IDE1Ny40MjYgODExLjY3OSAxNTYuNzEzIDgwOC4zMDUgMTU1LjI4N0M4MDQuOTMgMTUzLjgxNCA4MDIuMjIxIDE1MS4zNDIgODAwLjE3OCAxNDcuODczQzc5OC4xODIgMTQ0LjQwNCA3OTcuMTg0IDEzOS42MDQgNzk3LjE4NCAxMzMuNDczVjg3LjEzNDhINzg2LjA2MlY4MS4zNjA0TDc5Ny4yNTUgNzYuNzI2Nkw4MDEuOTYgNTkuNzU5OEg4MDkuMDg5Vjc3Ljg2NzJIODMxLjk3M1Y4Ny4xMzQ4SDgwOS4wODlWMTMzLjExNkM4MDkuMDg5IDEzOC4wMTEgODEwLjIwNiAxNDEuNjcxIDgxMi40MzkgMTQ0LjA5NUM4MTQuNzIxIDE0Ni41MTkgODE3LjczOSAxNDcuNzMgODIxLjQ5MyAxNDcuNzNaTTkzMy44NDUgMTU2TDg5OC44NDIgNjMuNzUySDg5OC4yNzFDODk4LjQ2MiA2NS43MDA1IDg5OC42MjggNjguMDc2OCA4OTguNzcxIDcwLjg4MDlDODk4LjkxMyA3My42ODQ5IDg5OS4wMzIgNzYuNzUwMyA4OTkuMTI3IDgwLjA3NzFDODk5LjIyMiA4My4zNTY0IDg5OS4yNyA4Ni43MDcgODk5LjI3IDkwLjEyODlWMTU2SDg4OC4wMDZWNTEuNzc1NEg5MDUuOTcxTDkzOC45NzggMTM4LjQ2M0g5MzkuNDc3TDk3Mi45ODIgNTEuNzc1NEg5OTAuODA1VjE1Nkg5NzguODI4Vjg5LjI3MzRDOTc4LjgyOCA4Ni4xODQyIDk3OC44NzYgODMuMDk1MSA5NzguOTcxIDgwLjAwNTlDOTc5LjA2NiA3Ni44NjkxIDk3OS4xODUgNzMuOTIyNSA5NzkuMzI3IDcxLjE2NkM5NzkuNDcgNjguMzYyIDk3OS42MTIgNjUuOTM4MiA5NzkuNzU1IDYzLjg5NDVIOTc5LjE4NUw5NDMuNzU0IDE1Nkg5MzMuODQ1Wk0xMDg0LjkxIDExNi43OTFDMTA4NC45MSAxMjMuMjA3IDEwODQuMDcgMTI4LjkzNCAxMDgyLjQxIDEzMy45NzJDMTA4MC43NSAxMzkuMDA5IDEwNzguMzUgMTQzLjI2MyAxMDc1LjIxIDE0Ni43MzJDMTA3Mi4wNyAxNTAuMjAyIDEwNjguMjcgMTUyLjg2MyAxMDYzLjggMTU0LjcxN0MxMDU5LjM4IDE1Ni41MjMgMTA1NC4zNyAxNTcuNDI2IDEwNDguNzYgMTU3LjQyNkMxMDQzLjUzIDE1Ny40MjYgMTAzOC43MyAxNTYuNTIzIDEwMzQuMzYgMTU0LjcxN0MxMDMwLjA0IDE1Mi44NjMgMTAyNi4yOCAxNTAuMjAyIDEwMjMuMSAxNDYuNzMyQzEwMTkuOTYgMTQzLjI2MyAxMDE3LjUxIDEzOS4wMDkgMTAxNS43NiAxMzMuOTcyQzEwMTQuMDQgMTI4LjkzNCAxMDEzLjE5IDEyMy4yMDcgMTAxMy4xOSAxMTYuNzkxQzEwMTMuMTkgMTA4LjIzNiAxMDE0LjY0IDEwMC45NjUgMTAxNy41NCA5NC45NzY2QzEwMjAuNDQgODguOTQwOCAxMDI0LjU3IDg0LjM1NDUgMTAyOS45NCA4MS4yMTc4QzEwMzUuMzYgNzguMDMzNSAxMDQxLjggNzYuNDQxNCAxMDQ5LjI2IDc2LjQ0MTRDMTA1Ni4zOSA3Ni40NDE0IDEwNjIuNjIgNzguMDMzNSAxMDY3Ljk0IDgxLjIxNzhDMTA3My4zMSA4NC40MDIgMTA3Ny40NyA4OS4wMTIgMTA4MC40MiA5NS4wNDc5QzEwODMuNDEgMTAxLjAzNiAxMDg0LjkxIDEwOC4yODQgMTA4NC45MSAxMTYuNzkxWk0xMDI1LjQ1IDExNi43OTFDMTAyNS40NSAxMjMuMDY0IDEwMjYuMjggMTI4LjUwNiAxMDI3Ljk1IDEzMy4xMTZDMTAyOS42MSAxMzcuNzI2IDEwMzIuMTggMTQxLjI5MSAxMDM1LjY1IDE0My44MUMxMDM5LjExIDE0Ni4zMjggMTA0My41OCAxNDcuNTg4IDEwNDkuMDUgMTQ3LjU4OEMxMDU0LjQ3IDE0Ny41ODggMTA1OC45MSAxNDYuMzI4IDEwNjIuMzggMTQzLjgxQzEwNjUuOSAxNDEuMjkxIDEwNjguNDkgMTM3LjcyNiAxMDcwLjE1IDEzMy4xMTZDMTA3MS44MSAxMjguNTA2IDEwNzIuNjQgMTIzLjA2NCAxMDcyLjY0IDExNi43OTFDMTA3Mi42NCAxMTAuNTY1IDEwNzEuODEgMTA1LjE5NSAxMDcwLjE1IDEwMC42OEMxMDY4LjQ5IDk2LjExNzIgMTA2NS45MiA5Mi42MDAzIDEwNjIuNDUgOTAuMTI4OUMxMDU4Ljk4IDg3LjY1NzYgMTA1NC40OSA4Ni40MjE5IDEwNDguOTggODYuNDIxOUMxMDQwLjg1IDg2LjQyMTkgMTAzNC44OSA4OS4xMDcxIDEwMzEuMDggOTQuNDc3NUMxMDI3LjMzIDk5Ljg0OCAxMDI1LjQ1IDEwNy4yODYgMTAyNS40NSAxMTYuNzkxWk0xMTQyLjM3IDc2LjQ0MTRDMTE1MS42MyA3Ni40NDE0IDExNTguNjQgNzguNzIyNyAxMTYzLjQgODMuMjg1MkMxMTY4LjE1IDg3LjgwMDEgMTE3MC41MiA5NS4wNzE2IDExNzAuNTIgMTA1LjFWMTU2SDExNTguODNWMTA1Ljg4NEMxMTU4LjgzIDk5LjM3MjcgMTE1Ny4zNCA5NC41MDEzIDExNTQuMzQgOTEuMjY5NUMxMTUxLjQgODguMDM3OCAxMTQ2Ljg2IDg2LjQyMTkgMTE0MC43MyA4Ni40MjE5QzExMzIuMDggODYuNDIxOSAxMTI1Ljk5IDg4Ljg2OTUgMTEyMi40OCA5My43NjQ2QzExMTguOTYgOTguNjU5OCAxMTE3LjIgMTA1Ljc2NSAxMTE3LjIgMTE1LjA4VjE1NkgxMTA1LjM3Vjc3Ljg2NzJIMTExNC45MkwxMTE2LjcgODkuMTMwOUgxMTE3LjM0QzExMTkuMDEgODYuMzc0MyAxMTIxLjEyIDg0LjA2OTMgMTEyMy42OSA4Mi4yMTU4QzExMjYuMjUgODAuMzE0OCAxMTI5LjEzIDc4Ljg4OSAxMTMyLjMxIDc3LjkzODVDMTEzNS41IDc2Ljk0MDQgMTEzOC44NSA3Ni40NDE0IDExNDIuMzcgNzYuNDQxNFpNMTIwNi43NCA3Ny44NjcyVjE1NkgxMTk0LjkxVjc3Ljg2NzJIMTIwNi43NFpNMTIwMC45NiA0OC42Mzg3QzEyMDIuOTEgNDguNjM4NyAxMjA0LjU4IDQ5LjI4MDMgMTIwNS45NiA1MC41NjM1QzEyMDcuMzggNTEuNzk5MiAxMjA4LjA5IDUzLjc0NzcgMTIwOC4wOSA1Ni40MDkyQzEyMDguMDkgNTkuMDIzMSAxMjA3LjM4IDYwLjk3MTcgMTIwNS45NiA2Mi4yNTQ5QzEyMDQuNTggNjMuNTM4MSAxMjAyLjkxIDY0LjE3OTcgMTIwMC45NiA2NC4xNzk3QzExOTguOTIgNjQuMTc5NyAxMTk3LjIxIDYzLjUzODEgMTE5NS44MyA2Mi4yNTQ5QzExOTQuNSA2MC45NzE3IDExOTMuODQgNTkuMDIzMSAxMTkzLjg0IDU2LjQwOTJDMTE5My44NCA1My43NDc3IDExOTQuNSA1MS43OTkyIDExOTUuODMgNTAuNTYzNUMxMTk3LjIxIDQ5LjI4MDMgMTE5OC45MiA0OC42Mzg3IDEyMDAuOTYgNDguNjM4N1pNMTI1Ny4wNyAxNDcuNzNDMTI1OS4wMiAxNDcuNzMgMTI2MS4wMSAxNDcuNTY0IDEyNjMuMDYgMTQ3LjIzMUMxMjY1LjEgMTQ2Ljg5OSAxMjY2Ljc2IDE0Ni40OTUgMTI2OC4wNSAxNDYuMDJWMTU1LjIxNkMxMjY2LjY3IDE1NS44MzQgMTI2NC43NCAxNTYuMzU2IDEyNjIuMjcgMTU2Ljc4NEMxMjU5Ljg1IDE1Ny4yMTIgMTI1Ny40NyAxNTcuNDI2IDEyNTUuMTQgMTU3LjQyNkMxMjUxLjAxIDE1Ny40MjYgMTI0Ny4yNiAxNTYuNzEzIDEyNDMuODggMTU1LjI4N0MxMjQwLjUxIDE1My44MTQgMTIzNy44IDE1MS4zNDIgMTIzNS43NSAxNDcuODczQzEyMzMuNzYgMTQ0LjQwNCAxMjMyLjc2IDEzOS42MDQgMTIzMi43NiAxMzMuNDczVjg3LjEzNDhIMTIyMS42NFY4MS4zNjA0TDEyMzIuODMgNzYuNzI2NkwxMjM3LjU0IDU5Ljc1OThIMTI0NC42N1Y3Ny44NjcySDEyNjcuNTVWODcuMTM0OEgxMjQ0LjY3VjEzMy4xMTZDMTI0NC42NyAxMzguMDExIDEyNDUuNzggMTQxLjY3MSAxMjQ4LjAyIDE0NC4wOTVDMTI1MC4zIDE0Ni41MTkgMTI1My4zMSAxNDcuNzMgMTI1Ny4wNyAxNDcuNzNaTTEzNTEuMjQgMTE2Ljc5MUMxMzUxLjI0IDEyMy4yMDcgMTM1MC40MSAxMjguOTM0IDEzNDguNzUgMTMzLjk3MkMxMzQ3LjA4IDEzOS4wMDkgMTM0NC42OCAxNDMuMjYzIDEzNDEuNTUgMTQ2LjczMkMxMzM4LjQxIDE1MC4yMDIgMTMzNC42MSAxNTIuODYzIDEzMzAuMTQgMTU0LjcxN0MxMzI1LjcyIDE1Ni41MjMgMTMyMC43MSAxNTcuNDI2IDEzMTUuMSAxNTcuNDI2QzEzMDkuODcgMTU3LjQyNiAxMzA1LjA3IDE1Ni41MjMgMTMwMC43IDE1NC43MTdDMTI5Ni4zNyAxNTIuODYzIDEyOTIuNjIgMTUwLjIwMiAxMjg5LjQzIDE0Ni43MzJDMTI4Ni4zIDE0My4yNjMgMTI4My44NSAxMzkuMDA5IDEyODIuMDkgMTMzLjk3MkMxMjgwLjM4IDEyOC45MzQgMTI3OS41MyAxMjMuMjA3IDEyNzkuNTMgMTE2Ljc5MUMxMjc5LjUzIDEwOC4yMzYgMTI4MC45NyAxMDAuOTY1IDEyODMuODcgOTQuOTc2NkMxMjg2Ljc3IDg4Ljk0MDggMTI5MC45MSA4NC4zNTQ1IDEyOTYuMjggODEuMjE3OEMxMzAxLjcgNzguMDMzNSAxMzA4LjE0IDc2LjQ0MTQgMTMxNS42IDc2LjQ0MTRDMTMyMi43MyA3Ni40NDE0IDEzMjguOTUgNzguMDMzNSAxMzM0LjI4IDgxLjIxNzhDMTMzOS42NSA4NC40MDIgMTM0My44IDg5LjAxMiAxMzQ2Ljc1IDk1LjA0NzlDMTM0OS43NSAxMDEuMDM2IDEzNTEuMjQgMTA4LjI4NCAxMzUxLjI0IDExNi43OTFaTTEyOTEuNzkgMTE2Ljc5MUMxMjkxLjc5IDEyMy4wNjQgMTI5Mi42MiAxMjguNTA2IDEyOTQuMjggMTMzLjExNkMxMjk1Ljk1IDEzNy43MjYgMTI5OC41MSAxNDEuMjkxIDEzMDEuOTggMTQzLjgxQzEzMDUuNDUgMTQ2LjMyOCAxMzA5LjkyIDE0Ny41ODggMTMxNS4zOCAxNDcuNTg4QzEzMjAuOCAxNDcuNTg4IDEzMjUuMjUgMTQ2LjMyOCAxMzI4LjcxIDE0My44MUMxMzMyLjIzIDE0MS4yOTEgMTMzNC44MiAxMzcuNzI2IDEzMzYuNDkgMTMzLjExNkMxMzM4LjE1IDEyOC41MDYgMTMzOC45OCAxMjMuMDY0IDEzMzguOTggMTE2Ljc5MUMxMzM4Ljk4IDExMC41NjUgMTMzOC4xNSAxMDUuMTk1IDEzMzYuNDkgMTAwLjY4QzEzMzQuODIgOTYuMTE3MiAxMzMyLjI2IDkyLjYwMDMgMTMyOC43OSA5MC4xMjg5QzEzMjUuMzIgODcuNjU3NiAxMzIwLjgzIDg2LjQyMTkgMTMxNS4zMSA4Ni40MjE5QzEzMDcuMTkgODYuNDIxOSAxMzAxLjIyIDg5LjEwNzEgMTI5Ny40MiA5NC40Nzc1QzEyOTMuNjYgOTkuODQ4IDEyOTEuNzkgMTA3LjI4NiAxMjkxLjc5IDExNi43OTFaTTE0MDcuMiA3Ni40NDE0QzE0MDguNzcgNzYuNDQxNCAxNDEwLjQxIDc2LjUzNjUgMTQxMi4xMiA3Ni43MjY2QzE0MTMuODMgNzYuODY5MSAxNDE1LjM1IDc3LjA4MyAxNDE2LjY5IDc3LjM2ODJMMTQxNS4xOSA4OC4zNDY3QzE0MTMuOTEgODguMDE0IDE0MTIuNDggODcuNzUyNiAxNDEwLjkxIDg3LjU2MjVDMTQwOS4zNCA4Ny4zNzI0IDE0MDcuODcgODcuMjc3MyAxNDA2LjQ5IDg3LjI3NzNDMTQwMy4zNSA4Ny4yNzczIDE0MDAuMzggODcuOTE4OSAxMzk3LjU4IDg5LjIwMjFDMTM5NC44MiA5MC40Mzc4IDEzOTIuNCA5Mi4yNDM4IDEzOTAuMzEgOTQuNjIwMUMxMzg4LjIyIDk2Ljk0ODkgMTM4Ni41OCA5OS43NzY3IDEzODUuMzkgMTAzLjEwNEMxMzg0LjIgMTA2LjM4MyAxMzgzLjYxIDExMC4wNDIgMTM4My42MSAxMTQuMDgyVjE1NkgxMzcxLjdWNzcuODY3MkgxMzgxLjU0TDEzODIuODIgOTIuMjY3NkgxMzgzLjMyQzEzODQuOTQgODkuMzY4NSAxMzg2Ljg5IDg2LjczMDggMTM4OS4xNyA4NC4zNTQ1QzEzOTEuNDUgODEuOTMwNyAxMzk0LjA5IDgwLjAwNTkgMTM5Ny4wOCA3OC41ODAxQzE0MDAuMTIgNzcuMTU0MyAxNDAzLjUgNzYuNDQxNCAxNDA3LjIgNzYuNDQxNFoiIGZpbGw9IiMzQjg2NDYiLz4KPHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik0yMzQuNTQ3IDQ5LjQzNzhDMjE1LjA4MiA2Mi4wNDM3IDE5OC4yNDYgNzkuNDE3NiAxODIuMzEyIDk2LjU5MjNDMTczLjEwNyA4Ni42NzA4IDE2My42MDEgNzYuNjgyOCAxNTMuNDYxIDY3LjU4NTlMMTQ1LjUwNyA3NS4xNzc4QzE1NS4zNTUgODMuOTYyMSAxNjQuODI4IDkzLjg1MTIgMTc0LjMxNiAxMDQuMDY0TDE4Mi4zMjYgMTEyLjUzM0wxOTUuMTE0IDk4LjgzN0wxOTUuMTU2IDk4Ljg3ODJDMjA5LjczOCA4My4zOTU2IDIyNC40NDEgNjguOTkxMyAyNDAuNjgyIDU4LjQ5NDJMMjM0LjU0NyA0OS40Mzc4Wk0yNjAuMTg2IDQ4LjI3NThDMjcwLjg0MyA0NC4wMDA4IDI4Mi4yNjEgNDEuNTUwOSAyOTQuNzUyIDQxLjU1MDlDMzE0LjYxIDQxLjU1MDkgMzI5LjE2NyA0OS40MTIzIDMzOC44NTMgNjEuMDEzM0MzNDguNjQyIDcyLjczOTIgMzUzLjY4MiA4OC41ODE3IDM1My42ODIgMTA0LjY0N0MzNTMuNjgyIDEyMC43MTIgMzQ4LjY0MiAxMzYuNTU0IDMzOC44NTMgMTQ4LjI4QzMyOS4xNjcgMTU5Ljg4MSAzMTQuNjEgMTY3Ljc0MyAyOTQuNzUyIDE2Ny43NDNDMjU4LjIwOCAxNjcuNzQzIDIzMC44NDggMTQ2Ljc3MyAyMDQuODQyIDEyMC40NzhMMTk3LjM1NSAxMjguNDk3QzIyMy44NjUgMTU1LjI3OCAyNTQuMDA5IDE3OC42ODQgMjk0Ljc1MiAxNzguNjg0QzMxNy44MjcgMTc4LjY4NCAzMzUuNDcxIDE2OS40MDQgMzQ3LjI1MiAxNTUuMjkyQzM1OC45MjkgMTQxLjMwNiAzNjQuNjIzIDEyMi44NjUgMzY0LjYyMyAxMDQuNjQ3QzM2NC42MjMgODYuNDI4NSAzNTguOTI5IDY3Ljk4NzcgMzQ3LjI1MiA1NC4wMDExQzMzNS40NzEgMzkuODg5NiAzMTcuODI3IDMwLjYwOTQgMjk0Ljc1MiAzMC42MDk0QzI3OS44MzcgMzAuNjA5NCAyNjYuMzQzIDMzLjc0NTkgMjUzLjkyNyAzOS4wMzYzTDI2MC4xODYgNDguMjc1OFpNMTY3LjA5IDEyOC42NzdDMTQwLjYyMyAxNTUuMzgzIDExMC41MjMgMTc4LjY4NCA2OS44NzA5IDE3OC42ODRDNDYuNzk1NyAxNzguNjg0IDI5LjE1MjUgMTY5LjQwNCAxNy4zNzEyIDE1NS4yOTJDNS42OTQxNSAxNDEuMzA2IDAgMTIyLjg2NSAwIDEwNC42NDdDMCA4Ni40Mjg1IDUuNjk0MTUgNjcuOTg3NyAxNy4zNzEyIDU0LjAwMTFDMjkuMTUyNSAzOS44ODk2IDQ2Ljc5NTcgMzAuNjA5NCA2OS44NzA5IDMwLjYwOTRDOTUuNjg0NyAzMC42MDk0IDExNy4yNDMgNDAuMDA0NiAxMzYuMzE5IDUzLjcwMDlMMTI4LjI1NSA2MS4zOTdDMTExLjAyNiA0OS4zNDg4IDkyLjE1NTcgNDEuNTUwOSA2OS44NzA5IDQxLjU1MDlDNTAuMDEyNyA0MS41NTA5IDM1LjQ1NTcgNDkuNDEyMyAyNS43NzAzIDYxLjAxMzNDMTUuOTgwNyA3Mi43MzkyIDEwLjk0MTUgODguNTgxNyAxMC45NDE1IDEwNC42NDdDMTAuOTQxNSAxMjAuNzEyIDE1Ljk4MDcgMTM2LjU1NCAyNS43NzAzIDE0OC4yOEMzNS40NTU3IDE1OS44ODEgNTAuMDEyNyAxNjcuNzQzIDY5Ljg3MDkgMTY3Ljc0M0MxMDYuMzA4IDE2Ny43NDMgMTMzLjYxNSAxNDYuODk1IDE1OS41NTMgMTIwLjcwOUwxNjcuMDkgMTI4LjY3N1oiIGZpbGw9IiMzQjg2NDYiLz4KPC9zdmc+Cg0K")
no-repeat center;
background-size: contain;
}

.pf-v5-c-login__header::before,
.login-pf-header::before {
content: none !important;
background: none !important;
}

#kc-page-title,
h1#kc-page-title {
font-family: inherit !important;
font-size: 20px !important;
font-weight: 400 !important;
color: var(--sm-text) !important;
text-align: center !important;
margin: 0 0 16px 0 !important;
padding: 0 !important;
line-height: 1.3 !important;
}

.login-pf-page .card-pf,
#kc-form-wrapper,
#kc-form-login,
#kc-otp-login-form,
form#kc-form-login,
.pf-v5-c-login__main-body form,
.pf-v5-c-login__main-body > div {
width: 100% !important;
max-width: 350px !important;
margin-left: auto !important;
margin-right: auto !important;
}

.pf-v5-c-form-control,
.pf-v5-c-input-group,
.pf-v5-c-input-group__item {
background: transparent !important;
border: none !important;
box-shadow: none !important;
padding: 0 !important;
width: 100% !important;
}

.pf-v5-c-form-control::before,
.pf-v5-c-form-control::after {
display: none !important;
border: none !important;
content: none !important;
}

input#username,
input#password,
input#otp,
input[type="text"],
input[type="password"] {
display: block !important;
width: 100% !important;
height: 40px !important;
background-color: #FFFFFF !important;
border: 1px solid var(--sm-border) !important;
border-radius: 4px !important;
box-shadow: none !important;
outline: none !important;
font-family: inherit !important;
font-size: 14px !important;
color: var(--sm-text) !important;
padding: 0 12px 0 42px !important;
background-repeat: no-repeat !important;
background-position: 13px center !important;
background-size: 17px 17px !important;
}

input#username {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%238C8C8C' stroke-width='1.8'%3E%3Ccircle cx='12' cy='8' r='3.5'/%3E%3Cpath d='M4.5 20c0-4 3.4-6.5 7.5-6.5s7.5 2.5 7.5 6.5'/%3E%3C/svg%3E") !important;
}

input#password,
input#otp {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%238C8C8C' stroke-width='1.8'%3E%3Crect x='5' y='10.5' width='14' height='9.5' rx='1.5'/%3E%3Cpath d='M8 10.5V7.5a4 4 0 0 1 8 0v3'/%3E%3C/svg%3E") !important;
}

input:focus {
border-color: var(--sm-green) !important;
box-shadow: 0 0 0 1px var(--sm-green) !important;
}

input::placeholder { color: #9AA0A6 !important; }

.pf-v5-c-input-group,
#kc-form-login .pf-v5-c-input-group {
position: relative !important;
display: block !important;
width: 100% !important;
}

.pf-v5-c-input-group__item {
display: block !important;
width: 100% !important;
}

.pf-v5-c-input-group__item:has(button),
.pf-v5-c-input-group__item.pf-m-plain {
position: absolute !important;
right: 4px !important;
top: 50% !important;
transform: translateY(-50%) !important;
width: auto !important;
}

.pf-v5-c-button.pf-m-control,
button[aria-label*="assword"],
button[aria-label*="ароль"],
#kc-form-login .pf-v5-c-input-group button {
position: absolute !important;
right: 4px !important;
top: 50% !important;
transform: translateY(-50%) !important;
margin: 0 !important;
padding: 0 8px !important;
height: 32px !important;
background: transparent !important;
border: none !important;
box-shadow: none !important;
color: #8C8C8C !important;
}

.pf-v5-c-form__label,
.pf-v5-c-form__label-text,
label[for="username"],
label[for="password"],
label[for="otp"] {
display: block !important;
font-family: inherit !important;
font-size: 13px !important;
font-weight: 400 !important;
line-height: 1.2 !important;
color: #5F6368 !important;
margin: 0 0 2px 2px !important;
padding: 0 !important;
text-align: left !important;
}

.pf-v5-c-form__group-label,
.pf-v5-c-form__group-label-main,
.pf-v5-c-form__group-main {
padding-bottom: 0 !important;
margin-bottom: 0 !important;
}

.pf-v5-c-form__group-control {
margin-top: 0 !important;
padding-top: 0 !important;
}

.pf-v5-c-form,
#kc-form-login,
#kc-otp-login-form {
--pf-v5-c-form--GridGap: 10px !important;
row-gap: 10px !important;
gap: 10px !important;
}

.pf-v5-c-form__group,
#kc-form-login .form-group {
--pf-v5-c-form__group--GridGap: 2px !important;
row-gap: 2px !important;
gap: 2px !important;
margin-bottom: 0 !important;
}

#kc-login,
input[type="submit"],
.pf-v5-c-button.pf-m-primary {
width: 100% !important;
height: 40px !important;
background-color: var(--sm-green) !important;
border: 1px solid var(--sm-green) !important;
border-radius: 4px !important;
color: #FFFFFF !important;
font-family: inherit !important;
font-size: 15px !important;
font-weight: 600 !important;
box-shadow: none !important;
margin-top: 4px !important;
cursor: pointer;
}

#kc-login:hover,
.pf-v5-c-button.pf-m-primary:hover {
background-color: var(--sm-green-dark) !important;
border-color: var(--sm-green-dark) !important;
}

[id*="locale" i],
[class*="locale" i],
#kc-locale,
#kc-locale-wrapper,
#kc-locale-dropdown,
select#locale,
.pf-v5-c-login__main-header .pf-v5-c-dropdown,
.pf-v5-c-login__main-header .pf-v5-c-menu-toggle,
.pf-v5-c-login__main-header-utilities,
#kc-form-options,
#kc-registration,
#kc-registration-container,
#kc-info,
#kc-info-wrapper,
.checkbox,
.pf-v5-c-login__main-footer-band,
#kc-social-providers {
display: none !important;
}

.pf-v5-c-login__main-header,
#kc-page-title {
display: block !important;
width: 100% !important;
}

.alert-error,
.pf-v5-c-alert.pf-m-danger,
#input-error,
.pf-v5-c-form__helper-text.pf-m-error {
font-family: inherit !important;
font-size: 13px !important;
color: #B3261E !important;
}

@media (max-width: 480px) {
#kc-header-wrapper::before {
width: 260px;
height: 40px;
margin-top: 28px;
}
}
/opt/keycloak/themes/smartmonitor/login/messages/messages_en.properties
loginTitle=Smart Monitor

loginAccountTitle=Sign in via 2FA

username=Username
password=Password
doLogIn=Sign in

loginOtpOneTime=One-time code
doSubmit=Confirm
/opt/keycloak/themes/smartmonitor/login/messages/messages_ru.properties
loginTitle=Smart Monitor

loginAccountTitle=Вход через 2FA

username=Имя пользователя
password=Пароль
doLogIn=Войти

loginOtpOneTime=Одноразовый код
doSubmit=Подтвердить
/opt/keycloak/themes/smartmonitor/login/theme.properties
parent=keycloak.v2

styles=css/styles.css css/smartmonitor.css
locales=en,ru

Set permissions on the theme directory:

sudo chown -R keycloak:keycloak /opt/keycloak/themes/smartmonitor
sudo chmod -R a+rX /opt/keycloak/themes/smartmonitor

To enable the theme, go to Realm settings - Themes and set:

  • setting Login theme — smartmonitor

Restart Keycloak:

systemctl restart keycloak

Active Directory federation​

Transfer the domain certificate first, otherwise Test connection will fail:

sudo cp $OS_HOME/config/opensearch-security/ldaps-ca-cert.pem /opt/keycloak/conf/
sudo chown keycloak:keycloak /opt/keycloak/conf/ldaps-ca-cert.pem
sudo chmod 644 /opt/keycloak/conf/ldaps-ca-cert.pem

In keycloak.conf:

truststore-paths=/opt/keycloak/conf/ldaps-ca-cert.pem

After that, restart the keycloak service:

sudo systemctl restart keycloak

The field values are taken from the current Smart Monitor Data Storage configuration:

sed -n '/^ ldap:/,/^ authz:/p' $OS_HOME/config/opensearch-security/config.yml

When configuring User Federation, use Test connection to check the connection to the AD domain controller and Test authentication to check authentication.

Go to User Federation - Add LDAP provider and set the values:

FieldValueSource
VendorActive Directory-
Connection URLldaps://<LDAP_DOMAIN>:636hosts + enable_ssl
Enable StartTLSOffenable_start_tls
Connection poolingOn-
Connection timeout30000connect_timeout
Bind typesimple-
Bind DNfrom bind_dn-
Bind credentialsfrom password-
Edit modeREAD_ONLYread-only access to AD
Sync RegistrationsOffcontradicts READ_ONLY
Users DNfrom userbase-
Username LDAP attributesAMAccountNameusername_attribute
RDN LDAP attributecnAD standard
UUID LDAP attributeobjectGUIDAD standard
User object classesperson, organizationalPerson, userAD standard
Search scopeSubtree-

A federated user is created in Keycloak the moment they are first accessed, so they do not appear in the general list on the Users tab until a search by name is performed.

Second factor - OTP​

Go to Authentication - Required actions - Configure OTP and set the value to Enabled.

The second-factor requirements are defined by the Authentication - Flows - browser flow:

Flow stepValue
Browser - Conditional 2FARequired
OTP FormRequired

Change both settings. If the subflow is Required but OTP Form is Alternative, Keycloak skips this step for a user without OTP configured instead of prompting them to set up an authenticator.

Session lifetime​

The second factor has no separate timer. The code is requested only during a new authentication; the SSO session determines when reauthentication is required.

The settings are on two tabs: Realm settings - Sessions controls session settings, and Realm settings - Tokens controls token lifetime.

Field in the consoleField in the APIDefaultPurpose
SSO Session IdlessoSessionIdleTimeout1800 (30 min)idle time without activity
SSO Session MaxssoSessionMaxLifespan36000 (10 h)maximum lifetime regardless of activity
Client Session IdleclientSessionIdleTimeout00 means "same as SSO"
Client Session MaxclientSessionMaxLifespan0the same
Access Token LifespanaccessTokenLifespan300 (5 min)token lifetime
Client Login TimeoutaccessCodeLifespan60 (1 min)lifetime of the authorization code before the exchange
Login timeoutaccessCodeLifespanLogin1800 (30 min)for the entire sign-in process
Login action timeoutaccessCodeLifespanUserAction300 (5 min)for each step, including OTP binding

Units of measurement​

In the console, select a time unit next to the field: seconds, minutes, hours, or days. The API and database store values in seconds, while the Smart Monitor Web configuration uses milliseconds:

smart_monitor.cookie.ttl: 3600000 # 1 hour
smart_monitor.session.ttl: 3600000
smart_monitor.session.keepalive: true

The actual realm values:

sudo -u keycloak JAVA_HOME=/opt/jdk-21 /opt/keycloak/bin/kcadm.sh get realms/<REALM_ID> \
--fields ssoSessionIdleTimeout,ssoSessionMaxLifespan,accessTokenLifespan,accessCodeLifespanUserAction

The time before the second factor is requested again is limited by SSO Session Idle during inactivity and by SSO Session Max regardless of activity. The shortest of three lifetimes applies: the token, the SSO session, and the Smart Monitor Web session. With keepalive: true, activity extends the latter, so SSO Session Idle is usually the practical limit.

Current sessions appear in the Sessions section and under Users - user - Sessions, in the Started and Last access fields. To view token contents, go to Clients - client - Client scopes - Evaluate - Generated ID Token and check the iat and exp fields.

Next: Setting Up the Keycloak Client.