SAML SSO — Integration Guide

Parameter Value

Application version

Spring Boot 3.5.6 / Java 25

Library

Spring Security SAML2 (Relying Party)

Standard

SAML 2.0, HTTP POST Binding

1. Overview

1.1. What is SAML SSO

SAML (Security Assertion Markup Language) is an open standard for exchanging authentication data between an Identity Provider (IdP) and a Service Provider (SP). In this project, the OneIoT application acts as the SP, while an external IdP (Keycloak, Azure AD, Okta, ADFS, etc.) performs user authentication.

1.2. How SAML SSO works in this project

The application supports two separate SAML client registrations, each with its own Identity Provider connection and signing certificate:

Portal Registration ID SP Entity ID (default) IdP certificate file

Support Center (SC)

saml-support-portal

oneiot-sp-sc

sc-idp.crt

Management Console (MC)

saml-management-portal

oneiot-sp-mc

mc-idp.crt

Each portal has its own ACS URL, Entity ID, IdP entity ID, IdP SSO URL, IdP signing certificate, and post-authentication redirect URL. The two portals may point to the same IdP or to two completely independent IdPs.

1.3. Authentication flow diagram

saml-auth-flow

2. Application configuration

2.1. Prerequisites

To enable SAML SSO:

  1. Set the app.authentication-type variable to saml:

    app:
      authentication-type: saml

    This value determines the authentication type returned by the POST /iot-webservice/iotw/Auth/type endpoint. The frontend uses it to choose between the login/password form and the SSO button.

  2. Set SAML_ENABLED=true — this activates the SamlAuthenticationSuccessHandler bean and the SAML filter chain in SecurityConfig.

2.2. Environment variables

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 for 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 for the Support Center portal. Example: https://keycloak.example.com/realms/oneiot-sc/protocol/saml

 — (not env-configurable)

app.saml.sc.idp-certificate-location

file:${app.home:./}/conf/sc-idp.crt

Path to the Support Center IdP signing certificate (X.509 PEM). Used to verify the SAMLResponse signature for the SC registration.

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 for the Management Console portal.

SAML_MC_IDP_SSO_URL

app.saml.mc.idp-sso-url

 — (required when SAML enabled)

IdP Single Sign-On URL for the Management Console portal.

 — (not env-configurable)

app.saml.mc.idp-certificate-location

file:${app.home:./}/conf/mc-idp.crt

Path to the Management Console IdP signing certificate (X.509 PEM). Used to verify the SAMLResponse signature for the MC registration.

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).

2.3. Spring Security SAML2 Relying Party configuration

Relying party registrations are built programmatically at startup by SamlRelyingPartyConfig, using values from the app.saml.* properties in application.yaml. Spring Boot’s built-in Saml2RelyingPartyAutoConfiguration is explicitly excluded, so the spring.security.saml2.relyingparty.registration YAML section is not used by this project.

The Spring Boot auto-configuration is disabled in application.yaml:

spring:
  autoconfigure:
    exclude:
      - org.springframework.boot.autoconfigure.security.saml2.Saml2RelyingPartyAutoConfiguration

The two registrations are driven by the following property block:

app:
  saml:
    redirect-url: ${SAML_REDIRECT_HOST:}
    enabled: ${SAML_ENABLED:false}
    sc-registration-id: saml-support-portal
    mc-registration-id: saml-management-portal
    sc:
      sp-entity-id: ${SAML_SC_SP_ENTITY_ID:oneiot-sp-sc}
      success-redirect-url: ${SAML_REDIRECT_HOST:}/support-portal/login/auth-callback
      idp-entity-id: ${SAML_SC_IDP_ENTITY_ID:}
      idp-sso-url: ${SAML_SC_IDP_SSO_URL:}
      idp-certificate-location: file:${app.home:./}/conf/sc-idp.crt
    mc:
      sp-entity-id: ${SAML_MC_SP_ENTITY_ID:oneiot-sp-mc}
      success-redirect-url: ${SAML_REDIRECT_HOST:}/management-portal/login/auth-callback
      idp-entity-id: ${SAML_MC_IDP_ENTITY_ID:}
      idp-sso-url: ${SAML_MC_IDP_SSO_URL:}
      idp-certificate-location: file:${app.home:./}/conf/mc-idp.crt

