# WebSocket Security

> Secure the Document Editor's WebSocket: wss, a token check in the handshake, allowed origins, endpoints that take connection IDs and the private backend link.

- **Product:** TX Text Control .NET Server
- **Section:** Documentation
- **Kind:** Topic
- **Path:** TX Text Control .NET Server > Security > WebSocket Security
- **2465 words**
- **Web URL:** https://docs.textcontrol.com/tx-text-control-dotnet-server/security/websockets/
- **LLMs URL:** https://docs.textcontrol.com/tx-text-control-dotnet-server/security/websockets/llms.txt
- **LLMs-Full URL:** https://docs.textcontrol.com/tx-text-control-dotnet-server/security/websockets/llms-full.txt

---

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](https://docs.textcontrol.com/tx-text-control-dotnet-server/integration/websockets/llms-full.txt) 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 with `wss://`, for example `wss://backend.example.com/api/TXWebSocket`.

[Serve the Application Over HTTPS](https://docs.textcontrol.com/tx-text-control-dotnet-server/security/deployment/#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 `UseAuthentication` and `UseAuthorization` come before the TX Text Control middleware, as [Require Signed-In Users](https://docs.textcontrol.com/tx-text-control-dotnet-server/security/authentication/#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](https://docs.textcontrol.com/tx-text-control-dotnet-server/security/access-tokens/#issue-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*:

```csharp
// 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:

```csharp
// 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:

- `IsWebSocketRequest` and `WebSocketRequestedProtocols` identify the handshake of the editor, which always requests the sub-protocol `TXTextControl.Web`. All other requests pass through unchanged.
- The editor sends the additional sub-protocols from `webSocketProtocols` in the `Sec-WebSocket-Protocol` header, next to `TXTextControl.Web`. The middleware takes the entry that starts with `token_` and removes the prefix. Without such an entry, it reads the `access_token` parameter 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](https://www.textcontrol.com/blog/2025/11/20/securing-websocket-connections-in-aspnet-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](https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.builder.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*:

```csharp
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](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/websockets) 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](https://docs.textcontrol.com/tx-text-control-dotnet-server/integration/websockets/#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](https://docs.textcontrol.com/tx-text-control-dotnet-server/reference/javascript/txtextcontrol/connectionid-property/llms-full.txt) 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](https://docs.textcontrol.com/tx-text-control-dotnet-server/security/authentication/llms-full.txt) 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](https://docs.textcontrol.com/tx-text-control-dotnet-server/reference/javascript/documenteditorsettings/connectionid-property/llms-full.txt), 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](https://www.textcontrol.com/blog/2025/06/20/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](https://docs.textcontrol.com/tx-text-control-dotnet-server/security/deployment/#keep-the-backend-private) in Secure Deployment describes.

### Next Steps

- [Access Tokens](https://docs.textcontrol.com/tx-text-control-dotnet-server/security/access-tokens/llms-full.txt): issue the tokens that the middleware checks and pass them from the frontend.
- [WebSocket Architecture](https://docs.textcontrol.com/tx-text-control-dotnet-server/integration/websockets/llms-full.txt): the path of a connection, proxies and hosting platforms.
- [Secure Deployment](https://docs.textcontrol.com/tx-text-control-dotnet-server/security/deployment/llms-full.txt): HTTPS, the backend port and the accounts on the server.
- [WebSockets](https://docs.textcontrol.com/tx-text-control-dotnet-server/troubleshooting/websockets/llms-full.txt) in Troubleshooting: when the editor doesn't connect.

---

## Nearby

- **Previous:** [Access Tokens](https://docs.textcontrol.com/tx-text-control-dotnet-server/security/access-tokens/llms-full.txt)
- **Next:** [Document Security](https://docs.textcontrol.com/tx-text-control-dotnet-server/security/documents/llms-full.txt)
