使用Azure B2C作为abp框架web应用程序中的PRIMARY身份验证提供商



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

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

我正在考虑Azure B2C,因为:

  • 它是一个由Azure自动管理的PaaS服务,实现和维护应该比身份服务器4更容易
  • 我不需要在应用程序数据库中存储凭据

另一方面,这是我的问题。如何替换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"
}

配置服务部分中的配置模块将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.cs.html。

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

我正在考虑做一些类似的事情。我想删除IdentityServer,然后添加Azure ADB2C。然而,一旦登录,您仍然需要在abpuser表中至少存储用户(从azure ADB2C复制(。这有点复杂,因为您必须覆盖相当多的服务。。。

我很高兴有人在做这件事,因为我也不想在数据库中输入密码。

相关内容

最新更新