You are viewing the documentation for Blueriq 17. Documentation for other versions is available in our documentation directory.

Introduction

As shown in 9. Concurrency Control on Multiple Nodes it is possible for a session to become de-synchronized when a slow Runtime node overwrites changes made by another Runtime node, and how this scenario is mitigated using keyspace notifications and interruptible sessions.

This chapter focuses on another scenario which may lead to session de-synchronization. Consider the following scenario, where the user is on Page 1 of an application:

 

 

StepActorDescription
1User/BrowserThe User clicks on button1. A request to handle the event is sent.
2Load BalancerThe load balancer forwards the request to Runtime 1.
3Runtime 1The Runtime reads the session from the key-value store and handles the event. The session is now on Page 2.
4Runtime 1The session is saved in the key-value store
5Runtime 1The Runtime crashes before sending the full response.
6Load BalancerThe load balancer re-sends the request to Runtime 2
7Runtime 2The Runtime reads the session from the key-value store and tries to handle the event. However, since the session is now on Page 2 which doesn't contain the clicked button, the event is ignored.
8Runtime 2The Runtime sends a response indicating there are no changes to the page
9Load BalancerThe load balancer forwards the response to the User. In the end, the user sees that clicking on the button had no effect.
10 - 14 The user can click the same button again, or any other button on the page, and nothing will happen. From the user's perspective, the application has stopped working.

 

The real problem, as before, is that the session is de-synchronized. The user sees the session as being on Page 1, while on the back-end the session is actually on Page 2. The user needs to refresh the page in order to re-synchronize with the back-end, but there is no feedback from the back-end informing the user that he/she should refresh the page.

De-Synchronization Protection Using Request-Wards

This scenario can be detected by attaching a unique identifier to each request, a randomly generated GUID we call a request-ward. For a given session, the Runtime always knows what is the next expected request-ward and can detect a de-synchronized request, as in the following example:

 

 

StepActorDescription
1User/BrowserThe User clicks on button1. A request to handle the event is sent, together with the current request-ward
2Load BalancerThe load balancer forwards the request to Runtime 1.
3Runtime 1The Runtime reads the session from the key-value store.
4Runtime 1The Runtime validates the request ward, generates a new request ward and handles the event. The session is now on Page 2
5Runtime 1The Runtime writes the session in the key-value store. The new request ward is also saved as part of the session.
6.Runtime 1The Runtime crashes before being able to send the response.
7Load BalancerThe load balancer re-sends the request to Runtime 2
8Runtime 2The Runtime reads the session from the key-value store, validates the request ward and sees that the received ward (ward1) doesn't match the expected ward (ward2)
9Runtime 2The Runtime responds with an HTTP 400 Bad Request and a JSON in the response body indicating that an invalid request ward was received
10Load BalancerThe load balancer sends the response back to the User. The user sees an error message stating that the page should be refreshed.
11User/BrowserThe User refreshes the page. This consists of a request to the current UI (for example: /session/{sessionId}/mvc/index.html when MVC UI is used)
12Load BalancerThe load balancer forwards the request to Runtime 2
13Runtime 2The Runtime reads the session from the key-value store
14Runtime 2The Runtime generates the current page model (for Page 2) and sends it in the response. The current request ward (ward2) is also sent as part of the response
15Load BalancerThe load balancer forwards the response to the User. The user now correctly sees that he/she is on Page 2.
16User/BrowserThe user clicks another button on Page 2. A request to handle the event is sent, together with the new request ward.
17Load BalancerThe load balancer fowards the request to Runtime 2
18Runtime 2The Runtime reads the session from the key-value store and validates the request ward. This time, the request ward is valid.
19Runtime 2The Runtime generates a new request ward (ward3) and handles the event
20Runtime 2The Runtime writes the session in the key-value store, together with the new request ward
21Runtime 2The Runtime responds with the page changes and the new request ward
22Load BalancerThe load balancer forwards the response to the User. The user correctly sees the changes on the page, and also has the new request ward for the next request.

 

Please note that while the request ward is similar to a CSRF token, it differs in a few key areas:

  1. An invalid CSRF token indicates a security incident. For safety purposes the session is terminated. An invalid request ward indicates a synchronization issue. Instead of terminating the session, a helpful error message is sent back to the user.
  2. CSRF tokens change only when the page is recomposed. Request Wards change on every request which may modify the session.

Technical Details

The request wards are sent from the client to the server and from the server to the client as a header named X-Request-Ward. By default, the server expects a request ward in the following cases:

  • the request is related to an AquimaSession. This is indicated by annotating the session ID parameter in the controller with @AquimaSessionId
  • the request is a modification request, one of: POST, PUT, PATCH or DELETE
  • the target endpoint does not have request ward validation disabled by annotating it with @RequestWard(validate = false)

