首页 > 解决方案 > 在 abp 框架 Web 应用程序中使用 Azure B2C 作为主要身份验证提供程序

问题描述

我有一个使用基于Asp.NET Core Identity的标准身份验证提供程序的abp 框架Web 应用程序。

我想将Asp.NET Core Identity abp 实现替换为 Azure B2C 作为主要身份验证提供程序,并管理其自己的身份存储和外部提供程序。

我正在考虑使用 Azure B2C,因为:

另一方面,这是我的问题。如何替换 abp 框架身份存储?覆盖登录/注销/注册/密码恢复/ ...用例?并与多租户和其他模块集成?

非常感谢你的想法,

标签: abp

解决方案


这是可能的,我做到了。阅读并尝试实现 abp 文档中的文章:

  • Azure Active Directory 身份验证
  • 自定义登录页面。
  • 自定义登录管理器

要理解这个概念,然后基本上你需要用 azureb2c 替换 azuread 库,我设法使用:

替代方法:AddOpenIdConnect

提示:身份服务器将继续存在于您的应用程序中,针对 azureb2c 进行身份验证只会在您的应用程序中创建一个带有外部身份验证的本地用户,如果您只想使用 azureb2c,您可以使默认登录页面始终重定向到 Azureb2c 身份验证页面和在他回来后创建/验证用户。

对不起我的英语不好。

见代码:

appsettings.json , 将 xxx 替换为你自己的设置

"AzureAdB2C": {
"ClientId": "xxx",
"Tenant": "xxx.onmicrosoft.com",
"AzureAdB2CInstance": "https://xxx.b2clogin.com",
"SignUpSignInPolicyId": "B2C_1_Logon_Signup",
"ResetPasswordPolicyId": "B2C_1_resetpass",
"EditProfilePolicyId": "B2C_1_edit",
"RedirectUri": "https://xxx:443/signin-oidc", //,
"ClientSecret": "xxx"
}

在 configureservices 部分配置您的模块,将 ClaimTypes 更改为 AbpClaimTypes:

//custom sign in configureservices
context.Services.GetObject<IdentityBuilder>().AddSignInManager<CustomSignInManager>();
//configure auth
private void ConfigureAuthentication(ServiceConfigurationContext context, 
IConfiguration configuration){
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Add("sub", 
ClaimTypes.NameIdentifier);
// JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Add("emails", ClaimTypes.Email); 
//not working
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Add("emails", AbpClaimTypes.Email);
context.Services.AddAuthentication()
.AddIdentityServerAuthentication(options =>{
options.Authority = configuration["AuthServer:Authority"];
options.RequireHttpsMetadata = false;
options.ApiName = "test";
}).AddAzureAdB2C(options => configuration.GetSection("AzureAdB2C").Bind(options)).AddCookie();
}

Openid 需要的类:

public class AzureAdB2COptions
{
    public const string PolicyAuthenticationProperty = "Policy";
    public string ClientId { get; set; }
    public string AzureAdB2CInstance { get; set; }
    public string Tenant { get; set; }
    public string SignUpSignInPolicyId { get; set; }
    public string SignInPolicyId { get; set; }
    public string SignUpPolicyId { get; set; }
    public string ResetPasswordPolicyId { get; set; }
    public string EditProfilePolicyId { get; set; }
    public string RedirectUri { get; set; }
    public string DefaultPolicy => SignUpSignInPolicyId;
    public string Authority => $"{AzureAdB2CInstance}/tfp/{Tenant}/{DefaultPolicy}/v2.0";
    public string ClientSecret { get; set; }
    public string ApiUrl { get; set; }
    public string ApiScopes { get; set; }
}
public class CustomSignInManager : SignInManager<Volo.Abp.Identity.IdentityUser>
{
    private const string LoginProviderKey = "LoginProvider";
    private const string XsrfKey = "XsrfId";
    public CustomSignInManager(
        UserManager<Volo.Abp.Identity.IdentityUser> userManager,
        Microsoft.AspNetCore.Http.IHttpContextAccessor contextAccessor,
        IUserClaimsPrincipalFactory<Volo.Abp.Identity.IdentityUser> claimsFactory,
        Microsoft.Extensions.Options.IOptions<IdentityOptions> optionsAccessor,
        Microsoft.Extensions.Logging.ILogger<SignInManager<Volo.Abp.Identity.IdentityUser>> logger,
        Microsoft.AspNetCore.Authentication.IAuthenticationSchemeProvider schemes,
        IUserConfirmation<Volo.Abp.Identity.IdentityUser> confirmation) : base(userManager, contextAccessor, claimsFactory, optionsAccessor, logger, schemes, confirmation)
    {
    }
    // https://github.com/aspnet/Identity/blob/feedcb5c53444f716ef5121d3add56e11c7b71e5/src/Identity/SignInManager.cs#L589-L624
    public override async Task<ExternalLoginInfo> GetExternalLoginInfoAsync(string expectedXsrf = null)
    {
        var auth = await Context.AuthenticateAsync(IdentityConstants.ExternalScheme);
        var items = auth?.Properties?.Items;
        if (auth?.Principal == null || items == null || !items.ContainsKey(LoginProviderKey))
        {
            return null;
        }

        if (expectedXsrf != null)
        {
            if (!items.ContainsKey(XsrfKey))
            {
                return null;
            }
            var userId = items[XsrfKey] as string;
            if (userId != expectedXsrf)
            {
                return null;
            }
        }

        var providerKey = auth.Principal.FindFirstValue(ClaimTypes.NameIdentifier);
        var provider = items[LoginProviderKey] as string;
        if (providerKey == null || provider == null)
        {
            return null;
        }

        var providerDisplayName = (await GetExternalAuthenticationSchemesAsync()).FirstOrDefault(p => p.Name == provider)?.DisplayName
                                  ?? provider;
        return new ExternalLoginInfo(auth.Principal, provider, providerKey, providerDisplayName)
        {
            AuthenticationTokens = auth.Properties.GetTokens()
        };
    }


}

public static class AzureAdB2CAuthenticationBuilderExtensions
{
    public static AuthenticationBuilder AddAzureAdB2C(this AuthenticationBuilder builder)
        => builder.AddAzureAdB2C(_ =>
        {
        });

    public static AuthenticationBuilder AddAzureAdB2C(this AuthenticationBuilder builder, Action<AzureAdB2COptions> configureOptions)
    {
        builder.Services.Configure(configureOptions);
        builder.Services.AddSingleton<IConfigureOptions<OpenIdConnectOptions>, OpenIdConnectOptionsSetup>();
        builder.AddOpenIdConnect();
        return builder;
    }

    public class OpenIdConnectOptionsSetup : IConfigureNamedOptions<OpenIdConnectOptions>
    {

        public OpenIdConnectOptionsSetup(IOptions<AzureAdB2COptions> b2cOptions)
        {
            AzureAdB2COptions = b2cOptions.Value;
        }

        public AzureAdB2COptions AzureAdB2COptions { get; set; }

        public void Configure(string name, OpenIdConnectOptions options)
        {
            options.ClientId = AzureAdB2COptions.ClientId;
            options.Authority = AzureAdB2COptions.Authority;
            options.UseTokenLifetime = true;
            options.TokenValidationParameters = new TokenValidationParameters() { NameClaimType = "name"  };
            options.Scope.Add("email");
            options.RequireHttpsMetadata = false;
            options.SaveTokens = true;
            options.GetClaimsFromUserInfoEndpoint = true;
            //options.ResponseType = OpenIdConnectResponseType.CodeIdToken;



            options.Events = new OpenIdConnectEvents()
            {
                OnTokenValidated = (async context =>
                {
                    var debugIdentityPrincipal = context.Principal.Identity;
                    var claimsFromOidcProvider = context.Principal.Claims.ToList();
                    await Task.CompletedTask;
                }),
                OnRedirectToIdentityProvider = OnRedirectToIdentityProvider,
                OnRemoteFailure = OnRemoteFailure,
                OnAuthorizationCodeReceived = OnAuthorizationCodeReceived
            };
        }

