#abp
#abp
Вопрос:
У меня есть веб-приложение abp framework, использующее стандартный поставщик аутентификации на основе Asp.NET Основная идентичность.
Я хочу заменить Asp.NET Внедрение Core Identity abp в Azure B2C в качестве основного поставщика аутентификации и управление собственным хранилищем удостоверений и внешними поставщиками.
Я думаю о Azure B2C, потому что:
- Это служба PaaS, автоматически управляемая Azure, и внедрение и обслуживание должны быть проще, чем на сервере идентификации 4.
- Мне не нужно хранить учетные данные в базе данных приложения.
С другой стороны, и здесь мой вопрос. Как заменить хранилище удостоверений abp Framework? перезапишите логин / выход / регистрацию / восстановление пароля / … примеры использования? и интегрировать с многопользовательскими и другими модулями?.
большое спасибо за ваши мысли,
Комментарии:
1. вам все еще нужен is4 для управления разрешениями — взгляните на community.abp.io/articles /… — это только начало…
Ответ №1:
Это возможно, и я это сделал. Прочитайте и попробуйте реализовать статьи в документации abp:
- Проверка подлинности Azure Active Directory
- Настройте страницу входа.
- Настройте диспетчер входа
Чтобы понять концепцию, а затем в основном вам нужно будет заменить библиотеку azuread на azureb2c, мне удалось сделать это с помощью:
Альтернативный подход: 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) amp;amp;
!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 amp;amp; 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 amp;amp; 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 AccountLogin.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;
}
}
}
Ответ №2:
Я подумываю о том, чтобы сделать что-то подобное. Я хочу удалить IdentityServer, затем добавить Azure ADB2C. Однако однажды вам все равно нужно сохранить хотя бы пользователя в таблице abpuser (скопировать из azure ADB2C) при входе в систему. Это немного сложно, потому что вам нужно переопределить довольно много служб…
Я думал, что у кого-то есть работа над этим, потому что мне тоже не нужен пароль в БД.