Skip to main content
All docs
V24.1
.NET Framework 4.5.2+

Active Directory and OAuth2 Authentication Providers in ASP.NET Core Blazor Applications

  • 10 minutes to read

This topic demonstrates how to extend your ASP.NET Core Blazor application with external authentication methods such as Windows Authentication and OAuth providers (Google, Azure, and GitHub).

Extended Logon Window in an XAF ASP.NET Core Blazor Application, DevExpress

Important

The Solution Wizard generates the code shown in this help topic when you create an application. Follow this article if you want to implement the demonstrated functionality in an existing XAF solution.

Prerequisites

Your application must use Standard Authentication. To enable Standard Authentication, select the cooresponding option in the Solution Wizard when you create a new application or follow the steps in the following help topic to enable it in an existing application: Use the Security System.

If you want to disable Standard Authentication after you add other types of authentication, navigate to the YourSolutionName.Blazor.Server folder, open the Startup.cs file, and comment out the AddPasswordAuthentication method call:

public class Startup {
    // ...
    public void ConfigureServices(IServiceCollection services) {
        // ...
        services.AddXaf(Configuration, builder => {
            // ...
            builder.Security
                // ...
                // .AddPasswordAuthentication()
                // ...
        });
        // ...
    }
    // ...
}

Windows Authentication

  1. In the MySolution.Blazor.Server\Properties\launchSettings.json file, set windowsAuthentication to true. You can also set anonymousAuthentication to false to hide the logon page and always use Windows authentication:

    {
      "iisSettings": {
        "windowsAuthentication": true,
        "anonymousAuthentication": false, // optional
        // ...
      },
      // ...
    }
    
  2. In the MySolution.Blazor.Server\Startup.cs file, call the AddWindowsAuthentication method in the security builder to add Windows Authentication. You can also automatically create a user object with a predefined role when a user attempts to log on for the first time:

    using DevExpress.Persistent.BaseImpl.EF.PermissionPolicy; // for EF Core-based applications
    
    public class Startup {
        // ...
        public void ConfigureServices(IServiceCollection services) {
            // ...
            services.AddXaf(Configuration, builder => {
                // ...
                builder.Security
                    // ...
                    .AddWindowsAuthentication(options => {
                        options.CreateUserAutomatically((objectSpace, user) => {
                            var defaultRole = objectSpace.FirstOrDefault<PermissionPolicyRole>(role => role.Name == "Default");
                            ((ApplicationUser)user).Roles.Add(defaultRole);
                        });
                    });
            });
            // ...
        }
        // ...
    }
    
  3. You can specify a page where to redirect unauthorized users. To do this, create a Razor component with the @page directive and specify its route in your Active Directory configuration:

    .AddWindowsAuthentication(options => {
        // ...
        options.SignOutRedirect = "/UserSignedOut";
    })
    

    This page prevents endless redirects under the following combination of circumstances:

    • You turned off the login page in step 1 (anonymousAuthentication is set to false).
    • You disabled automatic user creation in step 2 (the CreateUserAutomatically method is not called).
    • The user who opened the application is not registered in the database.

See Also

Windows Authentication in ASP.NET Core

