SAML SSO Docker Deployment Guide

Parameter Value

Application version

Spring Boot 3.5.6 / Java 25

Identity Provider

Keycloak 26.0 (Docker)

Standard

SAML 2.0, HTTP POST Binding

Networking

Docker Compose with shared bridge network

1. Overview

This guide covers deploying Keycloak (as a test Identity Provider) in Docker alongside the OneIoT backend, with full SAML SSO configuration. Both services run in separate Docker Compose stacks connected via a shared Docker network, allowing container-to-container communication by hostname.

The deployment consists of two independent Compose stacks:

  • Keycloak stack — Keycloak IdP + PostgreSQL database for Keycloak metadata storage

  • OneIoT stack — UI Backend (Spring Boot) + UI Portals (Nginx), using the existing compose.yml

A shared Docker bridge network (ft-shared-network) connects the two stacks, enabling the backend to reach Keycloak for SAML metadata exchange and assertion verification.

1.1. Deployment diagram

saml-docker-deployment

2. Prerequisites

  • Docker Engine 24+ and Docker Compose v2+ installed on the deployment host.

  • OneIoT backend Docker image built and available locally or in a registry.

  • Prepare and build the all in one compose file from:

FT_DISK → prerelease → ONE IOT UI → docker → all in one → saml

docker compose up -d

3. Keycloak Docker deployment

3.1. Docker Compose for Keycloak

Run compose-saml.yml with the following content:

version: "3.9"

networks:
  ft-network:
    name: ft-shared-network
    driver: bridge

services:
  keycloak:
    image: quay.io/keycloak/keycloak:latest
    command: start --hostname=https://<DOCKER_HOST>:8500
    volumes:
     - ./<YOUR.crt>.crt:/opt/keycloak/conf/server.crt
     - ./<YOUR.key>.key:/opt/keycloak/conf/server.key
    environment:
      KC_LOG_LEVEL: "INFO,org.keycloak.protocol.saml:DEBUG,org.keycloak.saml:DEBUG,org.keycloak.events:DEBUG"
      KEYCLOAK_ADMIN: admin
      KEYCLOAK_ADMIN_PASSWORD: admin
      KC_HTTPS_CERTIFICATE_FILE: /opt/keycloak/conf/server.crt
      KC_HTTPS_CERTIFICATE_KEY_FILE: /opt/keycloak/conf/server.key
    ports:
      - "8500:8443"
    networks:
      - ft-network
<DOCKER_HOST> should be replaced with valid hostname or IP
The start-dev command runs Keycloak in development mode — no TLS, no hostname verification, relaxed security. In production, use the start command with proper TLS certificates and a production database. See the Keycloak production configuration guide.
The ft-network is a named bridge network (ft-shared-network) that will be shared with the OneIoT backend stack. Keycloak is attached to both keycloak-internal (for PostgreSQL access) and ft-network (for backend communication).
The Keycloak Admin Console will be available at http://<DOCKER_HOST>:<KEYCLOAK_PORT>/admin with the credentials defined in KEYCLOAK_ADMIN / KEYCLOAK_ADMIN_PASSWORD.

3.2. Starting Keycloak

# Start the Keycloak stack in detached mode
docker compose -f compose-saml.yml up -d

# Verify container are running and healthy
docker compose -f compose-saml.yml ps

# Check Keycloak startup logs
docker logs keycloak --tail 50

Open the Admin Console in your browser:

https://localhost:8500/admin

Log in with admin / admin (default credentials from the compose file).

3.3. Keycloak configuration

For detailed step-by-step instructions on configuring Keycloak as a SAML IdP, see SAML SSO Setup — Keycloak setup (test IdP).

The following configuration steps must be completed in the Keycloak Admin Console. The SC and MC portals use two separate realms, each containing a single SAML client:

  1. Create two realms in Realm settings:

    • oneiot-sc — for the Support Center portal

    • oneiot-mc — for the Management Console portal

  2. Create one SAML client per realm:

    • In realm oneiot-sc: client oneiot-sp-sc (Client type SAML)

    • In realm oneiot-mc: client oneiot-sp-mc (Client type SAML)

  3. Disable Client signature required on the Keys tab of each client.

  4. Export IdP signing certificates — in each realm’s Realm Settings > Keys, export the RS256 signing certificate and save it as sc-idp.crt (from oneiot-sc) and mc-idp.crt (from oneiot-mc). See Exporting IdP Signing Certificate.

  5. Configure User Profile attributes in each realm — add domain, user-group, and portal-type at Realm Settings → User profile (repeat per realm).

  6. Create test users in each realm with the custom attributes filled in at Users → Create user.

  7. Create Protocol Mappers for the dedicated client scope of each client — username, email, domain, user-group, portal-type at Clients → oneiot-sp-sc/oneiot-sp-mc → Client scopes → oneiot-sp-sc-dedicated / oneiot-sp-mc-dedicated → Configure new mapper → User Attribute.

