{{announcement.body}}
{{announcement.title}}

Secure Communication with Token-based RSocket

DZone 's Guide to

Secure Communication with Token-based RSocket

Take a look at how you can establish secure communication with token-based RSocket, and establish a clear understanding of JWT.

· Security Zone ·
Free Resource

RSocket provides a message-driven communication mechanism, by using the reactive streaming framework, and supports most of the protocols (TCP/WebSocket/HTTP 1.1&HTTP 2). Furthermore, it’s program language-agnostic interaction models (REQUEST_RESPONSE/REQUEST_FNF/REQUEST_STREAM/REQUEST_CHANNEL) cover most communication scenarios, from the Microservices, API Gateway, and Sidecar Proxy, to the Message Queue.

Considering security for the communication, it's easy to use TLS-based and Token-based solution in RSocket-based productions. RSocket can reuse the TLS over the TCP or WebSocket directly, but to demonstrate the RBAC feature vividly, in this article, we only talk about the token-based implementation.

As you know, JSON Web Token (JWT) is the most popular technology for OAuth2 in the world. And it’s also program language-agnostic. After some investigations, I believe RSocket with JWT is a great way to implement secure communication between the services, especially for Open API. Now, let’s take a deeper look into what happens.

RSocket

The first question is how to use the token to talk between the services in RSocket.

There are two ways to send a token from the requester to the responder. We can put the token into metadata API at the setup time, and the other way is to take the token as metadata, along with payload as data, in every request time.

Beyond that, the routing plays the crucial role for authorization, which indicates the resource on responder side. In RSocket extensions, there is a routing metadata extension to extend the four interaction models. If the tag payloads are supported in both requester and responder, beneath,  then it's easy to define the authorization on the top layer.

JWT

In brief, to understand this article, if you know five things below about JWT, that's enough.

  1. JWT includes JSON Web Signature (JWS), JSON Web Encryption (JWE), JSON Web Key (JWK), and JSON Web Algorithms (JWA).

  2. HS256 is a secret-based algorithm, and RS256/ES256 is a PKI-based one. All of them are defined in the JWA spec. HS256 is HMAC (Keyed-Hash Message Authentication Code) + SHA-256, RS256 is RSASSA + SHA-256, and RS256 is ECDSA(Elliptic Curve Digital Signature Algorithm) + SHA-256.

  3. In regard to the secret length, assuming that we use HS256 as the token algorithm, the secret characters should be more than 32, because in HS256, the secret should be at least 256 bits (1 character = 8 bits).

  4. Access Token is used by the responder to decode/verify and authorize, and Refresh Token is used to regenerate the tokens, especially when the access token is expired.

  5. It must be handled after the user signs out, and the access token is still valid during the period.

Secure Communication

It’s time to show the demo. We have two kinds of API, token and resource. Only when the token is verified, the resource API could be accessed.

Workflow

  • We use signin API to generate tokens to requester, and it takes username and password. After authenticate, the responder will sign, save and return the Access Token and Refresh Token to the requester.
  • The refresh api is to renew the tokens, and it takes refresh token. After decode and authorize, the responder will sign, save and return the Access Token and Refresh Token to the requester.

  • We define info/list/hire/fire as the resource API to demonstrate different read/write actions.

  • The signout API is to handle the stolen case, as we talked above.

Workflow diagram

Authentication

Since we use Role-Based Access Control (RBAC) as the authorization mechanism, in the authentication part, we should provide an identity (User-Role-Permission) repository to save and retrieve the identity information in the responder. 

Besides, we provide a token repository to store/revoke/read the tokens, which is used to verify the authentication decoded from the token. Since the authorization information is encrypted and compressed in the token, we use the information from the repository to double-check these two authorizations. If they are the same, we can say that the request is authentic.

Authorization

api interaction model role
signin Request/Response all
signout Fire-and-Forget authenticated
refresh Request/Response all
info Request/Response user, admin
list Request/Stream user, admin
hire Request/Response admin
fire Request/Response admin

Spring Boot Implementation

Sign Token

As my plan is to use multiple program languages to show this demo, we must ensure the algorithm and some constants, to unify the way to encrypt and compress. 

In this demo, we use HS256 as the algorithm, and define the access token expired time as 5 minutes, refresh token expire time as 7 days.

Java
 




xxxxxxxxxx
1


 
1
public static final long ACCESS_EXPIRE = 5;
2
public static final long REFRESH_EXPIRE = 7;
3
private static final MacAlgorithm MAC_ALGORITHM = MacAlgorithm.HS256;
4
private static final String HMAC_SHA_256 = "HmacSHA256";


I will show you the token generated code:

Java
 




