NUEVO
La configuración de seguridad estándar de FundeWebJS permite solamente validar los tokens JWT de OAUTH obtenidos desde micampus.
En ocasiones una API necesitará validar otro tipo de tokens OAUTH obtenidos desde otras aplicaciones, scopes u otros flujos OAUTH o, simplemente, querremos ampliar la seguridad de nuestros endpoints.
Por ejemplo: validación del tokens OAUTH del flujo CLIENT CREDENTIALS para la comunicación entre APIs FundeWeb y FundeWebJS.
La autorización por Scope se puede configurar siguiendo la guía Autorización por Scopes .
Esta página trata sobre la configuración de nuestra API FundeWebJS para validar tokens que contengan claims específicos.
Los claims son cada uno de los atributos que forman el payload del token JWT (RFC JWT). Puede verse el contenido de un JWT desde la web https://jwt.io/.
Guía detallada
La librería fundewebjs-security contiene un fichero FundeWebJSSecurityConfig por defecto. Para utilizar otra configuración hay que realizar los siguientes pasos:
- Si no existe en nuestro proyecto, crear una clase SecurityConfig.java nueva a partir del código de FundeWebJSSecurityConfig (ubicada en la librería fundewebjs-security).
En el application.properties (de local y de los entornos en Helm Chart), añadir la siguiente property:
//Deshabilita SecurityFilterChain por defecto fdwjs.starter.security.enable=false
A continuación se detallan las configuraciones específicas para validar claims:
Existen dos formas de configurar esta validación:
- Global: Se configura para toda la aplicación y todos los endpoints realizarán la misma validación.
- Por método: Sólo el endpoint configurado validará el claim
1. Configuración global
Con esta configuración se realizará la validación de claims en TODOS los endpoints de la aplicación.
En la clase SecurityConfig.java (ver pasos 1 y 2 anteriores), añadir el siguiente Bean JwtDecoder:
import org.springframework.security.oauth2.core.DelegatingOAuth2TokenValidator;
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.JwtValidators;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;.....@Configuration
public class SecurityConfig {
............
@Bean
JwtDecoder jwtDecoder() {
NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withJwkSetUri( jwkSetUri )
.jwsAlgorithm( SignatureAlgorithm.RS512 ).build();
OAuth2TokenValidator<Jwt> clientIdValidator = new FundeWebJSClaimValidator( "client_id", "tramites" );
OAuth2TokenValidator<Jwt> grantValidator = new FundeWebJSClaimValidator( "grant_type", "CLIENT_CREDENTIALS" );
OAuth2TokenValidator<Jwt> withIssuer = JwtValidators.createDefaultWithIssuer( issuerUri );
OAuth2TokenValidator<Jwt> withAudience = new DelegatingOAuth2TokenValidator<>( withIssuer, clientIdValidator,
grantValidator );
jwtDecoder.setJwtValidator( withAudience );
return jwtDecoder;
}
Este método es un ejemplo para validar que el token se haya obtenido para el flujo Oauth "CLIENT_CREDENTIALS" y para el servicio "tramites".
Para añadir la validación de claim hay que:
- Crear un objeto OAuth2TokenValidator<Jwt> miClaimValidator = new FundeWebJSClaimValidator( MI_CLAIM, VALOR_A_VALIDAR );
con el nombre del claim y el valor a validar en el constructor.
- Añadir el objeto miClaimValidator como otro parámetro más en new DelegatingOAuth2TokenValidator<>( withIssuer, miClaimValidator,..);
Solo deben realizarse estos dos paso, el resto de código del método debe permanecer tal cuál está en el ejemplo.
-- Respuesta de error --
Cuando llegue una petición con un token que no cumpla las restricciones configuradas con los FundeWebJSClaimValidator, la librería spring-security-oauth2-resource-server toma el control y devuelve la respuesta de la siguiente forma:
- Status Code: 401 Unauthorized
- Body: VACIO
- Cabecera WWW-Authenticate: Bearer error="invalid_token", error_description="An error occurred while attempting to decode the Jwt: The client_id claim is not valid", error_uri="https://tools.ietf.org/html/rfc6750#section-3.1"
(Siguiendo el ejemplo del bloque de código descrito más arriba).
Repetimos: ESTA CONFIGURACIÓN GLOBAL VALIDARÁ LOS CLAIMS CONFIGURADOS EN TODOS LOS ENDPOINTS DEL API.
2. Configurar endpoints individualmente
Con la configuración del punto 1 se configura TODA la aplicación para permitir tokens con los claims configurados.
Dependiendo de la situación y del diseño de nuestra API, esto puedo ser necesario, pero el caso más habitual será tener diferentes endpoints y querer realizar una validación de claims diferente en cada uno de ellos.
Por ejemplo, en nuestra API que ofrece servicios a miscampus, podremos querer añadir un endpoint que valide tokens del flujo OAUTH CLIENT_CREDENTIALS para el servicio con claim client_id "tramites".
Esta validación específica de un endpoint se realiza de la siguiente manera:
PreAuthorize
Es necesario utilizar la anotación de spring-security @PreAuthorize con la clase propia de FundeWebJS creada para tal efecto FundeWebJsJwtClaimPreAuthorizer (@umuJwtAuth):
Anotar la clase SecurityConfig con @EnableGlobalMethodSecurity( prePostEnabled = true )
@Configuration @EnableGlobalMethodSecurity( prePostEnabled = true ) public class SecurityConfig {....
Anotar el método java que publica nuestro endpoint (o directamente todo el RestController en caso de hacer una clase específica validar ese claim) con @PreAuthorize @umuJwtAuth.hasClaim
@PreAuthorize("@umuJwtAuth.hasClaim(authentication, 'MI_CLAIM', 'VALOR_A_VALIDAR')")
Por ejemplo, a este endpoint sólo se podrá acceder con tokens del flujo Oauth: CLIENT_CREDENTIALS:La expresión dentro del PreAuthorize evalúa un booleano. Esto significa que:
Pueden añadirse varias validaciones de claims en la misma anotación PreAuthorize añadiendo otro @umuJwtAuth.hasClaim separando cada comprobación por "and" (o por un "or" en caso de querer una u otra validación).
Por ejemplo, a este endpoint sólo se podrá acceder con tokens del flujo Oauth: "CLIENT_CREDENTIALS" Y del servicio: "tramites":@GetMapping( "/claimsTest" ) @PreAuthorize("@umuJwtAuth.hasClaim(authentication, 'grant_type', 'CLIENT_CREDENTIALS') and @umuJwtAuth.hasClaim(authentication, 'client_id', 'tramites')") public ResponseEntity<String> validateClaimTest( Jwt jwt ) { return ResponseEntity.ok( "Claims validados" ); }El primer parámetro del método hasClaim siempre debe ser "authentication". Es la variable de Spring Security con el contexto de seguridad en cada petición.
-- Respuesta de error --
La clase FundeWebJsJwtClaimPreAuthorizer (@umuJwtAuth) lanza una excepción FundeWebJsJwtClaimsAccessDeniedException cuando llegan tokens con claims inválidos.
El código de la librería Fundewebjs-security incluye por defecto un ExceptionHandler para estas excepciones FundeWebJsJwtClaimsAccessDeniedException. Ver clase FundeWebJsRestControllerAdvice.
Cuando llega una petición a un endpoint anotado con PreAuthorize("@umuJwtAuth.hasClaim..") que incluye un token inválido, la respuesta por defecto será::
- Status Code: 401 Unauthorized
Body: JSON con formato org.springframework.security.oauth2.server.resource.BearerTokenError. Por ejemplo:
{ "errorCode": "invalid_token", "description": "An error occurred while attempting to decode the Jwt: The grant_type claim is not valid", "uri": "https://tools.ietf.org/html/rfc6750#section-3.1", "httpStatus": "UNAUTHORIZED", "scope": null }- Cabecera WWW-Authenticate: Bearer error="invalid_token", error_description="An error occurred while attempting to decode the Jwt: The grant_type claim is not valid", error_uri="https://tools.ietf.org/html/rfc6750#section-3.1"
- Cabecera UMU-Authenticate= invalid_token
Si se desea cambiar el formato de la respuesta de error por defecto, puede crearse un ExceptionHandler propio y deshabilitar la clase FundeWebJsRestControllerAdvice estableciendo la property: fdwjs.api.security.claims.exceptionhandler.enable=false.
3. Documentar con Springdoc-OPENAPI
Utilizando @PreAuthorize es posible configurar la generación de la documentación para que aparezca el Scope requerido.
Para hacerlo solo sería necesario añadir una clase de configuración como ésta:
@Configuration
public class OpenApiConfiguration { @Bean
public OperationCustomizer operationCustomizer() {
return ( operation, handlerMethod ) -> {
Optional<PreAuthorize> preAuthorizeAnnotation = Optional
.ofNullable( handlerMethod.getMethodAnnotation( PreAuthorize.class ) );
StringBuilder sb = new StringBuilder();
if ( preAuthorizeAnnotation.isPresent() ) {
//Documentar auth por scope
Pattern patternAuthority = Pattern.compile( "'SCOPE_\\w+'" );
Matcher matcher = patternAuthority.matcher( ( preAuthorizeAnnotation.get() ).value() );
if ( matcher.find() ) {
sb.append( "Este endpoint requiere **SCOPE " ).append( matcher.group().replaceAll( "SCOPE_", "" ) )
.append( "**<br />" );
}
//Documentar auth por claim
Pattern patternClaims = Pattern.compile( "hasClaim\\(authentication,\\s*'\\w+',\\s*'\\w+'\\)" );
matcher = patternClaims.matcher( ( preAuthorizeAnnotation.get() ).value() );
if ( matcher.find() ) {
String[] claims = matcher.group().replaceAll( "hasClaim\\(authentication,|\\)", "" ).split( "," );
sb.append( "Este endpoint requiere **CLAIM " ).append( claims[0] ).append( ": " )
.append( claims[1] ).append( "**<br />" );
}
sb.append( "<br />" );
}
sb.append( operation.getDescription() );
operation.setDescription( sb.toString() );
return operation;
};
}
}
El resultado de esta configuración se puede ver en la siguiente imagen:
La configuración solo sirve para documentar un solo claim. Si el endpoint tiene varios claims y quieres documentarlos, modifica el pattern y el código matcher.find(). Diviértete un rato. Hombre ya.
Referencias
Artículos Relacionados
No hay ningún contenido con las etiquetas especificadas