When setting the Master SAML Processing URL and for example https://<UI_HOST>:8843/iot-webservice/login/saml2/sso/saml-support-portal or saml-management-portal Valid Redirect URIs for example https://<UI_HOST>:8843/iot-webservice/* in Keycloak, use the externally accessible URL of the UI (the same value as SAML_REDIRECT_HOST=https://<UI_HOST>:8843/), not the Docker-internal hostname.

4. Backend deployment with SAML

4.1. Connecting to the shared network

The existing OneIoT backend compose.yml (located at iot-web-app-build/docker/compose.yml) must be connected to the same Docker network as Keycloak so the backend container can reach Keycloak by its container name.

Add the external network declaration and attach the ui-backend service to it:

# Add to the top-level networks section of compose.yml:
networks:
  ft-network:
    external: true
    name: ft-shared-network

# Add network membership to ui-backend service:
services:
  ui-backend:
    networks:
      - ft-network
      - default
    # ... rest of the service definition ...

4.2. SAML environment variables

The following environment variables control SAML SSO behavior. They are passed to the backend via the .env.saml file.

Environment variable YAML path Default value Description

SAML_ENABLED

app.saml.enabled

false

Enables/disables SAML SSO. When false, the SamlAuthenticationSuccessHandler bean is not created and the filter chain is not registered.

SAML_REDIRECT_HOST

app.saml.redirect-url

 — (required)

Base application URL used to construct redirect links and the ACS URL. Example: https://portal.example.com

SAML_SC_SP_ENTITY_ID

app.saml.sc.sp-entity-id

oneiot-sp-sc

Service Provider Entity ID for the Support Center portal.

SAML_SC_IDP_ENTITY_ID

app.saml.sc.idp-entity-id

 — (required when SAML enabled)

IdP Entity ID used by the Support Center portal. Example: https://keycloak.example.com/realms/oneiot-sc

SAML_SC_IDP_SSO_URL

app.saml.sc.idp-sso-url

 — (required when SAML enabled)

IdP Single Sign-On URL used by the Support Center portal. Example: https://keycloak.example.com/realms/oneiot-sc/protocol/saml

SAML_MC_SP_ENTITY_ID

app.saml.mc.sp-entity-id

oneiot-sp-mc

Service Provider Entity ID for the Management Console portal.

SAML_MC_IDP_ENTITY_ID

app.saml.mc.idp-entity-id

 — (required when SAML enabled)

IdP Entity ID used by the Management Console portal.

SAML_MC_IDP_SSO_URL

app.saml.mc.idp-sso-url

 — (required when SAML enabled)

IdP Single Sign-On URL used by the Management Console portal.

SAML_ATTR_USERNAME

app.saml.mapping.username-attribute

username

Name of the SAML attribute containing the username.

SAML_ATTR_EMAIL

app.saml.mapping.email-attribute

email

Name of the SAML attribute containing the user email.

SAML_ATTR_CLIENT_TYPE

app.saml.mapping.client-type-attribute

portal-type

Name of the SAML attribute determining the portal type (sc / mc).

SAML_ATTR_DOMAIN

app.saml.mapping.domain-attribute

domain

Name of the SAML attribute containing the user domain. The value must match the domain name in the database.

SAML_ATTR_USER_GROUP

app.saml.mapping.user-group-attribute

user-group

Name of the SAML attribute containing the user group. The value must match the group name in the database.

SAML_ATTR_EXPIRE_DATE

app.saml.mapping.expire-date-attribute

expire-date

Name of the SAML attribute with the account expiration date. Formats: ISO-8601 (2027-01-01T00:00:00Z) or date (2027-01-01).

