Spring WebFlux authenticated WebSocket connection - spring-boot

I have running Spring Boot#2.2.x server with exposed WebSocket endpoint. Here is my WebSocketConfiguration:
#Slf4j
#Configuration
public class WebSocketConfiguration {
private static final String WS_PATH = "/ws/notifications";
#Bean
public HandlerMapping webSocketHandlerMapping() {
Map<String, WebSocketHandler> handlersMap = new HashMap<>();
handlersMap.put(WS_PATH, session -> session.send(session.receive()
.map(WebSocketMessage::getPayloadAsText)
.doOnEach(logNext(log::info))
.map(msg -> format("notification for your msg: %s", msg))
.map(session::textMessage)));
SimpleUrlHandlerMapping handlerMapping = new SimpleUrlHandlerMapping();
handlerMapping.setOrder(Ordered.HIGHEST_PRECEDENCE);
handlerMapping.setUrlMap(handlersMap);
return handlerMapping;
}
#Bean
public WebSocketHandlerAdapter handlerAdapter(WebSocketService webSocketService) {
return new WebSocketHandlerAdapter(webSocketService);
}
#Bean
public WebSocketService webSocketService() {
return new HandshakeWebSocketService(new ReactorNettyRequestUpgradeStrategy());
}
}
The question is how I can implement authentication for establishing WS connection either using Basic Authentication or Bearer Authentication or access_token query parameter?
The preferable option is to avoid using Spring Security.
Thanks.

Websocket connection starts out life as an HTTP request that is Upgraded. You can do JWT token authentication before the upgrade happens. In spring boot it works as follows:
Expose a custom WebSocketService bean:
#Bean
public WebSocketService webSocketService(RequestUpgradeStrategy upgradeStrategy) {
return new HandshakeWebSocketService(upgradeStrategy);
}
Implement the RequestUpgradeStrategy interface in your own class:
#Override
public Mono<Void> upgrade(ServerWebExchange exchange, WebSocketHandler handler, #Nullable String subProtocol, Supplier<HandshakeInfo> handshakeInfoFactory) {
ServerHttpResponse response = exchange.getResponse();
HttpServerResponse reactorResponse = getNativeResponse(response);
HandshakeInfo handshakeInfo = handshakeInfoFactory.get();
NettyDataBufferFactory bufferFactory = (NettyDataBufferFactory) response.bufferFactory();
var authResult = validateAuth(handshakeInfo);
if (authResult == unauthorised) return Mono.just(reactorResponse.status(rejectedStatus))
.flatMap(HttpServerResponse::send);
else return reactorResponse.sendWebsocket(subProtocol, //
this.maxFramePayloadLength,//
(in, out) -> {
ReactorNettyWebSocketSession session = new ReactorNettyWebSocketSession(in, out,
handshakeInfo,
bufferFactory,
this.maxFramePayloadLength);
return handler.handle(session);
});
}
Notes:
The above class is based on ReactorNettyRequestUpgradeStrategy.
Returning reactorResponse.sendWebsocket is the existing behaviour that upgrades the connection to a WebSocket connection
reactorResponse.status can be returned to stop the connection being upgraded. For example, you can return a 401 response in the case of an unauthorised connection.
Query params and Authentication headers can be found in handshake info. How to do the authentication itself is outside the scope of the question.

Related

Authorize OAuth2 request in #Async method

