RSocket Jwt Authentication/Authorization using Spring Security 5.2.0.RC1

Author: Shazin Sadakath


In a previous post we spoke about how to protect a RSocket producer application using Basic Authentication and using an RSocket consumer to connect to by Authenticating all using the latest Spring Security 5.2.0.RC1 release with support for RSocket. Today we are going to look at a slightly advanced version of it with Jwt token based Authentication/Authorization.

Most of the code looks similar to the Basic Authentication version so it is adviced that you read that first. In order to implement Jwt based Authentication/Authorization I had to write some custom classes as well as some of them didn't exist as of writing this. Jwt token generation no longer part of this and it can be generated using a different Authorization server. First we will look at producer side.

In the producer end I had to write a custom DefaultReactiveJwtDecoder to decode the Jwt token sent by the consumer which looks like following;

public class DefaultReactiveJwtDecoder implements ReactiveJwtDecoder {

    private SignatureVerifier signatureVerifier;
    private JsonParser objectMapper = new JacksonJsonParser();

    public DefaultReactiveJwtDecoder(String key) {
        this.signatureVerifier = new MacSigner(key);
    }

    @Override
    public Mono decode(String token) throws JwtException {
        try {
            org.springframework.security.jwt.Jwt jwt = JwtHelper.decodeAndVerify(token, this.signatureVerifier);
            String content = jwt.getClaims();
            Map map = objectMapper.parseMap(content);
            if (map.containsKey("exp") && map.get("exp") instanceof Integer) {
                Integer intValue = (Integer)map.get("exp");
                map.put("exp", new Long((long)intValue));
            }

            Map headers = Collections.singletonMap("Content-Type", "application/json");

            return Mono.just(new org.springframework.security.oauth2.jwt.Jwt(token, Instant.now(), Instant.now().plus(10, ChronoUnit.DAYS), headers, map));
        } catch (Exception var6) {
            throw new JwtException("Cannot convert access token to JSON", var6);
        }
    }

    public void setSignatureVerifier(SignatureVerifier signatureVerifier) {
        this.signatureVerifier = signatureVerifier;
    }
}

Which verifies the signature of the Jwt token which is in HMACSHA256 format by default (Customizable ofcourse). I am using the secret key "5H@Z!N123" to sign (Ofcourse it is weak and only used for demo purpose). And the following SecurityConfig makes use of this;

@Configuration
@EnableRSocketSecurity
public class SecurityConfig {

    @Bean
    public RSocketMessageHandler messageHandler() {
        RSocketMessageHandler handler = new RSocketMessageHandler();
        handler.setRSocketStrategies(rsocketStrategies());
        return handler;
    }

    @Bean
    public RSocketStrategies rsocketStrategies() {
        return RSocketStrategies.builder()
                .decoder(new BasicAuthenticationDecoder(), new Jackson2JsonDecoder())
                .encoder(new Jackson2JsonEncoder())
                .build();
    }

    @Bean
    public PayloadSocketAcceptorInterceptor rsocketInterceptor(RSocketSecurity rsocket) {
        rsocket.authorizePayload(authorize -> {
            authorize
                    // must have ROLE_SETUP to make connection
                    .setup().hasRole("SETUP")
                    // must have ROLE_ADMIN for routes starting with "taxis."
                    .route("taxis*").hasRole("ADMIN")
                    // any other request must be authenticated for
                    .anyRequest().authenticated();
        }).jwt(jwtSpec -> {
            jwtSpec.authenticationManager(jwtReactiveAuthenticationManager());
        });

        return rsocket.build();
    }

    @Bean
    public JwtReactiveAuthenticationManager jwtReactiveAuthenticationManager() {
        JwtReactiveAuthenticationManager jwtReactiveAuthenticationManager = new JwtReactiveAuthenticationManager(defaultReactiveJwtDecoder());

        JwtAuthenticationConverter authenticationConverter = new JwtAuthenticationConverter();
        JwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
        jwtGrantedAuthoritiesConverter.setAuthorityPrefix("ROLE_");
        authenticationConverter.setJwtGrantedAuthoritiesConverter(jwtGrantedAuthoritiesConverter);
        jwtReactiveAuthenticationManager.setJwtAuthenticationConverter( new ReactiveJwtAuthenticationConverterAdapter(authenticationConverter));
        return jwtReactiveAuthenticationManager;
    }

    @Bean
    public DefaultReactiveJwtDecoder defaultReactiveJwtDecoder() {
        return new DefaultReactiveJwtDecoder("5H@Z!N123");
    }

}

In the above configuration inside rsocketInterceptor method jwt is configured and a JwtReactiveAuthenticationManager is used which in turn makes use of the custom DefaultReactiveJwtDecoder we wrote. That is the producer end, now we will look at the consumer end.

In the consumer end we had to write a BearerTokenEncoder as it was not available as of writing this post.

