WebSocket Security
The WebSocket connection of the Document Editor carries every change a user makes, and it stays open for as long as the editor does. Protecting it takes an encrypted connection, a check of each handshake before the connection is established, a list of the origins that may connect, care with the server code that reaches into running sessions and a private link to the backend. How the connection itself works is the subject of WebSocket Architecture in the Integration Guide.
Use an Encrypted Connection
A WebSocket URL starts with wss when the page runs on HTTPS and with ws when it runs on HTTP, and the WebSocket traffic uses the same security layer as the HTTP connection it starts from. With wss, the connection is encrypted with TLS like the page.
- ASP.NET Core views build the URL from the address of the page, so a page on HTTPS connects with
wss. - JavaScript, Angular and React set the URL themselves, in
webSocketURL. Write it withwss://, for examplewss://backend.example.com/api/TXWebSocket.
Serve the Application Over HTTPS in Secure Deployment sets up HTTPS on the server.
Check the Handshake
A WebSocket connection starts with an HTTP request, the handshake. A middleware between app.UseWebSockets() and app.UseTXWebSocketMiddleware() sees this request before the connection is established, and when it rejects the request, the handshake fails and the editor doesn't connect. There are two ways to check it:
- With the sign-in cookie. When users sign in to your application, the browser sends the authentication cookie with the handshake. A fallback policy that requires signed-in users covers the handshake when
UseAuthenticationandUseAuthorizationcome before the TX Text Control middleware, as Require Signed-In Users describes. Restrict the origins as well, as described below. - With an access token. Frontends without the cookie pass a token, which a middleware of your own checks. The rest of this section shows this middleware.
The following middleware accepts the handshake of the editor only with a valid token from the AccessTokenStore of Access Tokens. It reads the token from an additional sub-protocol, which the JavaScript editor and the ASP.NET Core view send, or from the query string, which the Angular and React components use. Create WebSocketTokenMiddleware.cs:
// Accepts the WebSocket handshake of the Document Editor only with a valid access token
public class WebSocketTokenMiddleware
{
private readonly RequestDelegate _next;
private readonly AccessTokenStore _tokens;
public WebSocketTokenMiddleware(RequestDelegate next, AccessTokenStore tokens)
{
_next = next;
_tokens = tokens;
}
public async Task InvokeAsync(HttpContext context)
{
if (context.WebSockets.IsWebSocketRequest &&
context.WebSockets.WebSocketRequestedProtocols.Contains("TXTextControl.Web"))
{
// The token as the sub-protocol "token_...", or in the query string
string? token = context.WebSockets.WebSocketRequestedProtocols
.FirstOrDefault(protocol => protocol.StartsWith("token_"))?.Substring(6);
token ??= context.Request.Query["access_token"];
if (!_tokens.Validate(token, out _))
{
context.Response.StatusCode = StatusCodes.Status401Unauthorized;
return;
}
}
await _next(context);
}
}
Register it in Program.cs between the two WebSocket calls:
// Accept WebSocket requests
app.UseWebSockets();
// Check the handshake before TX Text Control accepts the connection
app.UseMiddleware<WebSocketTokenMiddleware>();
app.UseTXWebSocketMiddleware();
The code works as follows:
IsWebSocketRequestandWebSocketRequestedProtocolsidentify the handshake of the editor, which always requests the sub-protocolTXTextControl.Web. All other requests pass through unchanged.- The editor sends the additional sub-protocols from
webSocketProtocolsin theSec-WebSocket-Protocolheader, next toTXTextControl.Web. The middleware takes the entry that starts withtoken_and removes the prefix. Without such an entry, it reads theaccess_tokenparameter of the URL. - A missing, unknown or expired token ends the request with the status code 401, before the connection is established. The editor on the page doesn't connect.
- A valid token passes the request on to the TX Text Control middleware, which accepts the connection. Your middleware only checks the request and never accepts the WebSocket itself.
The editor opens two WebSocket connections while it initializes, and both are checked. The script of the editor, which JavaScript pages load from /api/TXWebSocket/GetResource, is an ordinary HTTP request and isn't checked by this middleware. The blog article Securing WebSocket Connections in ASP.NET Core using Sec WebSocket Protocol Header introduces the sub-protocol approach with a fixed token.
Restrict the Origins
CORS policies don't protect WebSocket connections: browsers send no preflight request for them. A page on another website can therefore open a WebSocket connection to your application, and when your application relies on the authentication cookie, the browser sends the cookie of the user along. Microsoft names this attack Cross-Site WebSocket Hijacking in the description of WebSocketOptions.AllowedOrigins.
Browsers do send the Origin header with the handshake, and AllowedOrigins accepts handshakes only from the origins you list. By default, all origins are allowed. List the origins of your pages in Program.cs:
var webSocketOptions = new WebSocketOptions();
webSocketOptions.AllowedOrigins.Add("https://app.example.com");
app.UseWebSockets(webSocketOptions);
app.UseTXWebSocketMiddleware();
A handshake from another origin then ends with the status code 403, and the editor on that page doesn't connect.
The list of origins isn't authentication: clients other than browsers can send any Origin header or none. It protects signed-in users against other websites, and a token check protects the connection against everybody else. A token in webSocketProtocols is added by your page's script, not by the browser, so a page on another website doesn't have it. WebSockets support in ASP.NET Core describes the options.
Protect Endpoints That Take a Connection ID
Each open editor has a connection ID, and WebSocketHandler.GetInstance returns the handler of the editor with a given ID. Server code can save the document of that editor, load another document into it or change its selection, as Reach a Session on the Server shows. An endpoint that takes a connection ID from a request therefore gives access to a running editing session.
The connection ID isn't a secret. The page reads it from TXTextControl.connectionID and sends it to your endpoint. Protect such endpoints like every other document endpoint:
- Require a signed-in user. Apply
[Authorize]or a fallback policy, as Authentication describes. - Check that the connection belongs to the user. Keep track of which connection IDs belong to which user. Your server can create the ID itself and pass it to the editor with DocumentEditorSettings.connectionID, so that it knows the owner of each ID from the start.
The blog articles about the WebSocketHandler, such as Best Practices: Reliable Auto-Save in TX Text Control Using the WebSocketHandler and Background Services, accept any connection ID to keep the samples short. Add these checks before you use their code.
Keep the Backend Connection Private
Behind the WebSocket middleware, each connection continues over plain TCP to the Document Editor backend TXTextControl.Web.Server.Core. Without configuration, the backend listens on port 4283 on all network interfaces and accepts connections from any client that reaches the port. Bind it to the loopback address, or keep the backend and the application in a private network, as Keep the Backend Private in Secure Deployment describes.
Next Steps
- Access Tokens: issue the tokens that the middleware checks and pass them from the frontend.
- WebSocket Architecture: the path of a connection, proxies and hosting platforms.
- Secure Deployment: HTTPS, the backend port and the accounts on the server.
- WebSockets in Troubleshooting: when the editor doesn't connect.