Chuyển đến nội dung chính

Lesson 7: SAML Clients and Protocol Mappers

Create and configure SAML 2.0 clients, SAML bindings (POST, Redirect, Artifact), assertions configuration, XML signature and encryption, Entity Descriptor import, IDP Initiated Login. Protocol Mappers for OIDC and SAML, Lightweight Access Tokens, Pairwise Subject Identifier.

🔒 DevSecOps — Lesson 7 Lesson 7: SAML Clients and Protocol Mappers

Keycloak from Basic to Advanced

Part 2: SSO Protocols - OpenID Connect and SAML

xdev.asia

1. Overview of SAML 2.0 in Keycloak

SAML 2.0 (Security Assertion Markup Language) is an XML-based authentication protocol widely used in the enterprise — especially when integrating with legacy systems, SaaS applications (Salesforce, ServiceNow, AWS), or government organizations.

SAML 2.0 vs OpenID Connect

FeaturesSAML 2.0OpenID Connect
FormatXMLJSON (JWT)
TransportHTTP Redirect, POST, ArtifactHTTP REST
TokenSAML Assertion (XML)JWT
SizeLarger (XML verbose)Compact (JSON)
Mobile supportPoor (heavy XML parsing)Good (JSON native)
Use case mainEnterprise SSO, legacy systemsModern web/mobile apps
ComplexityHighLower
LogoutSLO (Single Logout)RP-Initiated, Backchannel, Front-channel

When to use SAML?

  • Integrate with SaaS applications that require SAML (Salesforce, Google Workspace, AWS)

  • Associate with an IdP or SP that only supports SAML

  • Requires compliance with government organization standards

  • Migration from ADFS system, Shibboleth

SAML Terms

TermDescriptionOIDC equivalent
Identity Provider (IdP)User authentication party (Keycloak)OpenID Provider (OP)
Service Provider (SP)Authentication requester (application)Relying Party (RP)
AssertionXML document containing authentication informationID Token
AuthnRequestAuthentication request from SP → IdPAuthorization Request
ACS URLAssertion Consumer Service URLRedirect URI
Entity IDUnique identifier cho SP/IdPClient ID / Issuer
MetadataXML describing endpoints, certificatesWell-Known Configuration
NameIDUser identifier trong assertionsub claim
Attribute StatementUser attributes trong assertionClaims trong JWT

2. Create SAML 2.0 Client

2.1 Create via Admin Console

  1. Access Admin Console → select realm → Clients → Create client

  2. General Settings:

    • Client type: SAML
    • Client ID: URL-based Entity ID, for example https://myapp.example.com/saml/metadata
    • Name: My SAML Application
  3. Click Next and Save

2.2 Import from Entity Descriptor (Metadata)

Fastest way to create SAML client — import XML metadata from Service Provider:

  1. Access Clients → Import client

  2. Upload metadata XML file or paste metadata URL

  3. Auto-populate Keycloak: Entity ID, ACS URL, SLO URL, certificates, bindings

Service Provider XML metadata example:

<?xml version="1.0" encoding="UTF-8"?>
<md:EntityDescriptor xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata"
    entityID="https://myapp.example.com/saml/metadata">
  <md:SPSSODescriptor
      AuthnRequestsSigned="true"
      WantAssertionsSigned="true"
      protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
&lt;md:KeyDescriptor use="signing"&gt;
  &lt;ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#"&gt;
    &lt;ds:X509Data&gt;
      &lt;ds:X509Certificate&gt;MIICzDCCAbSg...&lt;/ds:X509Certificate&gt;
    &lt;/ds:X509Data&gt;
  &lt;/ds:KeyInfo&gt;
&lt;/md:KeyDescriptor&gt;

&lt;md:KeyDescriptor use="encryption"&gt;
  &lt;ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#"&gt;
    &lt;ds:X509Data&gt;
      &lt;ds:X509Certificate&gt;MIICzDCCAbSg...&lt;/ds:X509Certificate&gt;
    &lt;/ds:X509Data&gt;
  &lt;/ds:KeyInfo&gt;