The IdP signing certificate paths are not exposed as environment variables. They are fixed to ${app.home}/conf/sc-idp.crt (Support Center) and ${app.home}/conf/mc-idp.crt (Management Console). In Docker, ${app.home} is /opt/app, so the files must be mounted into /opt/app/conf/sc-idp.crt and /opt/app/conf/mc-idp.crt — see Certificate mount.

4.3. Post-authentication redirect URLs

After successful SAML authentication, the backend redirects the browser to the frontend with a JWT token and session hash as query parameters.

Portal URL Configuration

SC

${SAML_REDIRECT_HOST}/support-portal/login/auth-callback

app.saml.success-redirect-url

MC

${SAML_REDIRECT_HOST}/management-portal/login/auth-callback

app.saml.mc.success-redirect-url

The frontend receives the JWT token and session hash as query parameters:

https://portal.example.com/support-portal/login/auth-callback?token=eyJhbG...&sessionHash=BASE64...

4.4. .env.saml configuration file

The project includes a template SAML environment file at iot-web-app-build/docker/.env.saml. Copy this file and fill in the values for your deployment.

Below is a complete filled-in example for a Docker deployment where Keycloak runs in the same Docker host:

# ==========================================================
# SAML Configuration for Docker Deployment
# ==========================================================

SAML_ENABLED=true
SAML_REDIRECT_HOST=https://<DOCKER_HOST>:8843

# --- SC (Support Center) registration ---
SAML_SC_SP_ENTITY_ID=oneiot-sp-sc
SAML_SC_IDP_ENTITY_ID=https://<DOCKER_HOST>:8500/realms/oneiot-sc
SAML_SC_IDP_SSO_URL=https://<DOCKER_HOST>:8500/realms/oneiot-sc/protocol/saml

# --- MC (Management Console) registration ---
SAML_MC_SP_ENTITY_ID=oneiot-sp-mc
SAML_MC_IDP_ENTITY_ID=https://<DOCKER_HOST>:8500/realms/oneiot-mc
SAML_MC_IDP_SSO_URL=https://<DOCKER_HOST>:8500/realms/oneiot-mc/protocol/saml

# --- Attribute mapping (defaults shown) ---
SAML_ATTR_USERNAME=username
SAML_ATTR_EMAIL=email
SAML_ATTR_CLIENT_TYPE=portal-type
SAML_ATTR_DOMAIN=domain
SAML_ATTR_USER_GROUP=user-group
SAML_ATTR_EXPIRE_DATE=expire-date
The SC and MC portals use two separate Keycloak realms (oneiot-sc and oneiot-mc in this example). Each realm has its own signing key, so the exported certificates must be placed in sc-idp.crt and mc-idp.crt respectively. SC and MC success-redirect URLs are built from SAML_REDIRECT_HOST automatically — no separate SAML_SUCCESS_REDIRECT / SAML_MC_SUCCESS_REDIRECT variables are needed.

4.5. Certificate mount

SAML IdP certificates must be mounted into the backend container. The application loads them from ${app.home}/conf/ (i.e. /opt/app/conf/ inside the container).

Place the certificate files in the ui-backend-conf/ directory on the host — this directory is mounted into /opt/app/conf by the backend container (see the volumes: section of compose.yml):

File Purpose Format Required

sc-idp.crt

Support Center IdP signing certificate (exported from the SC realm in Keycloak)

X.509 PEM

true (when SAML_ENABLED=true)

mc-idp.crt

Management Console IdP signing certificate (exported from the MC realm in Keycloak)

X.509 PEM

true (when SAML_ENABLED=true)

No SP key/certificate is required. The application does not sign outgoing AuthnRequests, so no SP signing credential is configured.

Each certificate must be in X.509 PEM format:

-----BEGIN CERTIFICATE-----
MIICnTCCAYUCBgGE... (base64-encoded certificate data)
...
-----END CERTIFICATE-----
Each certificate MUST include the -----BEGIN CERTIFICATE----- and -----END CERTIFICATE----- header/footer lines. If both portals share the same Keycloak realm, the same content is written to both sc-idp.crt and mc-idp.crt. See Exporting IdP Signing Certificate for how to obtain them.

4.6. Starting the backend with SAML

Follow this sequence to start the full deployment:

# 1. Ensure Keycloak is running and healthy
docker compose -f compose-saml.yml ps
# Expected: keycloak (healthy), if exist: keycloak-db (healthy)

# 2. Verify the shared network exists
docker network ls | grep ft-shared-network

# 3. Start the backend stack with both env files
cd iot-web-app-build/docker
docker compose --env-file .env.mysql --env-file .env.saml up -d

# 4. Verify the backend can reach Keycloak via the shared network
docker exec <backend-container> curl -s http://keycloak:8080/realms/oneiot-sc/.well-known/openid-configuration | head -5

# 5. Verify SAML metadata is published by the backend
curl http://localhost:8880/iot-webservice/saml2/metadata/saml-support-portal
If step 4 fails with "Could not resolve host: keycloak", the backend container is not connected to ft-shared-network. See Connecting to the shared network or run docker network connect ft-shared-network <backend-container>.

5. Shared Docker network setup

5.1. How it works

When two Docker Compose stacks need to communicate, they must share a common Docker network. Each Compose stack creates its own isolated default network, so containers from different stacks cannot reach each other unless explicitly connected.

In this deployment:

  1. The Keycloak stack creates the ft-shared-network bridge network (defined as ft-network with name: ft-shared-network).

  2. The OneIoT backend stack joins this network as an external network.

  3. Containers on the shared network can reach each other by container name — Docker’s embedded DNS server resolves container names to IP addresses within the same network.

5.2. Network topology diagram

docker-network-topology
Network Scope Connected containers

ft-shared-network

Cross-stack communication

keycloak, portals

keycloak-internal

Keycloak stack internal

keycloak, keycloak-db

default (OneIoT)

OneIoT stack internal

ui-backend, portals

5.3. Verifying network connectivity

# Check that the shared network exists
docker network inspect saml-shared-network

# List containers connected to the shared network
docker network inspect saml-shared-network --format '{{range .Containers}}{{.Name}} {{end}}'

# Test DNS resolution from the backend container
docker exec <backend-container> ping -c 3 keycloak

# Test Keycloak SAML descriptor endpoint from the backend container
docker exec <backend-container> curl -sf http://keycloak:8080/realms/oneiot-sc/protocol/saml/descriptor | head -5

5.4. Troubleshooting network issues

5.4.1. "SAML signature validation failed"

Cause: The sc-idp.crt or mc-idp.crt file mounted into the backend container does not match the current signing key of the corresponding Keycloak realm.

Solution:

  1. Identify which portal is failing (SC or MC) from the failure URL / stack trace.

  2. Re-export the IdP signing certificate from the affected realm (see Exporting IdP Signing Certificate).

  3. Replace the sc-idp.crt or mc-idp.crt file on the host in ui-backend-conf/.

  4. Restart the backend container:

    docker compose restart ui-backend

5.4.2. "Invalid redirect URI" in Keycloak

Cause: The SAML_REDIRECT_HOST value does not match the Valid Redirect URIs configured in the Keycloak SAML client.

Solution:

  1. Open the Keycloak Admin Console > Clients > oneiot-sp-sc (or oneiot-sp-mc).

  2. Verify that Valid Redirect URIs includes the redirect URL pattern, e.g. https://portal.example.com/*.

  3. Verify that Master SAML Processing URL matches the ACS URL pattern: https://portal.example.com/iot-webservice/login/saml2/sso/saml-support-portal.

6. Complete deployment example

This section provides a full end-to-end walkthrough for deploying Keycloak and the OneIoT backend with SAML SSO enabled.

6.1. Directory structure

Prepare the following directory structure on the deployment host:

/usr/local/ft-services/
├── compose-saml.yml                     # Keycloak stack (from this guide)
├── compose.yml                          # OneIoT stack (from iot-web-app-build/docker/)
├── .env.mysql                           # Database env (from iot-web-app-build/docker/)
├── .env.saml                            # SAML env (filled in from template)
├── ui-backend-conf/
│   ├── hazelcast-client.xml             # Main Hazelcast client configuration
│   ├── ftacs.keystore                   # Backend keystore used when HTTPS is enabled
│   ├── logback-spring.xml               # Default external logging configuration
│   ├── interfaceItems.json              # Interface seed data, loaded if present
│   ├── sc-idp.crt                       # Exported from the SC Keycloak realm
│   ├── mc-idp.crt                       # Exported from the MC Keycloak realm
│   ├── customization/
│   │   ├── def/
│   │   ├── mc/
│   │   └── sc/
│   └── ssl/                             # HTTPS certificates for Nginx/portals when configured
└── ft-data/ui-portals/
    ├── backend/logs/
    └── nginx/logs/