xxxxxxxxxx
1
23


 
1
public static UserToken generateAccessToken(HelloUser user) {
2
    Algorithm ACCESS_ALGORITHM = Algorithm.HMAC256(ACCESS_SECRET_KEY);
3
    return generateToken(user, ACCESS_ALGORITHM, ACCESS_EXPIRE, ChronoUnit.MINUTES);
4
}
5
 
          
6
private static UserToken generateToken(HelloUser user, Algorithm algorithm, long expire, ChronoUnit unit) {
7
    String tokenId = UUID.randomUUID().toString();
8
    Instant instant;
9
    Instant now = Instant.now();
10
    if (now.isSupported(unit)) {
11
        instant = now.plus(expire, unit);
12
    } else {
13
        log.error("unit param is not supported");
14
        return null;
15
    }
16
    String token = JWT.create()
17
            .withJWTId(tokenId)
18
            .withSubject(user.getUserId())
19
            .withClaim("scope", user.getRole())
20
            .withExpiresAt(Date.from(instant))
21
            .sign(algorithm);
22
    return UserToken.builder().tokenId(tokenId).token(token).user(user).build();
23
}


A Word of Caution:

The claim key name in the above code is not arbitrary, since “scope” is used in framework as the default way to decode the role from the token. 

Accordingly, the token decoder code is here:

Java
 




xxxxxxxxxx
1
24


 
1
public static ReactiveJwtDecoder getAccessTokenDecoder() {
2
    SecretKeySpec secretKey = new SecretKeySpec(ACCESS_SECRET_KEY.getBytes(), HMAC_SHA_256);
3
    return NimbusReactiveJwtDecoder.withSecretKey(secretKey)
4
            .macAlgorithm(MAC_ALGORITHM)
5
            .build();
6
}
7
 
          
8
public static ReactiveJwtDecoder jwtAccessTokenDecoder() {
9
    return new HelloJwtDecoder(getAccessTokenDecoder());
10
}
11
 
          
12
// HelloJwtDecoder
13
@Override
14
public Mono<Jwt> decode(String token) throws JwtException {
15
    return reactiveJwtDecoder.decode(token).doOnNext(jwt -> {
16
        String id = jwt.getId();
17
        HelloUser auth = tokenRepository.getAuthFromAccessToken(id);
18
        if (auth == null) {
19
            throw new JwtException("Invalid HelloUser");
20
        }
21
        //TODO
22
        helloJwtService.setTokenId(id);
23
    });
24
}


The decode method in HelloJwtDecoder will be invoked by the framework in every request handling time, to convert the token string value to jwt:

Java
 




xxxxxxxxxx
1
28


1
@Bean
2
PayloadSocketAcceptorInterceptor authorization(RSocketSecurity rsocketSecurity) {
3
    RSocketSecurity security = pattern(rsocketSecurity)
4
            .jwt(jwtSpec -> {
5
                try {
6
                    jwtSpec.authenticationManager(jwtReactiveAuthenticationManager(jwtDecoder()));
7
                } catch (Exception e) {
8
                    throw new RuntimeException(e);
9
                }
10
            });
11
    return security.build();
12
}
13
 
          
14
@Bean
15
public ReactiveJwtDecoder jwtDecoder() throws Exception {
16
    return TokenUtils.jwtAccessTokenDecoder();
17
}
18
 
          
19
@Bean
20
public JwtReactiveAuthenticationManager jwtReactiveAuthenticationManager(ReactiveJwtDecoder decoder) {
21
    JwtAuthenticationConverter converter = new JwtAuthenticationConverter();
22
    JwtGrantedAuthoritiesConverter authoritiesConverter = new JwtGrantedAuthoritiesConverter();
23
    authoritiesConverter.setAuthorityPrefix("ROLE_");
24
    converter.setJwtGrantedAuthoritiesConverter(authoritiesConverter);
25
    JwtReactiveAuthenticationManager manager = new JwtReactiveAuthenticationManager(decoder);
26
    manager.setJwtAuthenticationConverter(new ReactiveJwtAuthenticationConverterAdapter(converter));
27
    return manager;
28
}


Revoke Token

To simplify the environment of the demo running, the way to revoke token implements here by guava cache. You can use some powerful components, like Redis, to do that.

Once the time is up, the access token will be revoked automatically.

On the other hand, when the requester sends signout, this cache will be invoked as event-driven.

Java
 




xxxxxxxxxx
1


1
Cache<String, HelloUser> accessTokenTable = CacheBuilder.newBuilder()
2
            .expireAfterWrite(TokenUtils.ACCESS_EXPIRE, TimeUnit.MINUTES).build();
3
 
          
4
public void deleteAccessToken(String tokenId) {
5
  accessTokenTable.invalidate(tokenId);
6
}



Authentication

The authenticate function for signin, is as simple as the HTTP basic authentication did:

Java
 




xxxxxxxxxx
1


 
1
HelloUser user = userRepository.retrieve(principal);
2
if (user.getPassword().equals(credential)) {
3
  return user;
4
}