&lt;/md:KeyDescriptor&gt;

&lt;md:SingleLogoutService
    Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
    Location="https://myapp.example.com/saml/slo"/&gt;

&lt;md:NameIDFormat&gt;
  urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress
&lt;/md:NameIDFormat&gt;

&lt;md:AssertionConsumerService
    Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
    Location="https://myapp.example.com/saml/acs"
    index="0"
    isDefault="true"/&gt;

</md:SPSSODescriptor> </md:EntityDescriptor>

2.3 Get Keycloak IdP Metadata

Service Provider needs Keycloak metadata (IdP) for configuration. Metadata URL:

GET https://<keycloak-host>/realms/<realm-name>/protocol/saml/descriptor

Metadata bao gồm: Entity ID, SSO endpoints, SLO endpoints, signing/encryption certificates.

3. SAML Client Settings chi tiết

3.1 Tab Settings

SettingMô tảGiá trị khuyến nghị
Client ID (Entity ID)SAML Entity ID — unique identifier cho SPURL format: https://app.example.com/saml
NameTên hiển thịTên ứng dụng
Client Signature RequiredSP phải ký AuthnRequestON (production)
Force POST BindingBắt buộc dùng POST binding cho responsesON
Front Channel LogoutLogout qua browser redirectON
Force Name ID FormatBắt buộc Name ID format cụ thểTùy yêu cầu
Name ID FormatFormat of NameIDemail hoặc persistent
Include AuthnStatementBao gồm AuthnStatement trong assertionON
Sign DocumentsKý toàn bộ SAML responseON
Sign AssertionsKý assertion bên trong responseON (khuyến nghị)

3.2 SAML Bindings

SAML hỗ trợ nhiều binding — cách thức truyền tải SAML messages giữa SP và IdP:

BindingMô tảUse case
HTTP-POSTMessage gửi qua HTML form auto-submitDefault cho assertions (lớn)
HTTP-RedirectMessage gửi qua URL query parameterAuthnRequest (nhỏ)
ArtifactChỉ gửi artifact reference, SP lấy assertion qua backchannelHigh-security, large assertions

Cấu hình Bindings trong client settings:

SettingMô tả
Master SAML Processing URLURL chung cho tất cả SAML bindings
Assertion Consumer Service POST Binding URLACS URL cho POST binding
Assertion Consumer Service Redirect Binding URLACS URL cho Redirect binding
Assertion Consumer Service Artifact Binding URLACS URL cho Artifact binding
Logout Service POST Binding URLSLO URL cho POST binding
Logout Service Redirect Binding URLSLO URL cho Redirect binding
Logout Service Artifact Binding URLSLO URL cho Artifact binding
# Ví dụ cấu hình bindings
Master SAML Processing URL: https://myapp.example.com/saml
Assertion Consumer Service POST Binding URL: https://myapp.example.com/saml/acs
Logout Service POST Binding URL: https://myapp.example.com/saml/slo

Artifact Binding details:

Artifact binding is different from POST/Redirect — instead of sending the entire assertion through the browser, Keycloak only sends a artifact (reference ID). The SP then calls directly (backchannel) to Keycloak to get the actual assertion.

┌──────────┐     ┌──────────┐     ┌──────────┐
│  Browser │     │    SP    │     │ Keycloak │
└────┬─────┘     └────┬─────┘     └────┬─────┘
     │                │                │
     │  1. Login      │                │
     │───────────────>│                │
     │  2. AuthnRequest               │
     │<──────────────────────────────>│
     │  3. Authentication              │
     │<──────────────────────────────>│
     │  4. Artifact (POST/Redirect)   │
     │<──────────────────────────────│
     │───────────────>│                │
     │                │ 5. ArtifactResolve (backchannel SOAP)
     │                │───────────────>│
     │                │ 6. ArtifactResponse (assertion)
     │                │<──────────────│
     │  7. Authenticated               │
     │<───────────────│                │