The request ward is renewed when the following conditions are met:

  • the request is related to an AquimaSession
  • the request is a modification request
  • the target endpoint does not have request ward renewal disabled by annotating it with @RequestWard(renew = false)

 

The following example illustrates these rules:

@Controller
public class ExampleController {
 
  @GetMapping("/index.html")
  public String index() {
    // omitted
  }
  
  @GetMapping("/{sessionId}/view-profile")
  @ResponseBody
  public ProfileModel viewProfile(String sessionId) {
    // omitted
  }
 
  @GetMapping("/{sessionId}/view-instance/{instanceId}")
  @ResponseBody
  public InstanceModel viewInstance(@AquimaSessionId String sessionId, String instanceId) {
    // omitted
  }
 
  @PostMapping("/{sessionId}/create-instance/{entityName}")
  @ResponseBody
  public InstanceModel createInstance(String sessionId, String entityName) {
    // omitted
  }
 
  @PostMapping("/{sessionId}/update-instance/{instanceId}")
  @ResponseBody
  public InstanceModel updateInstance(@AquimaSessionId String sessionId, String instanceId, @RequestBody InstanceModel instance) {
    // omitted
  }
 
  @PostMapping("/{sessionId}/operation1")
  @ResponseBody
  @RequestWard(validate = false, renew = false)
  public void customOperation(@AquimaSessionId String sessionId) {
    // omitted
  }
}

 

The index() endpoint will not require a request ward, because it is not session related.

The viewProfile() endpoint will not require a request ward either, because even though it is session related, the sessionId parameter is not annotated with @AquimaSessionId. The Runtime is not able to determine that this parameter represents the ID of an AquimaSession.

The viewInstance() endpoint will not require a request ward, because even though it is session related and the sessionId parameter is annotated with @AquimaSessionId, it responds only to GET requests.

The createInstance() endpoint will not require a request ward, because even though it is session related and responds to POST requests, the sessionId parameter is not annotated with @AquimaSessionId

The updateInstance() endpoint requires a request ward and after succesful validation a new request ward will be generated as well.

The customOperation() endpoint does not require a request ward because request ward validation is specifically disabled for this endpoint by using the @RequestWard annotation and setting validate = false. A new request ward will not be generated either because the @RequestWard annotation sets renew = false.

 

For custom endpoints there are 2 methods to disable request wards:

  1. do not annotate the sessionId parameter with @AquimaSessionId. Please note that in this case CSRF validation is also disabled !
  2. annotate the endpoint method with @RequestWard and disable either validation, renewal or both

The reasons to disable request wards might include:

  • the request mapping uses POST, PUT, PATCH or DELETE but doesn't actually modify the session
  • requests may be made in parallel (but we highly advise against concurrent modifications of the same session)
  • the session is destroyed in this endpoint

The following default Blueriq endpoints have request wards disabled:

  • /Runtime/server/api/v2/session/{sessionId}/load - this endpoint loads the page model. Request ward validation is disabled for this endpoint, as the front-end doesn't yet know the request ward. Request ward renewal is enabled however.
  • /Runtime/server/api/v2/session/{sessionId}/keepalive - this endpoint extends the session timeout. Request ward validation and renewal are disabled for this endpoint as keep-alive requests may be performed in parallel with other modification requests for the same session
  • /Runtime/server/api/v2/session/{sessionId}/close - this endpoint closes the session. Request ward validation and renewal are disabled for this endpoint as session de-synchronization is not possible. If the session is deleted and then a failover occurs, then repeating the request on the backup node will result in an error (because the session is already deleted).

The response in case of an invalid request ward has status code 400 and a JSON in the response body like in the following example:

{
  "type":"INVALID_REQUEST_WARD",
  "title":"Invalid Request",
  "message":"Please refresh the page"
}

The title and message are internationalized and can be configured in the usual way in messages.properties. The language of the request is determined based on the Accept-Language request header.

The default MVC v2 front-end implementation just displays this message to the user. Custom front-end implementations may use a combination of the response status code and type field to detect this exception and automatically reload the page. However, notifying the user is recommended, so the user has a chance to take note of the data entered on the page, which will be lost once the page is reloaded.

Multipart Requests

For multipart requests the request ward is also accepted as a request parameter named X-Request-Ward. This exception is made in order to accomodate older browsers which do not support true AJAX multipart requests / file uploads and require the use of hidden forms and iframes to simulate AJAX file uploads. The request ward is accepted as a parameter only for multipart requests. If both the header and parameter are present, the header takes precedence. All other rules from regular requests apply as well.

Configuration

Request wards can be enabled or disabled globally with the following property in application.properties:

blueriq.session.request-ward-enabled=false

By default, request wards are disabled, as Dashboards do not work with request wards enabled due to a few endpoints that accept parallel requests for the same session. 

The error messages may be internationalized in messages.properties and messages_<language>-<country>.properties using the following keys:

request-ward.invalid.title=Invalid request
request-ward.invalid.message=Please refresh the page