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.
Join the DZone community and get the full member experience.
Join For FreeRSocket 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.
JWT includes JSON Web Signature (JWS), JSON Web Encryption (JWE), JSON Web Key (JWK), and JSON Web Algorithms (JWA).
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.
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
).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.
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.
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.
xxxxxxxxxx
public static final long ACCESS_EXPIRE = 5;
public static final long REFRESH_EXPIRE = 7;
private static final MacAlgorithm MAC_ALGORITHM = MacAlgorithm.HS256;
private static final String HMAC_SHA_256 = "HmacSHA256";
I will show you the token generated code:
xxxxxxxxxx
public static UserToken generateAccessToken(HelloUser user) {
Algorithm ACCESS_ALGORITHM = Algorithm.HMAC256(ACCESS_SECRET_KEY);
return generateToken(user, ACCESS_ALGORITHM, ACCESS_EXPIRE, ChronoUnit.MINUTES);
}
private static UserToken generateToken(HelloUser user, Algorithm algorithm, long expire, ChronoUnit unit) {
String tokenId = UUID.randomUUID().toString();
Instant instant;
Instant now = Instant.now();
if (now.isSupported(unit)) {
instant = now.plus(expire, unit);
} else {
log.error("unit param is not supported");
return null;
}
String token = JWT.create()
.withJWTId(tokenId)
.withSubject(user.getUserId())
.withClaim("scope", user.getRole())
.withExpiresAt(Date.from(instant))
.sign(algorithm);
return UserToken.builder().tokenId(tokenId).token(token).user(user).build();
}
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:
xxxxxxxxxx
public static ReactiveJwtDecoder getAccessTokenDecoder() {
SecretKeySpec secretKey = new SecretKeySpec(ACCESS_SECRET_KEY.getBytes(), HMAC_SHA_256);
return NimbusReactiveJwtDecoder.withSecretKey(secretKey)
.macAlgorithm(MAC_ALGORITHM)
.build();
}
public static ReactiveJwtDecoder jwtAccessTokenDecoder() {
return new HelloJwtDecoder(getAccessTokenDecoder());
}
// HelloJwtDecoder
public Mono<Jwt> decode(String token) throws JwtException {
return reactiveJwtDecoder.decode(token).doOnNext(jwt -> {
String id = jwt.getId();
HelloUser auth = tokenRepository.getAuthFromAccessToken(id);
if (auth == null) {
throw new JwtException("Invalid HelloUser");
}
//TODO
helloJwtService.setTokenId(id);
});
}
The decode method in HelloJwtDecoder
will be invoked by the framework in every request handling time, to convert the token string value to jwt:
xxxxxxxxxx
PayloadSocketAcceptorInterceptor authorization(RSocketSecurity rsocketSecurity) {
RSocketSecurity security = pattern(rsocketSecurity)
.jwt(jwtSpec -> {
try {
jwtSpec.authenticationManager(jwtReactiveAuthenticationManager(jwtDecoder()));
} catch (Exception e) {
throw new RuntimeException(e);
}
});
return security.build();
}
public ReactiveJwtDecoder jwtDecoder() throws Exception {
return TokenUtils.jwtAccessTokenDecoder();
}
public JwtReactiveAuthenticationManager jwtReactiveAuthenticationManager(ReactiveJwtDecoder decoder) {
JwtAuthenticationConverter converter = new JwtAuthenticationConverter();
JwtGrantedAuthoritiesConverter authoritiesConverter = new JwtGrantedAuthoritiesConverter();
authoritiesConverter.setAuthorityPrefix("ROLE_");
converter.setJwtGrantedAuthoritiesConverter(authoritiesConverter);
JwtReactiveAuthenticationManager manager = new JwtReactiveAuthenticationManager(decoder);
manager.setJwtAuthenticationConverter(new ReactiveJwtAuthenticationConverterAdapter(converter));
return manager;
}
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.
xxxxxxxxxx
Cache<String, HelloUser> accessTokenTable = CacheBuilder.newBuilder()
.expireAfterWrite(TokenUtils.ACCESS_EXPIRE, TimeUnit.MINUTES).build();
public void deleteAccessToken(String tokenId) {
accessTokenTable.invalidate(tokenId);
}
Authentication
The authenticate
function for signin
, is as simple as the HTTP basic authentication did:
xxxxxxxxxx
HelloUser user = userRepository.retrieve(principal);
if (user.getPassword().equals(credential)) {
return user;
}
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
xxxxxxxxxx
return reactiveJwtDecoder.decode(refreshToken).map(jwt -> {
try {
HelloUser user = HelloUser.builder().userId(jwt.getSubject()).role(jwt.getClaim("scope")).build();
log.info("verify successfully. user:{}", user);
HelloUser auth = tokenRepository.getAuthFromRefreshToken(jwt.getId());
if (user.equals(auth)) {
return user;
}
} catch (Exception e) {
log.error("", e);
}
return new HelloUser();
});
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:
xxxxxxxxxx
// HelloSecurityConfig
protected RSocketSecurity pattern(RSocketSecurity security) {
return security.authorizePayload(authorize -> authorize
.route("signin.v1").permitAll()
.route("refresh.v1").permitAll()
.route("signout.v1").authenticated()
.route("hire.v1").hasRole(ADMIN)
.route("fire.v1").hasRole(ADMIN)
.route("info.v1").hasAnyRole(USER, ADMIN)
.route("list.v1").hasAnyRole(USER, ADMIN)
.anyRequest().authenticated()
.anyExchange().permitAll()
);
}
// HelloJwtSecurityConfig
public class HelloJwtSecurityConfig extends HelloSecurityConfig {
PayloadSocketAcceptorInterceptor authorization(RSocketSecurity rsocketSecurity) {
RSocketSecurity security = pattern(rsocketSecurity)
...
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.
"signin.v1") (
Mono<HelloToken> signin(HelloUser helloUser) {
...
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
xxxxxxxxxx
bash build.sh
xxxxxxxxxx
bash run_responder.sh
bash run_requester.sh
xxxxxxxxxx
bash curl_test.sh
curl Test
xxxxxxxxxx
echo "signin as user"
read accessToken refreshToken < <(echo $(curl -s "http://localhost:8989/api/signin?u=0000&p=Zero4" | jq -r '.accessToken,.refreshToken'))
echo "Access Token :${accessToken}"
echo -e "Refresh Token :${refreshToken}\\n"
echo "[user] refresh:"
curl -s "http://localhost:8989/api/refresh/${refreshToken}" | jq
echo
echo "[user] info:"
curl "http://localhost:8989/api/info/1"
echo -e "\\n"
echo "[user] list:"
curl -s "http://localhost:8989/api/list" | grep data -c
echo
echo "[user] hire:"
curl -s "http://localhost:8989/api/hire" \
-H "Content-Type: application/stream+json;charset=UTF-8" \
-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.
Opinions expressed by DZone contributors are their own.
Comments