TX Text Control .NET Server

Authentication

Topic

The Document Editor and the Document Viewer add endpoints to your ASP.NET Core application, and ASP.NET Core authentication decides who may use them, as it does for your own controllers. TX Text Control has no user management of its own. Read on for which requests an application with TX Text Control has to protect, how the order of the middleware covers the endpoints of the editor and the viewer, how policies and per-document checks protect your document endpoints, and what changes for the signing endpoint and for frontends on another origin.

Which Requests Need Protection

RequestHandled byProtected by
The WebSocket connection of the Document Editor at /api/TXWebSocket, and its scriptThe middleware that app.UseTXWebSocketMiddleware() addsThe order of the middleware and a fallback policy, or a token check in the handshake
The requests of the Document ViewerThe TextControl controller of the TXTextControl.Web.DocumentViewer packageA fallback policy, or a token check
Loading, saving and generating documentsYour controllers[Authorize] and policies, and a check of each document
Signed documents from the Document ViewerYour signing endpointA token in its URL

You can't put an [Authorize] attribute on the TX Text Control middleware or on the controller in the viewer package, because neither is your code. Instead, the authorization middleware of ASP.NET Core applies a fallback policy to them. According to Microsoft, the fallback policy applies to endpoints that don't specify authorization metadata, and a request that doesn't match an endpoint, such as a WebSocket request that a middleware handles, uses the fallback policy when the authorization middleware runs for it. Static files that the static file middleware serves before the authorization middleware aren't protected by the fallback policy. Policy-based authorization in ASP.NET Core describes the rules.

Require Signed-In Users

When users sign in to your application, for example with cookie authentication, a fallback policy that requires an authenticated user protects the editor, the viewer and every controller action without authorization metadata:

  • Register the policy. AddAuthorization with a FallbackPolicy built by RequireAuthenticatedUser.
  • Authenticate and authorize before TX Text Control. In Program.cs, app.UseAuthentication() and app.UseAuthorization() come after app.UseRouting() and before app.UseWebSockets(), app.UseTXWebSocketMiddleware() and app.UseTXDocumentViewer(). Placed after the TX Text Control middleware, the policy doesn't cover the WebSocket connection of the editor.
  • Allow the sign-in page. Mark the page or action where users sign in with [AllowAnonymous], or nobody can reach it.

The following code in Program.cs uses cookie authentication:

using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authorization;

// Before builder.Build()
builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
    .AddCookie();
builder.Services.AddAuthorization(options =>
{
    options.FallbackPolicy = new AuthorizationPolicyBuilder()
        .RequireAuthenticatedUser()
        .Build();
});

// After builder.Build(): authenticate and authorize before the TX Text Control middleware
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();

app.UseWebSockets();
app.UseTXWebSocketMiddleware();
app.UseTXDocumentViewer();

With the policy in place, the server rejects the WebSocket handshake of the editor, the script of the editor and the requests of the viewer when the user isn't signed in, and accepts them with the authentication cookie of the user. Since ASP.NET Core 10, API endpoints that a user without the cookie calls return the status codes 401 or 403 instead of redirecting to the sign-in page, as Use cookie authentication without ASP.NET Core Identity describes.

Authorize Actions With Policies

A signed-in user isn't necessarily allowed to do everything. Policies express what a user may do, and attributes on your controllers apply them. The following controller lets every signed-in user load documents, but only users in the role Editor save them. It extends the DocumentController of Add a Document Controller. Register the policy in Program.cs before builder.Build(), in the same AddAuthorization call as the fallback policy if your application has one:

builder.Services.AddAuthorization(options =>
{
    // Only users in the role "Editor" may save documents
    options.AddPolicy("DocumentEditors", policy => policy.RequireRole("Editor"));
});

Then apply the policies in Controllers/DocumentController.cs:

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

