Skip to main content
All docs
V23.2

Authentication System Architecture (Blazor)

  • 8 minutes to read

The XAF Security System for .NET is built on top of ASP.NET Core standard authentication. If you are not familiar with this authentication mechanism, learn about its basic concepts first. That’s a must for Security System behavior customization in XAF. Review the following topic in the Microsoft documentation for ASP.NET Core:

Overview of ASP.NET Core authentication.

Authentication Flow (Standard Authentication, Windows Active Directory Authentication)

If your application uses one of the built-in authentication schemes, the user authentication process involves the following steps:

Authentication Flow Diagram

Note

If you use Windows Active Directory as the default authentication method, the client passes user identity information to the server with each request. In this instance, neither a temporary token nor an authentication cookie are ever issued by the server or used by the client. Consequently, the flow described below does not apply.

The diagram illustrates the following steps:

1. The client collects logon parameters and sends them to the server to initiate the authentication process

When an unauthenticated client (a client that does not have a valid authentication cookie) sends a GET request to the server, the server redirects it to the application’s Login Form. After a user submits the form, the client sends logon data to the server.

The server initiates the authentication process (calls the Security.Authentication.Authenticate method). XAF allows any number of authentication providers to be registered simultaneously (StandardAuthenticationProvider, WindowsAuthenticationProvider, and any number of custom providers). During the authentication process, the server sequentially tries to use every registered provider to authenticate the user.

2. The server responds with a temporary authentication token

The process continues when one of the registered providers successfully authenticates a user. The server generates a temporary authentication token with the user’s data and sends that token to the client. (The token stays active for only 15 seconds).

The token contains a ClaimsPrincipal object generated by the ASP.NET Core authentication system. A ClaimsPrincipal is a dictionary that contains a number of Claims – key-value pairs. Each pair is a statement about the user. The application can use these statements to make authorization decisions.

Standard Authentication allows you to intercept this process and customize the set of claims added to the token. To do so, specify the AuthenticationStandardEvents.OnCustomizeLoginToken delegate. For example, you can use this approach to save a logon parameter to claims so you can use it later in your custom authorization logic.

File: MySolution.Blazor.Server\Startup.cs

services.AddXaf(Configuration, builder => {
    // ...
    builder.Security
        .AddPasswordAuthentication(options => {
            options.IsSupportChangePassword = true;
            options.Events.OnCustomizeLoginToken = context => {
                var logonParameters = (CustomLogonParameters)context.LogonParameters;
                context.Claims.Add(new Claim("CompanyId", logonParameters.Company.Id));
                // If your login form allows a user to select a database,
                // you can also add a "DatabaseId" claim:
                // context.Claims.Add(new Claim("DatabaseId", logonParameters.Database.Id));
            };
        });
        // ...
});

The client receives the temporary token and sends it back to the server. The server validates the token and issues an authentication cookie if the validation succeeds.

At this stage, you can customize the set of claims added to the authentication cookie. Supported authentication schemes are Standard and Windows Active Directory (if it’s not the default scheme). Note that customization methods differ depending on the authentication scheme:

Standard Authentication

Specify the AuthenticationStandardEvents.OnCustomizeClaims delegate to customize claims added to the authentication cookie by Standard Authentication. Note that this authentication provider copies all claims from the temporary authentication token as is. Use the OnCustomizeClaims delegate to either alter the copied claims or to add additional claims if you need to do so at this stage:

File: MySolution.Blazor.Server\Startup.cs

services.AddXaf(Configuration, builder => {
    // ...
    builder.Security
        .AddPasswordAuthentication(options => {
            options.IsSupportChangePassword = true;
            options.Events.OnCustomizeClaims = context => {
                context.Claims.Add(new Claim("CustomClaim", "ClaimValue"));
            };
        });
        // ...
});

Windows Active Directory Authentication

Specify the WindowsActiveDirectoryAuthenticationProviderEvents.OnCustomizeClaims delegate to customize claims added to the authentication cookie.

Windows Active Directory authentication generates a WindowsPrincipal object with a massive number of claims. To keep the cookies small, only the following necessary claims are copied to the cookie:

  • ClaimTypes.Name
  • ClaimTypes.NameIdentifier

You may need to keep other claims and use them as factors in your custom authorization logic. If that’s the case, copy the required claims manually in your WindowsActiveDirectoryAuthenticationProviderEvents.OnCustomizeClaims delegate implementation. See example code below:

File: MySolution.Blazor.Server\Startup.cs

using System.Security.Claims;
// ...
services.AddXaf(Configuration, builder => {
    // ...
    builder.Security
        .AddWindowsAuthentication(options => {
            options.CreateUserAutomatically();
            options.Events.OnCustomizeClaims = context => {
                Claim primaryGroupSid = context.Principal.Claims.First(claim => claim.Type == ClaimTypes.PrimaryGroupSid);
                context.Claims.Add(primaryGroupSid);
            };
        });
        // ...
});

At this point, the client switches to the cookie-based authentication and uses it until the cookie is invalidated or deleted (for example, when the user logs out).

The server validates the cookie and authenticates the user. This process uses data from the cookie and ignores logon parameters. The LogonParameters object stays available through the ILogonParameterProvider service but it does not include sensitive data such as the user’s password.