By contrast, the other authenticate, which is for refresh, is a little more complex, the steps are: 

  • Getting decoder and using it to decode the token string value to a JWT object

  • Using the reactive style to map JWT to auth

  • Retrieving the auth from repository

  • Verifying the auths from db and token are same

  • Returning the auth object in streaming way

Java
 




xxxxxxxxxx
1
13


1
return reactiveJwtDecoder.decode(refreshToken).map(jwt -> {
2
    try {
3
        HelloUser user = HelloUser.builder().userId(jwt.getSubject()).role(jwt.getClaim("scope")).build();
4
        log.info("verify successfully. user:{}", user);
5
        HelloUser auth = tokenRepository.getAuthFromRefreshToken(jwt.getId());
6
        if (user.equals(auth)) {
7
            return user;
8
        }
9
    } catch (Exception e) {
10
        log.error("", e);
11
    }
12
    return new HelloUser();
13
});



Authorization

As I said, this demo is based on RBAC; the routing is the key. And for brevity, no more for showing the open APIs version, just a little:

Java
 




xxxxxxxxxx
1
23


1
// HelloSecurityConfig
2
protected RSocketSecurity pattern(RSocketSecurity security) {
3
    return security.authorizePayload(authorize -> authorize
4
            .route("signin.v1").permitAll()
5
            .route("refresh.v1").permitAll()
6
            .route("signout.v1").authenticated()
7
            .route("hire.v1").hasRole(ADMIN)
8
            .route("fire.v1").hasRole(ADMIN)
9
            .route("info.v1").hasAnyRole(USER, ADMIN)
10
            .route("list.v1").hasAnyRole(USER, ADMIN)
11
            .anyRequest().authenticated()
12
            .anyExchange().permitAll()
13
    );
14
}
15
 
          
16
// HelloJwtSecurityConfig
17
@Configuration
18
@EnableRSocketSecurity
19
public class HelloJwtSecurityConfig extends HelloSecurityConfig {
20
  @Bean
21
  PayloadSocketAcceptorInterceptor authorization(RSocketSecurity rsocketSecurity) {
22
    RSocketSecurity security = pattern(rsocketSecurity)
23
    ...


I put the route-based RBAC defination in parent class to easy to extend the security by using other way, e.g. TLS.

SpringBoot provides MessageMapping annotation to let us define the route for messaging, which means streaming api in RSocket.

Java
 




x


 
1
@MessageMapping("signin.v1")
2
    Mono<HelloToken> signin(HelloUser helloUser) {
3
    ...


Dependencies

From 2.2.0-Release , Spring Boot start to support RSocket. And from 2.3, it supports RSocket security. Since 2.3.0 is not GA when I write this article, the version I show you is 2.3.0.M4.

  • spring-boot.version 2.3.0.M4

  • spring.version 5.2.5.RELEASE

  • spring-security.version 5.3.1.RELEASE

  • rsocket.version 1.0.0-RC6

  • reactor-netty.version 0.9.5.RELEAS

  • netty.version 4.1.45.Final

  • reactor-core.version 3.3.3.RELEASE

  • jjwt.version 0.9.1

Build, Run, and Test

Shell
 







Shell
 






Shell
 




xxxxxxxxxx
1


1
bash curl_test.sh


curl Test

Shell
 




xxxxxxxxxx
1
21


1
echo "signin as user"
2
read accessToken refreshToken < <(echo $(curl -s "http://localhost:8989/api/signin?u=0000&p=Zero4" | jq -r '.accessToken,.refreshToken'))
3
echo "Access Token  :${accessToken}"
4
echo -e "Refresh Token :${refreshToken}\\n"
5
 
          
6
echo "[user] refresh:"
7
curl -s "http://localhost:8989/api/refresh/${refreshToken}" | jq
8
echo
9
 
          
10
echo "[user] info:"
11
curl "http://localhost:8989/api/info/1"
12
echo -e "\\n"
13
 
          
14
echo "[user] list:"
15
curl -s "http://localhost:8989/api/list" | grep data -c
16
echo
17
 
          
18
echo "[user] hire:"
19
curl -s "http://localhost:8989/api/hire" \
20
-H "Content-Type: application/stream+json;charset=UTF-8" \
21
-d '{"id":"18","value":"伏虎羅漢"}' | jq -r ".message"


Easter Egg

The resource API part shows the hiring and firing employee. Please read more details from Eighteen_Arhats!

In the End

I was going to show a golang version, but so far, the RSocket for golang is not an open routing API and it’s not convenient to achieve that. But, there’s a good news that Jeff will open them, soon. 

It’s funny for me to show this demo by using other languages, Rust/NodeJs and so on. Maybe, I would go on to write a series of articles. 

By the way, the source code for this demo is on GitHub.

Topics:
authorization, jwt, rbac, role based access control, rsocket, security, spring boot, token

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}