Índice de contenidos
- Introducción
- Spring Authorization Server
2.1 Dependencias
2.2 Configuración
2.3 Probando nuestro Authorization Server - Spring Resource Server
3.1 Dependencias
3.2 Configuración
3.3 Probando nuestro Resource Server - Conclusiones
Introducción
En el mundo digital actual, la seguridad de las aplicaciones es una preocupación fundamental. A medida que estas se vuelven más sofisticadas y se conectan con diversos usuarios y servicios, es esencial garantizar que solo las personas autorizadas tengan acceso a la información y los recursos adecuados. Aquí es donde entran en juego las dos piezas sobre las que trata este tutorial, los servidores de autorización (Spring Authorization Server) y recursos (Spring Resource Server).
Spring Authorization Server nos permite implementar un servidor de autorización, emitiendo y validando tokens de acceso para autenticar y autorizar las solicitudes de los usuarios. Al utilizar Spring Authorization Server, podemos establecer políticas de seguridad flexibles y personalizadas para nuestras aplicaciones, gestionando de manera eficiente los permisos y los roles de los usuarios.
Por otro lado, Spring Resource Server se encarga de proteger los recursos de nuestra aplicación y asegurarse de que solo los usuarios autorizados puedan acceder a ellos. Funciona en conjunto con Spring Authorization Server para validar los tokens de acceso y garantizar que el usuario que solicita los recursos tenga los permisos necesarios. Al integrar Spring Resource Server en nuestra aplicación, podemos asegurar endpoints específicos y proteger la confidencialidad e integridad de los datos sensibles.
Spring Authorization Server
A continuación, realizaremos los pasos necesarios para poner en funcionamiento nuestro servidor de autorización.
Dependencias
Al ser Spring Authorization Server una capa construida por encima de Spring Security, necesitamos añadir también esta dependencia, al igual que Spring Web para el manejo de solicitudes y respuestas HTTP que utiliza Spring Authorization Server para realizar las tareas de autenticación y autorización.
Podemos hacer uso de Spring Initializr para facilitar la creación del proyecto.
1 2 3 4 5 6 7 8 9 10 11 12 |
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-oauth2-authorization-server</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-web</artifactId> </dependency> |
Configuración
En este ejemplo, vamos a realizar una configuración básica de Spring Authorization Server. Este framework ofrece muchas posibilidades de configuración y dependerá mucho de las necesidades del proyecto en el que estemos implementándolo.
En primer lugar, vamos a configurar el puerto en el que se ejecutará el servidor de autorización en nuestro .properties. Es necesario ya que no configuraremos ninguno en el servidor de recursos y utilizará por defecto el 8080:
1 |
server.port=9000 |
A continuación, mediante una clase de @Configuration
de Spring, vamos a añadir los componentes necesarios:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
@Configuration @EnableWebSecurity public class AuthorizationServerSecurityConfig { @Bean @Order(1) public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception { OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http); http.getConfigurer(OAuth2AuthorizationServerConfigurer.class) .oidc(Customizer.withDefaults()); http.exceptionHandling(exceptions -> exceptions.defaultAuthenticationEntryPointFor( new LoginUrlAuthenticationEntryPoint("/login"), new MediaTypeRequestMatcher(MediaType.TEXT_HTML)) ).oauth2ResourceServer(resourceServer -> resourceServer .jwt(Customizer.withDefaults()) ); return http.build(); } @Bean @Order(2) public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests((authorize) -> authorize .anyRequest().authenticated() ) .formLogin(Customizer.withDefaults()); return http.build(); } |
authorizationServerSecurityFilterChain
: se aplican las configuraciones de seguridad predeterminadas y se habilita OpenID Connect 1.0.defaultSecurityFilterChain
: se configura la autorización para que todas las solicitudes requieran autenticación y se define una forma de inicio de sesión predeterminada.
1 2 3 4 5 6 7 8 9 10 |
@Bean public UserDetailsService userDetailsService() { UserDetails userDetails = User.withDefaultPasswordEncoder() .username("admin") .password("password") .roles("ADMIN") .build(); return new InMemoryUserDetailsManager(userDetails); } |
userDetailsService
: crea un objetoUserDetailsManager
en memoria que contiene los detalles de un usuario de prueba.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
@Bean public RegisteredClientRepository registeredClientRepository() { RegisteredClient oidcClient = RegisteredClient.withId(UUID.randomUUID().toString()) .clientId("oidc-client") .clientSecret("{noop}secret") .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC) .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN) .redirectUri("https://oauthdebugger.com/debug") .scope(OidcScopes.OPENID) .build(); return new InMemoryRegisteredClientRepository(oidcClient); } |
registeredClientRepository
: crea un objetoRegisteredClientRepository
en memoria que define un cliente registrado para el servidor de autorización. Los parámetros que se están configurando son los siguientes:.clientId
identifica al cliente que solicita acceso a los recursos protegidos, mientras que.clientSecret
es una clave secreta utilizada para autenticar al cliente ante el servidor de autorización. Ambos valores son necesarios para validar y autorizar las solicitudes del cliente en el servidor.clientAuthenticationMethod
: utilizamosClientAuthenticationMethod.CLIENT_SECRET_BASIC
, el cliente envía suClientId
yClientSecret
en el encabezado de la solicitud utilizando la autenticación básica (Basic Authentication). Se envía elClientId
yClientSecret
concatenados en un mismo string y codificados en base64.authorizationGrantTypes
:AuthorizationGrantType.AUTHORIZATION_CODE
: Se utiliza para obtener un token de acceso en nombre del usuario final mediante un authorization code obtenido del servidor de autorización. Es útil para acceder a recursos protegidos en nombre del usuario.AuthorizationGrantType.REFRESH_TOKEN
: Se utiliza para obtener un nuevo token de acceso cuando el token actual ha expirado. Permite renovar el token sin requerir que el usuario vuelva a autenticarse. Es útil para mantener la sesión activa y prolongar el acceso a los recursos.
redirectUri("https://oauthdebugger.com/debug")
especifica la URL a la cual el servidor de autorización redirige al cliente junto al authorization code generado una vez el usuario haya dado su consentimiento. También comprobará en el momento en el que se haga la petición para generar el authorization code que estaredirectUri
está registrada en el servidor. En este caso utilizamos esa ya que vamos a apoyarnos en oauthdebugger para probar el flujo.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
@Bean public JWKSource jwkSource() { KeyPair keyPair = generateRsaKey(); RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic(); RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate(); RSAKey rsaKey = new RSAKey.Builder(publicKey) .privateKey(privateKey) .keyID(UUID.randomUUID().toString()) .build(); JWKSet jwkSet = new JWKSet(rsaKey); return new ImmutableJWKSet<>(jwkSet); } private static KeyPair generateRsaKey() { KeyPair keyPair; try { KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA"); keyPairGenerator.initialize(2048); keyPair = keyPairGenerator.generateKeyPair(); } catch (Exception ex) { throw new IllegalStateException(ex); } return keyPair; } @Bean public JwtDecoder jwtDecoder(JWKSource jwkSource) { return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource); } |
jwkSource
: genera un par de claves RSA y crea un objetoJWKSource
que contiene la clave pública y privada en un conjunto de claves JSON. Esto se utiliza para la descodificación y verificación de los tokens JWT.jwtDecoder
: configura un decodificador JWT utilizando la fuente de claves JWK generada anteriormente.
1 2 3 4 5 6 7 8 |
@Bean public AuthorizationServerSettings authorizationServerSettings() { return AuthorizationServerSettings.builder().build(); } } |
authorizationServerSettings
: crea un objetoAuthorizationServerSettings
que encapsula las configuraciones del servidor de autorización.
De esta manera ya tendríamos todo lo necesario para hacer funcionar nuestro Spring Authorization Server.
Sin embargo, tenemos más opciones de configuración que podemos realizar, una de ellas es la personalización del JWT.
En uno de los pasos previos, hemos dado de alta en memoria un usuario con el rol «ADMIN», podemos añadir esta información a nuestro JWT de la siguiente manera:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
@Configuration public class JwtTokenCustomizerConfig { @Bean public OAuth2TokenCustomizer tokenCustomizer() { return (context) -> { if (OAuth2TokenType.ACCESS_TOKEN.equals(context.getTokenType())) { Authentication principal = context.getPrincipal(); Set authorities = principal.getAuthorities().stream() .map(GrantedAuthority::getAuthority) .collect(Collectors.toSet()); context.getClaims().claim("roles", authorities); } }; } } |
Probando nuestro Authorization Server
Una vez ejecutemos nuestro servidor de autorización, podemos conocer todos los endpoint haciendo una petición a http://localhost:9000/.well-known/openid-configuration
que nos devolverá lo siguiente:
En este caso, nos interesa el endpoint de autorización(/oauth2/authorize
) que utilizaremos en oauthdebugger para iniciar el proceso de autorización como se muestra a continuación:
Como se puede apreciar, a parte de los campos que hemos configurado previamente, aparecen dos nuevos:
Nonce
: es un valor único generado por el cliente antes de realizar una solicitud de autorización. Se utiliza para prevenir ataques de reproducción y garantizar la integridad de los tokens de acceso y los tokens de identificación. El servidor de autorización incluirá este valor en la respuesta junto con el token de acceso, y el cliente debe verificar que elnonce
devuelto coincida con el valor originalmente enviado.State
: es un parámetro opcional utilizado para mantener el estado del cliente entre la solicitud de autorización y la respuesta del servidor de autorización. El cliente envía un valor aleatorio en el parámetrostate
durante la solicitud de autorización, y el servidor de autorización lo incluirá en la respuesta de redireccionamiento. Esto permite que el cliente recupere el estado original y realice las acciones correspondientes, como redirigir al usuario a la página correcta después de la autorización.
Al realizar la petición nos enviará a la página /login para que introduzcamos las credenciales del usuario:
Una vez hagamos login nos redirigirá de nuevo a oauthdebugger el cual nos devolverá el Authorization code que utilizaremos para recuperar los tokens.
Podemos ayudarnos de una herramienta como postman para realizar la petición a /oauth2/token
que nos devolverá los tokens:
- Como hemos establecido en nuestra configuración, indicarle a la herramienta que vamos a hacer uso de Basic Authorization e introducir las credenciales:
- Realizar la petición con los datos necesarios:
De esta manera habríamos completado nuestro Authorization Code Flow.
Por último, vamos a hacer uso de la herramienta jwt para comprobar que dentro del access_token, se encuentra el rol que introdujimos mediante configuración.
Spring Resource Server
Al igual que hemos hecho con Spring Authorization Server, vamos a configurar lo necesario para poner en funcionamiento nuestro Resource Server.
Dependencias
1 2 3 4 5 6 7 8 |
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-oauth2-resource-server</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> |
Configuración
En primer lugar, en nuestro archivo .properties
indicamos nuestro issuer de los tokens JWT(Authoriation Server) que permitirá que a la hora de acceder a los recursos, se compruebe la autenticidad de los mimos.
1 |
spring.security.oauth2.resourceserver.jwt.issuer-uri=http://localhost:9000 |
Seguidamente, una clase de @Configuration
para establecer los componentes necesarios:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
@Configuration @EnableMethodSecurity public class ResourceServerConfig { @Value("${spring.security.oauth2.resourceserver.jwt.issuer-uri}") private String issuer; @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { return http .authorizeHttpRequests(authorize -> authorize.anyRequest().authenticated()) .oauth2ResourceServer(oAuth2 -> oAuth2 .jwt(it -> it.decoder(JwtDecoders.fromIssuerLocation(issuer)))) .build(); } @Bean public JwtAuthenticationConverter jwtAuthenticationConverter() { JwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter(); jwtGrantedAuthoritiesConverter.setAuthoritiesClaimName("roles"); jwtGrantedAuthoritiesConverter.setAuthorityPrefix(""); JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter(); jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(jwtGrantedAuthoritiesConverter); return jwtAuthenticationConverter; } } |
- Añadimos
@EnableMethodSecurity
para habilitar la seguridad y tener control sobre el acceso y los permisos de forma más detallada. securityFilterChain
define un filtro de seguridad que garantice que todas las peticiones están autenticadas y que utilicen tokens JWT para la autenticación, extrayendo las authorities del token y estableciéndolas en el contexto de autenticación.jwtAuthenticationConverter
en el que se configura un conversor de Authorities, en este caso los roles, para establecerlos en el contexto de autenticación.
Por último, vamos a crear un controlador y aplicar la capa de seguridad para permitir los accesos según los roles del usuario autenticado:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
@RestController @RequestMapping("/resources") public class ResourceController { @GetMapping("/user") @PreAuthorize("hasAuthority('ROLE_USER')") public ResponseEntity user(Authentication authentication) { return ResponseEntity.ok(authentication.getName() + " access"); } @GetMapping("/admin") @PreAuthorize("hasAuthority('ROLE_ADMIN')") public ResponseEntity admin(Authentication authentication) { return ResponseEntity.ok(authentication.getName() + " access"); } } |
Con la notación @PreAuthorize("hasAuthority('roleName')")
estamos indicando que el método solo puede ejecutarse si el usuario autenticado tiene el authority (rol) especificado.
Probando nuestro Resource Server
Podemos comprobar su funcionamiento realizando el mismo flujo de autorización previo y utilizar el access_token que nos devuelve la petición con el rol para hacer las llamadas a los endpoints securizados:
Como nuestro usuario tiene el rol «ADMIN», la petición funciona correctamente.
En el caso llamar al endpoint de user, nos devolverá de manera acertada un 403 Forbidden
ya que no poseemos este rol.
Conclusiones
Spring Authorization Server y Spring Resource Server proporcionan un sólido framework para implementar la autenticación y autorización en aplicaciones basadas en Spring. Estas herramientas ofrecen una solución integral para gestionar la seguridad y el acceso a los datos, permitiendo a los desarrolladores implementar fácilmente flujos de autenticación y autorización en sus aplicaciones.