The client attaches the authentication cookie to subsequent requests to stay authenticated. The cookie is processed by ExternalAuthentication that is implicitly registered in every XAF Blazor application, so you only need to add it manually to register a custom authentication scheme. Based on the cookie, ExternalAuthentication produces a ClaimsPrincipal object that the XAF Security System uses internally.

Tip

The ASP.NET Core authentication system produces and processes an authentication cookie throughout the entire authentication process. You never need to interact with a cookie directly. All customizations are made through claims. The following section describes how you can access claims at runtime.

Access Claims at Runtime

You can use one of the following techniques to access claims from an ASP.NET Core Blazor application’s code at runtime:

  • Use the DevExpress.ExpressApp.Security.IPrincipalProvider service’s User.Claims property.
  • In an MVC controller, you can use the controller’s User.Claims property. In middleware, use the HttpContext’s context.User.Claims property.

The code sample below demonstrates how to implement a custom Controller that injects the IPrincipalProvider service and uses it to access claims:

File: MySolution.Blazor.Server\Controllers\MyController.cs

using System.Security.Claims;
using DevExpress.ExpressApp;
using DevExpress.ExpressApp.Security;

namespace MainDemo.Blazor.Server.Controllers {
    public class MyController : ViewController {
        readonly IPrincipalProvider principalProvider;

        public MyController() { }

        [ActivatorUtilitiesConstructor]
        public MyController(IServiceProvider serviceProvider) : this() {
            principalProvider = serviceProvider.GetRequiredService<IPrincipalProvider>();
            var _claimsPrincipal = (ClaimsPrincipal)principalProvider.User;
            var customClaim = _claimsPrincipal.FindFirst(c => c.Type == "CustomClaim");
            if(customClaim != null && customClaim.Value == "ClaimValue") {
                Active.SetItemValue("CustomClaim", false);
            }
        }
    }
}

Interaction Between the Authentication and Authorization Systems

XAF Security System does not use the standard ASP.NET Core authorization system internally and implements its own role-based access control (RBAC) mechanisms. These mechanisms require only the ClaimTypes.NameIdentifier claim and fetch all the user roles and permissions from the database when required.

Because ASP.NET Core authorization is not used, user roles are not added to ClaimsPrincipal. An important implication is that it is incorrect to use the AuthorizeAttribute with the specified Roles property on a custom MVC or API controller, unless you manually add the roles to the ClaimsPrincipal specifically for this purpose as shown below:

File: MySolution.Blazor.Server\API\MyEndpointController.cs

[Route("api/[controller]")]
// The `AuthenticationSchemes` property is required.
[Authorize (AuthenticationSchemes = CookieAuthenticationDefaults.AuthenticationScheme, Roles = "Administrators")]
[ApiController]
public class MyEndpointController : ControllerBase {
    [HttpGet("GetExampleData")]
    public string GetExampleData() {
        return "Example response data";
    }
}

File: MySolution.Blazor.Server\Startup.cs

public void ConfigureServices(IServiceCollection services) {
    // ...
    services.AddXaf(Configuration, builder => {
        // ...
        builder.Security
            // ...
            .AddPasswordAuthentication(options => {
                options.IsSupportChangePassword = true;
                options.Events.OnCustomizeLoginToken = context => {
                    using IObjectSpace os = context.ServiceProvider.GetRequiredService<INonSecuredObjectSpaceFactory>().CreateNonSecuredObjectSpace<ApplicationUser>();
                    var user = os.FirstOrDefault<ApplicationUser>(u => u.ID == new Guid(context.UserId));
                    // The authorization in the previous code sample will always fail unless you manually
                    // populate the user roles in the `ClaimsPrincipal`:
                    foreach (var role in user.Roles) {
                        context.Claims.Add(new Claim(ClaimTypes.Role, role.Name));
                    }
                };
            });
    });
}

Custom Authentication Usage Considerations

In ASP.NET Core Blazor, you should take the following considerations into account when you customize authentication in your application.

Custom Logon Parameters (Standard Authentication)

You can implement an AuthenticationStandardProviderV2 descendant to create your custom authentication object that operates on a custom set of logon parameters. After you register your provider, the standard authentication flow is in effect. You may want to add custom logon parameters to claims for further use as described in the Authentication Flow section.

See the following section for information on how to use custom logon parameters with Standard Authentication: Customize Standard Authentication Behavior and Supply Additional Logon Parameters (Blazor).

Using Third-Party OAuth2 Authentication

If your application uses a third-party OAuth2 authentication provider (Google, Azure, and so on), the standard authentication flow is completely skipped. Instead, the authentication process is as follows:

  1. After a user selects a third-party provider as an authentication method, they need to proceed through that provider’s own authentication flow.
  2. As the result of a successful authentication on the provider side, the provider produces its own ClaimsPrincipal.
  3. Your application needs to make decisions against this ClaimsPrincipal. For example, the application decides whether to authenticate the user on the application side based on data queried from an Object Space; or whether or not to create a new user record if the user was not found. You can implement the logic required for this in a custom implementation of the IAuthenticationProviderV2 interface.

See the following topic for more information on how to use third-party OAuth2 authentication providers in your application: Active Directory and OAuth2 Authentication Providers in ASP.NET Core Blazor Applications.