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
| Features | SAML 2.0 | OpenID Connect |
|---|---|---|
| Format | XML | JSON (JWT) |
| Transport | HTTP Redirect, POST, Artifact | HTTP REST |
| Token | SAML Assertion (XML) | JWT |
| Size | Larger (XML verbose) | Compact (JSON) |
| Mobile support | Poor (heavy XML parsing) | Good (JSON native) |
| Use case main | Enterprise SSO, legacy systems | Modern web/mobile apps |
| Complexity | High | Lower |
| Logout | SLO (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
| Term | Description | OIDC equivalent |
|---|---|---|
| Identity Provider (IdP) | User authentication party (Keycloak) | OpenID Provider (OP) |
| Service Provider (SP) | Authentication requester (application) | Relying Party (RP) |
| Assertion | XML document containing authentication information | ID Token |
| AuthnRequest | Authentication request from SP → IdP | Authorization Request |
| ACS URL | Assertion Consumer Service URL | Redirect URI |
| Entity ID | Unique identifier cho SP/IdP | Client ID / Issuer |
| Metadata | XML describing endpoints, certificates | Well-Known Configuration |
| NameID | User identifier trong assertion | sub claim |
| Attribute Statement | User attributes trong assertion | Claims trong JWT |
2. Create SAML 2.0 Client
2.1 Create via Admin Console
Access Admin Console → select realm → Clients → Create client
General Settings:
- Client type: SAML
- Client ID: URL-based Entity ID, for example
https://myapp.example.com/saml/metadata - Name: My SAML Application
Click Next and Save
2.2 Import from Entity Descriptor (Metadata)
Fastest way to create SAML client — import XML metadata from Service Provider:
Access Clients → Import client
Upload metadata XML file or paste metadata URL
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"><md:KeyDescriptor use="signing"> <ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#"> <ds:X509Data> <ds:X509Certificate>MIICzDCCAbSg...</ds:X509Certificate> </ds:X509Data> </ds:KeyInfo> </md:KeyDescriptor> <md:KeyDescriptor use="encryption"> <ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#"> <ds:X509Data> <ds:X509Certificate>MIICzDCCAbSg...</ds:X509Certificate> </ds:X509Data> </ds:KeyInfo> </md:KeyDescriptor> <md:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://myapp.example.com/saml/slo"/> <md:NameIDFormat> urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress </md:NameIDFormat> <md:AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://myapp.example.com/saml/acs" index="0" isDefault="true"/>
</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
| Setting | Mô tả | Giá trị khuyến nghị |
|---|---|---|
| Client ID (Entity ID) | SAML Entity ID — unique identifier cho SP | URL format: https://app.example.com/saml |
| Name | Tên hiển thị | Tên ứng dụng |
| Client Signature Required | SP phải ký AuthnRequest | ON (production) |
| Force POST Binding | Bắt buộc dùng POST binding cho responses | ON |
| Front Channel Logout | Logout qua browser redirect | ON |
| Force Name ID Format | Bắt buộc Name ID format cụ thể | Tùy yêu cầu |
| Name ID Format | Format of NameID | email hoặc persistent |
| Include AuthnStatement | Bao gồm AuthnStatement trong assertion | ON |
| Sign Documents | Ký toàn bộ SAML response | ON |
| Sign Assertions | Ký assertion bên trong response | ON (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:
| Binding | Mô tả | Use case |
|---|---|---|
| HTTP-POST | Message gửi qua HTML form auto-submit | Default cho assertions (lớn) |
| HTTP-Redirect | Message gửi qua URL query parameter | AuthnRequest (nhỏ) |
| Artifact | Chỉ gửi artifact reference, SP lấy assertion qua backchannel | High-security, large assertions |
Cấu hình Bindings trong client settings:
| Setting | Mô tả |
|---|---|
| Master SAML Processing URL | URL chung cho tất cả SAML bindings |
| Assertion Consumer Service POST Binding URL | ACS URL cho POST binding |
| Assertion Consumer Service Redirect Binding URL | ACS URL cho Redirect binding |
| Assertion Consumer Service Artifact Binding URL | ACS URL cho Artifact binding |
| Logout Service POST Binding URL | SLO URL cho POST binding |
| Logout Service Redirect Binding URL | SLO URL cho Redirect binding |
| Logout Service Artifact Binding URL | SLO 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:
| Setting | Description |
|---|---|
| Signature Algorithm | XML signing algorithm: RSA_SHA256 (recommended), RSA_SHA512, DSA_SHA1 |
| SAML Signature Key Name | Key name trong signature: KEY_ID, CERT_SUBJECT, NONE |
| Canonicalization Method | XML 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:
| Format | Description | Use case |
|---|---|---|
urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress | Email address | Most popular |
urn:oasis:names:tc:SAML:2.0:nameid-format:persistent | Unique persistent ID for each SP | Don't want to reveal email |
urn:oasis:names:tc:SAML:2.0:nameid-format:transient | Temporary ID, changes each session | Privacy-sensitive |
urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified | Username or Keycloak user ID | Flexible |
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</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
Open SAML Client → tab Advanced
Find IDP-Initiated SSO URL name: enter the URL name, for example
my-appThe 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
| Setting | Mô tả |
|---|---|
| IDP-Initiated SSO URL name | Phần cuối URL cho IDP-Initiated Login |
| IDP-Initiated SSO Relay State | Default RelayState gửi đến SP |
| Assertion Consumer Service POST Binding URL | URL 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:
| Setting | Mô tả |
|---|---|
| Name | Tên mapper (dùng để quản lý) |
| Mapper Type | Loại mapper (User Attribute, Hardcoded Claim,...) |
| Add to ID token | Thêm vào ID Token (OIDC) |
| Add to access token | Thêm vào Access Token (OIDC) |
| Add to userinfo | Thêm vào UserInfo response (OIDC) |
| Add to token introspection | Thêm vào Token Introspection response |
| Add to lightweight access token | Thê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:
| Format | Description | Example |
|---|---|---|
| Basic | BasicName | email, firstName |
| URI Reference | OID format, standard | urn:oid:0.9.2342.19200300.100.1.3 |
| Unspecified | Unspecified format | Optional |
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:
For each mapper, turn off Add to access token on unnecessary claims in lightweight token
Use Client Policy (see next article) to enforce lightweight tokens for specific clients
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
Add Protocol Mapper type Pairwise subject identifier to client or client scope
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
Create SAML client with Entity ID
https://localhost:8443/samlConfigure ACS URL, Sign Documents, Sign Assertions = ON
Use samltool.com or SAML-tracer browser extension to capture SAML Response
Analyze SAML Assertion: NameID, AttributeStatement, Conditions, Signature
Lab 2: Protocol Mappers cho OIDC
Create user attribute
employee_idin User ProfileCreate User Attribute Mapper:
employee_id→ token claimemp_idCreate Group Membership Mapper: groups → token claim
groupsCreate Hardcoded Claim:
env=stagingTest: Get token and verify claims in jwt.io
Lab 3: Protocol Mappers cho SAML
Create SAML User Attribute Mapper for
departmentCreate Role List Mapper with
Single Role Attribute= ONConfiguration Name ID Format = emailAddress
Capture SAML Response and verify AttributeStatement
Lab 4: Pairwise Subject Identifier
Create 2 OIDC clients:
app-aandapp-bAdd Pairwise Subject Identifier mapper to both clients with the same salt
Sign in with the same user on both clients
Compare the value
subin access tokens — must be differentConfigure Sector Identifier URI for 2 clients to share
sub