Spring unable to authenticate RSocket streams using JWT, while able to auth response requests

747 Views Asked by At

Expected result: connecting to the RSocket websocket based endpoint from front end that includes authentication information as metadata will trigger PayloadSocketAcceptorInterceptor's jwt authentication system.

Actual result: This only happens when sending responseRequest from JS frontend, fails when doing the same with streamRequest. No errors. Not one of the authentication related methods get called in the classes below. I've logged all of them.

Code for RSocketConfig:

@Configuration
@EnableRSocketSecurity
@EnableReactiveMethodSecurity
class RSocketConfig {
  @Autowired
  lateinit var rSocketAuthenticationManager: RSocketAuthenticationManager

  @Bean
  fun rSocketMessageHandler(strategies: RSocketStrategies?): RSocketMessageHandler? {
    val handler = RSocketMessageHandler()
    handler.argumentResolverConfigurer.addCustomResolver(AuthenticationPrincipalArgumentResolver())
    handler.rSocketStrategies = strategies!!
    return handler
  }

  @Bean
  fun authorization(rsocket: RSocketSecurity): PayloadSocketAcceptorInterceptor {
    rsocket.authorizePayload { authorize: AuthorizePayloadsSpec ->
      authorize
          .route("flux-stream").authenticated()
          .anyRequest().authenticated()
          .anyExchange().permitAll()
    }
        .jwt { jwtSpec: RSocketSecurity.JwtSpec ->
          try {
            jwtSpec.authenticationManager(rSocketAuthenticationManager)
          } catch (e: Exception) {
            throw RuntimeException(e)
          }
        }

    return rsocket.build()
  }

  @Bean
  fun rSocketRequester(strategies: RSocketStrategies, props: RSocketProperties): Mono<RSocketRequester> =
      RSocketRequester.builder()
          .rsocketStrategies(strategies)
          .connectWebSocket(getUri(props))


  fun getUri(props: RSocketProperties): URI =
      URI.create(String.format("ws://localhost:${props.server.port}${props.server.mappingPath}"))
}

Code for RSocketAuthenticationManager:

@Component
class RSocketAuthenticationManager(): ReactiveAuthenticationManager {
  @Autowired
  lateinit var cognitoConfig: CognitoConfig

  @Override
  override fun authenticate(authentication: Authentication): Mono<Authentication> {
    val authToken: String = authentication.credentials.toString()

    try {
      return if(isTokenValid(authToken)) {
        val decoded = JWT.decode(authToken)
        decoded.claims.entries.forEach { (key, value) -> println("$key = ${value.asString()}") }
        val authorities: MutableList<GrantedAuthority> = ArrayList()
        println("authentication successful!")
        Mono.just(UsernamePasswordAuthenticationToken(decoded.subject, null, authorities))
      } else {
        println("invalid authentication token")
        Mono.empty<Authentication>();
      }
    } catch (e: Exception) {
      println("authentication errored")
      e.printStackTrace()
      return Mono.empty<Authentication>()
    }
  }

  @Throws(Exception::class)
  fun isTokenValid(token: String): Boolean {
    // code borrowed from
    // https://github.com/awslabs/cognito-proxy-rest-service/blob/2f9a9ffcc742c8ab8a694b7cf39dc5d8b3247898/src/main/kotlin/com/budilov/cognito/services/CognitoService.kt#L41

    // Decode the key and set the kid
    val decodedJwtToken = JWT.decode(token)
    val kid = decodedJwtToken.keyId

    val http = UrlJwkProvider(URL(cognitoConfig.jwksUrl))
    // Let's cache the result from Cognito for the default of 10 hours
    val provider = GuavaCachedJwkProvider(http)
    val jwk = provider.get(kid)

    val algorithm = Algorithm.RSA256(jwk.publicKey as RSAKey)
    val verifier = JWT.require(algorithm)
        .withIssuer(cognitoConfig.jwtTokenIssuer)
        .build() //Reusable verifier instance

    val jwt = try {
      verifier.verify(token)
    } catch (e: Exception) {
      false
    }

    return (jwt != null)
  }
}

Dependencies related to the issue:

implementation("org.springframework.boot:spring-boot-starter-webflux:2.3.0.RELEASE")
implementation("org.springframework.boot:spring-boot-starter-websocket:2.3.0.RELEASE")
implementation("org.springframework.boot:spring-boot-configuration-processor:2.3.0.RELEASE")
implementation("org.springframework.boot:spring-boot-starter-rsocket:2.3.0.RELEASE")
implementation("org.springframework.boot:spring-boot-starter-integration:2.3.0.RELEASE")

implementation("org.springframework.boot:spring-boot-starter-security:2.3.0.RELEASE")
implementation("org.springframework.security:spring-security-rsocket:5.4.2")
implementation("org.springframework.security:spring-security-messaging:5.4.2")
implementation("org.springframework.boot:spring-boot-starter-oauth2-resource-server:2.3.0.RELEASE")

implementation("com.auth0:java-jwt:3.3.0")
implementation("com.auth0:jwks-rsa:0.1.0")

I'm not too familiar with Spring Security, so maybe I'm missing something obvious.

2

There are 2 best solutions below

0
On BEST ANSWER

The issue has been solved, for anyone in need check the repos history with fixes: https://github.com/Braffolk/spring-rsocket-stream-jwt-authentication/commits/master

1
On

You should have a service method annotated to receive the authentication principal.

  @MessageMapping("runCommand")
  suspend fun runCommand(request: CommandRequest, rSocketRequester: RSocketRequester, @AuthenticationPrincipal jwt: String): Flow<CommandResponse> {

Can you extract a simpler project that you can share on github to work through why it's not working?

A full example is here https://spring.io/blog/2020/06/17/getting-started-with-rsocket-spring-security