Artifact binding is safer because the assertion does not pass through the browser — useful when the assertion contains sensitive data.

3.3 XML Signature and Encryption

Signing Configuration:

SettingDescription
Signature AlgorithmXML signing algorithm: RSA_SHA256 (recommended), RSA_SHA512, DSA_SHA1
SAML Signature Key NameKey name trong signature: KEY_ID, CERT_SUBJECT, NONE
Canonicalization MethodXML canonicalization: EXCLUSIVE (recommended)

Encryption Configuration:

Enable Encrypt Assertions to encrypt assertions — only the SP with the corresponding private key can decrypt:

  • Upload SP's encryption certificate trong tab Keys

  • Encryption Algorithm: AES128, AES256 (recommended)

# Keycloak sẽ mã hóa assertion bằng SP's public key
# Flow: Sign assertion → Encrypt signed assertion → Send to SP
# SP: Decrypt assertion → Verify signature → Extract user info

3.4 Tab Keys

Manage certificates for SAML clients:

  • Signing Key: Certificate that SP uses to sign AuthnRequest — Keycloak used to verify

  • Encryption Key: Certificate that Keycloak uses to encrypt assertions — SP uses private key to decrypt

Import certificate from PEM, JKS, or PKCS12 file:

# Generate self-signed certificate cho SP
openssl req -x509 -newkey rsa:2048 \
  -keyout sp-private.pem -out sp-certificate.pem \
  -days 365 -nodes \
  -subj "/CN=myapp.example.com"

Import sp-certificate.pem vào Keycloak client Keys tab

4. SAML Assertions Configuration

4.1 Name ID Format

Name ID determines how Keycloak sends the user identifier in assertion:

FormatDescriptionUse case
urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddressEmail addressMost popular
urn:oasis:names:tc:SAML:2.0:nameid-format:persistentUnique persistent ID for each SPDon't want to reveal email
urn:oasis:names:tc:SAML:2.0:nameid-format:transientTemporary ID, changes each sessionPrivacy-sensitive
urn:oasis:names:tc:SAML:1.1:nameid-format:unspecifiedUsername or Keycloak user IDFlexible

4.2 Assertion Lifespan

Configuration in Realm Settings → Tokens tab:

  • Assertion Lifespan: Valid assertion time (default 5 minutes, recommended to keep short)

  • Not Before: Assertion not valid before this time (clock skew tolerance)

4.3 Example SAML Assertion

<saml:Assertion xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
    ID="_abc123" IssueInstant="2026-03-30T10:00:00Z" Version="2.0">
  <saml:Issuer>http://localhost:8080/realms/my-company</saml:Issuer>

<!-- Subject — user identity --> <saml:Subject> <saml:NameID Format="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"> [email protected] </saml:NameID> <saml:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer"> <saml:SubjectConfirmationData NotOnOrAfter="2026-03-30T10:05:00Z" Recipient="https://myapp.example.com/saml/acs"/> </saml:SubjectConfirmation> </saml:Subject>

<!-- Conditions — khi nào assertion hợp lệ --> <saml:Conditions NotBefore="2026-03-30T10:00:00Z" NotOnOrAfter="2026-03-30T10:05:00Z"> <saml:AudienceRestriction> <saml:Audience>https://myapp.example.com/saml/metadata&lt;/saml:Audience> </saml:AudienceRestriction> </saml:Conditions>

<!-- AuthnStatement — thông tin xác thực --> <saml:AuthnStatement AuthnInstant="2026-03-30T10:00:00Z" SessionIndex="session_abc123"> <saml:AuthnContext> <saml:AuthnContextClassRef> urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport </saml:AuthnContextClassRef> </saml:AuthnContext> </saml:AuthnStatement>

