springspring-bootkotlinspring-securityrsocket

Spring RSocket Security JWT Access Denied Error


I am trying to use Spring Boot RSocket with Security using JWT Tokens. It is giving me an Access Denied error with no other useful information to help debug with?

Access Denied.
ApplicationErrorException (0x201): Access Denied at app//io.rsocket.exceptions.Exceptions.from(Exceptions.java:76) at app//io.rsocket.core.RSocketRequester.handleFrame(RSocketRequester.java:261) at app//io.rsocket.core.RSocketRequester.handleIncomingFrames(RSocketRequester.java:211) at app//reactor.core.publisher.LambdaSubscriber.onNext(LambdaSubscriber.java:160) at app//io.rsocket.core.ClientServerInputMultiplexer$InternalDuplexConnection.onNext(ClientServerInputMultiplexer.java:248) at app//io.rsocket.core.ClientServerInputMultiplexer.onNext(ClientServerInputMultiplexer.java:129) at app//io.rsocket.core.ClientServerInputMultiplexer.onNext(ClientServerInputMultiplexer.java:48) at app//reactor.core.publisher.FluxMap$MapSubscriber.onNext(FluxMap.java:122) at app//reactor.netty.channel.FluxReceive.onInboundNext(FluxReceive.java:364) at app//reactor.netty.channel.ChannelOperations.onInboundNext(ChannelOperations.java:404) at app//reactor.netty.http.client.HttpClientOperations.onInboundNext(HttpClientOperations.java:725) at app//reactor.netty.http.client.WebsocketClientOperations.onInboundNext(WebsocketClientOperations.java:161) at app//reactor.netty.channel.ChannelOperationsHandler.channelRead(ChannelOperationsHandler.java:93) at app//io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379) at app//io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365) at app//io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:357) at app//io.netty.handler.codec.ByteToMessageDecoder.fireChannelRead(ByteToMessageDecoder.java:327) at app//io.netty.handler.codec.ByteToMessageDecoder.channelRead(ByteToMessageDecoder.java:299) at app//io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379) at app//io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365) at app//io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:357) at app//io.netty.channel.DefaultChannelPipeline$HeadContext.channelRead(DefaultChannelPipeline.java:1410) at app//io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379) at app//io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365) at app//io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:919) at app//io.netty.channel.nio.AbstractNioByteChannel$NioByteUnsafe.read(AbstractNioByteChannel.java:166) at app//io.netty.channel.nio.NioEventLoop.processSelectedKey(NioEventLoop.java:722) at app//io.netty.channel.nio.NioEventLoop.processSelectedKeysOptimized(NioEventLoop.java:658) at app//io.netty.channel.nio.NioEventLoop.processSelectedKeys(NioEventLoop.java:584) at app//io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:496) at app//io.netty.util.concurrent.SingleThreadEventExecutor$4.run(SingleThreadEventExecutor.java:997) at app//io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74) at app//io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30) at java.base@11.0.14/java.lang.Thread.run(Thread.java:834)

Security Config file

@Configuration
@EnableRSocketSecurity
@EnableReactiveMethodSecurity
class SecurityConfig {

    @Bean
    fun authorization(rsocketSecurity: RSocketSecurity): PayloadSocketAcceptorInterceptor {
        val security: RSocketSecurity =
            rsocketSecurity.authorizePayload { authorize: RSocketSecurity.AuthorizePayloadsSpec ->
                authorize
                    .anyRequest().authenticated()
                    .anyExchange().permitAll()
            }
                .jwt { jwtSpec ->
                    jwtSpec.authenticationManager(jwtReactiveAuthenticationManager(jwtDecoder()))
                }
        return security.build()
    }

    @Bean
    fun jwtDecoder(): ReactiveJwtDecoder {
        return TokenUtils.jwtAccessTokenDecoder()
    }

    @Bean
    fun jwtReactiveAuthenticationManager(decoder: ReactiveJwtDecoder): JwtReactiveAuthenticationManager {
        val converter = JwtAuthenticationConverter()
        val authoritiesConverter = JwtGrantedAuthoritiesConverter()
        authoritiesConverter.setAuthorityPrefix("ROLE_")
        converter.setJwtGrantedAuthoritiesConverter(authoritiesConverter)
        val manager = JwtReactiveAuthenticationManager(decoder)
        manager.setJwtAuthenticationConverter(ReactiveJwtAuthenticationConverterAdapter(converter))
        return manager
    }

    @Bean
    fun rsocketMessageHandler() = RSocketMessageHandler() .apply {
        argumentResolverConfigurer.addCustomResolver(AuthenticationPrincipalArgumentResolver())
        routeMatcher = PathPatternRouteMatcher()
        rSocketStrategies = rsocketStrategies()
    }

    @Bean
    fun rsocketStrategies() = RSocketStrategies.builder()
        .routeMatcher(PathPatternRouteMatcher())
        .build()
}

Message Controller file

@MessageMapping("api.v1.messages")
@Controller
class MessageController {

    @MessageMapping("stream")
    suspend fun receive(
        @Payload inboundMessages: Flow<String>,
        @AuthenticationPrincipal jwt: String
    ) {
        println("MessageController: jwt: $jwt")
        println("MessageController: inbound message: " + inboundMessages.first())
    }
}

Testing using MessageControllerTest file

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class MessageControllerTest(
    @Autowired val rsocketBuilder: RSocketRequester.Builder,
    @LocalServerPort val serverPort: Int
) {

    @ExperimentalTime
    @ExperimentalCoroutinesApi
    @Test
    fun `test that messages API streams latest messages`() {
        val admin = HelloUser(userId = "9527", password = "password", role = HelloRole.ADMIN)

        val token: UserToken = TokenUtils.generateAccessToken(admin)!!

        val authenticationMimeType: MimeType =
            MimeTypeUtils.parseMimeType(WellKnownMimeType.MESSAGE_RSOCKET_AUTHENTICATION.string)

        runBlocking {
            val rSocketRequester = rsocketBuilder.websocket(URI("ws://localhost:${serverPort}/rsocket"))

            launch {

                rSocketRequester.route("api.v1.messages.stream")
                    .metadata(token.token!!, authenticationMimeType)
                    .dataWithType(flow {
                        emit(
                            "Hey from test class"
                        )
                    })
                    .retrieveFlow<Void>()
                    .collect()
            }
        }
    }
}

I've add the rest of the code example I did to GitHub https://github.com/CJMobileApps/rsocket-jwt-security-example


Solution

  • I figured it out. RSocket currently has a bug or just bad documentation. The MimeType for JWTs BearerTokenMetadata.BEARER_AUTHENTICATION_MIME_TYPE says that it is deprecated and to use MimeTypeUtils.parseMimeType(WellKnownMimeType.MESSAGE_RSOCKET_AUTHENTICATION.string) instead, however that does not work.

    When passing tokens continue using/use BearerTokenMetadata.BEARER_AUTHENTICATION_MIME_TYPE along with the token string.

    .metadata(token.token!!, BearerTokenMetadata.BEARER_AUTHENTICATION_MIME_TYPE)