Por cuestiones de trabajo tuve que aprender a configurar un servicio de Spring Boot que pueda realizar login con algún proveedor de indentidad (IDP) para así evitar tener más de un usuario para acceder a N cantidad de aplicaciones. En este post les voy a explicar la forma más sencilla de poner en marcha un login que no requiera credenciales propias y se autentique por medio de terceros, en este caso OKTA.
Lo primero que debemos tener claro son las URL que se van a configurar previamente en el servidor de identidad, que básicamente son 2, la principal es la url para el login, y la segunda para el logout.
Por defecto Spring security nos coloca la url del login en la siguiente ruta /login/saml2/sso/{registrationId}. Para el caso del logout /logout/saml2/sso. Para efectos de este tutorial, vamos a personalizar la url del logout, por lo que en realidad quedará de la siguiente manera: /saml/logout.
Una vez tengamos estas dos URLs, podemos configurar nuestra aplicación en el servidor de identidad, que, en el caso de este post, será Okta (https://developer.okta.com/).
Herramientas y librerías utilizadas:
- Intellij IDEA Community Edition
- Java 11
- Gradle 8.4
- Spring Boot 2.7.17
- Spring Dependency Management 1.0.15.RELEASE
Una vez tengamos nuestro proyecto inicializado y cargado en Intellij, procedemos a agregar las siguientes dependencias a nuestro archivo build.gradle:
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.springframework.security:spring-security-saml2-service-provider'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'
Damos clic en Load Gradle Changes para que comience la descarga de las mismas. Una vez finalizado procedemos a generar las clases Java correspondientes a la configuración de SAML.
En caso de que queramos ver logs más detallados de SAML, recomiendo configurar Log4j2 y en su archivo de configuración de XML agregar las siguientes líneas:
<logger name="org.springframework.security" level="debug"/>
<logger name="org.springframework.security.saml2" level="trace" />
<logger name="org.springframework.security.authentication" level="trace" />
<logger name="org.springframework.security.authorization" level="trace" />
<logger name="org.opensaml" level="info" />
<logger name="org.opensaml.saml" level="trace" />
Una vez tengamos las librerías o dependencias correspondientes, es momento de comenzar con la configuración del proveedor de identidad.
El primer paso es tener un cuenta en okta, para registrarnos basta con iniciar sesión con Google.
Una vez dentro, nos vamos a la sección de Applications -> Applications y damos clic en Create App Integration.
Se nos mostrará un modal en dónde nos preguntará el tipo de aplicación, en la cual elegiremos SAML 2.0, ahora nos muestra una pantalla en dónde debemos configurar el nombre de la aplicación, posteriormente damos clic en Next.
Ahora nos pide ingresar la configuración básica para nuestra aplicación, aquí es dónde debemos meter la url definida para el login, en este caso sería la siguiente: http://localhost/auth-server/login/saml2/sso/okta. En el campo de Audience Restriction colocamos un identificador que creamos conveniente, en mi caso coloque dev.mamasoyjuanito.
Más abajo hay un apartado en el cual podemos configurar los atributos que nos pueden compartir al momento de realizar el login, estos son opcionales.
Ahora vamos a dar clic en Next y nos mostrará un feedback de nuestra aplicación, en Are you a customer or partner? seleccionamos la opción I'm an Okta customer adding an internal app y en App Type seleccionamos This is an internal app that we have created. Damos clic en Finish y ahora nos muestra el resumen de la aplicación, de lado derecho vienen algunas explicaciones y un botón, que nos manda a una pagina dónde nos da los datos necesarios para configurar nuestro proyecto.
Una vez estemos en la pagina antes mencionada, lo único que necesitamos es el XML que se encuentra en la parte inferior, lo copiamos y guardamos en un archivo, que posteriormente ocuparemos en nuestro proyecto Java.
Ya que hayamos terminado la configuración en Okta, es momento de hacer lo mismo del lado de Java, para ello, el archivo que guardamos en pasos anteriores, lo debemos agregar a la carpeta de resources de nuestro proyecto, yo lo agregue en el siguente path: resources/metadata/metadata-idp-okta.xml.
Ahora vamos a generar dos clases de configuración, una para la seguridad de spring y la otra para la configuración de SAML.
com.example.authorization.config.SamlConfig.java
package com.example.authorization.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.DefaultResourceLoader;
import org.springframework.security.saml2.core.Saml2X509Credential;
import org.springframework.security.saml2.provider.service.registration.InMemoryRelyingPartyRegistrationRepository;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrations;
import java.io.IOException;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.cert.CertificateException;
import java.security.spec.InvalidKeySpecException;
@Configuration
public class SamlConfig {
private final DefaultResourceLoader resourceLoader = new DefaultResourceLoader();
@Bean
public RelyingPartyRegistrationRepository relyingPartyRegistrations(){
var oktaRe = RelyingPartyRegistrations.fromMetadataLocation("classpath:metadata/metadata-idp-okta.xml")
.registrationId("okta")
.assertionConsumerServiceLocation("{baseUrl}/login/saml2/sso/okta")
.entityId("dev.mamasoyjuanito")
.build();
return new InMemoryRelyingPartyRegistrationRepository(oktaRe);
}
}
En la clase anterior estamos cargando el archivo con las especificaciones del IDP desde los resources. Nombramos nuestro registro (okta) y configuramos la url en la que recibiremos la respuesta del IDP, así como nuestro entityId, que hace referencia al Audience Restriction que configuramos en Okta.
com.example.authorization.config.SecurityConfig.java
package com.example.authorization.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.saml2.provider.service.metadata.OpenSamlMetadataResolver;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository;
import org.springframework.security.saml2.provider.service.servlet.filter.Saml2WebSsoAuthenticationFilter;
import org.springframework.security.saml2.provider.service.web.DefaultRelyingPartyRegistrationResolver;
import org.springframework.security.saml2.provider.service.web.RelyingPartyRegistrationResolver;
import org.springframework.security.saml2.provider.service.web.Saml2MetadataFilter;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
private final RelyingPartyRegistrationRepository relyingPartyRegistrationRepository;
public SecurityConfig(RelyingPartyRegistrationRepository relyingPartyRegistrationRepository) {
this.relyingPartyRegistrationRepository = relyingPartyRegistrationRepository;
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf().disable()
.authorizeRequests(authorize ->
authorize.antMatchers("/").permitAll().anyRequest().authenticated()
).saml2Login()
.successHandler(new SamlAuthenticationSuccessHandler());
var metadataResolver = new OpenSamlMetadataResolver();
var relyingPartyRegistrationResolver = new DefaultRelyingPartyRegistrationResolver(relyingPartyRegistrationRepository);
Saml2MetadataFilter filter = new Saml2MetadataFilter((RelyingPartyRegistrationResolver) relyingPartyRegistrationResolver, metadataResolver);
http.addFilterBefore(filter, Saml2WebSsoAuthenticationFilter.class);
return http.build();
}
}
En esta clase estamos configurando la seguridad de spring, en dónde unicamente permitimos el index o la pagina de inicio y configuramos el login de saml, además de que agregamos el Handler para manejar un inicio de sesión exitoso y también el filtro de SAML que creamos. Esta dependencia que se inyecta a través del contructor (relyingPartyRegistrationRepository), es el bean que creamos en la clase SamlConfig.
Ahora les pongo una breve explicación de las clases usadas de la librería de Spring security, extraidas de su documentación:
- OpenSamlMetadataResolver: Resuelve los metadatos de la parte que confía SAML 2.0 para un determinado RelyingPartyRegistration mediante la API de OpenSAML
- RelyingPartyRegistration: Representa una configuración de parte de confianza (también conocido como proveedor de servicios) y parte afirmante (también conocido como proveedor de identidad).
- Saml2MetadataFilter: Un filtro que devuelve los metadatos de una parte que confía.
Por último, el código de nuestro handler queda de la siguiente manera:
package com.example.authorization.config;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.DefaultRedirectStrategy;
import org.springframework.security.web.RedirectStrategy;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class SamlAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
private RedirectStrategy REDIRECT = new DefaultRedirectStrategy();
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authentication) throws IOException, ServletException {
AuthenticationSuccessHandler.super.onAuthenticationSuccess(request, response, chain, authentication);
}
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
REDIRECT.sendRedirect(request, response,"/secured/hello");
}
}
Dónde únicamente hacemos un redirect a una ruta configurada en un controlador que devuelve una pantalla con el nombre del usuario loggeado.
package com.example.authorization.controllers;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticatedPrincipal;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
public class HelloController {
@RequestMapping("/")
public String index() {
return "home";
}
@RequestMapping("/secured/hello")
public String hello(@AuthenticationPrincipal Saml2AuthenticatedPrincipal principal, Model model) {
model.addAttribute("name", principal.getName());
return "hello";
}
}
Y el html que se retorna en ambos métodos.
home.html
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="https://www.thymeleaf.org">
<head>
<title>Spring Boot + Spring Security with SAML 2.0</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body>
<h1>Home Page</h1>
<p>Click <a th:href="@{/secured/hello}">here</a> to see a secured page.</p>
</body>
</html>
hello.html
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="https://www.thymeleaf.org">
<head>
<title>Spring Boot + Spring Security with SAML 2.0</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body>
<h1>Secured Page</h1>
<p th:text="'Hello, ' + ${name} + '!'"></p>
</body>
</html>