1. Tổng quan SAML 2.0 trong Keycloak
SAML 2.0 (Security Assertion Markup Language) là giao thức xác thực dựa trên XML, được sử dụng rộng rãi trong enterprise — đặc biệt khi tích hợp với hệ thống legacy, SaaS applications (Salesforce, ServiceNow, AWS), hoặc các tổ chức chính phủ.
SAML 2.0 vs OpenID Connect
| Đặc điểm | SAML 2.0 | OpenID Connect |
|---|---|---|
| Định dạng | XML | JSON (JWT) |
| Transport | HTTP Redirect, POST, Artifact | HTTP REST |
| Token | SAML Assertion (XML) | JWT |
| Kích thước | Lớn hơn (XML verbose) | Nhỏ gọn (JSON) |
| Mobile support | Kém (XML parsing nặng) | Tốt (JSON native) |
| Use case chính | Enterprise SSO, legacy systems | Modern web/mobile apps |
| Complexity | Cao | Thấp hơn |
| Logout | SLO (Single Logout) | RP-Initiated, Backchannel, Front-channel |
Khi nào dùng SAML?
Tích hợp với SaaS applications yêu cầu SAML (Salesforce, Google Workspace, AWS)
Liên kết với IdP hoặc SP chỉ hỗ trợ SAML
Yêu cầu tuân thủ standards của tổ chức chính phủ
Migration từ hệ thống ADFS, Shibboleth
Thuật ngữ SAML
| Thuật ngữ | Mô tả | Tương đương OIDC |
|---|---|---|
| Identity Provider (IdP) | Bên xác thực user (Keycloak) | OpenID Provider (OP) |
| Service Provider (SP) | Bên yêu cầu xác thực (ứng dụng) | Relying Party (RP) |
| Assertion | XML document chứa thông tin xác thực | ID Token |
| AuthnRequest | Yêu cầu xác thực từ SP → IdP | Authorization Request |
| ACS URL | Assertion Consumer Service URL | Redirect URI |
| Entity ID | Unique identifier cho SP/IdP | Client ID / Issuer |
| Metadata | XML mô tả endpoints, certificates | Well-Known Configuration |
| NameID | User identifier trong assertion | sub claim |
| Attribute Statement | User attributes trong assertion | Claims trong JWT |
2. Tạo SAML 2.0 Client
2.1 Tạo qua Admin Console
Truy cập Admin Console → chọn realm → Clients → Create client
General Settings:
- Client type: SAML
- Client ID: URL-based Entity ID, ví dụ
https://myapp.example.com/saml/metadata - Name: My SAML Application
Click Next và Save
2.2 Import từ Entity Descriptor (Metadata)
Cách nhanh nhất để tạo SAML client — import metadata XML từ Service Provider:
Truy cập Clients → Import client
Upload file metadata XML hoặc paste URL metadata
Keycloak tự động populate: Entity ID, ACS URL, SLO URL, certificates, bindings
Ví dụ metadata XML của Service Provider:
<?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 Lấy Keycloak IdP Metadata
Service Provider cần metadata của Keycloak (IdP) để cấu hình. 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 chi tiết:
Artifact binding khác biệt so với POST/Redirect — thay vì gửi toàn bộ assertion qua browser, Keycloak chỉ gửi một artifact (reference ID). SP sau đó gọi trực tiếp (backchannel) đến Keycloak để lấy 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 an toàn hơn vì assertion không đi qua browser — hữu ích khi assertion chứa sensitive data.
3.3 XML Signature và Encryption
Signing Configuration:
| Setting | Mô tả |
|---|---|
| Signature Algorithm | Algorithm ký XML: RSA_SHA256 (khuyến nghị), RSA_SHA512, DSA_SHA1 |
| SAML Signature Key Name | Key name trong signature: KEY_ID, CERT_SUBJECT, NONE |
| Canonicalization Method | XML canonicalization: EXCLUSIVE (khuyến nghị) |
Encryption Configuration:
Bật Encrypt Assertions để mã hóa assertion — chỉ SP với private key tương ứng mới giải mã được:
Upload SP's encryption certificate trong tab Keys
Encryption Algorithm: AES128, AES256 (khuyến nghị)
# 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
Quản lý certificates cho SAML client:
Signing Key: Certificate mà SP dùng để ký AuthnRequest — Keycloak dùng để verify
Encryption Key: Certificate mà Keycloak dùng để encrypt assertions — SP dùng private key để decrypt
Import certificate từ file PEM, JKS, hoặc PKCS12:
# 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 xác định cách Keycloak gửi user identifier trong assertion:
| Format | Mô tả | Use case |
|---|---|---|
urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress | Email address | Phổ biến nhất |
urn:oasis:names:tc:SAML:2.0:nameid-format:persistent | ID persistent duy nhất cho mỗi SP | Không muốn reveal email |
urn:oasis:names:tc:SAML:2.0:nameid-format:transient | ID tạm thời, thay đổi mỗi session | Privacy-sensitive |
urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified | Username hoặc Keycloak user ID | Linh hoạt |
4.2 Assertion Lifespan
Cấu hình trong Realm Settings → Tokens tab:
Assertion Lifespan: Thời gian assertion hợp lệ (mặc định 5 phút, khuyến nghị giữ ngắn)
Not Before: Assertion không hợp lệ trước thời điểm này (clock skew tolerance)
4.3 Ví dụ 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)
Trong flow bình thường (SP-Initiated), user truy cập SP → SP redirect đến IdP → IdP xác thực → redirect về SP. Với IDP-Initiated Login, user bắt đầu từ IdP (Keycloak) mà không cần qua SP trước.
Cấu hình IDP-Initiated Login
Mở SAML Client → tab Advanced
Tìm IDP-Initiated SSO URL name: nhập tên URL, ví dụ
my-appURL để IDP-Initiated Login sẽ là:
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
Kết quả trong 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 vào 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 có sẵn: clientAddress, clientHost, identity_provider, identity_provider_identity.
Hardcoded Claim Mapper — Thêm claim với giá trị cố định:
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 — Thêm danh sách groups của user vào 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
Kết quả:
{
"groups": ["/Engineering", "/Engineering/Backend"]
}
Audience Mapper — Thêm audience vào 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
Kết quả:
{
"aud": ["my-api-service", "account"]
}
Script Mapper — Custom logic bằng 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
Lưu ý: Script Mapper sử dụng Nashorn JavaScript engine. Trong Keycloak 24+, cần deploy script mappers dưới dạng custom JAR provider thay vì inline script. Xem Deploy Scripts trong tài liệu Keycloak.
6.3 SAML Protocol Mappers
SAML mappers tương tự OIDC nhưng output là SAML Attribute thay vì 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
Các SAML Attribute NameFormat:
| Format | Mô tả | Ví dụ |
|---|---|---|
| Basic | Tên đơn giản | email, firstName |
| URI Reference | OID format, tiêu chuẩn | urn:oid:0.9.2342.19200300.100.1.3 |
| Unspecified | Không xác định format | Tùy ý |
7. Lightweight Access Tokens
Mặc định, Keycloak access tokens chứa rất nhiều claims (realm_access, resource_access, email, name, preferred_username,...). Lightweight Access Tokens giảm kích thước token bằng cách chỉ giữ lại claims thiết yếu.
7.1 Tại sao cần Lightweight Access Tokens?
Giảm bandwidth: Token nhỏ hơn = gửi qua HTTP header nhanh hơn
Giảm thông tin nhạy cảm: Access token thường được gửi đến nhiều services, không nên chứa quá nhiều PII
Cải thiện security: Resource server sử dụng Token Introspection để lấy full claims khi cần
7.2 Cấu hình Lightweight Access Tokens
Mặc định, Protocol Mappers có option Add to lightweight access token. Để sử dụng:
Với mỗi mapper, tắt Add to access token ở những claims không cần trong lightweight token
Sử dụng Client Policy (xem bài sau) để enforce lightweight tokens cho clients cụ thể
Resource server gọi Token Introspection endpoint để lấy 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
Theo mặc định, Keycloak sử dụng public subject identifier — giá trị sub claim giống nhau cho tất cả clients. Điều này cho phép các clients liên kết (correlate) user across services.
Pairwise subject identifier tạo sub khác nhau cho mỗi client — ngăn chặn cross-service user tracking.
8.1 Cấu hình Pairwise Identifier
Thêm Protocol Mapper loại Pairwise subject identifier vào client hoặc client scope
Cấu hình:
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
Kết quả:
# 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:
Nếu bạn muốn một nhóm clients chia sẻ cùng sub (ví dụ: web app và mobile app của cùng service), sử dụng Sector Identifier URI. URI này trỏ đến JSON array chứa redirect URIs của các clients trong cùng sector:
# https://myservice.example.com/sector-identifier.json
["https://webapp.example.com/callback", "myapp://callback"]
9. Tích hợp SAML với Spring Boot
Sử dụng spring-security-saml2-service-provider để tích hợp 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. Bài tập thực hành
Lab 1: Tạo SAML Client và kiểm tra Assertion
Tạo SAML client với Entity ID
https://localhost:8443/samlCấu hình ACS URL, Sign Documents, Sign Assertions = ON
Sử dụng samltool.com hoặc SAML-tracer browser extension để capture SAML Response
Phân tích SAML Assertion: NameID, AttributeStatement, Conditions, Signature
Lab 2: Protocol Mappers cho OIDC
Tạo user attribute
employee_idtrong User ProfileTạo User Attribute Mapper:
employee_id→ token claimemp_idTạo Group Membership Mapper: groups → token claim
groupsTạo Hardcoded Claim:
env=stagingTest: Lấy token và verify claims trong jwt.io
Lab 3: Protocol Mappers cho SAML
Tạo SAML User Attribute Mapper cho
departmentTạo Role List Mapper với
Single Role Attribute= ONCấu hình Name ID Format = emailAddress
Capture SAML Response và verify AttributeStatement
Lab 4: Pairwise Subject Identifier
Tạo 2 OIDC clients:
app-avàapp-bThêm Pairwise Subject Identifier mapper vào cả 2 clients với cùng salt
Đăng nhập cùng user vào cả 2 clients
So sánh giá trị
subtrong access tokens — phải khác nhauCấu hình Sector Identifier URI để 2 clients share cùng
sub