Autenticación y Autorización: Medidas de Seguridad con Spring Security

Maria Paula Vizcaíno
Pragma
Published in
8 min readFeb 15, 2024

Cuando adquirimos un producto valioso, una vez lo tenemos, lo primero que adquirimos es algo que lo proteja velando por su correcto funcionamiento asegurando su integridad para seguir aprovechando todo lo que nos ofrece dicho producto. En el mundo del desarrollo web la seguridad en las aplicaciones juega ese papel del protector de la pantalla de un celular o de un candado para una bicicleta. Confiamos en el protector en caso de alguna caída o golpe y confiamos en ese candado para dejar nuestra bicicleta en algún lugar público.

Al igual que nosotros confiamos en esos accesorios, los usuarios de una aplicación confían en esta para la integridad y seguridad de su información esté a salvo. Ahora, uno de los lenguajes de programación que inevitablemente está constantemente presente en nuestros sistemas es Java, debido a su implementación intuitiva permitiendo que la escritura y compilación del código sea una labor más sencilla, siendo así, la seguridad de las aplicaciones de Java es un factor indispensable en la actualidad.

El constante crecimiento de amenazas cibernéticas ha originado medidas sólidas en pro a la seguridad, confiabilidad y disponibilidad de los sistemas. Una de las herramientas empleadas con este fin es la subcapa del framework de código abierto Spring Security, donde se han implementado medidas especializadas para los aspectos de la autenticación y la autorización en las aplicaciones desarrolladas en Java. Por medio de Spring Security manejamos las políticas de procesos de autorización dinámicas permitiéndonos, como desarrolladores, enfocarnos en la escalabilidad y adaptabilidad de nuestras aplicaciones confiando en la seguridad asociada a ellas.

La primera línea de Defensa.

Nuestro primer acceso como usuarios a una aplicación consta en la verificación de nuestra información relacionada a esta, siendo este proceso de autenticación un paso crucial correspondiente a la seguridad en un sistema. Spring Security nos brinda varias estrategias para la implementación de un sistema compuesto por una autenticación robusta.

Independiente de dicha flexibilidad, en todo caso nos asegura que los usuarios, una vez autenticados, puedan acceder a los recursos y funcionalidades del sistema. Una autenticación efectiva puede incluir la verificación de unas credenciales establecidas por el desarrollador, comúnmente siendo un usuario único y una contraseña, Spring Security nos brinda una implementación convencional para una autenticación básica por medio de la autenticación por formularios, estando estructurada por ejemplo de la forma:

@Configuration
@Enablewebsecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth
.inkenoryAuthentication()
.withUser("usuario").password("contraseña").roles("PRAGMATICO");
}