[Authorize]
[ApiController]
[Route("[controller]")]
public class DocumentController : ControllerBase
{
    // Every signed-in user may load a document
    [HttpGet("Load")]
    public DocumentData Load(string name)
    {
        var file = Path.Combine("Documents", Path.GetFileName(name));

        return new DocumentData
        {
            Name = name,
            Data = Convert.ToBase64String(System.IO.File.ReadAllBytes(file))
        };
    }

    // Only users of the policy "DocumentEditors" may save it
    [Authorize(Policy = "DocumentEditors")]
    [HttpPost("Save")]
    public bool Save([FromBody] DocumentData documentData)
    {
        var file = Path.Combine("Documents", Path.GetFileName(documentData.Name));
        System.IO.File.WriteAllBytes(file, Convert.FromBase64String(documentData.Data));

        return true;
    }
}

public class DocumentData
{
    public string Name { get; set; } = string.Empty;
    public string Data { get; set; } = string.Empty;
}

The code works as follows:

  • [Authorize] on the controller requires a signed-in user for all of its actions, also when the application has no fallback policy.
  • [Authorize(Policy = "DocumentEditors")] on Save adds the policy: the request of a signed-in user who isn't in the role Editor is refused.
  • Path.GetFileName keeps only the file name of the name that a client sends, so that a request can't read or write outside the Documents folder.

Warning

Read-Only Is Not Authorization. The editor itself can be read-only for users who may not save, with TextControlSettings.EditMode in a view or editMode in JavaScript. That changes what users see, not what the server accepts. A document that users edit exists only in the memory of the Document Editor backend until your code saves it, so the save endpoint is where a change becomes permanent, and it has to check the permission itself.

Check Access to Each Document

A policy decides whether a user may load documents at all, not whether the user may load a particular one. [Authorize] runs before the action loads a document, so it can't take the document into account. For a check per document, the action loads the information about the document first, such as its owner, and then asks the IAuthorizationService whether the user may access it. When the check fails, the action returns Forbid() for a signed-in user and Challenge() for a user who isn't signed in. Microsoft calls this resource-based authorization and describes it in Resource-based authorization in ASP.NET Core MVC.

Check every place where a document enters or leaves your application:

  • Loading. Check the document before you return it to the editor or the viewer.
  • Saving. Check that the user may change the document before you overwrite it.
  • Paths in views. When a view passes a DocumentPath to the Document Viewer, the viewer loads the document of this path on the server. Don't build the path from a value of the request without checking it.

Protect the Signing Endpoint

When users sign a document in the Document Viewer, the viewer's TextControl controller in your application posts the signed document on to the URL of RedirectUrlAfterSignature. Because your server sends this request and not the browser, it carries no authentication cookie of the user, and [Authorize] can't tell who signed. Protect the endpoint with a token of the signing process in its URL instead. If your application has a fallback policy, mark the signing action with [AllowAnonymous], so that the token check decides. Protect the Signing Endpoint in Digital Signatures shows the code.

Frontends on Another Origin

The authentication cookie reaches the backend only when the frontend and the backend share an origin. That's the case for these setups:

  • ASP.NET Core views and Razor Pages in the application that hosts the editor and the viewer.
  • An Angular application served by the ASP.NET Core project, as in Build and Host the Application.
  • A React application during development with the Vite proxy for /api/TXWebSocket, /TextControl and your controllers, as in Proxy the Backend During Development.

A frontend on another origin needs a CORS policy for the Document Viewer and your controllers, as Configure Program.cs for Separate Frontends shows. For the Document Editor, Access Tokens describes how a frontend gets a token and passes it with its requests. The Document Viewer in JavaScript, Angular and React has no documented setting for a token, so serve such a frontend from the origin of the backend, as in the setups above, and let the cookie protect the viewer.

Next Steps

  • Access Tokens: issue tokens and pass them from the frontend to the editor and the viewer.
  • WebSocket Security: check the WebSocket handshake of the Document Editor and restrict its origins.
  • Digital Signatures: protect the signing endpoint and the signing certificate.
  • Security Overview: the endpoints and ports of an application with TX Text Control.