Используйте Azure B2C в качестве ОСНОВНОГО поставщика аутентификации в веб-приложении abp framework

#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) при входе в систему. Это немного сложно, потому что вам нужно переопределить довольно много служб…

Я думал, что у кого-то есть работа над этим, потому что мне тоже не нужен пароль в БД.