At startup, SamlRelyingPartyConfig.relyingPartyRegistrationRepository():

  1. Loads the SC IdP certificate from app.saml.sc.idp-certificate-location and builds the saml-support-portal registration.

  2. Loads the MC IdP certificate from app.saml.mc.idp-certificate-location and builds the saml-management-portal registration.

  3. For each registration, sets the ACS URL to ${SAML_REDIRECT_HOST}/iot-webservice/login/saml2/sso/{registrationId}, binding POST, and wantAuthnRequestsSigned=false.

  4. Registers both in an InMemoryRelyingPartyRegistrationRepository.

Key points:

  • No SP signing credentials are configured. The AuthnRequest is not signed (wantAuthnRequestsSigned=false), so there is no SP private key or SP certificate — only the two IdP verification certificates exist.

  • SC and MC are fully independent: each has its own SP entity ID, IdP entity ID, IdP SSO URL, and IdP signing certificate. They may point to the same IdP realm or to two different IdPs.

  • The ACS URL for both registrations is built from ${SAML_REDIRECT_HOST} — the externally accessible base URL of the application.

2.4. ACS URL format

${SAML_REDIRECT_HOST}/iot-webservice/login/saml2/sso/{registrationId}

Specific values:

  • SC: https://portal.example.com/iot-webservice/login/saml2/sso/saml-support-portal

  • MC: https://portal.example.com/iot-webservice/login/saml2/sso/saml-management-portal

2.5. Post-authentication redirect URLs

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 sessionHash as query parameters:

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

2.6. SP Metadata URL

The application automatically publishes SP metadata at:

/iot-webservice/saml2/metadata/{registrationId}

Examples:

  • https://portal.example.com/iot-webservice/saml2/metadata/saml-support-portal

  • https://portal.example.com/iot-webservice/saml2/metadata/saml-management-portal

This URL can be used in the IdP for automatic SP configuration.

3. Certificates

The application requires two IdP signing certificates — one per portal registration. They are loaded from the external conf/ directory under ${app.home} (typically mounted as /opt/app/conf in Docker deployments):

File Purpose Format Configuration property

sc-idp.crt

Support Center IdP signing certificate — used to verify SAMLResponse signatures for the saml-support-portal registration

X.509 PEM

app.saml.sc.idp-certificate-location

mc-idp.crt

Management Console IdP signing certificate — used to verify SAMLResponse signatures for the saml-management-portal registration

X.509 PEM

app.saml.mc.idp-certificate-location

No Service Provider key pair is configured. The application does not sign outgoing AuthnRequests (wantAuthnRequestsSigned=false in SamlRelyingPartyConfig), so there is no sp.key / sp.crt and no SP signing credential to manage.

3.1. IdP certificates (sc-idp.crt, mc-idp.crt)

Each certificate is used for:

  • Verifying the signature of the SAMLResponse received from the corresponding IdP

  • Must exactly match the signing certificate configured in that IdP

If both portals are served by the same IdP (same realm / same signing key), the same certificate content is used for both files — but the application still loads them as two separate files, one per registration.

Each file must be in X.509 PEM format with the following structure:

-----BEGIN CERTIFICATE-----
MIICnTCCAYUCBgGE... (base64-encoded certificate data)
...
-----END CERTIFICATE-----
Each certificate MUST include the -----BEGIN CERTIFICATE----- and -----END CERTIFICATE----- header/footer lines. The content between headers must be base64-encoded. If an IdP rotates its signing keys, you MUST update the corresponding sc-idp.crt or mc-idp.crt file. A certificate mismatch will cause signature verification failure and authentication denial for the affected portal. See Exporting IdP Signing Certificate for how to obtain the certificate from Keycloak.

3.2. Obtaining the IdP certificate

For Keycloak, the IdP certificate can be obtained from:

  1. Realm Settings > Keys — copy the public key of the RS256 entry with usage SIG

  2. SAML Descriptor URL: https://<KEYCLOAK_HOST>/realms/<REALM>/protocol/saml/descriptor — extract the certificate from <ds:X509Certificate>

  3. Admin Console: Realm Settings > Keys > select RSA (sig) > View Certificate

See Exporting IdP Signing Certificate for detailed step-by-step instructions. Repeat the export for each IdP/realm used by the SC and MC portals, saving the results as sc-idp.crt and mc-idp.crt respectively.

4. API Endpoints

4.1. POST /iot-webservice/iotw/Auth/saml/login-url

Returns the URL for redirecting the user to the IdP.

Authentication: not required (public endpoint).

4.1.1. Request

POST /iot-webservice/iotw/Auth/saml/login-url HTTP/1.1
Content-Type: application/json

{
  "clientType": "sc"
}
Field Type Required Description

clientType

string

Yes

