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, queremos querremos ampliar la seguridad de un endpoint o de toda nuestra APInuestros endpoints.
Por ejemplo: validación del tokens OAUTH del flujo CLIENT CREDENTIALS parala comunicación entre APIs FundeWeb y FundeWebJS.
...
| Info |
|---|
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/. |
...
| Tabla de contenidos |
|---|
Guía detallada
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
...
| Bloque de código | ||||
|---|---|---|---|---|
| ||||
#Valida los scopes 'micampus' y 'miotroscope'
server.scopes=SCOPE_micampus,SCOPE_miotroscope |
2. Configurar endpoints por scope
...
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:
Bloque de código //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:
| Bloque de código |
|---|
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, MI_CLAIM_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,..);
| Advertencia |
|---|
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 Authorized
- 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).
| Advertencia |
|---|
Repetimos: ESTA CONFIGURACIÓN GLOBAL VALIDARÁ LOS CLAIMS CONFIGURADOS EN TODOS LOS ENDPOINTS DEL API. |
2. Configurar endpoints por scope
Con la configuración del punto 1 se configura TODA la aplicación para permitir tokens que contengan cualquiera de los scopes configurados.
Esto significa que un token obtenido desde micampus por cualquier usuario sería válido para acceder a cualquier endpoint.
Si hemos tenido que configurar varios scopes, lo normal es que está situación sea no deseada (no queremos que cualquier token sirva para acceder a endpoints que requieran un nivel diferente de acceso).
En este paso se explica cómo configurar nuestra aplicación para securizar diferentes endpoints con scopes distintos.
Existen dos alternativas :
- Configurar los mvcMatchers en el SecurityConfig.
- Utilizar la anotación PreAuthorize en cada método Java de nuestro RestController.
Configuración con PreAuthorize
La
- Configurar los mvcMatchers en el SecurityConfig.
- Utilizar la anotación PreAuthorize en cada método Java de nuestro RestController.
Configuración de MvcMatchers
La primera forma de configuración es añadir un mvcMatcher en el método configure de nuestra clase "SecurityConfig".
Se añade el mvcMatcher que capture la/s rutas/s a securizar con nuestro nuevo scope (antes del mvcMatcher configurado por defecto para private-apiPath) y se le añade .hasAuthority("SCOPE_miotroscope").
Por ejemplo:
| Bloque de código | ||||||
|---|---|---|---|---|---|---|
| ||||||
@Override
protected void configure( HttpSecurity http ) throws Exception {
http.requestMatchers().antMatchers( "/public/**" ).and().requestMatchers().antMatchers( apiPath + "/**" ).and()
.sessionManagement().sessionCreationPolicy( SessionCreationPolicy.STATELESS )
.and().cors()
.and().csrf().disable().authorizeRequests()
.mvcMatchers( "/public/**" ).permitAll()
.mvcMatchers( apiPath + "**/mirecurso/misubrecursoconotroscope/**" ).hasAuthority( "SCOPE_miotroscope" )
.mvcMatchers( apiPath + "/**" ).hasAnyAuthority( serverScopes ).anyRequest().authenticated()
.and().addFilterAfter( loggingFilterBean(), BearerTokenAuthenticationFilter.class )
.oauth2ResourceServer().jwt();
} |
...
- Status Code: 403 Forbidden
- Body: VACIO
- Cabecera WWW-Authenticate: Bearer error="insufficient_scope", error_description="The request requires higher privileges than provided by the access token.", error_uri="https://tools.ietf.org/html/rfc6750#section-3.1"
| Advertencia |
|---|
¡¡OJO!! Como la librería spring-security-oauth2-resource-server ha tomado el control, cualquier otro manejo de la excepción configurado según Manejo de Errores en FundeWebJS NO TENDRÁ EFECTO . |
Para sobreescribir el comportamiento de la respuesta de error habrá que crear una clase AccessDeniedExceptionHandler propia y configurarla en el mismo método configure de nuesta clase "SecurityConfig" añadiendo el exceptionHandling de esta forma:
| Bloque de código | ||||
|---|---|---|---|---|
| ||||
...
.mvcMatchers( apiPath + "**/mirecurso/misubrecursoconotroscope/**" ).hasAuthority( "SCOPE_miotroscope" )
.mvcMatchers( apiPath + "/**" ).hasAnyAuthority( serverScopes ).anyRequest().authenticated()
.and().exceptionHandling().accessDeniedHandler( new MiOtroAccessDeniedHandler())
.and().addFilterAfter( loggingFilterBean(), BearerTokenAuthenticationFilter.class )
.oauth2ResourceServer().jwt()
... |
| Info |
|---|
Tomar de ejemplo la clase BearerTokenAccessDeniedHandler (handler por defecto de spring-security-oauth2-resource-server) para crear nuestra clase AccessDeniedExceptionHandler propia. |
Configuración con PreAuthorize
La segunda forma para configurar endpoints y securizarlos por scope es utilizando la anotación de spring-security @PreAuthorize.
...
Como se ha mencionado en los apartados de respuesta de error, los mecanismos estándar de manejo de excepciones de spring-security y spring-security-oauth2-resource-server devuelven siempre un 403 Forbidden, con body vacío y con la cabecera WWW-Authenticate informando del error "insufficient_scope".
Esta configuración se puede sobreescribir pero, siguiendo el RFC de Oauth se recomienda mantener el 403 Forbidden y la cabecera con "insufficient_scope" => "The resource server SHOULD respond with the HTTP 403 (Forbidden) status code and MAY include the "scope" attribute with the scope necessary to access the protected resource".
-resource-server devuelven siempre un 403 Forbidden, con body vacío y con la cabecera WWW-Authenticate informando del error "insufficient_scope".
Esta configuración se puede sobreescribir pero, siguiendo el RFC de Oauth se recomienda mantener el 403 Forbidden y la cabecera con "insufficient_scope" => "The resource server SHOULD respond with the HTTP 403 (Forbidden) status code and MAY include the "scope" attribute with the scope necessary to access the protected resource".
| Bloque de código | ||||||||
|---|---|---|---|---|---|---|---|---|
| ||||||||
3.1. Error Codes
When a request fails, the resource server responds using the
appropriate HTTP status code (typically, 400, 401, 403, or 405) and
includes one of the following error codes in the response:
invalid_request
The request is missing a required parameter, includes an
unsupported parameter or parameter value, repeats the same
parameter, uses more than one method for including an access
token, or is otherwise malformed. The resource server SHOULD
respond with the HTTP 400 (Bad Request) status code.
invalid_token
The access token provided is expired, revoked, malformed, or
invalid for other reasons. The resource SHOULD respond with
the HTTP 401 (Unauthorized) status code. The client MAY
request a new access token and retry the protected resource
request.
insufficient_scope
The request requires higher privileges than provided by the
access token. The resource server SHOULD respond with the HTTP
403 (Forbidden) status code and MAY include the "scope"
attribute with the scope necessary to access the protected
resource.
If the request lacks any authentication information (e.g., the client
was unaware that authentication is necessary or attempted using an
unsupported authentication method) | ||||||||
| Bloque de código | ||||||||
| ||||||||
3.1. Error Codes When a request fails, the resource server respondsSHOULD using theNOT appropriateinclude HTTPan statuserror code (typically, 400, 401, 403, or 405) and or other error information. For example: HTTP/1.1 401 Unauthorized includes one of the following error codes in the response: WWW-Authenticate: Bearer realm="example" |
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:
| Bloque de código | ||||
|---|---|---|---|---|
| ||||
@Configuration public class OpenApiConfiguration { @Bean public OperationCustomizer operationCustomizer() { invalid_request The request is missing a required parameter, includes an unsupported parameterreturn or( parameteroperation, value,handlerMethod repeats) the-> same{ parameter, uses more thanOptional<PreAuthorize> onepreAuthorizeAnnotation method= forOptional including an access token, or is otherwise malformed. The resource server SHOULD .ofNullable( handlerMethod.getMethodAnnotation( PreAuthorize.class ) ); respond with the HTTP 400 (Bad Request) status code. invalid_token StringBuilder sb = new StringBuilder(); The access token provided isif expired, revoked, malformed, or( preAuthorizeAnnotation.isPresent() ) { invalid for other reasons. The resource SHOULD respondPattern with patternAuthority = Pattern.compile( "'SCOPE_\\w+'" ); the HTTP 401 (Unauthorized) status code. The client MAY Matcher matcher = patternAuthority.matcher( ( preAuthorizeAnnotation.get() request a new access token and retry the protected resource ).value() ); if ( request. insufficient_scopematcher.find() ) { The request requires higher privileges than provided by the sb.append( "Este endpoint requiere **SCOPE access" token). The resource server SHOULD respond with the HTTPappend( matcher.group().replaceAll( "SCOPE_", "" ) ) 403 (Forbidden) status code and MAY include the "scope" attribute with the scope necessary to access the protected .append( "**<br />" ); resource. If} the request lacks any authentication information (e.g., the client was unaware that authentication isPattern necessarypatternClaims or attempted using an= Pattern.compile( "hasClaim\\(authentication,'\\w+','\\w+'\\)" ); unsupported authentication method), the resource server SHOULD NOT include an error codematcher or other error information. For example: HTTP/1.1 401 Unauthorized = patternClaims.matcher( ( preAuthorizeAnnotation.get() ).value() ); WWW-Authenticate: Bearer realm="example" |
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:
| Bloque de código | ||||
|---|---|---|---|---|
| ||||
@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() ) { Pattern patternAuthority = Pattern.compile( "'SCOPE_\\w+'" ); Matcher matcher = patternAuthority.matcher( ( preAuthorizeAnnotation.get() ).value() ); if ( matcher.find() ) { if ( matcher.find() ) { String[] claims = matcher.group().replaceAll( "hasClaim\\(authentication,|\\)", "" ).split( "," ); sb.append( "Este endpoint requiere: **SCOPECLAIM " ).append( claims[0] matcher).group().replaceAll( "SCOPE_", "" ) ) 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:
Referencias
https://wwwdocs.baeldungspring.comio/spring-security-method-security/reference/servlet/oauth2/resource-server/jwt.html#oauth2resourceserver-jwt-validation-custom
Artículos Relacionados
| Contenido por etiqueta | ||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
...
