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:

  1. Create realm oneiot in Realm settings

  2. Create SAML clients:

    • oneiot-sp-sc — Support Center portal use SAML client type

    • oneiot-sp-mc — Management Console portal use SAML client type

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

  4. Export IdP signing certificate to your idp.crt (see Exporting IdP Signing Certificate)

  5. Configure User Profile attributes — add domain, user-group, and portal-type at Realm Settings → User profile

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

  7. Create Protocol Mappers for each client — username, email, domain, user-group, portal-type at Clients → oneiot-sp-sc/mc → Client scopes → oneiot-sp-sc/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_IDP_ENTITY_ID

app.saml.idp.entity-id

 — (required)

Identity Provider Entity ID. Example: https://keycloak.example.com/realms/oneiot

SAML_IDP_SSO_URL

app.saml.idp.sso-url

 — (required)

IdP Single Sign-On URL. Example: https://keycloak.example.com/realms/oneiot/protocol/saml

SAML_SC_SP_ENTITY_ID

app.saml.sp.entity-id

oneiot-sp-sc

Service Provider Entity ID for the Support Center portal.

SAML_MC_SP_ENTITY_ID

app.saml.mc.sp-entity-id

oneiot-sp-mc

Service Provider Entity ID for 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).

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

# --- SC (Support Center) SP registration ---
SAML_SC_SP_ENTITY_ID=oneiot-sp-sc
SAML_SUCCESS_REDIRECT=https://<DOCKER_HOST>:8843/support-portal/login/auth-callback

# --- MC (Management Console) SP registration ---
SAML_MC_SP_ENTITY_ID=oneiot-sp-mc
SAML_MC_SUCCESS_REDIRECT=https://<DOCKER_HOST>:8843/management-portal/login/auth-callback

# --- IdP (Keycloak running in Docker) ---
# Use the Docker container name as hostname for server-to-server communication
SAML_IDP_ENTITY_ID=https://<DOCKER_HOST>:8500/realms/oneiot
SAML_IDP_SSO_URL=https://<DOCKER_HOST>:8500/realms/oneiot/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

4.5. Certificate mount

SAML certificates must be mounted into the backend container.

Place the certificate files in the ui-backend-conf/ directory on the host:

File Purpose Format Required

idp.crt

Identity Provider certificate (exported from Keycloak)

X.509 PEM

true

sp.key

Service Provider private key

PKCS#8 PEM

false

sp.crt

Service Provider certificate

X.509 PEM

false

The idp.crt file must be in X.509 PEM format:

-----BEGIN CERTIFICATE-----
MIICnTCCAYUCBgGE... (base64-encoded certificate data)
...
-----END CERTIFICATE-----
The certificate MUST include the -----BEGIN CERTIFICATE----- and -----END CERTIFICATE----- header/footer lines. See Exporting IdP Signing Certificate for how to obtain it from Keycloak.

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/.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/protocol/saml/descriptor | head -5

5.4. Troubleshooting network issues

5.4.1. "SAML signature validation failed"

Cause: The idp.crt file mounted into the backend container does not match Keycloak’s current signing key.

Solution:

  1. Re-export the IdP signing certificate from Keycloak (see Exporting IdP Signing Certificate).

  2. Replace the idp.crt file on the host in ui-backend-conf/.

  3. 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
│   ├── idp.crt                          # Exported from Keycloak after setup
│   ├── sp.key (optional)                # Generated SP private key
│   ├── sp.crt (optional)                # Generated SP certificate
│   ├── 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 realm oneiot exists

Check in Admin Console

3

SAML clients are configured

Check in Admin Console > Clients

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 certificate matches Keycloak

Compare idp.crt with Keycloak > 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).