All docs
V21.1
21.2 (EAP/Beta)
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.
.NET Framework 4.5.2+
.NET Framework 4.5.2+
.NET Standard 2.0+
.NET Core 3.0+

How to: Use Active Directory and OAuth2 Authentication Providers in ASP.NET Core Blazor Applications

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

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:

    • OAuthPermissionPolicyUserLoginInfo: contains information on providers that a user uses to log in to the application;
    • OAuthPermissionPolicyUser: extends the default PermissionPolicyUser type with a collection of OAuthPermissionPolicyUserLoginInfo objects.
    using DevExpress.ExpressApp;
    using DevExpress.ExpressApp.Security;
    using DevExpress.Persistent.BaseImpl;
    using DevExpress.Persistent.BaseImpl.PermissionPolicy;
    using DevExpress.Xpo;
    using System;
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.Linq;
    // ...
    
    public class OAuthPermissionPolicyUser : PermissionPolicyUser, IObjectSpaceLink, IOAuthSecurityUser, IXafEntityObject {
        public OAuthPermissionPolicyUser(Session session) : base(session) { }
    
        [Browsable(false)]
        [DevExpress.Xpo.Aggregated, Association("User-LoginInfo")]
        public XPCollection<OAuthPermissionPolicyUserLoginInfo> UserLogins {
            get { return GetCollection<OAuthPermissionPolicyUserLoginInfo>(nameof(UserLogins)); }
        }
    
        IEnumerable<ISecurityUserLoginInfo> IOAuthSecurityUser.UserLogins => UserLogins.OfType<ISecurityUserLoginInfo>();
    
        IObjectSpace IObjectSpaceLink.ObjectSpace { get; set; }
    
        void IXafEntityObject.OnCreated() {
            SetPassword(Guid.NewGuid().ToString());
            CreateUserLoginInfo(this, ((IObjectSpaceLink)this).ObjectSpace, SecurityDefaults.DefaultClaimsIssuer, Guid.NewGuid().ToString());
        }
        void IXafEntityObject.OnLoaded() { }
        void IXafEntityObject.OnSaving() { }
    
        public static void CreateUserLoginInfo(OAuthPermissionPolicyUser user, IObjectSpace os, string providerName, string providerUserKey) {
            var userLoginInfo = os.CreateObject<OAuthPermissionPolicyUserLoginInfo>();
            userLoginInfo.ProviderUserKey = providerUserKey;
            userLoginInfo.LoginProviderName = providerName;
            userLoginInfo.User = user;
        }
    }
    
    [Browsable(false)]
    public class OAuthPermissionPolicyUserLoginInfo : BaseObject, ISecurityUserLoginInfo {
        private string loginProviderName;
        private OAuthPermissionPolicyUser user;
        private string providerUserKey;
        public OAuthPermissionPolicyUserLoginInfo(Session session) : base(session) { }
    
        [Indexed("ProviderUserKey", Unique = true)]
        public string LoginProviderName {
            get { return loginProviderName; }
            set { SetPropertyValue(nameof(LoginProviderName), ref loginProviderName, value); }
        }
    
        public string ProviderUserKey {
            get { return providerUserKey; }
            set { SetPropertyValue(nameof(ProviderUserKey), ref providerUserKey, value); }
        }
    
        [Association("User-LoginInfo")]
        public OAuthPermissionPolicyUser 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 OAuthPermissionPolicyUser and OAuthPermissionPolicyUserLoginInfo 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<OAuthPermissionPolicyUser> OAuthUsers { get; set; }
        public DbSet<OAuthPermissionPolicyUserLoginInfo> UserLoginInfos { get; set; }
        protected override void OnModelCreating(ModelBuilder modelBuilder) {
            base.OnModelCreating(modelBuilder);
            modelBuilder.Entity<OAuthPermissionPolicyUserLoginInfo>(b => {
                b.HasIndex(nameof(ISecurityUserLoginInfo.LoginProviderName), 
                    nameof(ISecurityUserLoginInfo.ProviderUserKey)).IsUnique();
            });
        }
    }
    
  3. Open the MySolution.Blazor.Server\Startup.cs file and set the UserType property to the OAuthPermissionPolicyUser type in the AddXafSecurity service:

    public class Startup {
        // ...
        public void ConfigureServices(IServiceCollection services) {
            // ...
            services.AddXafSecurity(options => {
                // ...
                options.UserType = typeof(OAuthPermissionPolicyUser);
            }
        }
    }
    
  4. In the same file, implement authentication logic for external providers in the options.Events.OnAuthenticated functional delegate:

    using DevExpress.Persistent.BaseImpl.PermissionPolicy; // for XPO-based applications
    using DevExpress.Persistent.BaseImpl.EF.PermissionPolicy; // for EF Core-based applications
    using DevExpress.Data.Filtering;
    using DevExpress.ExpressApp;
    using System.Security.Principal;
    using System.Security.Claims;
    // ...
    public class Startup {
        // ...
        public void ConfigureServices(IServiceCollection services) {
            // ...
            services.AddXafSecurity(options => {
                // ... 
            })
            .AddExternalAuthentication<HttpContextPrincipalProvider>(options => {
                options.Events.OnAuthenticated = (externalAuthenticationContext) => {
                    if(externalAuthenticationContext.AuthenticatedUser == null) {
                        const bool autoCreateUser = true;
    
                        IObjectSpace objectSpace = externalAuthenticationContext.LogonObjectSpace;
                        ClaimsPrincipal externalUser = (ClaimsPrincipal)externalAuthenticationContext.Principal;
                        string authenticationType = externalUser.Identity.AuthenticationType;
    
                        string providerUserId;
                        if(externalUser is WindowsPrincipal) {
                            // this code is executed when you use Windows Authentication
                            // with anonymousAuthentication disabled
                            // see Step 1 in the Windows Authentication section for more information
                            providerUserId = externalUser.Identity.Name;
                            authenticationType = "Windows";
                        }
                        else {
                            var userIdClaim = externalUser.FindFirst("sub") ?? externalUser.FindFirst(ClaimTypes.NameIdentifier) ?? throw new InvalidOperationException("Unknown user id");
                            providerUserId = userIdClaim.Value;
                        }
    
                        var userLoginInfo = FindUserLoginInfo(authenticationType, providerUserId);
                        if(userLoginInfo != null || autoCreateUser) {
                            externalAuthenticationContext.AuthenticatedUser = userLoginInfo?.User ?? CreatePermissionPolicyUser(externalUser.Identity.Name, providerUserId);
                        }
    
                        object CreatePermissionPolicyUser(string userName, string providerUserId) {
                            var user = objectSpace.CreateObject<OAuthPermissionPolicyUser>();
                            user.UserName = userName;
                            user.Roles.Add(objectSpace.FirstOrDefault<PermissionPolicyRole>(role => role.Name == "Default"));
                            OAuthPermissionPolicyUser.CreateUserLoginInfo(user, objectSpace, authenticationType, providerUserId);
                            objectSpace.CommitChanges();
                            return user;
                        }
                        ISecurityUserLoginInfo FindUserLoginInfo(string loginProviderName, string providerUserId) {
                            return objectSpace.FirstOrDefault<OAuthPermissionPolicyUserLoginInfo>(userLoginInfo =>
                                                userLoginInfo.LoginProviderName == loginProviderName &&
                                                userLoginInfo.ProviderUserKey == providerUserId);
                        }
                    }
                };
            })
            .AddAuthenticationProvider<OAuthAuthenticationProvider>()
            // ...
        }
    }
    

    When a user uses an external provider to log on, the code above creates a new OAuthPermissionPolicyUser with the same name, autogenerated password, and Default Role. This prevents users from using this user name and an empty password to log on with the standard authentication.

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, call the UseMiddleware method to add WindowsSignInMiddleware to the application’s request pipeline:

    using Microsoft.AspNetCore.Builder;
    using DevExpress.ExpressApp.Blazor.Services;
    // ...
    public class Startup {
        // ...
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env) {
            // ...
            app.UseMiddleware<WindowsSignInMiddleware>();
            app.UseXaf();
            // ...
        }
    }
    
  3. In the same 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";
            });
        }
    }
    

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.OpenIdConnect;
    using Microsoft.AspNetCore.Authentication.OAuth;
    using Microsoft.Identity.Web;
    using System.Text.Json;
    using System.Net.Http;
    using System.Net.Http.Headers;
    // ...
    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;
            })
            .AddOAuth("GitHub", "GitHub", options => {
                Configuration.Bind("Authentication:GitHub", options);
                options.ClaimActions.MapJsonKey(ClaimTypes.NameIdentifier, "id");
                options.ClaimActions.MapJsonKey(ClaimTypes.Name, "login");
                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. 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}"
        },
        //...
      },
      // ...
    }
    

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;
        }
    }