public class BearerTokenEncoder extends AbstractEncoder {

    public BearerTokenEncoder() {
        super(BearerTokenMetadata.BEARER_AUTHENTICATION_MIME_TYPE);
    }

    @Override
    public Flux encode(Publisher publisher, DataBufferFactory dataBufferFactory, ResolvableType resolvableType, MimeType mimeType, Map hints) {
        return Flux.from(publisher).map((credentials) -> {
            return this.encodeValue(credentials, dataBufferFactory, resolvableType, mimeType, hints);
        });
    }

    @Override
    public DataBuffer encodeValue(BearerTokenMetadata credentials, DataBufferFactory bufferFactory, ResolvableType valueType, MimeType mimeType, Map hints) {
        String token = credentials.getToken();
        byte[] tokenBytes = token.getBytes(StandardCharsets.UTF_8);
        DataBuffer metadata = bufferFactory.allocateBuffer();
        boolean release = true;

        DataBuffer dataBuffer;
        try {
            metadata.write(tokenBytes);
            release = false;
            dataBuffer = metadata;
        } finally {
            if (release) {
                DataBufferUtils.release(metadata);
            }
        }

        return dataBuffer;
    }
}

And when creating RSocketStrategies the above implementation needs to be configured as below;

    @Bean
 public RSocketStrategies rsocketStrategies() {
  return RSocketStrategies.builder()
    .encoder(new BasicAuthenticationEncoder(), new Jackson2JsonEncoder(), new BearerTokenEncoder())
    .decoder(new Jackson2JsonDecoder())
    .build();
 }

And during setup of the RSocketRequest a user with role SETUP needs to be authenticated with a Jwt Token as below;

    @Bean
 public RSocketRequester rSocketRequester(RSocketStrategies rSocketStrategies) {
  BearerTokenMetadata credentials = new BearerTokenMetadata("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6InNldHVwIiwiaWF0IjoxNTE2MjM5MDIyLCJzY29wZSI6WyJTRVRVUCJdfQ.l2N5TT7hsN6KJLHwzYXxjS48-fqIqjNWHcJ13ll3ExU");
  return RSocketRequester.builder()
    .dataMimeType(MimeTypeUtils.APPLICATION_JSON)
    .rsocketStrategies(rSocketStrategies)
    .rsocketFactory(clientRSocketFactory -> {
     clientRSocketFactory.frameDecoder(PayloadDecoder.ZERO_COPY);
    })
    .setupMetadata(credentials, BearerTokenMetadata.BEARER_AUTHENTICATION_MIME_TYPE)
    .connect(TcpClientTransport.create(TcpClient.create().port(7000)))
    .block();
 }

 

Finally during requests to message endpoints a user with role ADMIN needs to be authenticated with a Jwt Token as below;

@RestController
class TaxisRestController {

   private final RSocketRequester rSocketRequester;

   TaxisRestController(RSocketRequester rSocketRequester) {
      this.rSocketRequester = rSocketRequester;
   }

   @GetMapping("/taxis/{type}/{from}/{to}")
   public Publisher taxis(@PathVariable String type, @PathVariable String from, @PathVariable String to) {
      BearerTokenMetadata credentials = new BearerTokenMetadata("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6InNoYXppbiIsImlhdCI6MTUxNjIzOTAyMiwic2NvcGUiOlsiQURNSU4iXX0.lieYZKrPVtEoH2prh_H2ae4z8iBCMc9wz82CWRHtRUI");
      return rSocketRequester
            .route("taxis")
            .metadata(credentials, BearerTokenMetadata.BEARER_AUTHENTICATION_MIME_TYPE)
            .data(new TaxisRequest(type, from, to))
            .retrieveMono(TaxisResponse.class);
   }

   @GetMapping(value = "/taxis/sse/{type}/{from}/{to}", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
   public Publisher taxisStream(@PathVariable String type, @PathVariable String from, @PathVariable String to) {
      BearerTokenMetadata credentials = new BearerTokenMetadata("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6InNoYXppbiIsImlhdCI6MTUxNjIzOTAyMiwic2NvcGUiOlsiQURNSU4iXX0.lieYZKrPVtEoH2prh_H2ae4z8iBCMc9wz82CWRHtRUI");
      return rSocketRequester
            .route("taxis-stream")
            .metadata(credentials, BearerTokenMetadata.BEARER_AUTHENTICATION_MIME_TYPE)
            .data(new TaxisRequest(type, from, to))
            .retrieveFlux(TaxisResponse.class);
   }


}

The complete source code is available at https://github.com/shazin/rsocket-spring-security-demo under branch jwt-token-security.



Tags: RSocket SpringBoot 2 Reactive Socket SpringSecurity JWT
Views: 179
Register for more exciting articles

Comments

Please login or register to post a comment.


There are currently no comments.