Google, Azure, and GitHub Providers

  1. Add the following NuGet packages to the ASP.NET Core Blazor application project (MySolution.Blazor.Server):
  2. In the MySolution.Blazor.Server\Startup.cs file, extend the default cookie-based authentication scheme with the following external schemes:

    using Microsoft.AspNetCore.Authentication;
    using Microsoft.AspNetCore.Authentication.Cookies;
    using Microsoft.AspNetCore.Authentication.OAuth;
    using Microsoft.Identity.Web;
    using System.Text.Json;
    using System.Net.Http;
    using System.Net.Http.Headers;
    using System.Security.Claims;
    // ...
    public class Startup {
        // ...
        public void ConfigureServices(IServiceCollection services) {
            // ...
            services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
            .AddCookie(options => options.LoginPath = "/LoginPage")
            .AddGoogle(options => {
                Configuration.Bind("Authentication:Google", options);
                options.AuthorizationEndpoint += "?prompt=consent";
                options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
                options.ClaimActions.MapJsonKey(XafClaimTypes.UserImageUrl, "picture");
            })
            .AddOAuth("GitHub", "GitHub", options => {
                Configuration.Bind("Authentication:GitHub", options);
                options.ClaimActions.MapJsonKey(ClaimTypes.NameIdentifier, "id");
                options.ClaimActions.MapJsonKey(ClaimTypes.Name, "login");
                options.ClaimActions.MapJsonKey(XafClaimTypes.UserImageUrl, "avatar_url");
    
                options.Events = new OAuthEvents {
                    OnCreatingTicket = async context => {
                        var request = new HttpRequestMessage(HttpMethod.Get, context.Options.UserInformationEndpoint);
                        request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", context.AccessToken);
                        request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
    
                        var response = await context.Backchannel.SendAsync(request, context.HttpContext.RequestAborted);
                        response.EnsureSuccessStatusCode();
                        var json = JsonDocument.Parse(await response.Content.ReadAsStringAsync());
                        context.RunClaimActions(json.RootElement);
                    }
                };
            })
            .AddMicrosoftIdentityWebApp(options => {
                Configuration.Bind("Authentication:AzureAd", options);
            }, openIdConnectScheme: "AzureAD", cookieScheme: null);
            // ...
        }
    }
    
  3. In the MySolution.Blazor.Server\Services folder, create the CustomAuthenticationProvider class that implements the IAuthenticationProviderV2 interface:

    using System;
    using System.Security.Claims;
    using System.Security.Principal;
    using DevExpress.ExpressApp;
    using DevExpress.ExpressApp.Security;
    using DevExpress.Persistent.BaseImpl.EF.PermissionPolicy;
    using MySolution.Module.BusinessObjects;
    
    namespace MySolution.Blazor.Server.Services;
    
    public class CustomAuthenticationProvider : IAuthenticationProviderV2 {
        private readonly IPrincipalProvider principalProvider;
    
        public CustomAuthenticationProvider(IPrincipalProvider principalProvider) {
            this.principalProvider = principalProvider;
        }
    
        public object Authenticate(IObjectSpace objectSpace) {
            if(!CanHandlePrincipal(principalProvider.User)) {
                return null;
            }
    
            const bool autoCreateUser = true;
    
            ClaimsPrincipal claimsPrincipal = (ClaimsPrincipal)principalProvider.User;
            var userIdClaim = claimsPrincipal.FindFirst("sub") ?? claimsPrincipal.FindFirst(ClaimTypes.NameIdentifier) ?? throw new InvalidOperationException("Unknown user id");
    
            var providerUserKey = userIdClaim.Value;
            var loginProviderName = claimsPrincipal.Identity.AuthenticationType;
            var userName = claimsPrincipal.Identity.Name;
    
            var userLoginInfo = FindUserLoginInfo(objectSpace, loginProviderName, providerUserKey);
            if(userLoginInfo != null) {
                return userLoginInfo.User;
            }
    
            if(autoCreateUser) {
                return CreateApplicationUser(objectSpace, userName, loginProviderName, providerUserKey);
            }
    
            return null;
        }
    
        private bool CanHandlePrincipal(IPrincipal user) {
            return user.Identity.IsAuthenticated &&
                user.Identity.AuthenticationType != SecurityDefaults.Issuer &&
                user.Identity.AuthenticationType != SecurityDefaults.PasswordAuthentication &&
                user.Identity.AuthenticationType != SecurityDefaults.WindowsAuthentication &&
                !(user is WindowsPrincipal);
        }
    
        private object CreateApplicationUser(IObjectSpace objectSpace, string userName, string loginProviderName, string providerUserKey) {
            if(objectSpace.FirstOrDefault<ApplicationUser>(user => user.UserName == userName) != null) {
                throw new ArgumentException($"The username ('{userName}') was already registered within the system");
            }
    
            var user = objectSpace.CreateObject<ApplicationUser>();
            user.UserName = userName;
            user.SetPassword(Guid.NewGuid().ToString());
            user.Roles.Add(objectSpace.FirstOrDefault<PermissionPolicyRole>(role => role.Name == "Default"));
            ((ISecurityUserWithLoginInfo)user).CreateUserLoginInfo(loginProviderName, providerUserKey);
            objectSpace.CommitChanges();
            return user;
        }
    
        private ISecurityUserLoginInfo FindUserLoginInfo(IObjectSpace objectSpace, string loginProviderName, string providerUserKey) {
            return objectSpace.FirstOrDefault<ApplicationUserLoginInfo>(userLoginInfo =>
                                userLoginInfo.LoginProviderName == loginProviderName &&
                                userLoginInfo.ProviderUserKey == providerUserKey);
        }
    }
    
  4. Navigate to the YourSolutionName.Blazor.Server folder. Register the CustomAuthenticationProvider class in the Startup.cs file:

    using DevExpress.Persistent.BaseImpl.EF.PermissionPolicy;
    using DevExpress.ExpressApp;
    using Microsoft.Extensions.DependencyInjection;
    using System.Security.Principal;
    using Microsoft.Extensions.DependencyInjection;
    
    public class Startup {
        // ...
        public void ConfigureServices(IServiceCollection services) {
            // ...
            services.AddXaf(Configuration, builder => {
                // ...
                builder.Security
                    // ...
                    .AddAuthenticationProvider<CustomAuthenticationProvider>();
            });
            // ...
        }
        // ...
    }
    

    When a user successfully logs in with an OAuth provider, XAF obtains the user’s unique key and finds an ApplicationUser object associated with this key. If a user logs in with specified credentials for the first time, XAF creates a new ApplicationUser object for this key, generates a random password, and assigns the Default Role. We recommend that you create a random password to prevent users from logging in with a username and an empty password.

    You can modify the CustomAuthenticationProvider.Authenticate method’s body to implement custom logic to process user logins with different authentication methods.

  5. Register your application in the corresponding developer account and obtain the Client ID and Application Secret token:

    We recommend that you use the Secret Manager tool to store the Client ID and Application Secret token. You can store them in the YourSolutionName.Blazor.Server\appsettings.json file for testing purposes only:

    {
      "Authentication": {
        "Google": {
          "ClientId": "{CLIENT ID}",
          "ClientSecret": "{CLIENT SECRET}"
        },
        "GitHub": {
          "ClientId": "{CLIENT ID}",
          "ClientSecret": "{CLIENT SECRET}"
        },
        "AzureAd": {
          "ClientId": "{CLIENT ID}",
          "ClientSecret": "{CLIENT SECRET}"
        }
      },
      // ...
    }
    

