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).
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
In the MySolution.Blazor.Server\Properties\launchSettings.json file, set
windowsAuthentication
totrue
. You can also setanonymousAuthentication
tofalse
to hide the logon page and always use Windows authentication:{ "iisSettings": { "windowsAuthentication": true, "anonymousAuthentication": false, // optional // ... }, // ... }
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); }); }); }); // ... } // ... }
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 tofalse
). - 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.
- You turned off the login page in step 1 (
See Also
Windows Authentication in ASP.NET Core
Google, Azure, and GitHub Providers
- Add the following NuGet packages to the ASP.NET Core Blazor application project (MySolution.Blazor.Server):
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); // ... } }
In the MySolution.Blazor.Server\Services folder, create the
CustomAuthenticationProvider
class that implements theIAuthenticationProviderV2
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); } }
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 newApplicationUser
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.Register your application in the corresponding developer account and obtain the Client ID and Application Secret token:
- Facebook, Google, and external provider authentication in ASP.NET Core
- Azure Active Directory with ASP.NET Core
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
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.
- Navigate to the YourSolutionName.Module\Controllers folder and create a Window Controller.
- In the
OnActivated
method, getAdditionalLogonActionsController
. 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"; } } } }
Navigate to the YourSolutionName.Blazor.Server folder and open the YourSolutionNameBlazorApplication.cs file. Override the
CreateLogonWindowControllers
method and addAdditionalLogonActionsCustomizationController
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):
The image below illustrates the result.
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.