All docs
V21.2
21.2
21.1
20.2
20.1
The page you are viewing does not exist in version 20.1. This link will take you to the root page.
19.2
The page you are viewing does not exist in version 19.2. This link will take you to the root page.
19.1
The page you are viewing does not exist in version 19.1. This link will take you to the root page.
18.2
The page you are viewing does not exist in version 18.2. This link will take you to the root page.
18.1
The page you are viewing does not exist in version 18.1. This link will take you to the root page.
17.2
The page you are viewing does not exist in version 17.2. This link will take you to the root page.

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).

The extended login form

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.

Common Steps

  1. In the platform-agnostic Module (MySolution.Module), create the following classes:

    • ApplicationUserLoginInfo: contains information on providers that a user uses to log in to the application.
    • ApplicationUser: extends the default PermissionPolicyUser type with a collection of ApplicationUserLoginInfo objects.

    File: MySolution.Module\BusinessObjects\ApplicationUser.cs.

    using DevExpress.ExpressApp;
    using DevExpress.ExpressApp.ConditionalAppearance;
    using DevExpress.ExpressApp.Security;
    using DevExpress.Persistent.BaseImpl;
    using DevExpress.Persistent.BaseImpl.PermissionPolicy;
    using DevExpress.Xpo;
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.Linq;
    // ...
    [MapInheritance(MapInheritanceType.ParentTable)]
    [DefaultProperty(nameof(UserName))]
    public class ApplicationUser : PermissionPolicyUser, IObjectSpaceLink, ISecurityUserWithLoginInfo {
        public ApplicationUser(Session session) : base(session) { }
    
        [Browsable(false)]
        [Aggregated, Association("User-LoginInfo")]
        public XPCollection<ApplicationUserLoginInfo> LoginInfo {
            get { return GetCollection<ApplicationUserLoginInfo>(nameof(LoginInfo)); }
        }
    
        IEnumerable<ISecurityUserLoginInfo> IOAuthSecurityUser.UserLogins => LoginInfo.OfType<ISecurityUserLoginInfo>();
    
        IObjectSpace IObjectSpaceLink.ObjectSpace { get; set; }
    
        ISecurityUserLoginInfo ISecurityUserWithLoginInfo.CreateUserLoginInfo(string loginProviderName, string providerUserKey) {
            ApplicationUserLoginInfo result = ((IObjectSpaceLink)this).ObjectSpace.CreateObject<ApplicationUserLoginInfo>();
            result.LoginProviderName = loginProviderName;
            result.ProviderUserKey = providerUserKey;
            result.User = this;
            return result;
        }
    }
    
    [DeferredDeletion(false)]
    [Persistent("PermissionPolicyUserLoginInfo")]
    public class ApplicationUserLoginInfo : BaseObject, ISecurityUserLoginInfo {
        private string loginProviderName;
        private ApplicationUser user;
        private string providerUserKey;
        public ApplicationUserLoginInfo(Session session) : base(session) { }
    
        [Indexed("ProviderUserKey", Unique = true)]
        [Appearance("PasswordProvider", Enabled = false, Criteria = "!(IsNewObject(this)) and LoginProviderName == '" + SecurityDefaults.PasswordAuthentication + "'", Context = "DetailView")]
        public string LoginProviderName {
            get { return loginProviderName; }
            set { SetPropertyValue(nameof(LoginProviderName), ref loginProviderName, value); }
        }
    
        [Appearance("PasswordProviderUserKey", Enabled = false, Criteria = "!(IsNewObject(this)) and LoginProviderName == '" + SecurityDefaults.PasswordAuthentication + "'", Context = "DetailView")]
        public string ProviderUserKey {
            get { return providerUserKey; }
            set { SetPropertyValue(nameof(ProviderUserKey), ref providerUserKey, value); }
        }
    
        [Association("User-LoginInfo")]
        public ApplicationUser User {
            get { return user; }
            set { SetPropertyValue(nameof(User), ref user, value); }
        }
    
        object ISecurityUserLoginInfo.User => User;
    }
    
  2. In EF Core-based applications, open the MySolution.Module\BusinessObjects\MySolutionDbContext.cs file and do the following:

    • Add the ApplicationUser and ApplicationUserLoginInfo properties to the MySolutionDbContext class to register these entities within the DbContext.
    • Override the MySolutionDbContext.OnModelCreating method and specify that pairs of ISecurityUserLoginInfo.LoginProviderName and ISecurityUserLoginInfo.ProviderUserKey should be unique.
    using DevExpress.ExpressApp.Security;
    // ... 
    public class MySolutionDbContext : DbContext {
        //...
        public DbSet<ApplicationUser> Users { get; set; }
        public DbSet<ApplicationUserLoginInfo> UserLoginInfos { get; set; }
        protected override void OnModelCreating(ModelBuilder modelBuilder) {
            modelBuilder.Entity<ApplicationUserLoginInfo>(builder => {
                builder.HasIndex(nameof(ISecurityUserLoginInfo.LoginProviderName),
                    nameof(ISecurityUserLoginInfo.ProviderUserKey)).IsUnique();
            });
        }
    }
    
  3. Add the DevExpress.ExpressApp.Security.Xpo or DevExpress.ExpressApp.EFCore NuGet package to the platform-agnostic Module project (MySolution.Module).

  4. In the MySolution.Module\DatabaseUpdate\Updater.cs file, create the non-administrative Default Role. In the following sections, this Role is automatically assigned for users logged in with OAuth or Active Directory credentials:

    using DevExpress.ExpressApp.Security;
    using DevExpress.ExpressApp.SystemModule;
    using DevExpress.Persistent.BaseImpl.PermissionPolicy; 
    // ...
    public class Updater : ModuleUpdater {
        // ...
        public override void UpdateDatabaseAfterUpdateSchema() {
            base.UpdateDatabaseAfterUpdateSchema();
            PermissionPolicyRole defaultRole = ObjectSpace.FirstOrDefault<PermissionPolicyRole>(role => role.Name == "Default");
            if (defaultRole == null) {
                defaultRole = ObjectSpace.CreateObject<PermissionPolicyRole>();
                defaultRole.Name = "Default";
    
                defaultRole.AddObjectPermissionFromLambda<PermissionPolicyUser>(
                    SecurityOperations.Read, cm => cm.Oid == (Guid)CurrentUserIdOperator.CurrentUserId(), SecurityPermissionState.Allow);
                defaultRole.AddNavigationPermission(@"Application/NavigationItems/Items/Default/Items/MyDetails", SecurityPermissionState.Allow);
                defaultRole.AddMemberPermissionFromLambda<PermissionPolicyUser>(
                    SecurityOperations.Write, "ChangePasswordOnFirstLogon", 
                    cm => cm.Oid == (Guid)CurrentUserIdOperator.CurrentUserId(), SecurityPermissionState.Allow);
                defaultRole.AddMemberPermissionFromLambda<PermissionPolicyUser>(
                    SecurityOperations.Write, "StoredPassword", 
                    cm => cm.Oid == (Guid)CurrentUserIdOperator.CurrentUserId(), SecurityPermissionState.Allow);
                defaultRole.AddTypePermissionsRecursively<PermissionPolicyRole>(
                    SecurityOperations.Read, SecurityPermissionState.Deny);
                defaultRole.AddTypePermissionsRecursively<ModelDifference>(
                    SecurityOperations.ReadWriteAccess, SecurityPermissionState.Allow);
                defaultRole.AddTypePermissionsRecursively<ModelDifferenceAspect>(
                    SecurityOperations.ReadWriteAccess, SecurityPermissionState.Allow);
                defaultRole.AddTypePermissionsRecursively<ModelDifference>(
                    SecurityOperations.Create, SecurityPermissionState.Allow);
                defaultRole.AddTypePermissionsRecursively<ModelDifferenceAspect>(
                    SecurityOperations.Create, SecurityPermissionState.Allow);
            }
            ObjectSpace.CommitChanges();
            // ...
        }
        // ...
    }
    
  5. Open the MySolution.Blazor.Server\Startup.cs file, and set the UserType property to the ApplicationUser type and the UserLoginInfoType property to the ApplicationUserLoginInfo type in the AddXafSecurity service:

    using DevExpress.ExpressApp.Security;
    // ...
    public class Startup {
        // ...
        public void ConfigureServices(IServiceCollection services) {
            // ...
            services.AddXafSecurity(options => {
                // ...
                options.UserType = typeof(ApplicationUser);
                options.UserLoginInfoType = typeof(ApplicationUserLoginInfo);
            })
            .AddAuthenticationActiveDirectory(options => {
                options.CreateUserAutomatically = true;
            })
            .AddAuthenticationStandard(options => {
                options.IsSupportChangePassword = true;
            })
            .AddExternalAuthentication<HttpContextPrincipalProvider>();
            services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
                .AddCookie(options => {
                options.LoginPath = "/LoginPage";
            });
        }
    }
    

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 login page and always use Windows authentication:

    {
      "iisSettings": {
        "windowsAuthentication": true,
        "anonymousAuthentication": false, // optional
        // ...
        }
      },
      // ...
    }
    
  2. In the MySolution.Blazor.Server\Startup.cs file, specify the IISServerOptions.AuthenticationDisplayName property to show an Action for this authentication schema on the Login page. The Login page shows Actions for schemas with the specified display name only.

    public class Startup {
        // ...
        public void ConfigureServices(IServiceCollection services) {
            // ...
            services.Configure<IISServerOptions>(options => {
                options.AuthenticationDisplayName = "Windows";
            });
        }
    }
    
  3. In the MySolution.Module project, create the WindowsUserInitializer class that implements the ICanInitializeNewUser interface. This initializer assigns the Default role to autogenerated users:

    using DevExpress.ExpressApp;
    using DevExpress.ExpressApp.Security;
    using DevExpress.Persistent.BaseImpl.PermissionPolicy;
    // ...
    public class WindowsUserInitializer : ICanInitializeNewUser {
        public void InitializeNewUser(IObjectSpace objectSpace, object user) {
            ((ApplicationUser)user).Roles.Add(objectSpace.FirstOrDefault<PermissionPolicyRole>(role => role.Name == "Default"));
        }
    }
    
  4. Register this initializer in your application in the MySolution.Blazor.Server\Startup.cs file:

    public class Startup {
        // ...
        public void ConfigureServices(IServiceCollection services) {
            // ...
            services.AddXafSecurity(options => {
                // ...
                options.UserType = typeof(ApplicationUser);
                options.UserLoginInfoType = typeof(ApplicationUserLoginInfo);
            })
            .AddAuthenticationActiveDirectory(options => {
                // ...
                options.SetNewUserInitializer(new WindowsUserInitializer());
            })
            // ...
        }
    }
    

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(Configuration, configSectionName: "Authentication:AzureAd", cookieScheme: null);
            // ...
        }
    }
    
  3. In the same file, implement authentication logic for external providers in the options.Events.OnAuthenticated delegate:

    using DevExpress.Persistent.BaseImpl.PermissionPolicy; // for XPO-based applications
    using DevExpress.Persistent.BaseImpl.EF.PermissionPolicy; // for EF Core-based applications
    using DevExpress.ExpressApp;
    using System.Security.Principal;
    // ...
    public class Startup {
        // ...
        public void ConfigureServices(IServiceCollection services) {
            // ...
            services.AddXafSecurity(options => {
                // ... 
            })
            .AddAuthenticationActiveDirectory(options => {
                // ... 
            })
            .AddAuthenticationStandard(options => {
                // ... 
            })
            .AddExternalAuthentication<HttpContextPrincipalProvider>(options => {
                options.Events.OnAuthenticated = (externalAuthenticationContext) => {
                    if(externalAuthenticationContext.AuthenticatedUser == null &&
                    externalAuthenticationContext.Principal.Identity.AuthenticationType != SecurityDefaults.PasswordAuthentication &&
                    externalAuthenticationContext.Principal.Identity.AuthenticationType != SecurityDefaults.WindowsAuthentication && !(externalAuthenticationContext.Principal is WindowsPrincipal)) {
                        const bool autoCreateUser = true;
    
                        IObjectSpace objectSpace = externalAuthenticationContext.LogonObjectSpace;
                        ClaimsPrincipal externalUser = (ClaimsPrincipal)externalAuthenticationContext.Principal;
    
                        var userIdClaim = externalUser.FindFirst("sub") ?? externalUser.FindFirst(ClaimTypes.NameIdentifier) ?? throw new InvalidOperationException("Unknown user id");
                        string providerUserId = userIdClaim.Value;
    
                        var userLoginInfo = FindUserLoginInfo(externalUser.Identity.AuthenticationType, providerUserId);
                        if(userLoginInfo != null || autoCreateUser) {
                            externalAuthenticationContext.AuthenticatedUser = userLoginInfo?.User ?? CreateApplicationUser(externalUser.Identity.Name, providerUserId);
                        }
    
                        object CreateApplicationUser(string userName, string providerUserId) {
                            if(objectSpace.FirstOrDefault<ApplicationUser>(user => user.UserName == userName) != null) {
                                throw new ArgumentException($"'{userName}' is 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(externalUser.Identity.AuthenticationType, providerUserId);
                            objectSpace.CommitChanges();
                            return user;
                        }
                        ISecurityUserLoginInfo FindUserLoginInfo(string loginProviderName, string providerUserId) {
                            return objectSpace.FirstOrDefault<ApplicationUserLoginInfo>(userLoginInfo =>
                                                userLoginInfo.LoginProviderName == loginProviderName &&
                                                userLoginInfo.ProviderUserKey == providerUserId);
                        }
                    }
                };
            });
            // ...
        }
    }
    

    When a user successfully logs in with an OAuth provider, the code above gets the user’s unique key and finds an ApplicationUser object associated with this key. If a user logs in with the specified credentials for the first time, this code 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 also use ExternalAuthenticationProvider.Events to implement custom logic to process user login with different authentication methods.

    Note that providers are called in the order in which they are registered in code. Register ExternalAuthenticationProvider after other providers; otherwise, it can override their logic.

  4. 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 MySolution.Blazor.Server\appsettings.json file for testing purposes only:

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

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 below the Log In button. To customize these Actions, follow the steps below:

  1. Create a Window Controller in the ASP.NET Core Blazor Module project.
  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. Override the ASP.NET Core Blazor application’s CreateLogonWindowControllers method and add AdditionalLogonActionsCustomizationController to the collection of Controllers activated for the Log On page:

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