Securizar una aplicación utilizando el estándar OAuth 2.0 mediante Spring Authorization Server y Spring Resource Server

5
5193

Índice de contenidos

  1. Introducción
  2. Spring Authorization Server
    2.1 Dependencias
    2.2 Configuración
    2.3 Probando nuestro Authorization Server
  3. Spring Resource Server
    3.1 Dependencias
    3.2 Configuración
    3.3 Probando nuestro Resource Server
  4. 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.

<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:

 server.port=9000

A continuación, mediante una clase de @Configuration de Spring, vamos a añadir los componentes necesarios:

@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.
    @Bean 
    public UserDetailsService userDetailsService() {
	    UserDetails userDetails = User.withDefaultPasswordEncoder()
			    .username("admin")
			    .password("password")
			    .roles("ADMIN")
			    .build();

	    return new InMemoryUserDetailsManager(userDetails);
    }
  • userDetailsService: crea un objeto UserDetailsManager en memoria que contiene los detalles de un usuario de prueba.
    @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 objeto RegisteredClientRepository 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: utilizamos  ClientAuthenticationMethod.CLIENT_SECRET_BASIC, el cliente envía su ClientId y ClientSecret en el encabezado de la solicitud utilizando la autenticación básica (Basic Authentication). Se envía el ClientId y ClientSecret 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 esta redirectUri está registrada en el servidor. En este caso utilizamos esa ya que vamos a apoyarnos en oauthdebugger para probar el flujo.
    @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 objeto JWKSource 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.

    @Bean 
	public AuthorizationServerSettings authorizationServerSettings() {
		return AuthorizationServerSettings.builder().build();
	}

}
  • authorizationServerSettings: crea un objeto AuthorizationServerSettings 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:


@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-configurationque nos devolverá lo siguiente:

authorizacion server configuration

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:

aouthdebugger authorize request

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 el nonce 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ámetro state 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:

spring security login page

Una vez hagamos login nos redirigirá de nuevo a oauthdebugger el cual nos devolverá el Authorization code que utilizaremos para recuperar los tokens.

oauthdebugger authorization code flow success

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:basic authorization
  • 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.authorization response access-token information

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

<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.

spring.security.oauth2.resourceserver.jwt.issuer-uri=http://localhost:9000

Seguidamente, una clase de @Configuration para establecer los componentes necesarios:


@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:

@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:

spring resource call admin endpoint

Como nuestro usuario tiene el rol «ADMIN», la petición funciona correctamente.

spring resource call user endpoint

En el caso llamar al endpoint de user, nos devolverá de manera acertada un 403 Forbidden ya que no poseemos este rol.

Conclusiones

En este tutorial, hemos aprendido mediante Spring Authorization Server y Spring Resource Server a implementar fácilmente flujos de autenticación y autorización en una aplicación.
El código utilizado está disponible en el siguiente repositorio.

5 COMENTARIOS

  1. Hola, esta bien pero como se puede acceder desde angular, tambien si se puede poner el servidor de recursos y de autorizacion en el mismo projecto y no dos proyectos separados con puertos distintos

  2. Muy bien explicado, gracias por subir el repo, me di cabezazos durante un rato y luego de ver tu repo me di cuenta que el problema lo estaba haciendo mi jdk22

DEJA UNA RESPUESTA

Por favor ingrese su comentario!

He leído y acepto la política de privacidad

Por favor ingrese su nombre aquí

Información básica acerca de la protección de datos

  • Responsable:
  • Finalidad:
  • Legitimación:
  • Destinatarios:
  • Derechos:
  • Más información: Puedes ampliar información acerca de la protección de datos en el siguiente enlace:política de privacidad