@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/publico/**").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login").permitAll()
.and()
.logout()
.permitAll()
}

}

En nuestro código, tenemos la autenticación por medio de usuario y contraseña para el rol de pragmático, para lo cual establecemos permisos dependiendo del estado de la autenticación por medio de las rutas ‘/publico/**’ y ‘/login’ siendo esta última la accedida una vez se completa el proceso de autenticación.

También nos permite restringir la funcionalidad de autenticación, habilitándola únicamente si es requerida, por medio del método ‘sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)’. De manera siminar, maneja diversas funciones como ‘maximunSession(n)’ que, como su nombre lo indica, establece un tiempo máximo de duración para una sesión, lo cual nos puede evitar acceso masivo no autorizado en nuestra aplicación. Cuenta también con funciones como ‘sessionFixation().migrateSession()’ la cual previene sesiones fijadas en la aplicación y ‘invalidSessionUrl(“/login?expired=true”)’ la cual notifica al usuario en caso de que su sesión haya expirado. Veamos una implementación simple de estas funcionalidades:

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

@Override
protected void configure(HttpSecurity http) throws Exception {
http
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
.invalidSessionUrl("/login?expired=true")
.sessionFixation().migrateSession()
.maximumSessions(n)
.expiredUrl("/login?duplicate=true");
}

}

Spring Security también cuenta con mecanismos para prevenir ataques denominados como ataques de fuerza bruta, los cuales corresponden a la repetición de intentos de inicios de sesión con el fin de dar con la contraseña correcta del usuario al cual buscan vulnerar, empleando herramientas como CAPTCHAs y bloqueos temporales debido a múltiples intentos fallidos.

Por otro lado, Spring Security nos da acceso a la integración de autenticadores externos, como Google o GitHub, mediante protocolos de autenticación federada, con OAuth 2.0. Una vez la autenticación externa es completada exitosamente, nos redirige nuevamente a nuestra aplicación junto con un token de acceso el cual proporciona la información del usuario por medio del método ‘oauth2Login()’.

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

@Override
protected void configure(HttpSecurity http) throws Exception \{
http
.authorizeRequests()
.antMatchers("/").permitAll()
.anyRequest().authenticated()
.and()
.oauth2Login()
.loginPage("/login")
.defaultSuccessUr1("/home")
.userInfoEndpoint()
.userService(oAuth2UserService());
}

@Bean
public OAuth2UserService<OAuth2UserRequest, OAuth2User> OAuth2UserService() {
return new DefaultOAuth2UserService();
}

}

Dicha autenticación mediante tokens nos da ventajas significativas como que los tokens generados no se almacenan en el servidor. Así, una vez este haya vencido, por ejemplo, por un tiempo de validez establecido, se debe realizar de nuevo el proceso de autenticación permitiéndonos manejar cada solicitud de manera independiente.

Spring Security incluye la implementación de tokens JWT (JSON Web Tokens), los cuales constan de un encabezado, donde se especifica el tipo de token y el algoritmo asociado a la firma, un payload, donde se almacena información del usuario e información adicional si es requerida, y una firma, la cual se emplea para verificar el token. Lo podemos implementar por medio del filtro ‘JwtAuthenticationFilter’, el cual se puede modificar para añadir requisitos particulares como la extracción de información adicional, la cual se almacenará en el payload. Veamos un ejemplo para esta implementación:

public class JwtAuthenticationFilter extends OncePerRequestFilter {

@Override
protected vold doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
String token = extractTokenFromRequest(request);

if (token != null && tokenProvider.validateToken(token)) {
Authentication authentication = tokenProvider.getAuthentication(token);
SecurityContextHolder.getContext().setAuthentication(authentication);
}

filterChain.doFilter(request, response);
}

}

Los tokens también pueden ser empleados en otros servicios, convirtiéndolos en elementos ideales para servicios construidos con arquitecturas de microservicios. Estos a su vez pueden ser cifrados, permitiendo transportar información adicional a la requerida en el proceso de autenticación, como el rol del pragmático al autenticar en nuestras plataformas, los permisos que tiene habilitados, entre otros, permitiendo extender la autenticación a una autenticación basada en roles.

Más Allá de los Roles Básicos.

Se puede observar en los fragmentos de código previos, que en algunas funciones se llama a la funcionalidad de autorización cuando hablamos de autenticación; es importante establecer las diferencias entre estos dos conceptos. La autenticación verifica y valida la identidad de un usuario asociado un sistema, por otro lado, la autorización determina las funcionalidades y recursos permitidos para un usuario una vez este ya se ha autenticado.

Estas acciones permitidas por el proceso de autorización van ligadas con la asignación de permisos y privilegios. Los roles de un usuario son los grupos de permisos previamente establecidos para un usuario y los privilegios son las acciones particulares que puede efectuar el usuario una vez dentro. Veamos cómo lo podemos implementar por medio de Spring Security:

@Entity
public class Usuario {

@ManyToMany(fetch = FetchType.EAGER)
@JoinTable(name = "usuario_rol",
joinColumns = @JoinColumn(name = "usuario_id"),
inverseJoinColumns = @JoinColumn(name = "rol_id"))
private Set<Rol> roles = new HashSet<>();

...
}

@Entity
public class Rol {

@Enumerated(EnumType.STRING)
@Column(length = 20)
private TipoRol nombre;

...
}

public enum TipoRol {
ROLE_PRAGMATICO,
ROLE_LIDER
}

En este ejemplo cada uno de los usuarios de nuestra aplicación tiene uno o más roles, siendo una relación many-to-many, siendo ‘ROLE_PRAGMATICO’ el rol correspondiente a los pragmáticos comunes quienes tiene un chapter asignado de calidad, backend y frontend, y ‘ROLE_LIDER’ aquellos pragmáticos que, como su nombre lo indica, son líderes de dichos chapters. Podemos asignar permisos en particular a estos usuarios, como permisos de administrador de la forma:

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/admin/**"). hasRole("ROLE_LIDER")
.antMatchers("/usuario/**"). hasRole("ROLE_PRAGMATICO")
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login")
.permitAl1()
.and()
.logout()
.permitAll();
}

}

Para definir privilegios, Spring Security proporciona unas reglas de autorización por medio del método ‘authorizationRequests()’ la cual se puede personalizar según lo demandado por la aplicación. Estas reglas se componen de métodos encadenados que establecen las rutas y los permisos que tendrá el usuario, por ejemplo, solo se permite a los pragmáticos asignados un proyecto particular tener información al respecto.

Existe unas expresiones específicas proporcionadas por Spring Security, llamadas Expresiones SpEL, las cuales establecen reglas de autorización dinámica y flexiblemente, por ejemplo, el método ‘access()’ es una expresión que nos permite brindar accesos específicos a los usuarios. Estas expresiones son empleadas para realizar verificaciones complejas, por ejemplo, verificar que un pragmático sea un líder en el sistema. Veamos cómo lo podemos implementar:

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/admin/**").access("hasRole('ROLE_LIDER') and #oauth2.hasScope('admin')")
.anyRequest().authenticated()
.and()
.oauth2Login()
.loginPage("/login")
.permitAll()
.and()
.logout()
.permitAl1();
}

}

En nuestro ejemplo, por medio de las expresiones SpEL ‘antMatchers()’ y ‘access()’ le otorgamos permisos de administrador a los líderes de Pragma.

Además de estas expresiones particulares, Spring Security nos da acceso como desarrolladores a anotaciones a nivel método y/o clase para nuestro código. Como su nombre lo indica, estas anotaciones nos permiten conceder permisos para acceder a dichos métodos o clases desde otras clases particulares. Exploremos algunas de estas anotaciones:

  • @PreAuthorize y @PostAuthorize: Estas anotaciones especifican si una expresión SpEL debe evaluarse previo o posterior al acceso del método, respectivamente.
  • @Secured: Esta anotación permite establecer los roles que pueden acceder al método o a la clase.
@Service
public class AppPragma {

@PreAuthorize("hasRole('ROLE_LIDER')")
public void requiereRolLider() {
...
}

@PostAuthorize("returnObject.pragmatico == principal.username")
public Usuario retornaPragmaticoActivo() \{
...
return usuario;
}

@Secured("ROLE_PRAGMATICO", "ROLE_LIDER"})
public void requiereRolesPragmaticoYLider() {
...
}

}

En nuestra app Pragma, el método ‘requiereRolLider()’ requiere que el usuario tenga rol líder para ejecutar el método, ‘retornaPragmaticoActivo()’ verifica una vez es completado que el usuario retornado corresponda al pragmático autenticado, y, finalmente, ‘requiereRolesPragmaticoYLider()’ permite el acceso al método solo a los roles de pragmático y líder, es decir, un usuario externo a Pragma no tendría los permisos adecuados para acceder.

Registro de Eventos.

Spring Security brinda soporte para la configuración de auditoría, lo que consta en una herramienta a la que podemos recurrir para el registro de eventos en nuestra aplicación, como el registro de inicios de sesión y el acceso a recursos particulares. Para ello, por medio de ‘AuditEventRepository’ podemos agregar a nuestra aplicación un repositorio de autoría de eventos, en sí, Sprint Security proporciona algunos que podemos implementar libremente como ‘JdbcAuditEventRepository’ y ‘MongoAuditEventRepository’.

Una vez implementado el repositorio que vayamos a utilizar, podemos modificarlo como requiramos por medio de ‘AuditingConfigurer’ especificando los eventos que queremos auditar con ‘AuditEventRepository’ como, por ejemplo, los inicios y cierres de sesión. Veamos un ejemplo de esta implementación:

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

@Autowired
private AuditEventRepository auditEventRepository;

@Override
protected void configure(HttpSecurity http) throws Exception \{
http
.logout()
.logoutUrl("/logout")
.logoutSuccessHandler(auditLogoutSuccessHandler())
.and()
.csrf()
.disable()
.apply(new AuditingConfigurer<>(auditEventRepository));
}

@Bean
public LogoutSuccessHandler auditLogoutSuccessHandler() {
return new AuditLogoutSuccessHandler(auditEventRepository);
}

}

Algunas de las buenas prácticas que como desarrolladores backend podemos seguir para un seguro registro de eventos es procurar almacenar los eventos relevantes en nuestra aplicación, como el acceso a información específica junto con información asociada a este evento ya sea el nombre del usuario implicado en el evento o IP del mismo, siempre almacenando estos eventos de forma segura como por medio de técnicas de cifrado o un control de accesos. Esto para facilitar el análisis e interpretación de los eventos brindando transparencia en el proceso.

Referencias.

--

--