CJMobileApps
CJMobileApps

Reputation: 197

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 [email protected]/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

Upvotes: 0

Views: 537

Answers (2)

Petro Prydorozhnyi
Petro Prydorozhnyi

Reputation: 155

BearerTokenMetadata.BEARER_AUTHENTICATION_MIME_TYPE is indeed deprecated and you should stop using it.

Code from org.springframework.security.config.annotation.rsocket.RSocketSecurity

protected List<AuthenticationPayloadInterceptor> build() {
            ReactiveAuthenticationManager manager = getAuthenticationManager();
            AuthenticationPayloadInterceptor legacy = new AuthenticationPayloadInterceptor(manager);
            legacy.setAuthenticationConverter(new BearerPayloadExchangeConverter());
            legacy.setOrder(PayloadInterceptorOrder.AUTHENTICATION.getOrder());
            AuthenticationPayloadInterceptor standard = new AuthenticationPayloadInterceptor(manager);
            standard.setAuthenticationConverter(new AuthenticationPayloadExchangeConverter());
            standard.setOrder(PayloadInterceptorOrder.AUTHENTICATION.getOrder());
            return Arrays.asList(standard, legacy);
        }

as you can see, even in code it is marked as legacy and used second.

instead of using deprecated thing you can modify your rSocketRequester. Bean configuration can look like:

    @Bean
    fun getRsocketRequester(strategies: RSocketStrategies): RSocketRequester {

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

        //build you bearer token
        val bearerMetadata = BearerTokenMetadata(token)

        // add bearer encoder so you will be able to build auth header properly
        val extendedStrategies = strategies.mutate().encoder(BearerTokenAuthenticationEncoder()).build()

        return RSocketRequester.builder()
            .rsocketConnector { rSocketConnector: RSocketConnector ->
                rSocketConnector.reconnect(
                    Retry.fixedDelay(
                        2,
                        Duration.ofSeconds(2)
                    )
                )
            }
            // pass updated strategy to rsocket builder
            .rsocketStrategies(extendedStrategies)
            // pass your bearer token with up-to-date auth mime type
            .setupMetadata(bearerMetadata, authenticationMimeType)
            .websocket(URI.create("ws://localhost:8090/rsocket"))
    }

Upvotes: 0

CJMobileApps
CJMobileApps

Reputation: 197

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)

Upvotes: 0

Related Questions