Versiones comparadas

Clave

  • Se ha añadido esta línea.
  • Se ha eliminado esta línea.
  • El formato se ha cambiado.

Estado
colourGreen
titleNUEVO

La configuración de seguridad estándar de FundeWebJS permite solamente validar los tokens JWT de OAUTH obtenidos desde micampus.

...

Advertencia

Solo deben realizarse estos dos pasopasos, el resto de código del método debe permanecer tal cuál está en el ejemplo.

...

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 AuthorizedUnauthorized
  • 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).

...

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 )

    Bloque de código
    @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


    Bloque de código
    @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:


    Expandir
    titleVer ejemplo...


    Bloque de código
    languagejava
    @GetMapping( "/claimsTest" )
    @PreAuthorize("@umuJwtAuth.hasClaim(authentication, 'grant_type', 'CLIENT_CREDENTIALS')")
    public ResponseEntity<String> validateClaimTest( Jwt jwt ) {
    		return ResponseEntity.ok( "Claims validados" );
    }
     

-- Respuesta de error --

...



  • Info

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

    Bloque de código
    @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" );
    }



    Advertencia

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

    Bloque de código
    languagejson
    {
        "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

Advertencia

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.

  • 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"

La diferencia con MvcMatchers es que con esta configuración la validación se realiza al final, justo antes de entrar al método, y que la aplicación puede tomar el control del manejo de la excepción usando alguno de los mecanismos de Manejo de Errores en FundeWebJS para capturar la excepción AccessDeniedException.

Por ejemplo , podríamos añadir a la respuesta un body y una cabecera propia con este ExceptionHandler: 

Bloque de código
languagejava
themeEclipse
titleExceptionHandler para AccessDeniedException
	@ExceptionHandler( AccessDeniedException.class )
	public ResponseEntity<ResponseException> accessDeniedException( HttpServletRequest request,
		AccessDeniedException e ) throws IOException {
        
        ....
		MiDTOdeError responseErrorBody = new MiDTOdeError([CON LOS ATRIBUTOS QUE QUIERA SACAR EN EL BODY]) //Puede usarse la clase BearerTokenError y BearerTokenErrors para generarlo.

		Map<String, String> parameters = new LinkedHashMap<>();
		if ( request.getUserPrincipal() instanceof AbstractOAuth2TokenAuthenticationToken ) {
			parameters.put( "error", BearerTokenErrorCodes.INSUFFICIENT_SCOPE );
			parameters.put( "error_description",
					"The request requires higher privileges than provided by the access token." );
			parameters.put( "error_uri", "https://tools.ietf.org/html/rfc6750#section-3.1" );
		}
		String wwwAuthenticate = computeWWWAuthenticateHeaderValue( parameters );

		HttpHeaders headers = new HttpHeaders();
		headers.add( HttpHeaders.WWW_AUTHENTICATE, wwwAuthenticate );
		headers.add( "UMU-Authenticate", parameters.get( "error" ) );

		return new ResponseEntity<>( responseErrorBody, headers, HttpStatus.FORBIDDEN );

	}

	private static String computeWWWAuthenticateHeaderValue( Map<String, String> parameters ) {
		StringBuilder wwwAuthenticate = new StringBuilder();
		wwwAuthenticate.append( "Bearer" );
		if ( !parameters.isEmpty() ) {
			wwwAuthenticate.append( " " );
			int i = 0;
			for ( Map.Entry<String, String> entry : parameters.entrySet() ) {
				wwwAuthenticate.append( entry.getKey() ).append( "=\"" ).append( entry.getValue() ).append( "\"" );
				if ( i != ( parameters.size() - 1 ) ) {
					wwwAuthenticate.append( ", " );
				}
				i++;
			}
		}
		return wwwAuthenticate.toString();
	}

3. Buenas prácticas

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

Bloque de código
languagetext
themeConfluence
titleRFC Oauth - Error Codes
collapsetrue
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), the resource server SHOULD NOT include an error code or other error information. For example: HTTP/1.1 401 Unauthorized WWW-Authenticate: Bearer realm="example"


3. Documentar con Springdoc-OPENAPI

...

Bloque de código
languagejava
themeEclipse
@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 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:

Image Added

Info

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

https://docs.spring.io/spring-security/reference/servlet/oauth2/resource-server/jwt.html#oauth2resourceserver-jwt-validation-custom

...

Contenido por etiqueta
showLabelsfalse
max5
spaces~ramon.ginel@ticarum.es
showSpacefalse
sortmodified
reversetrue
typepage
cqllabel in ("oauth2","fundewebjs","authorization_code_pkcs","spring-boot","scopes","client_credentials","autorización") and type = "page" and space = "~ramon.ginel@ticarum.es"
labelsspring-boot fundewebjs autorización scopes oauth2 client_credentials authorization_code_pkcs

...