6.2. Verification checklist

After completing the deployment, verify each item:

# Check Command

1

Keycloak Admin Console is accessible

Open http://localhost:8080/admin

2

Keycloak realms oneiot-sc and oneiot-mc exist

Check in Admin Console

3

SAML clients are configured (one per realm)

Check oneiot-sc > Clients > oneiot-sp-sc and oneiot-mc > Clients > oneiot-sp-mc

4

Shared Docker network exists

docker network ls | grep saml-shared-network

5

Backend container is on the shared network

docker inspect <backend> --format '{{json .NetworkSettings.Networks}}'

6

Backend can resolve Keycloak hostname

docker exec <backend> ping -c 1 keycloak

7

Backend SAML metadata is published

curl https://localhost:8843/iot-webservice/saml2/metadata/saml-support-portal

8

IdP certificates match Keycloak

Compare sc-idp.crt with oneiot-sc > Realm Settings > Keys and mc-idp.crt with oneiot-mc > Realm Settings > Keys

9

SAML login URL endpoint responds

curl -X POST https://localhost:8843/iot-webservice/iotw/Auth/saml/login-url -H "Content-Type: application/json" -d '{"clientType":"sc"}'

10

Auth type returns saml

curl -X POST https://localhost:8843/iot-webservice/iotw/Auth/type

7. Appendix: Full docker-compose example with SAML

Below is the complete compose.yml with all SAML modifications applied — external network, env file reference, and certificate volume mounts:

version: "3.8"
name: ft-ui-portals

networks:
  ft-network:                                     (1)
    external: true
    name: ft-shared-network

services:
  ui-backend:
    image: hub.friendly-tech.com/ui/backend:latest
    ports:
      - "${FT_UI_BACKEND_HTTP_PORT:-8881}:8880"
    env_file:
      - .env.mysql
      - .env.saml                                 (2)
    environment:
      USE_NOHUP: 0
      APP_HOME: "/opt/app"
      CLICKHOUSE_SCHEMA: "${CLICKHOUSE_DATABASE}"
      JAVA_OPTS: "${FT_UI_BACKEND_JAVA_RAM:--Xms512m -Xmx2g} -Duser.timezone=${TZ}"
    volumes:
      - ${DATA_FOLDER:-.}/ui-backend-conf:/opt/app/conf
      - ${DATA_FOLDER:-.}/ft-data/ui-portals/backend/logs:/opt/app/logs
    networks:                                    (4)
      - default
      - ft-network
    extra_hosts:
      - "host.docker.internal:host-gateway"
    restart: unless-stopped
    healthcheck:
      test: [ "CMD", "curl", "-f", "http://127.0.0.1:8880/iot-webservice/swagger-ui/index.html" ]
      interval: 10s
      timeout: 5s
      retries: 5
    stop_grace_period: 45s

  portals:
    image: hub.friendly-tech.com/ui/portals:latest
    depends_on:
      ui-backend:
        condition: service_healthy
    ports:
      - "${FT_UI_HTTP_PORT:-8880}:80"
      - "${FT_UI_HTTPS_PORT:-8843}:443"
    env_file:
      - .env.mysql
    volumes:
      - ${DATA_FOLDER:-.}/ft-data/ui-portals/nginx/logs:/var/log/nginx
      - ${DATA_FOLDER:-.}/ui-backend-conf/ssl:/etc/nginx/ssl:ro
    extra_hosts:
      - "host.docker.internal:host-gateway"
    restart: unless-stopped
    healthcheck:
      test: [ "CMD", "curl", "-f", "http://127.0.0.1/support-portal/index.html" ]
      interval: 10s
      timeout: 5s
      retries: 5
    stop_grace_period: 45s
1 Declares ft-shared-network as an external network (created by the Keycloak stack).
2 Loads SAML environment variables from .env.saml in addition to the database env file.
3 Mounts SAML certificates from the host into the container at the paths expected by the application.
4 Attaches ui-backend to both the default network (for communication with portals) and ft-shared-network (for communication with keycloak).