Portal type. Allowed values: sc (Support Center), mc (Management Console).

4.1.2. Successful response (200 OK)

{
  "loginUrl": "https://portal.example.com/iot-webservice/saml2/authenticate/saml-support-portal"
}
Field Type Description

loginUrl

string

Full URL for user redirection. Format: ${SAML_REDIRECT_HOST}/iot-webservice/saml2/authenticate/{registrationId}

4.1.3. Error responses (400 Bad Request)

SAML disabled:

{
  "error": "SAML authentication is not enabled"
}

Invalid clientType:

{
  "error": "Invalid clientType. Must be 'sc' or 'mc'"
}

4.1.4. Registration ID resolution logic

clientType registrationId Description

sc

saml-support-portal

Registration for the Support Center portal

mc

saml-management-portal

Registration for the Management Console portal

4.1.5. curl examples

# Get login URL for SC portal
curl -X POST https://portal.example.com/iot-webservice/iotw/Auth/saml/login-url \
  -H "Content-Type: application/json" \
  -d '{"clientType": "sc"}'

# Get login URL for MC portal
curl -X POST https://portal.example.com/iot-webservice/iotw/Auth/saml/login-url \
  -H "Content-Type: application/json" \
  -d '{"clientType": "mc"}'

4.2. POST /iot-webservice/iotw/Auth/type

Auxiliary endpoint returning the current authentication type.

curl -X POST https://portal.example.com/iot-webservice/iotw/Auth/type

Response when SAML is configured:

"saml"

The frontend uses this endpoint to determine whether to show the login/password form or the SSO button.

5. Keycloak setup (test IdP)

Keycloak is used here solely as a test Identity Provider. In production, any SAML 2.0-compliant IdP may be used: Azure AD (Entra ID), Okta, ADFS, OneLogin, Shibboleth, etc.

5.1. Creating Realms

The SC and MC portals use two separate realms — one per portal. Each realm hosts a single SAML client, so the two portals have fully independent users, signing keys, and attribute mappers.