I've got a REST api secured with Spring Security and OAuth2 and JWT. This service is an OAuth client as well, as it needs to connect to other services (using client credentials grant).
Requests to other services, which are as well secured with OAuth is done using OpenFeign and here is the configuration for OAuth2.
#Slf4j
#Configuration
public class OAuth2OpenFeignConfig {
#Value("${client-name}")
private String clientName;
private final ClientRegistrationRepository clientRegistrationRepository;
public OAuth2OpenFeignConfig(ClientRegistrationRepository clientRegistrationRepository) {
this.clientRegistrationRepository = clientRegistrationRepository;
}
#Bean
public RequestInterceptor requestInterceptor(OAuth2AuthorizedClientManager authorizedClientManager) {
var clientRegistration = clientRegistrationRepository.findByRegistrationId(clientName);
var clientCredentialsFeignManager = new OAuthClientCredentialsFeignManager();
return requestTemplate -> requestTemplate.header("Authorization", "Bearer " + clientCredentialsFeignManager.getAccessToken(authorizedClientManager, clientRegistration));
}
static class OAuthClientCredentialsFeignManager {
public String getAccessToken(OAuth2AuthorizedClientManager manager, ClientRegistration clientRegistration) {
try {
var oAuth2AuthorizeRequest = OAuth2AuthorizeRequest
.withClientRegistrationId(clientRegistration.getRegistrationId())
.principal(SecurityContextHolder.getContext().getAuthentication())
.build();
var client = manager.authorize(oAuth2AuthorizeRequest);
if (isNull(client)) {
throw new IllegalStateException("client credentials flow on " + clientRegistration.getRegistrationId() + " failed, client is null");
}
return client.getAccessToken().getTokenValue();
} catch (Exception ex) {
log.error("client credentials error " + ex.getMessage());
}
return null;
}
}
}
All this works as expected in a sync configuration: Feign configuration requests and injeect a JWT in the header. Problems started when tried to rewrite the calls using async (either Runnable or #Async have the same result).
As explained here and here the Security Context is not propagated by default to other threads, for which I need to manually configure
#Bean("threadPoolTaskExecutor")
public TaskExecutor getAsyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(20);
executor.setMaxPoolSize(1000);
executor.setWaitForTasksToCompleteOnShutdown(true);
executor.setThreadNamePrefix("Async-");
executor.initialize(); // this is important, otherwise an error is thrown
return new DelegatingSecurityContextAsyncTaskExecutor(executor);
}
Now I can see that the context gets propagated to the async thread. However, when trying to authenticate the OpenFeign client, an exception is thrown in
var client = manager.authorize(oAuth2AuthorizeRequest)
as internally it calls
HttpServletRequest servletRequest = getHttpServletRequestOrDefault(authorizeRequest.getAttributes());
but the servletRequest is null and the credentials are not set in the request.
Any idea on how to pass the HttpContext to a thread? Should I be concerned by https://docs.spring.io/spring-framework/docs/5.0.2.RELEASE/kdoc-api/spring-framework/org.springframework.web.filter/-request-context-filter/set-thread-context-inheritable.html ?
Please have a look at AuthorizedClientServiceOAuth2AuthorizedClientManager. It is capable of working outside of servlet context.

Spring Boot RSocketRequester deal with server restart

I have a question about Springs RSocketRequester. I have a rsocket server and client. Client connects to this server and requests #MessageMapping endpoint. It works as expected.
But what if I restart the server. How to do automatic reconnect to rsocket server from client? Thanks
Server:
#Controller
class RSC {
#MessageMapping("pong")
public Mono<String> pong(String m) {
return Mono.just("PONG " + m);
}
}
Client:
#Bean
public RSocketRequester rSocketRequester() {
return RSocketRequester
.builder()
.connectTcp("localhost", 7000)
.block();
}
#RestController
class RST {
#Autowired
private RSocketRequester requester;
#GetMapping(path = "/ping")
public Mono<String> ping(){
return this.requester
.route("pong")
.data("TEST")
.retrieveMono(String.class)
.doOnNext(System.out::println);
}
}
Updated for Spring Framework 5.2.6+
You could achieve it with io.rsocket.core.RSocketConnector#reconnect.
#Bean
Mono<RSocketRequester> rSocketRequester(RSocketRequester.Builder rSocketRequesterBuilder) {
return rSocketRequesterBuilder
.rsocketConnector(connector -> connector
.reconnect(Retry.fixedDelay(Integer.MAX_VALUE, Duration.ofSeconds(1))))
.connectTcp("localhost", 7000);
}
#RestController
public class RST {
#Autowired
private Mono<RSocketRequester> rSocketRequesterMono;
#GetMapping(path = "/ping")
public Mono<String> ping() {
return rSocketRequesterMono.flatMap(rSocketRequester ->
rSocketRequester.route("pong")
.data("TEST")
.retrieveMono(String.class)
.doOnNext(System.out::println));
}
}
I don't think I would create a RSocketRequester bean in an application. Unlike WebClient (which has a pool of reusable connections), the RSocket requester wraps a single RSocket, i.e. a single network connection.
I think it's best to store a Mono<RSocketRequester> and subscribe to that to get an actual requester when needed. Because you don't want to create a new connection for each call, you can cache the result. Thanks to Mono retryXYZ operators, there are many ways you can refine the reconnection behavior.
You could try something like the following:
#Service
public class RSocketPingService {
private final Mono<RSocketRequester> requesterMono;
// Spring Boot is creating an auto-configured RSocketRequester.Builder bean
public RSocketPingService(RSocketRequester.Builder builder) {
this.requesterMono = builder
.dataMimeType(MediaType.APPLICATION_CBOR)
.connectTcp("localhost", 7000).retry(5).cache();
}
public Mono<String> ping() {
return this.requesterMono.flatMap(requester -> requester.route("pong")
.data("TEST")
.retrieveMono(String.class));
}
}
the answer here https://stackoverflow.com/a/58890649/2852528 is the right one. The only thing I would like to add is that reactor.util.retry.Retry has many options for configuring the logic of your retry including even logging.
So I would slightly improve the original answer, so we'd be increasing the time between the retry till riching the max value (16 sec) and before each retry log the failure - so we could monitor the activity of the connector:
#Bean
Mono<RSocketRequester> rSocketRequester(RSocketRequester.Builder builder) {
return builder.rsocketConnector(connector -> connector.reconnect(Retry.backoff(Integer.MAX_VALUE, Duration.ofSeconds(1L))
.maxBackoff(Duration.ofSeconds(16L))
.jitter(1.0D)
.doBeforeRetry((signal) -> log.error("connection error", signal.failure()))))
.connectTcp("localhost", 7000);
}

