Search code examples
kotlinspring-securitypostmanspring-messaging

How do I call a secure web socket using Stomp and Spring-Messaging from Postman?


I have the following demo app this includes a JWTDecoder I can add to fake real auth

@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()
        }
    }
}

and configuration to handle authorization

@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()
    }
}

I have a couple integration tests that seem to pass as expected, however, when I try to manually call the endpoint with something like

SEND
destination:/app/hello
Authorization: Bearer test.token

{"name":"test"}

It gets access denied...

Caused by: org.springframework.security.access.AccessDeniedException: Access Denied

The ones that do not require auth seem to work fine like

SEND
destination:/app/status

{"name":"test"}

work fine so I am confused why it works with test but I can't get it to work manually?

Full project here to be clear I am using hex-encoding etc...

enter image description here


Solution

  • In Postman, you'll need to set Authorization inside headers instead of in the messages.

    In your code, .oauth2ResourceServer { oauth2 -> oauth2.jwt { } } expects a valid bearer token in the Authorization header. The security context is propagated to WebSocket security as described here.

    Auth header inside Postman

    In your controller method, you can log the Authentication like this to verify it's working.

        @MessageMapping("/hello")
        @SendTo("/topic/greetings")
        fun greeting(
            message: TestMessage,
            authentication: Optional<Authentication>
        ): Greeting {
            logger.info(message.toString())
            authentication.ifPresent {
                logger.info("Authentication: {}", it)
            }
            return Greeting("Hello, ${message.name}!")
        }
    

    Log Excerpt

    Logs when sending two messages after connect. Note Securing GET /ws followed by Set SecurityContextHolder to JwtAuthenticationToken.

    2025-02-06T14:59:27.354+01:00 DEBUG 3224140 --- [websocket] [nio-8080-exec-1] o.s.security.web.FilterChainProxy        : Securing GET /ws
    2025-02-06T14:59:27.360+01:00 DEBUG 3224140 --- [websocket] [nio-8080-exec-1] o.s.s.o.s.r.a.JwtAuthenticationProvider  : Authenticated token
    2025-02-06T14:59:27.361+01:00 DEBUG 3224140 --- [websocket] [nio-8080-exec-1] .s.r.w.a.BearerTokenAuthenticationFilter : Set SecurityContextHolder to JwtAuthenticationToken [Principal=org.springframework.security.oauth2.jwt.Jwt@3d7ec21, Credentials=[PROTECTED], Authenticated=true, Details=WebAuthenticationDetails [RemoteIpAddress=127.0.0.1, SessionId=null], Granted Authorities=[]]
    2025-02-06T14:59:27.363+01:00 DEBUG 3224140 --- [websocket] [nio-8080-exec-1] o.s.security.web.FilterChainProxy        : Secured GET /ws
    2025-02-06T14:59:36.814+01:00 DEBUG 3224140 --- [websocket] [nio-8080-exec-2] .s.m.a.i.AuthorizationChannelInterceptor : Authorizing message send
    2025-02-06T14:59:36.815+01:00 DEBUG 3224140 --- [websocket] [nio-8080-exec-2] .s.m.a.i.AuthorizationChannelInterceptor : Authorized message send
    2025-02-06T14:59:36.867+01:00  INFO 3224140 --- [websocket] [nboundChannel-1] e.w.controller.WebSocketController       : TestMessage(name=test)
    2025-02-06T14:59:36.868+01:00  INFO 3224140 --- [websocket] [nboundChannel-1] e.w.controller.WebSocketController       : Authentication: JwtAuthenticationToken [Principal=org.springframework.security.oauth2.jwt.Jwt@3d7ec21, Credentials=[PROTECTED], Authenticated=true, Details=WebAuthenticationDetails [RemoteIpAddress=127.0.0.1, SessionId=null], Granted Authorities=[]]
    2025-02-06T14:59:40.626+01:00 DEBUG 3224140 --- [websocket] [nio-8080-exec-3] .s.m.a.i.AuthorizationChannelInterceptor : Authorizing message send
    2025-02-06T14:59:40.626+01:00 DEBUG 3224140 --- [websocket] [nio-8080-exec-3] .s.m.a.i.AuthorizationChannelInterceptor : Authorized message send
    2025-02-06T14:59:40.627+01:00  INFO 3224140 --- [websocket] [nboundChannel-4] e.w.controller.WebSocketController       : TestMessage(name=test)
    2025-02-06T14:59:40.627+01:00  INFO 3224140 --- [websocket] [nboundChannel-4] e.w.controller.WebSocketController       : Authentication: JwtAuthenticationToken [Principal=org.springframework.security.oauth2.jwt.Jwt@3d7ec21, Credentials=[PROTECTED], Authenticated=true, Details=WebAuthenticationDetails [RemoteIpAddress=127.0.0.1, SessionId=null], Granted Authorities=[]]
    

    Tested and verified with code from OP.