I'm using .springframework.security:spring-security-messaging.
To start with I have a project that is successfully authenticating using oauth...
spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: ...
audiences:
- ...
@Configuration
@EnableWebSecurity
class SecurityConfig(
@Autowired(required = false) private val requestLoggingFilter: RequestLoggingFilter?
) {
@Bean
fun filterChain(http: HttpSecurity): SecurityFilterChain {
http
.csrf { it.disable() }
.oauth2ResourceServer { oauth2 ->
oauth2.jwt { }
}
.authorizeHttpRequests { auth ->
auth
.requestMatchers("/protected/**").authenticated()
.anyRequest().permitAll()
}
return http.build()
}
The basic http JWT functions as expected. So I try to add a web socket example that I can also try...
@Configuration
@EnableWebSocketMessageBroker
class WebsocketConfig : WebSocketMessageBrokerConfigurer {
override fun configureMessageBroker(config: MessageBrokerRegistry) {
config.enableSimpleBroker("/topic")
config.setApplicationDestinationPrefixes("/app")
}
override fun registerStompEndpoints(registry: StompEndpointRegistry) {
registry.addEndpoint("/gs-guide-websocket")
}
}
@Controller
class WebSocketController {
private val logger = LoggerFactory.getLogger(WebSocketController::class.java)
@MessageMapping("/hello")
@SendTo("/topic/greetings")
fun greeting(message: TestMessage): Greeting {
logger.info(message.toString())
return Greeting("Hello, " + message.name + "!")
}
@MessageMapping("/status")
@SendTo("/topic/chat")
fun status(message: String): String {
return "Status: $message"
}
}
Now I am trying to add security so I add the following to the security config...
@Profile("secure")
@Configuration
@EnableWebSocketSecurity
class WebSocketSecurityConfig {
@Bean
fun messageAuthorizationManager(
messages: MessageMatcherDelegatingAuthorizationManager.Builder
): AuthorizationManager<Message<*>> {
messages
// Next 2 lines are required for requests without auth.
// Remove these if all paths require auth
.simpTypeMatchers(SimpMessageType.CONNECT).permitAll()
.simpTypeMatchers(SimpMessageType.DISCONNECT).permitAll()
.simpDestMatchers("/app/status").permitAll()
.simpDestMatchers("/app/hello").authenticated()
.anyMessage().authenticated()
return messages.build()
}
}
When I try sending the non-protected message like
SEND
destination:/app/status
{"name":"test"}
I see that the message is working and being send, however, when I try to call with something like
SEND
destination:/app/hello
Authorization: Bearer ey...
{"name":"test"}
And I still get
.springframework.messaging.MessageDeliveryException: Failed to send message to ExecutorSubscribableChannel[clientInboundChannel]
at .springframework.messaging.support.AbstractMessageChannel.send(AbstractMessageChannel.java:149) ~[spring-messaging-6.2.2.jar:6.2.2]
at .springframework.messaging.support.AbstractMessageChannel.send(AbstractMessageChannel.java:125) ~[spring-messaging-6.2.2.jar:6.2.2]
at .springframework.web.socket.messaging.StompSubProtocolHandler.handleMessageFromClient(StompSubProtocolHandler.java:343) ~[spring-websocket-6.2.2.jar:6.2.2]
...
Caused by: .springframework.security.access.AccessDeniedException: Access Denied
at .springframework.security.messaging.access.intercept.AuthorizationChannelInterceptor.preSend(AuthorizationChannelInterceptor.java:75) ~[spring-security-messaging-6.4.2.jar:6.4.2]
at .springframework.messaging.support.AbstractMessageChannel$ChannelInterceptorChain.applyPreSend(AbstractMessageChannel.java:181) ~[spring-messaging-6.2.2.jar:6.2.2]
at .springframework.messaging.support.AbstractMessageChannel.send(AbstractMessageChannel.java:135) ~[spring-messaging-6.2.2.jar:6.2.2]
... 44 common frames omitted
Same as with no header at all. The same token works fine on a regular web endpoint so what am I missing?
even when I add the following...
logging:
level:
.springframework.security: DEBUG
.springframework.messaging: DEBUG
.springframework.web.socket: DEBUG
.springframework.security.oauth2: TRACE
I changed the endpoint to ws but not I see
2025-02-02T10:47:16.609-05:00 DEBUG 88460 --- [love-monkey] [nio-7080-exec-3] o.s.w.s.s.s.WebSocketHttpRequestHandler : GET /ws
2025-02-02T10:47:16.641-05:00 DEBUG 88460 --- [love-monkey] [nio-7080-exec-3] s.w.s.h.LoggingWebSocketHandlerDecorator : New StandardWebSocketSession[id=e28c3083-e231-c00d-dc20-2cdb4d01ecac, uri=ws://.../ws]
2025-02-02T10:47:18.340-05:00 DEBUG 88460 --- [love-monkey] [nio-7080-exec-4] .s.m.a.i.AuthorizationChannelInterceptor : Authorizing message send
2025-02-02T10:47:18.341-05:00 DEBUG 88460 --- [love-monkey] [nio-7080-exec-4] .s.m.a.i.AuthorizationChannelInterceptor : Failed to authorize message with authorization manager .springframework.security.messaging.access.intercept.MessageMatcherDelegatingAuthorizationManager@5a4d7ce9 and result AuthorizationDecision [granted=false]
2025-02-02T10:47:18.341-05:00 DEBUG 88460 --- [love-monkey] [nio-7080-exec-4] o.s.w.s.m.StompSubProtocolHandler : Failed to send message to MessageChannel in session e28c3083-e231-c00d-dc20-2cdb4d01ecac
.springframework.messaging.MessageDeliveryException: Failed to send message to ExecutorSubscribableChannel[clientInboundChannel]
Also here is an example project that has the same issue if you want to play
I'm using .springframework.security:spring-security-messaging.
To start with I have a project that is successfully authenticating using oauth...
spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: ...
audiences:
- ...
@Configuration
@EnableWebSecurity
class SecurityConfig(
@Autowired(required = false) private val requestLoggingFilter: RequestLoggingFilter?
) {
@Bean
fun filterChain(http: HttpSecurity): SecurityFilterChain {
http
.csrf { it.disable() }
.oauth2ResourceServer { oauth2 ->
oauth2.jwt { }
}
.authorizeHttpRequests { auth ->
auth
.requestMatchers("/protected/**").authenticated()
.anyRequest().permitAll()
}
return http.build()
}
The basic http JWT functions as expected. So I try to add a web socket example that I can also try...
@Configuration
@EnableWebSocketMessageBroker
class WebsocketConfig : WebSocketMessageBrokerConfigurer {
override fun configureMessageBroker(config: MessageBrokerRegistry) {
config.enableSimpleBroker("/topic")
config.setApplicationDestinationPrefixes("/app")
}
override fun registerStompEndpoints(registry: StompEndpointRegistry) {
registry.addEndpoint("/gs-guide-websocket")
}
}
@Controller
class WebSocketController {
private val logger = LoggerFactory.getLogger(WebSocketController::class.java)
@MessageMapping("/hello")
@SendTo("/topic/greetings")
fun greeting(message: TestMessage): Greeting {
logger.info(message.toString())
return Greeting("Hello, " + message.name + "!")
}
@MessageMapping("/status")
@SendTo("/topic/chat")
fun status(message: String): String {
return "Status: $message"
}
}
Now I am trying to add security so I add the following to the security config...
@Profile("secure")
@Configuration
@EnableWebSocketSecurity
class WebSocketSecurityConfig {
@Bean
fun messageAuthorizationManager(
messages: MessageMatcherDelegatingAuthorizationManager.Builder
): AuthorizationManager<Message<*>> {
messages
// Next 2 lines are required for requests without auth.
// Remove these if all paths require auth
.simpTypeMatchers(SimpMessageType.CONNECT).permitAll()
.simpTypeMatchers(SimpMessageType.DISCONNECT).permitAll()
.simpDestMatchers("/app/status").permitAll()
.simpDestMatchers("/app/hello").authenticated()
.anyMessage().authenticated()
return messages.build()
}
}
When I try sending the non-protected message like
SEND
destination:/app/status
{"name":"test"}
I see that the message is working and being send, however, when I try to call with something like
SEND
destination:/app/hello
Authorization: Bearer ey...
{"name":"test"}
And I still get
.springframework.messaging.MessageDeliveryException: Failed to send message to ExecutorSubscribableChannel[clientInboundChannel]
at .springframework.messaging.support.AbstractMessageChannel.send(AbstractMessageChannel.java:149) ~[spring-messaging-6.2.2.jar:6.2.2]
at .springframework.messaging.support.AbstractMessageChannel.send(AbstractMessageChannel.java:125) ~[spring-messaging-6.2.2.jar:6.2.2]
at .springframework.web.socket.messaging.StompSubProtocolHandler.handleMessageFromClient(StompSubProtocolHandler.java:343) ~[spring-websocket-6.2.2.jar:6.2.2]
...
Caused by: .springframework.security.access.AccessDeniedException: Access Denied
at .springframework.security.messaging.access.intercept.AuthorizationChannelInterceptor.preSend(AuthorizationChannelInterceptor.java:75) ~[spring-security-messaging-6.4.2.jar:6.4.2]
at .springframework.messaging.support.AbstractMessageChannel$ChannelInterceptorChain.applyPreSend(AbstractMessageChannel.java:181) ~[spring-messaging-6.2.2.jar:6.2.2]
at .springframework.messaging.support.AbstractMessageChannel.send(AbstractMessageChannel.java:135) ~[spring-messaging-6.2.2.jar:6.2.2]
... 44 common frames omitted
Same as with no header at all. The same token works fine on a regular web endpoint so what am I missing?
even when I add the following...
logging:
level:
.springframework.security: DEBUG
.springframework.messaging: DEBUG
.springframework.web.socket: DEBUG
.springframework.security.oauth2: TRACE
I changed the endpoint to ws but not I see
2025-02-02T10:47:16.609-05:00 DEBUG 88460 --- [love-monkey] [nio-7080-exec-3] o.s.w.s.s.s.WebSocketHttpRequestHandler : GET /ws
2025-02-02T10:47:16.641-05:00 DEBUG 88460 --- [love-monkey] [nio-7080-exec-3] s.w.s.h.LoggingWebSocketHandlerDecorator : New StandardWebSocketSession[id=e28c3083-e231-c00d-dc20-2cdb4d01ecac, uri=ws://.../ws]
2025-02-02T10:47:18.340-05:00 DEBUG 88460 --- [love-monkey] [nio-7080-exec-4] .s.m.a.i.AuthorizationChannelInterceptor : Authorizing message send
2025-02-02T10:47:18.341-05:00 DEBUG 88460 --- [love-monkey] [nio-7080-exec-4] .s.m.a.i.AuthorizationChannelInterceptor : Failed to authorize message with authorization manager .springframework.security.messaging.access.intercept.MessageMatcherDelegatingAuthorizationManager@5a4d7ce9 and result AuthorizationDecision [granted=false]
2025-02-02T10:47:18.341-05:00 DEBUG 88460 --- [love-monkey] [nio-7080-exec-4] o.s.w.s.m.StompSubProtocolHandler : Failed to send message to MessageChannel in session e28c3083-e231-c00d-dc20-2cdb4d01ecac
.springframework.messaging.MessageDeliveryException: Failed to send message to ExecutorSubscribableChannel[clientInboundChannel]
Also here is an example project that has the same issue if you want to play
Share Improve this question edited Feb 5 at 13:35 Jackie asked Feb 1 at 18:09 JackieJackie 23.6k41 gold badges171 silver badges330 bronze badges 5- enable debug logging and read the real reason for your your access denied – Toerktumlare Commented Feb 2 at 12:10
- Added your line @Toerktumlare don't see much useful but let me know if I missed something or if there is another package I should set – Jackie Commented Feb 2 at 15:51
- WebSocket Security appears to be well documented: docs.spring.io/spring-security/reference/servlet/integrations/… Also have a look at the tests in the Spring Security source. – Roar S. Commented Feb 3 at 11:26
- That is what I used and should map to what is there @RoarS. I thought I included the link in the question but thanks for pointing it out because I fot. But it still isn't working. – Jackie Commented Feb 3 at 13:50
- The main sticking point I think is how do I debug why it is getting denied when there is a valid token that works as an http header? Is my message wrong somehow? – Jackie Commented Feb 3 at 14:06
1 Answer
Reset to default 1To help debug
Let's try putting code on top of this repo from OP and creating a test environment that uses fake bearer tokens.
CustomCsrfChannelInterceptor & WebSocketSecurityConfig
First make sure you have a custom csrf interceptor to avoid confusion. This was kind of tricky because csrf-header cannot be turned off, but I found a hack in this answer. Not for production use.
// https://stackoverflow/a/77057255/14072498
@Component("csrfChannelInterceptor")
class CustomCsrfChannelInterceptor : ChannelInterceptor {
override fun preSend(
message: Message<*>,
channel: MessageChannel
): Message<*> {
return message
}
}
@Configuration
@EnableWebSocketSecurity
class WebSocketSecurityConfig {
@Bean
fun messageAuthorizationManager(
messages: MessageMatcherDelegatingAuthorizationManager.Builder
): AuthorizationManager<Message<*>> {
messages
// Next 2 lines are required for requests without auth.
// Remove these if all paths require auth
.simpTypeMatchers(SimpMessageType.CONNECT).permitAll()
.simpTypeMatchers(SimpMessageType.DISCONNECT).permitAll()
.simpDestMatchers("/app/status").permitAll()
.simpDestMatchers("/app/hello").authenticated()
.anyMessage().authenticated()
return messages.build()
}
}
Also make a custom JWTDecoder to avoid having to use a real one. You can make this a TestConfig or a real config with a profile if you want to test in a dev environment, I chose the later.
@Configuration
@Profile("fake-jwt")
class FakeJwtConfig {
@Bean
fun jwtDecoder(): JwtDecoder {
return JwtDecoder { token ->
if (token != "test.token") {
throw JwtException("Invalid token")
}
Jwt.withTokenValue(token)
.header("alg", "none")
.claim("sub", "user")
.issuedAt(Instant.now())
.expiresAt(Instant.now().plusSeconds(3600))
.build()
}
}
}
WebSocketController
Let's log the Authentication
in the controller
@Controller
class WebSocketController {
private val logger = LoggerFactory.getLogger(WebSocketController::class.java)
@MessageMapping("/hello")
@SendTo("/topic/greetings")
fun greeting(message: TestMessage): Greeting {
logger.info(message.toString())
return Greeting("Hello, " + message.name + "!")
}
@MessageMapping("/status")
@SendTo("/topic/chat")
fun status(message: String): String {
return "Status: $message"
}
}
The Test
To ensure the issue isn't with Postman (since that can be complicated) lets create a few tests to make sure everything is working correctly
The test should pass showing that everything is working.