FeignClient is passing on headers

I have about 10 microservices all built with Spring boot 2 using Eureka and FeignClients. My microservices use certain header values to keep track of data so when a FeignClient is used it needs to pass on certain values that are in the incoming request. So if Microservice 1 does a call to Microservice 2 it must pass on the headers from the incoming request onto microservice 2. I haven't been able to find out how I can do that. I understand their is #Header however if you have 20 FeignClients then you don't want to have to manually add the #header to all the FeignClients. Can you indicate that FeignClients must read a certain header from the incoming request and pass it on in the FeignClient?
You can use request interceptor in Feign.
Example Implementation:
Request Interceptor:
#Component
public class MyRequestInterceptor implements RequestInterceptor {
#Override
public void apply(RequestTemplate template) {
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
String authorization = requestAttributes.getRequest().getHeader(HttpHeaders.AUTHORIZATION);
if(null != authorization) {
template.header(HttpHeaders.AUTHORIZATION, authorization);
}
}
}
Bean Configuration:
#Configuration
public class CustomFeignConfig {
#Bean
public Contract feignContract() {
return new feign.Contract.Default();
}
#Bean
public MyRequestInterceptor basicAuthRequestInterceptor() {
return new MyRequestInterceptor();
}
#Bean
public OkHttpClient client() {
return new OkHttpClient();
}
}

If resource server are supposed to be stateless, how to send message to queue with websocket