5.1.1. Support Center realm (oneiot-sc)

  1. Open the Keycloak Admin Console (https://<KEYCLOAK_HOST>/admin).

  2. In the left menu, click the current realm and select Create realm.

  3. Enter:

    • Realm name: oneiot-sc

  4. Click Create.

5.1.2. Management Console realm (oneiot-mc)

  1. From the realm selector, click Create realm again.

  2. Enter:

    • Realm name: oneiot-mc

  3. Click Create.

The realm names (oneiot-sc / oneiot-mc) are examples — use whatever naming convention fits your deployment. They only need to match the SAML_SC_IDP_ENTITY_ID / SAML_SC_IDP_SSO_URL and SAML_MC_IDP_ENTITY_ID / SAML_MC_IDP_SSO_URL values.

5.2. Creating SAML clients

Each realm contains one SAML client. The client for the Support Center lives in the oneiot-sc realm; the client for the Management Console lives in the oneiot-mc realm.

5.2.1. Support Center (SC) client — in realm oneiot-sc

  1. Switch to realm oneiot-sc using the realm selector.

  2. Navigate to Clients > Create client.

  3. Fill in:

    • Client type: SAML

    • Client ID: oneiot-sp-sc

  4. Click Next, then Save.

  5. On the Settings tab, configure:

    • Valid Redirect URIs: https://<PORTAL_HOST>.rd.friendly-tech.com/*

    • Master SAML Processing URL: https://<PORTAL_HOST>.rd.friendly-tech.com/iot-webservice/login/saml2/sso/saml-support-portal

    • Name ID Format: username

    • Force Name ID Format: ON

5.2.2. Management Console (MC) client — in realm oneiot-mc

  1. Switch to realm oneiot-mc using the realm selector.

  2. Navigate to Clients > Create client.

  3. Fill in:

    • Client type: SAML

    • Client ID: oneiot-sp-mc

  4. Click Next, then Save.

  5. On the Settings tab, configure:

    • Valid Redirect URIs: https://<PORTAL_HOST>.rd.friendly-tech.com/*

    • Master SAML Processing URL: https://<PORTAL_HOST>.rd.friendly-tech.com/iot-webservice/login/saml2/sso/saml-management-portal

    • Name ID Format: username

    • Force Name ID Format: ON

5.3. Keys settings

In each realm, for its respective client (oneiot-sp-sc in oneiot-sc, oneiot-sp-mc in oneiot-mc):

  1. Go to the client’s Keys tab.

  2. Set Client signature required = OFF.

This corresponds to wantAuthnRequestsSigned=false in SamlRelyingPartyConfig. The application does not sign the AuthnRequest, so neither IdP may require a client signature.

5.4. Exporting IdP Signing Certificate

Each realm has its own signing key, so the export must be performed once per realm: from oneiot-sc for the Support Center, and from oneiot-mc for the Management Console. The result is two certificate files (sc-idp.crt and mc-idp.crt) in the application’s conf/ directory.

Repeat the following steps for each realm:

  1. Switch to the target realm (oneiot-sc or oneiot-mc) using the Keycloak realm selector.

  2. Go to Realm Settings > Keys.

  3. Find the RS256 key with provider rsa-generated and usage SIG.

  4. Click the Certificate button in that row to view the X.509 certificate.

  5. Copy the entire certificate content displayed by Keycloak.

  6. Paste it into the corresponding file in the application conf/ directory:

    • ${app.home}/conf/sc-idp.crt — from realm oneiot-sc (Support Center)

    • ${app.home}/conf/mc-idp.crt — from realm oneiot-mc (Management Console)

The file must be in X.509 PEM format with the following structure:

-----BEGIN CERTIFICATE-----
MIICnTCCAYUCBgGE... (base64-encoded certificate data)
...
-----END CERTIFICATE-----
The certificate MUST include the -----BEGIN CERTIFICATE----- and -----END CERTIFICATE----- header/footer lines. The certificate content between headers must be base64-encoded (this is what Keycloak displays). If Keycloak only shows the raw base64 without headers, you must manually add the -----BEGIN CERTIFICATE----- and -----END CERTIFICATE----- wrapper. When Keycloak rotates its signing keys, you MUST update the affected certificate file (sc-idp.crt and/or mc-idp.crt), otherwise SAML signature validation will fail for that portal.
You can also obtain the certificate from the SAML Descriptor URL: https://<KEYCLOAK_HOST>/realms/<REALM>/protocol/saml/descriptor. The certificate is inside the <ds:X509Certificate> element — copy that value and wrap it with the PEM header/footer lines.

5.5. User Profile (attributes)

Repeat this step in both realms (oneiot-sc and oneiot-mc) so that users in either realm can carry the required custom attributes.

  1. Switch to the target realm using the Keycloak realm selector.

  2. Navigate to Realm Settings > User Profile.

  3. Create custom attributes that will be assigned to each user.

The username and email attributes already exist by default. You need to create the following additional attributes:

Attribute Name Display Name Description

domain

${domain}

User domain. The value must exactly match the domain name in the application database.

user-group

${user-group}

User group. The value must exactly match the group name in the application database.

portal-type

${portal-type}

Portal type: sc (Support Center) or mc (Management Console).

For each attribute:

  1. Click Create attribute.

  2. Fill in Attribute name and Display name and SAML name according to the table above.

  3. Optionally set Required field = ON (recommended for domain and user-group).

  4. Click Save.

5.6. Creating users

Create users in the realm that matches the portal they are supposed to log into: Support Center users go into oneiot-sc, Management Console users go into oneiot-mc. The same person needing access to both portals must exist as a user in both realms.

  1. Switch to the target realm (oneiot-sc or oneiot-mc).

  2. Navigate to Users > Add user.

  3. Fill in the required fields:

    • Username — the username

    • Email — the user email

    • First name, Last name — first and last name (optional)

  4. Fill in the custom attributes:

    • domain — domain name (e.g., SuperDomain, TenantA)

    • user-group — group name (e.g., admin, operator)

    • portal-type — sc or mc

  5. Click Create.

  6. Go to the Credentials tab and set a password.

The domain and user-group values must exactly match existing records in the application database. If a domain or group is not found in the database, the user will be created with domainId = null or userGroupId = null. The portal-type attribute must contain the value sc or mc.

5.7. Creating Mappers (Protocol Mappers)

To pass custom attributes in the SAML assertion, you need to create the same set of mappings in each realm’s client dedicated scope. Each client lives in its own realm, so the mappers must be added twice — once in oneiot-sc > oneiot-sp-sc, and once in oneiot-mc > oneiot-sp-mc.

5.7.1. Configuration for oneiot-sp-sc client (realm oneiot-sc)

  1. Switch to realm oneiot-sc.

  2. Navigate to Clients > oneiot-sp-sc > Client scopes.

  3. Click on oneiot-sp-sc-dedicated.

  4. Click Add mapper > By configuration > User Attribute.

  5. Create the following mappings:

Mapper Name User Attribute SAML Attribute Name SAML Attribute NameFormat

username

username

username

Basic

email

email

email

Basic

domain

domain

domain

Basic

user-group

user-group

user-group

Basic

portal-type

portal-type

portal-type

Basic

For each mapping:

  1. Fill in the fields according to the table.

  2. Ensure SAML Attribute NameFormat is set to Basic.

  3. Click Save.

5.7.2. Configuration for oneiot-sp-mc client (realm oneiot-mc)

Repeat the same steps in the other realm for the oneiot-sp-mc client:

  1. Switch to realm oneiot-mc.

  2. Navigate to Clients > oneiot-sp-mc > Client scopes.

  3. Click on oneiot-sp-mc-dedicated.

  4. Click Add mapper > By configuration > User Attribute.

  5. Create the same 5 mappings from the table above.

After completing all steps in both realms, the Keycloak configuration is finished.

6. SAML attribute mapping

6.1. General principle

After successful authentication, the IdP returns a SAMLResponse containing a SAML assertion with user attributes. The SamlAuthService.authenticateFromSamlPrincipal() method extracts these attributes and maps them to internal application fields.

6.2. Attribute mapping table

SAML attribute (default) Environment variable YAML path Used for Required

username

SAML_ATTR_USERNAME

app.saml.mapping.username-attribute

Username in the system. If the attribute is missing, principal.getName() (NameID from assertion) is used.

Recommended

email

SAML_ATTR_EMAIL

app.saml.mapping.email-attribute

User email. If missing — empty string.

Optional

portal-type

SAML_ATTR_CLIENT_TYPE

app.saml.mapping.client-type-attribute

Determines the portal type (SC or MC).

Optional

domain

SAML_ATTR_DOMAIN

app.saml.mapping.domain-attribute

Domain name for user binding. Looked up in the database by name.

Recommended

user-group

SAML_ATTR_USER_GROUP

app.saml.mapping.user-group-attribute

User group name. Looked up in the database by name and client type.

Recommended

expire-date

SAML_ATTR_EXPIRE_DATE

app.saml.mapping.expire-date-attribute

Account expiration date. Formats: 2027-01-01T00:00:00Z or 2027-01-01.

Optional

6.3. ClientType resolution (3-level strategy)

SamlAuthService.extractClientType() determines the portal type using a three-level priority strategy:

clienttype-resolution
Level Source Example Result

1 (highest)

SAML attribute portal-type

portal-type=mc

ClientType.mc

2

Registration ID

saml-management-portal

ClientType.mc

2

Registration ID

saml-support-portal

ClientType.sc

3 (lowest)

Default value

 — 

ClientType.sc

6.4. Domain resolution

  1. The SAML attribute specified in app.saml.mapping.domain-attribute (default: domain) is read.

  2. If the attribute is present, the domain is looked up by name in the database via DomainService.getDomainIdByName().

  3. Special case: if the domain name matches SuperDomain (the SUPER_DOMAIN_NAME constant), domainId = 0 is returned.

  4. If the domain is not found in the database, domainId is set to null.

6.5. User group resolution

  1. The SAML attribute user-group is read.

  2. If the attribute is present, the group is looked up by name and client type via UserGroupService.findGroupIdForSaml(groupName, clientType).

  3. If the group is not found in the database, userGroupId is set to null.

The group is not created automatically. It must exist in the database before the first SAML authentication of the user.

6.6. User creation/update

After all attributes are extracted, UserService.createOrGetExternalUser(userRequest, clientType, zoneId) is called:

  • If a user with the given username already exists — the existing user is returned.

  • If the user does not exist — a new user is created with the extracted attributes.

7. Architecture

7.1. Component architecture diagram

saml-architecture

7.2. SecurityConfig — SAML Filter Chain

The SecurityConfig class defines two SecurityFilterChain beans:

Order Bean Purpose Condition

@Order(1)

samlFilterChain

SAML SSO processing

@ConditionalOnProperty(name = "app.saml.enabled", havingValue = "true")

@Order(2)

filterChain

JWT authentication for REST API

Always active

The SAML filter chain intercepts requests matching:

  • /iot-webservice/saml2/** — sending AuthnRequest to the IdP

  • /iot-webservice/login/saml2/** — receiving SAMLResponse from the IdP

7.2.1. saml2Login configuration

.saml2Login(saml2 -> saml2
    .successHandler(samlSuccessHandler)
    .loginProcessingUrl("/iot-webservice/login/saml2/sso/{registrationId}")
    .authenticationRequestUri("/iot-webservice/saml2/authenticate/{registrationId}")
    .failureHandler(...)
)
Parameter URL Description

loginProcessingUrl

/iot-webservice/login/saml2/sso/{registrationId}

ACS URL where the IdP sends the SAMLResponse (HTTP POST).

authenticationRequestUri

/iot-webservice/saml2/authenticate/{registrationId}

URL that initiates the AuthnRequest to the IdP.

metadataUrl

/iot-webservice/saml2/metadata/{registrationId}

URL for publishing SP metadata (XML).

7.2.2. Public SAML endpoints

All SAML endpoints are registered as public in SecurityConstants.SAML_PUBLIC_ENDPOINTS:

public static final String[] SAML_PUBLIC_ENDPOINTS = {
    "/saml2/authenticate/**",
    "/login/saml2/sso/**",
    "/saml2/metadata/**",
    "/saml2/service-provider-metadata/**"
};

7.3. SamlAuthenticationSuccessHandler

Activated via @ConditionalOnProperty(name = "app.saml.enabled", havingValue = "true").

Sequence of actions on successful SAML authentication:

  1. Extracts registrationId from DefaultSaml2AuthenticatedPrincipal.getRelyingPartyRegistrationId().

  2. Extracts timezone from the HTTP session (saml_timezone attribute).

  3. Calls SamlAuthService.authenticateFromSamlPrincipal() to create the JWT session.

  4. Determines the redirect URL based on registrationId:

    • saml-management-portal — uses app.saml.mc.success-redirect-url

    • All others — uses app.saml.success-redirect-url

  5. Redirects the browser to a URL of the form:

    {redirectBaseUrl}?token={JWT}&sessionHash={BASE64_HASH}

7.4. SamlAuthService

The main SAML authentication service. The authenticateFromSamlPrincipal() method performs:

  1. Attribute extraction from the SAML assertion (username, email, groups, domain, user-group, expire-date).

  2. ClientType resolution using the 3-level strategy.

  3. Domain resolution via DomainService.getDomainIdByName().

  4. Group resolution via UserGroupService.findGroupIdForSaml().

  5. User creation/retrieval via UserService.createOrGetExternalUser().

  6. Session creation via SessionService.createOrUpdateSession().

  7. JWT generation using the HS256 algorithm, with token lifetime determined by jwt.token.expired (default 36,000,000 ms = 10 hours).

7.5. AuthenticationManager

During SAML authentication, SecurityConfig returns a no-op AuthenticationManager that throws ProviderNotFoundException. This is necessary to avoid StackOverflowError when AOP-proxying an empty ProviderManager.

8. Appendix: Full configuration example

# === SAML SSO ===
export SAML_ENABLED=true
export SAML_REDIRECT_HOST=https://portal.example.com

# SP + IdP (Support Center) -- Keycloak realm `oneiot-sc`
export SAML_SC_SP_ENTITY_ID=oneiot-sp-sc
export SAML_SC_IDP_ENTITY_ID=https://keycloak.example.com/realms/oneiot-sc
export SAML_SC_IDP_SSO_URL=https://keycloak.example.com/realms/oneiot-sc/protocol/saml
# SC IdP signing certificate file path is fixed at ${app.home}/conf/sc-idp.crt

# SP + IdP (Management Console) -- Keycloak realm `oneiot-mc`
export SAML_MC_SP_ENTITY_ID=oneiot-sp-mc
export SAML_MC_IDP_ENTITY_ID=https://keycloak.example.com/realms/oneiot-mc
export SAML_MC_IDP_SSO_URL=https://keycloak.example.com/realms/oneiot-mc/protocol/saml
# MC IdP signing certificate file path is fixed at ${app.home}/conf/mc-idp.crt

# Attribute Mapping (default values, shown for reference)
export SAML_ATTR_USERNAME=username
export SAML_ATTR_EMAIL=email
export SAML_ATTR_CLIENT_TYPE=portal-type
export SAML_ATTR_DOMAIN=domain
export SAML_ATTR_USER_GROUP=user-group
export SAML_ATTR_EXPIRE_DATE=expire-date

# Authentication type
# In application.yaml: app.authentication-type: saml
The example above uses two separate Keycloak realms (oneiot-sc and oneiot-mc), each containing a single SAML client. Each realm has its own signing key, which must be exported to sc-idp.crt and mc-idp.crt respectively. If you prefer a single shared realm, point both SAML_SC_IDP_* and SAML_MC_IDP_* to that realm and write the same certificate content to both files.