Spring Boot with SAML2 and Keycloak
This article will teach you how to use SAML2 authentication with Spring Boot and Keycloak. Security Assertion Markup Language (SAML) is a standard for exchanging authentication and authorization identities between an Identity Provider (IdP) and a Service Provider. It is an XML-based protocol that uses security tokens with information about a principal. Currently, it is less popular than OICD (OpenID Connect) but is not outdated yet. In fact, many organizations still use SAML for SSO.
In our example, Keycloak will act as an Identity Provider. Keycloak supports SAML 2.0. We can also use Spring Security mechanisms supporting SAML authentication on the service provider side (our sample Spring Boot application). There are several articles about Spring Boot and SAML, but relatively few of them are up to date and use Keycloak as the IdP.
If you are interested in Keycloak and Spring Security you can read my article about microservices with Spring Cloud Gateway, OAuth2, and Keycloak. You can also take a look at the another post about best practices for securing Spring Boot microservices available here.
Source Code
If you would like to try this exercise by yourself, you may always take a look at my source code. First, you need to clone the following GitHub repository. It contains several sample Java applications for a Spring Boot security showcase. You must go to the saml
directory, to proceed with exercise. The sample Spring Boot aplication is available in the callme-saml
directory. Then you should follow my further instructions.
Prerequisites
Before we start the development, we must install some tools on our laptops. Of course, we should have Maven and at least Java 17 installed (Java 21 preferred). We must also have access to the container engine like Docker or Podman to run the Keycloak instance.
Run Keycloak
We will run Keycloak as the Docker container. The repository contains the docker-compose.yml
file in the saml
directory and the realm manifest in the saml/config
directory. Docker Compose run Keycloak in the development mode and imports the realm file on startup. Thanks to that you won’t have to create many resources in Keycloak by yourself. However, I’m describing step by step what should be done. The docker-compose.yml
manifest also set the default administrator password to admin
, enables HTTPS, and uses the Red Hat build of Keycloak instead of the community edition.
services:
keycloak:
# image: quay.io/keycloak/keycloak:24.0
image: registry.redhat.io/rhbk/keycloak-rhel9:24-17
environment:
- KEYCLOAK_ADMIN=admin
- KEYCLOAK_ADMIN_PASSWORD=admin
- KC_HTTP_ENABLED=true
- KC_HTTPS_CERTIFICATE_KEY_FILE=/opt/keycloak/conf/localhost.key.pem
- KC_HTTPS_CERTIFICATE_FILE=/opt/keycloak/conf/localhost.crt.pem
- KC_HTTPS_TRUST_STORE_FILE=/opt/keycloak/conf/truststore.jks
- KC_HTTPS_TRUST_STORE_PASSWORD=123456
ports:
- 8080:8080
- 8443:8443
volumes:
- /Users/pminkows/IdeaProjects/sample-spring-security-microservices/oauth/localhost-key.pem:/opt/keycloak/conf/localhost.key.pem
- /Users/pminkows/IdeaProjects/sample-spring-security-microservices/oauth/localhost-crt.pem:/opt/keycloak/conf/localhost.crt.pem
- /Users/pminkows/IdeaProjects/sample-spring-security-microservices/oauth/truststore.jks:/opt/keycloak/conf/truststore.jks
- ./config/:/opt/keycloak/data/import:ro
command: start-dev --import-realm
YAMLFinally, we must run the following command to start the instance of Keycloak.
docker compose up
ShellSessionThen, we should have the running instance of Keycloak exposes on the localhost
over the HTTPS 8443
port.
After logging to the Keycloak UI with the admin / admin
crendentials you should see the spring-boot-keycloak realm.
In the spring-boot-keycloak
realm details there is a link to the SAML2 provider metadata file. We can download the file and pass directly to the Spring Boot application or save the link address for the future use. Keycloak published the IdP metadata for the spring-boot-keycloak
realm under the https://localhost:8443/realms/spring-boot-keycloak/protocol/saml/descriptor link. Let’s copy that address to clipboard and proceed to the Spring Boot app implementation.
Create Spring Boot App with SAML2 Support
Let’s begin with the dependencies list. We must include the Spring Web and Spring Security modules. For the SAML2 support we must add the spring-security-saml2-service-provider
dependency. That dependency uses the OpenSAML library, which is published in the dedicated Shibboleth Maven repository. Our application also requires Thymeleaf to provide a single web page with an authenticated principal details.
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity6</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-saml2-service-provider</artifactId>
</dependency>
</dependencies>
<repositories>
<repository>
<id>shibboleth-build-releases</id>
<name>Shibboleth Build Releases Repository</name>
<url>https://build.shibboleth.net/nexus/content/repositories/releases/</url>
</repository>
</repositories>
XMLOur goal is to start with something basic. The application will publish a metadata endpoint using the saml2Metadata
DSL method. We also enable authorization at the method level with the @EnableMethodSecurity
annotation. We can access the resources the after authentication in Keycloak.
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(authorize -> authorize.anyRequest()
.authenticated())
.saml2Login(withDefaults())
.saml2Metadata(withDefaults());
return http.build();
}
}
JavaHere’s the Spring Boot application.yml
file. It changes the default app HTTP port to the 8081
. The SAML2 configuration must be provided within the spring.security.saml2.relyingparty.registration.{registrationId}.*
properties. We must set the address of the IdP metadata file in the assertingparty.metadata-uri
property. We also need to set the entity-id
and the SSO service address. Both those settings are exposed in the metadata IdP file. We should also provided certificates for signing requests and verifying SAML responses. The key and certificate all already present in the repository.
server.port: 8081
spring:
security:
saml2:
relyingparty:
registration:
keycloak:
identityprovider:
entity-id: https://localhost:8443/realms/spring-boot-keycloak
verification.credentials:
- certificate-location: classpath:rp-certificate.crt
singlesignon.url: https://localhost:8443/realms/spring-boot-keycloak/protocol/saml
singlesignon.sign-request: false
signing:
credentials:
- private-key-location: classpath:rp-key.key
certificate-location: classpath:rp-certificate.crt
assertingparty:
metadata-uri: https://localhost:8443/realms/spring-boot-keycloak/protocol/saml/descriptor
YAMLLets’ run our application with the following command:
mvn spring-boot:run
ShellSessionAfter startup, we can display a metadata endpoint exposed with the saml2Metadata
DSL method. The most important element in the file is the entityID
. Let’s save it for the future usage.
<md:EntityDescriptor entityID="http://localhost:8081/saml2/service-provider-metadata/keycloak">
<md:SPSSODescriptor protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
<md:KeyDescriptor use="signing">
<ds:KeyInfo>
<ds:X509Data>
<ds:X509Certificate>...</ds:X509Certificate>
</ds:X509Data>
</ds:KeyInfo>
</md:KeyDescriptor>
<md:AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="http://localhost:8081/login/saml2/sso/keycloak" index="1"/>
</md:SPSSODescriptor>
</md:EntityDescriptor>
XMLConfigure Keycloak SAML2 Client
Let’s switch to the Keycloak UI. We must create a SAML client. The client ID must be the same as the entityID
retrieved in the previous section. We should also set the application root URL and valid redirects URIs. Notice that you don’t have do anything – Keycloak imports all requeired configuration at startup from the exported realm manifest.
Don’t forget to create a test user with password. We will use to autenticate against Keycloak in the step.
This is the optional step. We can add the client scope with some mappers. This force Keycloak to pass information about user group, email, and surname in the authentication response.
Let’s include the newly created scope to the client scopes.
We can also create the admins
group and assign our test user to that group.
After providing the whole configuration we can make a first test. Our application is already running. It will print the authenticated user details on the main site after signing in. Here’s the MainController
class.
@Controller
public class MainController {
@GetMapping("/")
public String getPrincipal(@AuthenticationPrincipal Saml2AuthenticatedPrincipal principal, Model model) {
String emailAddress = principal.getFirstAttribute("email");
model.addAttribute("emailAddress", emailAddress);
model.addAttribute("userAttributes", principal.getAttributes());
return "index";
}
}
JavaHere’s the main application site. It uses the Thymeleaf extension for Spring Security.
<!doctype html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="https://www.thymeleaf.org"
xmlns:sec="https://www.thymeleaf.org/thymeleaf-extras-springsecurity6">
<head>
<title>SAML 2.0 Login</title>
</head>
<body>
<main role="main" class="container">
<h1 class="mt-5">SAML 2.0 Login with Spring Security</h1>
<p class="lead">You are successfully logged in as <span sec:authentication="name"></span></p>
<h2 class="mt-2">User Identity Attributes</h2>
<table class='table table-striped'>
<thead>
<tr>
<th>Attribute</th>
<th>Value</th>
</tr>
</thead>
<tbody>
<tr th:each="userAttribute : ${userAttributes}">
<th th:text="${userAttribute.key}"></th>
<td th:text="${userAttribute.value}"></td>
</tr>
</tbody>
</table>
</main>
</body>
</html>
HTMLOnce we access the application main site over the http://localhost:8081
URL, Spring should redirect us to the Keycloak login site. Let’s provide our test user credentials.
Success! We are logged in to the application. Our Spring Boot app prints the details taken from SAML2 authentication token.
REST Methods Authorization
Let’s consider the following @RestController
in our application. We have already enabled authorization at the method level with the @EnableMethodSecurity
annotation. Our controller creates two REST endpoints. The GET /greetings/user
endpoint requires the ROLE_USER
authority granted, while the GET /greetings/admin
requires the ROLE_ADMIN
authority granted.
@RestController
@RequestMapping("/greetings")
public class GreetingController {
@GetMapping("/user")
@PreAuthorize("hasAuthority('ROLE_USER')")
public String greeting() {
return "I'm SAML user!";
}
@GetMapping("/admin")
@PreAuthorize("hasAuthority('ROLE_ADMINS')")
public String admin() {
return "I'm SAML admin!";
}
}
JavaLet’s call the GET /greetings/user
endpoint. It returns the expected response.
Then, let’s call the GET /greetings/admin
endpoint. Unfortunately, we don’t have the access to that endpoint. That’s because it requires the user to have the ROLE_ADMINS
authority.
Previosuly, we used a default implementation the SAML2 authentication provider in Spring Boot. It sets only the ROLE_USER
as the granted authorities. We can override that behaviour with the setResponseAuthenticationConverter
method SAML2 AuthenticationProvider
implementation. Here’s the current implementation. Our method tries to find the member attrobute in the SAML2 response token. Then, it maps the group name taken from that attribute to the Spring Security GrantedAuthority
name. Finally, it returns a new Saml2Authentication
object with a new list of GrantedAuthority
objects.
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {
private static final Logger LOG = LoggerFactory
.getLogger(SecurityConfig.class);
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
OpenSaml4AuthenticationProvider provider =
new OpenSaml4AuthenticationProvider();
provider.setResponseAuthenticationConverter(token -> {
var auth = OpenSaml4AuthenticationProvider
.createDefaultResponseAuthenticationConverter()
.convert(token);
LOG.info("AUTHORITIES: {}", auth.getAuthorities());
var attrValues = token.getResponse().getAssertions().stream()
.flatMap(as -> as.getAttributeStatements().stream())
.flatMap(attrs -> attrs.getAttributes().stream())
.filter(attrs -> attrs.getName().equals("member"))
.findFirst().orElseThrow().getAttributeValues();
if (!attrValues.isEmpty()) {
var member = ((XSStringImpl) attrValues.getFirst()).getValue();
LOG.info("MEMBER: {}", member);
List<GrantedAuthority> authoritiesList = List.of(
new SimpleGrantedAuthority("ROLE_USER"),
new SimpleGrantedAuthority("ROLE_" +
member.toUpperCase().replaceFirst("/", ""))
);
LOG.info("NEW AUTHORITIES: {}", authoritiesList);
return new Saml2Authentication(
(AuthenticatedPrincipal) auth.getPrincipal(),
auth.getSaml2Response(),
authoritiesList);
} else return auth;
});
http.csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(authorize -> authorize.anyRequest()
.authenticated())
.saml2Login(saml2 -> saml2
.authenticationManager(new ProviderManager(provider))
)
.saml2Metadata(withDefaults());
return http.build();
}
}
JavaLet’s run a new version of our application.
mvn spring-boot:run
ShellSessionOnce we open the http://localhost:8081 site once again we can take a look at the logs. I highlighted the part of the SAML response message that contains the member attribute. As you on the bottom, we created a new granted authorities list containing ROLE_USER
and ROLE_ADMINS
.
Fibnally, we can call the GET /greetings/admin
endpoint once again. It works!
Final Thoughts
This article shows how to simply start with Spring Boot, SAML2 and Keycloak. It also provides more advanced solution with the custom authentication provider implementation that maps a user group name to the granted authority name. Although SAML2 is rather a mature technology, there are no many examples with Spring Boot, SAML2 and Keycloak. I hope that my article will fill this gap 🙂
2 COMMENTS