Question
Is there a way to globally handle Spring Messaging MessageDeliveryException caused by error (usualy insufficient authorities) in Spring WebSocket module?
Use case
I have implemented Spring WebSockets over STOMP to support ws connection in my webapp. To secure websocket endpoint I have created interceptor that authorizes user to start STOMP session at STOMP CONNECT time (as suggested in Spring documentation here in 22.4.11 section):
#Component
public class StompMessagingInterceptor extends ChannelInterceptorAdapter {
// Some code not important to the problem
#Override
public Message<?> preSend(Message<?> message, MessageChannel channel) {
StompHeaderAccessor headerAccessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
switch (headerAccessor.getCommand()) {
// Authenticate STOMP session on CONNECT using jwt token passed as a STOMP login header - it's working great
case CONNECT:
authorizeStompSession(headerAccessor);
break;
}
// Returns processed message
return message;
}
// Another part of code not important for the problem
}
and included spring-security-messaging configuration to add some fine-grained control over authorities when messaging:
#Configuration
public class WebSocketSecurityConfig extends AbstractSecurityWebSocketMessageBrokerConfigurer {
#Override
protected void configureInbound(MessageSecurityMetadataSourceRegistry messages) {
messages
.simpTypeMatchers(
SimpMessageType.CONNECT,
SimpMessageType.DISCONNECT,
SimpMessageType.HEARTBEAT
).authenticated()
.simpSubscribeDestMatchers("/queue/general").authenticated()
.simpSubscribeDestMatchers("/user/queue/priv").authenticated()
.simpDestMatchers("/app/general").authenticated()
.simpDestMatchers("/user/*/queue/priv").hasAuthority("ADMIN")
.anyMessage().denyAll();
}
#Override
protected boolean sameOriginDisabled() {
return true;
}
}
First of all - this configuration works as expected, the problem is when some security exception happens during websocket communication (say user without admin authority tries to send message on "/user/{something}/queue/priv" endpoint) it will end in org.springframework.messaging.MessageDeliveryException being rised and:
Full exception stack trace being written down to my server log
Returning STOMP ERROR frame containing part of stack trace as it's message field.
What I would like to do is catching (if possible globally) DeliveryException, checking what caused it and accoridingly to that create my own message for returning in STOMP ERROR frame (lets say with some error code like just 403 to mimic HTTP) and instead of throwing original exception further just logging some warning with my logger. Is it possible?
What I tried
When looking for solution I found some people using #MessageExceptionHandler to catch messaging exceptions, Spring 4.2.3 (which is version I use) documentation mentions it only once here in 25.4.11 section. I tried to use it like this:
#Controller
#ControllerAdvice
public class WebSocketGeneralController {
...
#MessageExceptionHandler
public WebSocketMessage handleException(org.springframework.messaging.MessageDeliveryException e) {
WebSocketMessage errorMessage = new WebSocketMessage();
errorMessage.setMessage(e.getClass().getName());
return errorMessage;
}
}
but it seems like method isn't called at any point (tried catching different exceptions, just Exception including - no results). What else should I look into?
#ControllerAdvice and #MessageExceptionHandler are working on business-logic level (like #MessageMapping or SimpMessagingTemplate).
To handle STOMP exceptions, you need to set STOMP error handler in STOMP registry:
#Configuration
#EnableWebSocketMessageBroker
class WebSocketConfiguration : WebSocketMessageBrokerConfigurer {
override fun configureMessageBroker(registry: MessageBrokerRegistry) {
// ...
}
override fun registerStompEndpoints(registry: StompEndpointRegistry) {
registry.addEndpoint("/ws")
// Handle exceptions in interceptors and Spring library itself.
// Will terminate a connection and send ERROR frame to the client.
registry.setErrorHandler(object : StompSubProtocolErrorHandler() {
override fun handleInternal(
errorHeaderAccessor: StompHeaderAccessor,
errorPayload: ByteArray,
cause: Throwable?,
clientHeaderAccessor: StompHeaderAccessor?
): Message<ByteArray> {
errorHeaderAccessor.message = null
val message = "..."
return MessageBuilder.createMessage(message.toByteArray(), errorHeaderAccessor.messageHeaders)
}
})
}
}
It does not work because of #ControllerAdvice catch exception from the request that passed dispatcher servlet. When you secure your endpoint and someone makes an unauthorized request it does not pass through dispatcher servlet. The request is caught by spring interceptors.
Related
I am using OpenFeign client in Spring Boot without using Ribbon or Eureka. I created a custom error decoder which handles response errors as intended but connection refused errors seem to bypass my custom decoder.
P.S. When my remote service is up, I can make requests and receive responses.
I am new to Java and Spring and I am wondering if I need to wrap all my calls with try catch, or adding my custom error handler should be catching the error since it seems cleaner to handle all errors in one place
public class FeignErrorDecoder implements ErrorDecoder {
private final ErrorDecoder defaultErrorDecoder = new Default();
#Override
public Exception decode(String methodKey, Response response) {
if (response.status() >= 400 && response.status() <= 499) {
//handle with custom exception
}
if (response.status() >=500) {
//handle with custom exception
}
return defaultErrorDecoder.decode(methodKey, response);
}
}
#Configuration
public class FeignConfig {
//other beans here
#Bean
public ErrorDecoder feignErrorDecoder() {
return new FeignErrorDecoder();
}
}
I have a simple Spring Boot REST service for the IFTTT platform. Each authorized request will contain a header IFTTT-Service-Key with my account's service key and I will use that to either process the request or return a 401 (Unauthorized). However, I only want to do this for select endpoints -- and specifically not for ANY of the Spring actuator endpoints.
I have looked into Spring Security, using filters, using HandlerInterceptors, but none seem to fit what I am trying to do exactly. Spring security seems to come with a lot of extra stuff (especially the default user login), filters don't really seem to match the use case, and the handler interceptor works fine but I would have to code logic in to watch specific URLs and ignore others.
What is the best way to achieve what I am trying to do?
For reference, this is the code I have now:
public class ServiceKeyValidator implements HandlerInterceptor {
private final String myIftttServiceKey;
public ServiceKeyValidator(#Value("${ifttt.service-key}") String myIftttServiceKey) {
this.myIftttServiceKey = myIftttServiceKey;
}
#Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// TODO will have to put logic in to skip this when actuator endpoints are added
String serviceKeyHeader = request.getHeader("IFTTT-Service-Key");
if (!myIftttServiceKey.equals(serviceKeyHeader)) {
var error = new Error("Incorrect value for IFTTT-Service-Key");
var errorResponse = new ErrorResponse(Collections.singletonList(error));
throw new UnauthorizedException(errorResponse);
}
return HandlerInterceptor.super.preHandle(request, response, handler);
}
}
You need to add filtering for the required endpoints in the place where you register your HandlerInterceptor.
For example:
#EnableWebMvc
#Configuration
public class AppConfig implements WebMvcConfigurer {
#Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(
new ServiceKeyValidator())
.addPathPatterns("/ifttt/**")
.excludePathPatterns("/actuator/**");
}
}
You can use different URLs path matchers to filter which URL endpoints must be handled by your interceptor and which are not. As the method addPathPatterns returns InterceptorRegistration object that configures this.
As far as Spring security is concerned, it is completely new to me. I found many sources online describing how to set up basic security and was able to get HTTPS REST calls to work with the following configuration on the server side:
#Configuration
#EnableWebSecurity
#EnableConfigurationProperties(SecurityAuthProperties.class)
public class ServerSecurityConfiguration extends WebSecurityConfigurerAdapter {
private final SecurityAuthProperties properties;
#Autowired
public ServerSecurityConfiguration(SecurityAuthProperties properties) {
this.properties = properties;
}
#Override
public void configure(HttpSecurity http) throws Exception {
properties.getEndpoints().forEach((key, value) -> {
try {
for (HttpMethod method : value.getMethods()) {
http.authorizeRequests().antMatchers(method, value.getPath()).permitAll().and()
.httpBasic().and().csrf().disable();
}
} catch (Exception e) {
throw new SecurityConfigurationException(
"Problem encountered while setting up endpoint restrictions", e);
}
});
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
}
Upon closer inspection, though, it looks as though some portion (not sure how much) is actually being disabled. Could this be why it allows access from a client?
When I modified the configuration to what follows below, I always get the response "Forbidden".
#Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().antMatchers("/rst/**").permitAll();
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
It seems to me that this code would allow access to anything in the path /rst and under, yet the opposite seems to be true. What am I missing?
Note: Another thing I should mention is that there is currently no "user" authentication. The "client" is not web based, but is a separate Spring Boot service that has its own client-side security configuration.
Update:
Here is one of the controllers:
#RestController
#RequestMapping("/rst/missionPlanning")
public class MissionPlannerController {
#Autowired
private MissionPlanner service;
#Autowired
private ThreadPoolTaskExecutor executor;
#PostMapping(value = "/planMission", produces = MediaType.APPLICATION_JSON_VALUE)
public DeferredResult<ResponseEntity<GeneralResponse>> planMission() {
DeferredResult<ResponseEntity<GeneralResponse>> result = new DeferredResult<>(60000L);
executor.execute(new Runner(result));
return result;
}
private class Runner implements ITask {
private DeferredResult<ResponseEntity<GeneralResponse>> result;
public Runner(DeferredResult<ResponseEntity<GeneralResponse>> result) {
this.result = result;
}
#Override
public void executeTask() {
// Invoke service and set result.
result.setResult(ResponseEntity.ok(service.planMission()));
}
}
}
Update:
Interesting. I found an example from another SO post (Security configuration with Spring-boot) that seems to work. The only thing that's different is the disabling of CSRF.
I see that stands for Cross-Site Request Forgery, but I don't really understand what that is, whether I should have it enabled, and if I do, how do I get it to work then?
#Override
public void configure(HttpSecurity http) throws Exception {
http.csrf().disable().authorizeRequests().antMatchers("/rst/**").permitAll();
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
There could be something wrong with how you've set up your controller. Does your controller that contains that path have #RequestMapping("/rst")?
It'd be helpful if you updated your post with what your controller looks like.
Edit:
It seems your issue was the type of request being made if you had to disble CSRF.
CSRF requires a token to be specified on all request methods that can cause a change (i.e. POST, PUT, DELETE, PATCH, but not GET).
The reason for this is that when you control the web page, it adds a layer of security where only you are allowed to make these API calls. Without the CSRF token specified in the request, a malicious user will not be able to make that request to your service since the CSRF token is impossible to guess.
You can read more about it here:
https://docs.spring.io/spring-security/site/docs/3.2.0.CI-SNAPSHOT/reference/html/csrf.html#csrf-include-csrf-token
And here: https://www.baeldung.com/spring-security-csrf
We have some security tests around our company in which the apps are tested in different ways. One of them is to try a CONNECT like:
telnet localhost 8080
CONNECT http://test.com HTTP/1.1
and in that case to return a 400 or 405. The existing Spring MVC apps return a 400, but it seems that our new Spring WebFlux:5.1.2.RELEASE app(Netty server) return a 200.
The first thing that I did was to shift to latest spring WebFlux version:5.1.4.RELEASE, and in this case the response http error code was:404, but was still not good enough. So I tried to:
Create a webFilter
Modify the CORS filter
Modify Spring Security chain
,but all these solutions failed. How'd you fix that? It would be a good idea to create a custom http handler ?
I've created a custom http handler:
public class AppContextPathCompositeHandler extends ContextPathCompositeHandler {
public AppContextPathCompositeHandler(Map<String, ? extends HttpHandler> handlerMap) {
super(handlerMap);
}
#Override
public Mono<Void> handle(ServerHttpRequest request, ServerHttpResponse response) {
if (HttpMethod.CONNECT.name().equals(request.getMethodValue())) {
response.setStatusCode(HttpStatus.METHOD_NOT_ALLOWED);
return response.setComplete();
}
return super.handle(request, response);
}
}
and it was configured like:
#Configuration
public class NettyReactiveWebServerConfig extends NettyReactiveWebServerFactory {
#Value("${server.context-path}")
private String contextPath;
#Override
public WebServer getWebServer(HttpHandler httpHandler) {
Map<String, HttpHandler> handlerMap = new HashMap<>();
handlerMap.put(contextPath, httpHandler);
return super.getWebServer(new AppContextPathCompositeHandler(handlerMap));
}
}
Im new to Spring Integration, I have to get a list of online agents from 3rd party web services, i tried to configure spring integration to get it, but for the channel part, i not really sure how to configure it.
My original configuration was the following, i copied from a sample that use to send request to 3rd party web services:
public interface WebServiceGateway {
#Gateway(requestChannel = "getStatusChannel")
public String getStatus(String var); <------ being forced to send something
}
In my integration configuration,
#Configuration
public class IntegrationConfiguration {
#Bean
public MessageChannel getStatusChannel() {
return MessageChannels.direct().get();
}
}
The problem is, im not sending any parameter to the webservices, in requestChannel it force me to do so, so i modified the gateway part:
public interface WebServiceGateway {
#Gateway(replyChannel = "getStatusChannel")
public String getStatus();
}
This part remains unchanged:
#Configuration
public class IntegrationConfiguration {
#Bean
public MessageChannel getStatusChannel() {
return MessageChannels.direct().get();
}
}
It prompted me java.lang.IllegalStateException: receive is not supported, because no pollable reply channel has been configured, why can't i use MessageChannel as the reply channel? How should i configure the IntegrationConfiguration?
Please go through this https://spring.io/blog/2014/11/25/spring-integration-java-dsl-line-by-line-tutorial
All you need is to define an IntegrationFlow like below:
IntegrationFlows.from(requestchannel())
.handle("requestHandler","handleInput")
.channel(replyChannel())
.get();