How To Use Spring Security With SAML Protocol Binding

In this post, I will show how we can use Spring Security with SAML Protocol Binding to integrate with Keycloak Identity Provider. And, if you want to read on how to use Keycloak, you can read here.

What is SAML?

SAML stands for Security Assertion Markup Language. It’s an open standard for
exchanging authentication and authorization data between a service provider (SP) and identity provider (IdP).

Identity Provider – performs authentication and validates user identity for authorization and passes that to Service Provider.

Service Provider – Trusts the identity provider and provides access to the user to service based on authorization.

SAML Authentication Flow

As part of this flow, we will be building a simple To-Do List Application. Henceforth, a user will access the application and he will be redirected for authentication.

SAML Authentication User Flow:

  1. User accesses Service Provider (SP) ToDo List Application.
  2. Application redirects user to Keycloak login screen. During this redirect, the application sends an AuthnRequest to Keycloak IDP.
  3. Keycloak IDP validates the request if it is coming from the right relying party/service provider. It checks for issuer and redirect URI (ACS URL).
  4. Keycloak IDP sends a SAML response back to Service Provider.
  5. Service Provider validates the signed response with provided IDP public certificate.
  6. If the response is valid, we will extract the attribute NameID from the assertion and logged the user in.

 

How to use Spring Security with SAML Protocol

NoteSpring Security SAML extension was a library that used to provide SAML support.
But after 2018, Spring Security team moved that project and now supports SAML
2 authentication as part of core Spring Security.

Use Spring Security with SAML Protocol Binding

Therefore, once you create a Spring Boot project, we will need to import the following dependencies.


dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
	implementation 'org.springframework.boot:spring-boot-starter-jdbc'
	implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
	implementation 'org.springframework.boot:spring-boot-starter-web'
	/*
	 * Spring Security
	 */
	implementation 'org.springframework.boot:spring-boot-starter-security'
	runtimeOnly 'mysql:mysql-connector-java'
	providedRuntime 'org.springframework.boot:spring-boot-starter-tomcat'
	implementation 'org.springframework.security:spring-security-saml2-service-provider:5.3.5' +
			'.RELEASE'

	/*
	 * Keycloak
	 */
	implementation 'org.keycloak:keycloak-spring-boot-starter:11.0.3'
	testImplementation('org.springframework.boot:spring-boot-starter-test') {
		exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
	}
}

Accordingly, the dependency spring-security-saml2-service-provider will allow us to add relying party registration. It also helps with identity provider registration.

Now, we will add this registration in ourSecurityConfig as below:


    @Bean
    public RelyingPartyRegistrationRepository relyingPartyRegistrationRepository() throws CertificateException
    {
        final String idpEntityId = "http://localhost:8180/auth/realms/ToDoListSAMLApp";
        final String webSSOEndpoint = "http://localhost:8180/auth/realms/ToDoListSAMLApp/protocol/saml";
        final String registrationId = "keycloak";
        final String localEntityIdTemplate = "{baseUrl}/saml2/service-provider-metadata" +
                "/{registrationId}";
        final String acsUrlTemplate = "{baseUrl}/login/saml2/sso/{registrationId}";


        Saml2X509Credential idpVerificationCertificate;
        try (InputStream pub = new ClassPathResource("credentials/idp.cer").getInputStream())
        {
            X509Certificate c = (X509Certificate) CertificateFactory.getInstance("X.509").generateCertificate(pub);
            idpVerificationCertificate = new Saml2X509Credential(c, VERIFICATION);
        }
        catch (Exception e)
        {
            throw new RuntimeException(e);
        }

        RelyingPartyRegistration relyingPartyRegistration = RelyingPartyRegistration
                .withRegistrationId(registrationId)
                .providerDetails(config -> config.entityId(idpEntityId))
                .providerDetails(config -> config.webSsoUrl(webSSOEndpoint))
                .providerDetails(config -> config.signAuthNRequest(false))
                .credentials(c -> c.add(idpVerificationCertificate))
                .assertionConsumerServiceUrlTemplate(acsUrlTemplate)
                .build();

        return new InMemoryRelyingPartyRegistrationRepository(relyingPartyRegistration);
    }

Our login will also change with HttpSecurity as follows:

httpSecurity.authorizeRequests()
.antMatchers("/js/**","/css/**","/img/**").permitAll()
.antMatchers("/signup","/forgotpassword").permitAll()
.antMatchers("/saml/**").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login").permitAll()
.and()
.saml2Login(Customizer.withDefaults()).exceptionHandling(exception ->
exception.authenticationEntryPoint(entryPoint()))
.logout()
.logoutUrl("/logout")
.logoutSuccessHandler(logoutSuccessHandler)
.deleteCookies("JSESSIONID")
.permitAll();

We are now using saml2Login . By default, if you access the application, it will redirect to an identity provider. We want to configure our custom login page before it can be redirected to the identity provider – keycloak. That’s why we have authenticationEntryPoint which allows us to configure our custom login page. So now if we access our application at https://localhost:8743/login, we will see the below login page:

So once you select the option for Login with Keycloak SAML, it will send a AuthnRequest to Keycloak. Also, this request is an unsigned request. Keycloak will send a signed response. A controller will receive this signed response to decode NameId attribute.


@GetMapping(value="/index")
public String getHomePage(Model model, @AuthenticationPrincipal Saml2AuthenticatedPrincipal saml2AuthenticatedPrincipal)
{
   String principal = saml2AuthenticatedPrincipal.getName();
   model.addAttribute("username", principal);
   return "index";
}

Once the NameId is retrieved, it will log the user in.

Configuration on Keycloak

We will have to configure our application in the Keycloak administration console.

  • Create a REALM for your application.
  • Select Endpoints – SAML 2.0 IdP Metadata
  • In Clients – Add the service provider.
  • For your client, configure Root URL, SAML Processing URL (https://localhost:8743/saml2/service-provider-metadata/keycloak)
  • You can also adjust the other settings like – signing assertions, including AuthnStatement.
  • Certainly, configure the ACS URL in “Fine Grain SAML Endpoint Configuration” section.

Code Repository

The code for this project is available in my github repository. I also covered this in more detail in my book Simplifying Spring Security. To learn more, you can buy my book here.

Conclusion

In this post, I showed how to use Spring Security with SAML Protocol. Over the years, there have been a lot of improvements in Spring Security and now it can easily be used with different protocols like OAuth, OIDC. If you enjoyed this post, subscribe to my blog here.