<!-- AttributeStatement — user attributes --> <saml:AttributeStatement> <saml:Attribute Name="email"> <saml:AttributeValue>[email protected]</saml:AttributeValue> </saml:Attribute> <saml:Attribute Name="firstName"> <saml:AttributeValue>John</saml:AttributeValue> </saml:Attribute> <saml:Attribute Name="Role"> <saml:AttributeValue>admin</saml:AttributeValue> </saml:Attribute> </saml:AttributeStatement> </saml:Assertion>

5. IDP-Initiated Login (Unsolicited Response)

In normal flow (SP-Initiated), user accesses SP → SP redirects to IdP → IdP authenticates → redirects to SP. With IDP-Initiated Login, the user starts from the IdP (Keycloak) without going through the SP first.

IDP-Initiated Login Configuration

  1. Open SAML Client → tab Advanced

  2. Find IDP-Initiated SSO URL name: enter the URL name, for example my-app

  3. The URL to IDP-Initiated Login will be:

https://<keycloak-host>/realms/<realm>/protocol/saml/clients/my-app

Lưu ý bảo mật: IDP-Initiated Login tiềm ẩn rủi ro CSRF — assertion không có InResponseTo attribute. Chỉ sử dụng khi SP yêu cầu (một số SaaS apps chỉ hỗ trợ IDP-Initiated).

Các settings cho IDP-Initiated

SettingMô tả
IDP-Initiated SSO URL namePhần cuối URL cho IDP-Initiated Login
IDP-Initiated SSO Relay StateDefault RelayState gửi đến SP
Assertion Consumer Service POST Binding URLURL SP nhận assertion

6. Protocol Mappers

Protocol Mappers quyết định thông tin nào được đưa vào tokens/assertions. Chúng biến đổi user attributes, roles, và metadata thành claims (OIDC) hoặc attributes (SAML).

6.1 Khái niệm cơ bản

Protocol Mappers có thể được thêm ở hai cấp:

  • Client level: Mappers áp dụng riêng cho client đó (Client → Client scopes → Dedicated scope)

  • Client Scope level: Mappers áp dụng cho tất cả clients sử dụng scope đó

Mỗi mapper có các cấu hình chung:

SettingMô tả
NameTên mapper (dùng để quản lý)
Mapper TypeLoại mapper (User Attribute, Hardcoded Claim,...)
Add to ID tokenThêm vào ID Token (OIDC)
Add to access tokenThêm vào Access Token (OIDC)
Add to userinfoThêm vào UserInfo response (OIDC)
Add to token introspectionThêm vào Token Introspection response
Add to lightweight access tokenThêm vào Lightweight Access Token

6.2 OIDC Protocol Mappers

User Attribute Mapper — Map user attribute sang token claim:

Mapper Type: User Attribute
Name: department-mapper
User Attribute: department         # attribute name trong User Profile
Token Claim Name: department       # claim name trong JWT
Claim JSON Type: String            # String, long, int, boolean, JSON
Add to ID token: ON
Add to access token: ON
Add to userinfo: ON
Multivalued: OFF

Result in JWT:

{
  "sub": "user-id",
  "email": "[email protected]",
  "department": "Engineering",
  ...
}

User Property Mapper — Map built-in user property (username, email, firstName, lastName):

Mapper Type: User Property
Name: full-name-mapper
Property: firstName
Token Claim Name: given_name
Claim JSON Type: String

User Session Note Mapper — Map session data to token:

Mapper Type: User Session Note
Name: client-ip-mapper
User Session Note: clientAddress    # hoặc clientHost, identity_provider, etc.
Token Claim Name: client_ip
Claim JSON Type: String
Add to access token: ON

Session notes available: clientAddress, clientHost, identity_provider, identity_provider_identity.

Hardcoded Claim Mapper — Add claim with fixed value:

Mapper Type: Hardcoded claim
Name: environment-mapper
Token Claim Name: env
Claim value: production
Claim JSON Type: String
Add to access token: ON

Group Membership Mapper — Add a list of user groups to token:

Mapper Type: Group Membership
Name: groups-mapper
Token Claim Name: groups
Full group path: ON                 # /parent/child hoặc chỉ child
Add to ID token: ON
Add to access token: ON

Result:

{
  "groups": ["/Engineering", "/Engineering/Backend"]
}

Audience Mapper — Add audience to access token:

Mapper Type: Audience
Name: api-audience
Included Client Audience: my-api-service   # Client ID của resource server
Add to access token: ON

Result:

{
  "aud": ["my-api-service", "account"]
}

Script Mapper — Custom logic using JavaScript:

Mapper Type: Script Mapper
Name: custom-role-mapper
Script:
  // Combine realm roles và client roles thành flat list
  var roles = [];

// Realm roles var realmRoles = user.getRealmRoleMappingsStream(); realmRoles.forEach(function(role) { roles.push(role.getName()); });

// Client roles cho client cụ thể var client = keycloakSession.clients() .getClientByClientId(realm, 'my-app'); if (client) { var clientRoles = user.getClientRoleMappingsStream(client); clientRoles.forEach(function(role) { roles.push('client:' + role.getName()); }); }

exports = Java.to(roles, "java.lang.String[]");

Token Claim Name: all_roles Claim JSON Type: JSON Multivalued: ON

Note: Script Mapper uses the Nashorn JavaScript engine. In Keycloak 24+, script mappers need to be deployed as a custom JAR provider instead of an inline script. See Deploy Scripts in the Keycloak documentation.

6.3 SAML Protocol Mappers

SAML mappers are similar to OIDC but the output is SAML Attribute instead of JWT claim:

User Attribute Mapper (SAML):

Mapper Type: User Attribute
Name: department-saml
User Attribute: department
Friendly Name: Department
SAML Attribute Name: urn:oid:2.16.840.1.113730.3.1.241  # hoặc friendly name
SAML Attribute NameFormat: URI Reference                  # URI, Basic, Unspecified

Role List Mapper (SAML):

Mapper Type: Role list
Name: role-list
Role attribute name: Role
Friendly Name: Roles
SAML Attribute NameFormat: Basic
Single Role Attribute: ON    # Tất cả roles trong 1 attribute (khuyến nghị)
                              # OFF = mỗi role 1 attribute riêng

Hardcoded Attribute Mapper (SAML):

Mapper Type: Hardcoded attribute
Name: tenant-id
SAML Attribute Name: tenant_id
SAML Attribute Value: my-company
Friendly Name: Tenant ID
SAML Attribute NameFormat: Basic

The SAML Attribute NameFormat:

FormatDescriptionExample
BasicBasicNameemail, firstName
URI ReferenceOID format, standardurn:oid:0.9.2342.19200300.100.1.3
UnspecifiedUnspecified formatOptional

7. Lightweight Access Tokens

By default, Keycloak access tokens contain many claims (realm_access, resource_access, email, name, preferred_username,...). Lightweight Access Tokens reduce token size by retaining only essential claims.

7.1 Why do we need Lightweight Access Tokens?

  • Reduce bandwidth: Smaller token = faster sending via HTTP header

  • Reduce sensitive information: Access tokens are often sent to many services, should not contain too much PII

  • Improved security: Resource server uses Token Introspection to retrieve full claims when needed

7.2 Configure Lightweight Access Tokens

By default, Protocol Mappers has the option Add to lightweight access token. To use:

  1. For each mapper, turn off Add to access token on unnecessary claims in lightweight token

  2. Use Client Policy (see next article) to enforce lightweight tokens for specific clients

  3. Resource server calls Token Introspection endpoint to get full claims:

# Token Introspection — lấy full claims
POST /realms/my-company/protocol/openid-connect/token/introspect
Content-Type: application/x-www-form-urlencoded