        public void Configure(OpenIdConnectOptions options)
        {
            Configure(Options.DefaultName, options);
        }

        public Task OnRedirectToIdentityProvider(RedirectContext context)
        {
            var defaultPolicy = AzureAdB2COptions.DefaultPolicy;
            if (context.Properties.Items.TryGetValue(AzureAdB2COptions.PolicyAuthenticationProperty, out var policy) &&
                !policy.Equals(defaultPolicy))
            {
                context.ProtocolMessage.Scope = OpenIdConnectScope.OpenIdProfile;
                context.ProtocolMessage.ResponseType = OpenIdConnectResponseType.IdToken;
                context.ProtocolMessage.IssuerAddress = context.ProtocolMessage.IssuerAddress.ToLower().Replace(defaultPolicy.ToLower(), policy.ToLower());
                context.Properties.Items.Remove(AzureAdB2COptions.PolicyAuthenticationProperty);
            }
            else if (!string.IsNullOrEmpty(AzureAdB2COptions.ApiUrl))
            {
                context.ProtocolMessage.Scope += $" offline_access {AzureAdB2COptions.ApiScopes}";
                context.ProtocolMessage.ResponseType = OpenIdConnectResponseType.CodeIdToken;
            }
            return Task.FromResult(0);
        }

        public Task OnRemoteFailure(RemoteFailureContext context)
        {
            context.HandleResponse();
            // Handle the error code that Azure AD B2C throws when trying to reset a password from the login page 
            // because password reset is not supported by a "sign-up or sign-in policy"
            if (context.Failure is OpenIdConnectProtocolException && context.Failure.Message.Contains("AADB2C90118"))
            {
                // If the user clicked the reset password link, redirect to the reset password route
                context.Response.Redirect("/Session/ResetPassword");
            }
            else if (context.Failure is OpenIdConnectProtocolException && context.Failure.Message.Contains("access_denied"))
            {
                context.Response.Redirect("/");
            }
            else
            {
                context.Response.Redirect("/Home/Error?message=" + Uri.EscapeDataString(context.Failure.Message));
            }
            return Task.FromResult(0);
        }

        public async Task OnAuthorizationCodeReceived(AuthorizationCodeReceivedContext context)
        {
            // Use MSAL to swap the code for an access token
            // Extract the code from the response notification
            var code = context.ProtocolMessage.Code;

            string signedInUserID = context.Principal.FindFirst(ClaimTypes.NameIdentifier).Value;
            IConfidentialClientApplication cca = ConfidentialClientApplicationBuilder.Create(AzureAdB2COptions.ClientId)
                .WithB2CAuthority(AzureAdB2COptions.Authority)
                .WithRedirectUri(AzureAdB2COptions.RedirectUri)
                .WithClientSecret(AzureAdB2COptions.ClientSecret)
                .Build();
            new MSALStaticCache(signedInUserID, context.HttpContext).EnablePersistence(cca.UserTokenCache);

            try
            {
                AuthenticationResult result = await cca.AcquireTokenByAuthorizationCode(AzureAdB2COptions.ApiScopes.Split(' '), code)
                    .ExecuteAsync();


                context.HandleCodeRedemption(result.AccessToken, result.IdToken);
            }
            catch (Exception ex)
            {
                //TODO: Handle
                throw;
            }
        }
    }
}

最后创建: \Pages\Account\Login.cshtml.cs 来替换原始登录名,您也需要更改 login.cshtml。

    using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.Net.Http.Headers;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Security.Claims;
using System.Text;
using System.Threading.Tasks;
using Volo.Abp.Account.Settings;
using Volo.Abp.Auditing;
using Volo.Abp.Identity;
using Volo.Abp.Security.Claims;
using Volo.Abp.Settings;
using Volo.Abp.Uow;
using Volo.Abp.Validation;
using IdentityUser = Volo.Abp.Identity.IdentityUser;

