DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Please enter at least three characters to search
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Zones

Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks

Last call! Secure your stack and shape the future! Help dev teams across the globe navigate their software supply chain security challenges.

Modernize your data layer. Learn how to design cloud-native database architectures to meet the evolving demands of AI and GenAI workloads.

Releasing software shouldn't be stressful or risky. Learn how to leverage progressive delivery techniques to ensure safer deployments.

Avoid machine learning mistakes and boost model performance! Discover key ML patterns, anti-patterns, data strategies, and more.

Related

  • How to Secure Your APIs
  • API and Security: From IT to Cyber
  • Four Essential Tips for Building a Robust REST API in Java
  • When APIs Go Wrong: Neglecting Rate Limiting

Trending

  • It’s Not About Control — It’s About Collaboration Between Architecture and Security
  • Issue and Present Verifiable Credentials With Spring Boot and Android
  • How to Practice TDD With Kotlin
  • Immutable Secrets Management: A Zero-Trust Approach to Sensitive Data in Containers
  1. DZone
  2. Data Engineering
  3. Databases
  4. Secure Communication with Token-based RSocket

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.

By 
Lu Han user avatar
Lu Han
·
Apr. 30, 20 · Tutorial
Likes (4)
Comment
Save
Tweet
Share
10.7K Views

Join the DZone community and get the full member experience.

Join For Free

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
 




xxxxxxxxxx
1


 
1
bash build.sh



Shell
 




xxxxxxxxxx
1


 
1
bash run_responder.sh
2
bash run_requester.sh


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.

RSocket security authentication Spring Framework API Java (programming language) IT

Opinions expressed by DZone contributors are their own.

Related

  • How to Secure Your APIs
  • API and Security: From IT to Cyber
  • Four Essential Tips for Building a Robust REST API in Java
  • When APIs Go Wrong: Neglecting Rate Limiting

Partner Resources

×

Comments
Oops! Something Went Wrong

The likes didn't load as expected. Please refresh the page and try again.

ABOUT US

  • About DZone
  • Support and feedback
  • Community research
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 100
  • Nashville, TN 37211
  • support@dzone.com

Let's be friends:

Likes
There are no likes...yet! 👀
Be the first to like this post!
It looks like you're not logged in.
Sign in to see who liked this post!