I am currently working in messaging system, where resource server is stateless with oAuth 2. Now, i have to send a message to single user with a queue but problem is that spring messaging needed a session in other to send a messaging as described in https://stackoverflow.com/a/31577152/3076403.
The problem with me is how to get currently login user in stateless restful service:
#MessageMapping("/messaging")
public void messaging( Message<Object> message) {
Principal user=
message.getHeaders()
.get(SimpMessageHeaderAccessor.USER_HEADER,Principal.class);
messageTemplate.convertAndSend("/topic/users", user.getName());
}
Spring will use the queue when we use simpMessagingTemplate.convertAndSendToUser(...) method and pass the username associated with session id. Otherwise it will use a topic, where all subscribed clients will eventually read the same message returned from the server.
As I have no session in resource server and need queue to send message to individual user.Any comments and ideas appreciated
Finally after all i get a solution. By decoding json web token for username and providing authentication to username solve above problems. JwtAuthentication is custom class which is responsible for decoding JWT and providing authentication to username of JWT
#Configuration
#EnableWebSocketMessageBroker
#Order(Ordered.HIGHEST_PRECEDENCE + 99)
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {
#Autowired
private SimpUserRegistry userRegistry;
#Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableSimpleBroker("/topic","/queue");
// use the /app prefix for others
config.setApplicationDestinationPrefixes("/app");
}
#Autowired
private JwtAuthentication jwtAuthentication;
#Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
// use the /messaging endpoint (prefixed with /app as configured above) for incoming requests
registry.addEndpoint("/messaging").setAllowedOrigins("http://localhost:8080").withSockJS();
}
#Override
public void configureClientInboundChannel(ChannelRegistration registration) {
registration.setInterceptors(new ChannelInterceptorAdapter() {
#Override
public Message<?> preSend(Message<?> message, MessageChannel channel) {
StompHeaderAccessor accessor =
MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
List<String> tokenList = accessor.getNativeHeader("Authorization");
String token = null;
if(tokenList != null && tokenList.size() > 0) {
token = tokenList.get(0).replaceAll("Bearer", "").trim();
}
if (StompCommand.CONNECT.equals(accessor.getCommand()) || StompCommand.SUBSCRIBE.equals(accessor.getCommand()) || StompCommand.SEND.equals(accessor.getCommand()) ) {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if(auth==null){
Authentication user = jwtAuthentication.getAuthentication(token); // access authentication header(s)
SecurityContextHolder.getContext().setAuthentication(user);
((DefaultSimpUserRegistry) userRegistry).onApplicationEvent(new SessionConnectedEvent(this, (Message<byte[]>) message, auth));
accessor.setUser(user);
} else {
accessor.setUser(auth);
((DefaultSimpUserRegistry) userRegistry).onApplicationEvent(new SessionConnectedEvent(this, (Message<byte[]>) message, auth));
}
}
accessor.setLeaveMutable(true);
return MessageBuilder.createMessage(message.getPayload(), accessor.getMessageHeaders());
}
});
}
}
In application context we need to register SimpUserRegistry
#Bean
#Primary
public SimpUserRegistry userRegistry() {
return new DefaultSimpUserRegistry();
}
#Bean
#Primary
public UserDestinationResolver userDestinationResolver() {
return new DefaultUserDestinationResolver(userRegistry());
}
Now We can send message to specific user
public void handle(Exchange exchange) {
Message camelMessage = exchange.getIn();
com.livetalk.user.utils.Message message = camelMessage.getBody( com.livetalk.user.utils.Message.class);
// send the message specifically to the destination user by using STOMP's user-directed messaging
msgTemplate.convertAndSendToUser(message.getRecipient(), "/queue/messages", message, defaultHeaders);
}

How to send HTTP OPTIONS request with body using Spring rest template?

I am trying to call a RESTfull web service resource, this resource is provided by a third party, the resource is exposed with OPTIONS http verb.
To integrate with the service, I should send a request with a specific body, which identities by a provider, but when I did that I got a bad request. After that I trace my code then I recognized that the body of the request is ignored by rest template based on the below code:
if ("POST".equals(httpMethod) || "PUT".equals(httpMethod) ||
"PATCH".equals(httpMethod) || "DELETE".equals(httpMethod)) {
connection.setDoOutput(true);
}
else {
connection.setDoOutput(false);
}
my question, is there a standard way to override this behavior or I should use another tool?
The code you've pasted is from
SimpleClientHttpRequestFactory.prepareConnection(HttpURLConnection connection, String httpMethod)
I know because I've debugged that code few hours ago.
I had to do a HTTP GET with body using restTemplate. So I've extend SimpleClientHttpRequestFactory, override prepareConnection and create a new RestTemplate using the new factory.
public class SimpleClientHttpRequestWithGetBodyFactory extends SimpleClientHttpRequestFactory {
#Override
protected void prepareConnection(HttpURLConnection connection, String httpMethod) throws IOException {
super.prepareConnection(connection, httpMethod);
if ("GET".equals(httpMethod)) {
connection.setDoOutput(true);
}
}
}
Create a new RestTemplate based on this factory
new RestTemplate(new SimpleClientHttpRequestWithGetBodyFactory());
A test to prove the solution is working using spring boot (#RunWith(SpringRunner.class)
#SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT))
public class TestRestTemplateTests extends AbstractIntegrationTests {
#Test
public void testMethod() {
RestTemplate restTemplate = new RestTemplate(new SimpleClientHttpRequestWithBodyForGetFactory());
HttpEntity<String> requestEntity = new HttpEntity<>("expected body");
ResponseEntity<String> responseEntity = restTemplate.exchange("http://localhost:18181/test", HttpMethod.GET, requestEntity, String.class);
assertThat(responseEntity.getBody()).isEqualTo(requestEntity.getBody());
}
#Controller("/test")
static class TestController {
#RequestMapping
public #ResponseBody String testMethod(HttpServletRequest request) throws IOException {
return request.getReader().readLine();
}
}
}

Resources