Automatic Login

An XAF ASP.NET Core Blazor application automatically tries to log in the user if there is only one authentication method enabled and it is not password authentication. Automatic login is disabled if two or more authentication schemes are registered (for example, if you allow users to log in with either Google or GitHub), or if password authentication is enabled.

Automatic Logoff

If you use an OpenID provider (such as Microsoft Entra ID) to authenticate users, you can force the lifetime of an authentication session to match that of an ID token issued during the authentication process. Set the OpenIdConnectOptions.UseTokenLifetime option to true:

File: MySolution.Blazor.Server/Startup.cs

// ...
public class Startup {
    // ...
    public void ConfigureServices(IServiceCollection services) {
        // ...
        authentication.AddMicrosoftIdentityWebApp(options => {
            // ...
            options.UseTokenLifetime = true;
        }, openIdConnectScheme: "AzureAD", cookieScheme: null);
    }
}

When you enable this option, users must re-authenticate after their ID token expires (after a user refreshes the browser tab). In the time period between the current session’s expiration and the next authentication, users can continue to interact with the application. Note that some features will not work if they require HTTP requests to the server. For example, images will not be loaded and dashboards may not respond to user interaction.

XAF automatically enables this behavior if you select the Single Sign-On option in the Solution Wizard.

See Also

Deployment Recommendations for XAF Blazor UI Applications

Log File Generated in Azure

Access External Authentication Provider Actions

Actions for additional authentication schemes registered in AuthenticationBuilder are displayed below the Log In button. To customize these Actions, follow the steps described in this section.

  1. Navigate to the YourSolutionName.Module\Controllers folder and create a Window Controller.
  2. In the OnActivated method, get AdditionalLogonActionsController.
  3. Use the Actions property to access the collection of the Controller’s Actions.

    using System.Linq;
    using DevExpress.ExpressApp;
    using DevExpress.ExpressApp.Blazor.SystemModule;
    // ...
    public class AdditionalLogonActionsCustomizationController : WindowController {
        protected override void OnActivated() {
            base.OnActivated();
            AdditionalLogonActionsController additionalLogonActionsController = Frame.GetController<AdditionalLogonActionsController>();
            if(additionalLogonActionsController != null) {
                var action = additionalLogonActionsController.Actions.Where(action => action.Id == "OpenIdConnect").FirstOrDefault();
                if(action != null) {
                    action.Caption = "Azure";
                    action.ImageName = "Action_LogOnViaAzureAD";
                }
            }
        }
    }
    
  4. Navigate to the YourSolutionName.Blazor.Server folder and open the YourSolutionNameBlazorApplication.cs file. Override the CreateLogonWindowControllers method and add AdditionalLogonActionsCustomizationController to the collection of Controllers activated for the Logon window:

    using DevExpress.ExpressApp.Blazor;
    using System.Collections.Generic;
    using DevExpress.ExpressApp;
    // ...
    public partial class YourSolutionNameBlazorApplication : BlazorApplication {
        // ...
        protected override List<Controller> CreateLogonWindowControllers() {
            var result = base.CreateLogonWindowControllers();
            result.Add(new AdditionalLogonActionsCustomizationController());
            return result;
        }
    }
    

Localize External Authentication Action Captions

Edit the Localization->Captions->LogInWithActionCaption item in the Blazor Application Model to modify the localization value for the external authentication caption (“Log In with” in en-US localization):

Model Editor - Localize External Authentication Provider Actions

The image below illustrates the result.

Login Form - Localize External Authentication Provider Actions

Note

In Blazor applications, an external authentication action caption contains the “Log In with” substring (or its localized version) only if a single action is available. Otherwise, only the external authentication method’s name is displayed.

See Also