token=ACCESS_TOKEN&
client_id=my-resource-server&
client_secret=CLIENT_SECRET

# Response chứa full claims
{
  "active": true,
  "sub": "user-id",
  "email": "[email protected]",
  "realm_access": { "roles": ["admin", "user"] },
  "resource_access": { ... },
  ...
}

8. Pairwise Subject Identifier

By default, Keycloak uses public subject identifier — the sub claim value is the same for all clients. This allows clients to correlate users across services.

Pairwise subject identifier creates a different sub for each client — prevents cross-service user tracking.

8.1 Pairwise Identifier Configuration

  1. Add Protocol Mapper type Pairwise subject identifier to client or client scope

  2. Configuration:

Mapper Type: Pairwise subject identifier
Name: pairwise-sub
Salt: random-salt-value-keep-secret   # Salt dùng để hash, PHẢI giữ bí mật
Pairwise Subject Identifier Algorithm: SHA-256
Sector Identifier URI: (tùy chọn)     # Nhóm clients share cùng sub

Result:

# Client A nhận sub:
{ "sub": "hashed-value-for-client-a" }

Client B nhận sub khác:

{ "sub": "hashed-value-for-client-b" }

Cùng 1 user nhưng sub khác nhau → không thể correlate

Sector Identifier URI:

If you want a group of clients to share the same sub (for example, web app and mobile app of the same service), use Sector Identifier URI. This URI points to a JSON array containing redirect URIs of clients in the same sector:

# https://myservice.example.com/sector-identifier.json
["https://webapp.example.com/callback", "myapp://callback"]

9. Integrating SAML with Spring Boot

Use spring-security-saml2-service-provider to integrate SAML SP:

<!-- pom.xml -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-saml2-service-provider</artifactId>
</dependency>
# application.yml
spring:
  security:
    saml2:
      relyingparty:
        registration:
          keycloak:
            entity-id: https://myapp.example.com/saml/metadata
            signing:
              credentials:
                - private-key-location: classpath:credentials/sp-private.pem
                  certificate-location: classpath:credentials/sp-certificate.pem
            assertingparty:
              metadata-uri: http://localhost:8080/realms/my-company/protocol/saml/descriptor
// SecurityConfig.java
@Configuration
@EnableWebSecurity
public class SamlSecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/public/**").permitAll()
                .anyRequest().authenticated()
            )
            .saml2Login(saml2 -> saml2
                .loginPage("/saml2/authenticate/keycloak")
            )
            .saml2Logout(Customizer.withDefaults());
        return http.build();
    }
}

10. Practice exercises

Lab 1: Create SAML Client and test Assertion

  1. Create SAML client with Entity ID https://localhost:8443/saml

  2. Configure ACS URL, Sign Documents, Sign Assertions = ON

  3. Use samltool.com or SAML-tracer browser extension to capture SAML Response

  4. Analyze SAML Assertion: NameID, AttributeStatement, Conditions, Signature

Lab 2: Protocol Mappers cho OIDC

  1. Create user attribute employee_id in User Profile

  2. Create User Attribute Mapper: employee_id → token claim emp_id

  3. Create Group Membership Mapper: groups → token claim groups

  4. Create Hardcoded Claim: env = staging

  5. Test: Get token and verify claims in jwt.io

Lab 3: Protocol Mappers cho SAML

  1. Create SAML User Attribute Mapper for department

  2. Create Role List Mapper with Single Role Attribute = ON

  3. Configuration Name ID Format = emailAddress

  4. Capture SAML Response and verify AttributeStatement

Lab 4: Pairwise Subject Identifier

  1. Create 2 OIDC clients: app-a and app-b

  2. Add Pairwise Subject Identifier mapper to both clients with the same salt

  3. Sign in with the same user on both clients

  4. Compare the value sub in access tokens — must be different

  5. Configure Sector Identifier URI for 2 clients to share sub