namespace Volo.Abp.Account.Web.Pages.Account
{
    public class CustomLoginModel : LoginModel
    {
     
        public CustomLoginModel(
                Microsoft.AspNetCore.Authentication.IAuthenticationSchemeProvider schemeProvider,
                Microsoft.Extensions.Options.IOptions<Volo.Abp.Account.Web.AbpAccountOptions> accountOptions)
       : base(schemeProvider, accountOptions)
        {
           
        }

        public override async Task<IActionResult> OnGetAsync()
        {
            string provider = "OpenIdConnect";
            var redirectUrl = Url.Page("./Login", pageHandler: "ExternalLoginCallback", values: new { ReturnUrl, ReturnUrlHash });
            var properties = SignInManager.ConfigureExternalAuthenticationProperties(provider, redirectUrl);
            properties.Items["scheme"] = provider;
            return await Task.FromResult(Challenge(properties, provider));
        }

        public override async Task<IActionResult> OnGetExternalLoginCallbackAsync(string returnUrl = "", string returnUrlHash = "", string remoteError = null)
        {
            //TODO: Did not implemented Identity Server 4 sample for this method (see ExternalLoginCallback in Quickstart of IDS4 sample)
            /* Also did not implement these:
             * - Logout(string logoutId)
             */
           
            if (remoteError != null)
            {
                Logger.LogWarning($"External login callback error: {remoteError}");
                return RedirectToPage("./Login");
            }

            var loginInfo = await SignInManager.GetExternalLoginInfoAsync();
            if (loginInfo == null)
            {
                Logger.LogWarning("External login info is not available");
                return RedirectToPage("./Login");
            }

            var result = await SignInManager.ExternalLoginSignInAsync(
                loginInfo.LoginProvider,
                loginInfo.ProviderKey,
                isPersistent: false,
                bypassTwoFactor: true
            );

            if (result.IsLockedOut)
            {
                throw new UserFriendlyException("Cannot proceed because user is locked out!");
            }

            if (result.Succeeded)
            {
                return RedirectSafely(returnUrl, returnUrlHash);
            }

            //TODO: Handle other cases for result!

            // Get the information about the user from the external login provider
            //var info = await SignInManager.GetExternalLoginInfoAsync();
            //if (info == null)
            //{
            //    throw new ApplicationException("Error loading external login information during confirmation.");
            //}

            var user = await CreateExternalUserAsync(loginInfo);

            await SignInManager.SignInAsync(user, false);
            return RedirectSafely(returnUrl, returnUrlHash);
        }

        protected override async Task<IdentityUser> CreateExternalUserAsync(ExternalLoginInfo info)
        {
            var emailAddress = info.Principal.FindFirstValue(AbpClaimTypes.Email);

            var user = new IdentityUser(GuidGenerator.Create(), emailAddress, emailAddress, CurrentTenant.Id);

            CheckIdentityErrors(await UserManager.CreateAsync(user));
            CheckIdentityErrors(await UserManager.SetEmailAsync(user, emailAddress));
            CheckIdentityErrors(await UserManager.AddLoginAsync(user, info));
            CheckIdentityErrors(await UserManager.AddDefaultRolesAsync(user));

            return user;
        }

        protected override async Task ReplaceEmailToUsernameOfInputIfNeeds()
        {
            if (!ValidationHelper.IsValidEmailAddress(LoginInput.UserNameOrEmailAddress))
            {
                return;
            }

            var userByUsername = await UserManager.FindByNameAsync(LoginInput.UserNameOrEmailAddress);
            if (userByUsername != null)
            {
                return;
            }

            var userByEmail = await UserManager.FindByEmailAsync(LoginInput.UserNameOrEmailAddress);
            if (userByEmail == null)
            {
                return;
            }

            LoginInput.UserNameOrEmailAddress = userByEmail.UserName;
        }
    }
}

推荐阅读