Как использовать проверку подлинности Windows Active Directory и утверждения на основе идентификатора?

проблема

Мы хотим использовать Windows Active Directory для аутентификации пользователя в приложении. Однако мы не хотим использовать группы Active Directory для управления авторизацией контроллеров / представлений.

Насколько я знаю, не существует простого способа объединить требования AD и идентификационные данные.

цели

  • Аутентифицировать пользователей с локальным Active Directory
  • Использование Identity Framework для управления заявками

Попытки (неудачи)

  • Windows.Owin.Security.ActiveDirectory - Doh. Это для Azure AD. Нет поддержки LDAP. Могли бы они называть это AzureActiveDirectory?
  • Аутентификация Windows - Это нормально для аутентификации NTLM или Keberos. Проблемы начинаются с: i) все токены и утверждения управляются AD, и я не могу понять, как использовать с ними утверждения личности.
  • LDAP - Но это, кажется, заставляет меня вручную проверять подлинность форм, чтобы использовать утверждения личности? Конечно, должен быть более простой способ?

Любая помощь будет более чем признательна. Я застрял в этой проблеме довольно долгое время и был бы признателен извне по этому вопросу.

6 ответов

Решение

Использование вышеупомянутого решения подтолкнуло меня в направлении, которое помогло мне на MVC6-Beta3 Identityframework7-Beta3 EntityFramework7-Beta3:

// POST: /Account/Login
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Login(LoginViewModel model, string returnUrl = null)
{
    if (!ModelState.IsValid)
    {
        return View(model);
    }

    //
    // Check for user existance in Identity Framework
    //
    ApplicationUser applicationUser = await _userManager.FindByNameAsync(model.eID);
    if (applicationUser == null)
    {
        ModelState.AddModelError("", "Invalid username");
        return View(model);
    }

    //
    // Authenticate user credentials against Active Directory
    //
    bool isAuthenticated = await Authentication.ValidateCredentialsAsync(
        _applicationSettings.Options.DomainController, 
        _applicationSettings.Options.DomainControllerSslPort, 
        model.eID, model.Password);
    if (isAuthenticated == false)
    {
        ModelState.AddModelError("", "Invalid username or password.");
        return View(model);
    }

    //
    // Signing the user step 1.
    //
    IdentityResult identityResult 
        = await _userManager.CreateAsync(
            applicationUser, 
            cancellationToken: Context.RequestAborted);

    if(identityResult != IdentityResult.Success)
    {
        foreach (IdentityError error in identityResult.Errors)
        {
            ModelState.AddModelError("", error.Description);
        }
        return View(model);
    }

    //
    // Signing the user step 2.
    //
    await _signInManager.SignInAsync(applicationUser,
        isPersistent: false,
        authenticationMethod:null,
        cancellationToken: Context.RequestAborted);

    return RedirectToLocal(returnUrl);
}

Просто нажмите AD с именем пользователя и паролем вместо аутентификации в вашей БД

// POST: /Account/Login
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Login(LoginViewModel model, string returnUrl)
{
    if (ModelState.IsValid)
    {
        var user = await UserManager.FindByNameAsync(model.UserName);
        if (user != null && AuthenticateAD(model.UserName, model.Password))
        {
            await SignInAsync(user, model.RememberMe);
            return RedirectToLocal(returnUrl);
        }
        else
        {
            ModelState.AddModelError("", "Invalid username or password.");
        }
    }
    return View(model);
}

public bool AuthenticateAD(string username, string password)
{
    using(var context = new PrincipalContext(ContextType.Domain, "MYDOMAIN"))
    {
        return context.ValidateCredentials(username, password);
    }
}

В ASPNET5 (бета6) идея заключается в использовании CookieAuthentication и Identity: вам нужно добавить в свой класс запуска:

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc();
    services.AddAuthorization();
    services.AddIdentity<MyUser, MyRole>()
        .AddUserStore<MyUserStore<MyUser>>()
        .AddRoleStore<MyRoleStore<MyRole>>()
        .AddUserManager<MyUserManager>()
        .AddDefaultTokenProviders();
}

В разделе конфигурации добавьте:

private void ConfigureAuth(IApplicationBuilder app)
{
    // Use Microsoft.AspNet.Identity & Cookie authentication
    app.UseIdentity();
    app.UseCookieAuthentication(options =>
    {
        options.AutomaticAuthentication = true;
        options.LoginPath = new PathString("/App/Login");
    });
}

Затем вам нужно будет реализовать:

Microsoft.AspNet.Identity.IUserStore
Microsoft.AspNet.Identity.IRoleStore
Microsoft.AspNet.Identity.IUserClaimsPrincipalFactory

и расширить / переопределить:

Microsoft.AspNet.Identity.UserManager
Microsoft.AspNet.Identity.SignInManager

Я на самом деле настроил пример проекта, чтобы показать, как это можно сделать. GitHub Ссылка.

Я тестировал на бета8 и с некоторыми небольшими адаптациями (такими как Context => HttpContext) он тоже работал.

Вы можете использовать ClaimTransformation, я только что начал работать сегодня днем, используя статью и код ниже. Я получаю доступ к приложению с помощью проверки подлинности окна, а затем добавляю заявки на основе разрешений, хранящихся в базе данных SQL. Это хорошая статья, которая должна вам помочь.

https://github.com/aspnet/Security/issues/863

В итоге...

services.AddScoped<IClaimsTransformer, ClaimsTransformer>();

app.UseClaimsTransformation(async (context) =>
{
IClaimsTransformer transformer = context.Context.RequestServices.GetRequiredService<IClaimsTransformer>();
return await transformer.TransformAsync(context);
});

public class ClaimsTransformer : IClaimsTransformer
    {
        private readonly DbContext _context;

        public ClaimsTransformer(DbContext dbContext)
        {
            _context = dbContext;
        }
        public async Task<ClaimsPrincipal> TransformAsync(ClaimsTransformationContext context)
        {

            System.Security.Principal.WindowsIdentity windowsIdentity = null;

            foreach (var i in context.Principal.Identities)
            {
                //windows token
                if (i.GetType() == typeof(System.Security.Principal.WindowsIdentity))
                {
                    windowsIdentity = (System.Security.Principal.WindowsIdentity)i;
                }
            }

            if (windowsIdentity != null)
            {
                //find user in database by username
                var username = windowsIdentity.Name.Remove(0, 6);
                var appUser = _context.User.FirstOrDefault(m => m.Username == username);

                if (appUser != null)
                {

                    ((ClaimsIdentity)context.Principal.Identity).AddClaim(new Claim("Id", Convert.ToString(appUser.Id)));

                    /*//add all claims from security profile
                    foreach (var p in appUser.Id)
                    {
                        ((ClaimsIdentity)context.Principal.Identity).AddClaim(new Claim(p.Permission, "true"));
                    }*/

                }

            }
            return await System.Threading.Tasks.Task.FromResult(context.Principal);
        }
    }

Мне пришлось разработать решение этой проблемы следующим образом:

      1. Any AD authenticated user will be able to access the application.
2. The roles and claims of the users are stored in the Identity database of the application.
3. An admin user will be able to assign roles to users (I have added this functionality to the app as well).

Читайте дальше, если хотите увидеть мое полное решение. Ссылка на полный исходный код находится ближе к концу этого ответа.

Основной дизайн

      1. User enters Active Directory credentials (Windows login credentials in this case).
2. The app checks if it's a valid login against AD.
    2.1. If it's not valid, app returns the page with 'Invalid login attempt' error message.
    2.2. If it's valid, go to next step.
3. Check if the user exists in the Identity database.
    3.1. If Not, create this user in our Identity database.
    3.2 If Yes, go to next step.
4. SignIn the user (using AD credentials). This is where we override UserManager.

Примечание. Пользователю, созданному на шаге 3.1, не назначены роли. Пользователь-администратор (с действительным именем пользователя AD) создается во время инициализации базы данных. НастроитьAdmin2UserNameс вашим именем пользователя AD, если вы хотите быть администратором, который будет назначать роли вновь добавленным пользователям. Даже не беспокойтесь о пароле, это может быть что угодно, потому что фактическая аутентификация будет происходить через AD, а не через базу данных Identity.

Решение

Шаг 1. Убедитесь, что в вашем приложении настроена идентификация. В качестве примера я использую приложение Blazor Server. Если у вас нет настройки удостоверения, следуйте этому руководству от Microsoft.

Используйте , чтобы следовать руководству.

Шаг 2: ДобавьтеADHelperстатический класс для помощи при входе в Active Directory. В моем примере это Areas/Identity/ADHelper.cs , и его содержимое выглядит следующим образом:

      using System.DirectoryServices.AccountManagement;

namespace HMT.Web.Server.Areas.Identity
{
    public static class ADHelper
    {
        public static bool ADLogin(string userName, string password)
        {
            using PrincipalContext principalContext = new(ContextType.Domain);
            bool isValidLogin = principalContext.ValidateCredentials(userName.ToUpper(), password);

            return isValidLogin;
        }
    }
}

Шаг 3: переопределитьCheckPasswordAsyncметод вUserManagerтак что вы можете аутентифицировать пользователей в Active Directory. Я сделал это в Areas/Identity/ADUserManager.cs , содержимое которого выглядит так:

      using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Options;

namespace HMT.Web.Server.Areas.Identity
{
    public class ADUserManager<TUser> : UserManager<TUser> where TUser : IdentityUser
    {
        public ADUserManager(IUserStore<TUser> store, IOptions<IdentityOptions> optionsAccessor,
            IPasswordHasher<TUser> passwordHasher, IEnumerable<IUserValidator<TUser>> userValidators,
            IEnumerable<IPasswordValidator<TUser>> passwordValidators, ILookupNormalizer keyNormalizer,
            IdentityErrorDescriber errors, IServiceProvider services, ILogger<UserManager<TUser>> logger)
            : base(store, optionsAccessor, passwordHasher, userValidators, passwordValidators, keyNormalizer,
                  errors, services, logger)
        {
        }

        public override Task<bool> CheckPasswordAsync(TUser user, string password)
        {
            var adLoginResult = ADHelper.ADLogin(user.UserName, password);
            return Task.FromResult(adLoginResult);
        }
    }
}

Шаг 4: Зарегистрируйте его в своемProgram.cs

      builder.Services
.AddDefaultIdentity<ApplicationUser>(options =>
{
    options.SignIn.RequireConfirmedAccount = false;
})
.AddRoles<ApplicationRole>()
.AddUserManager<CustomUserManager<ApplicationUser>>()  <----- THIS GUY
.AddEntityFrameworkStores<ApplicationDbContext>();

ApplicationUser,ApplicationRoleиApplicationDbContextвыглядеть так:

      public class ApplicationUser : IdentityUser
{
}

public class ApplicationRole : IdentityRole
{
}

public class ApplicationDbContext : IdentityDbContext<ApplicationUser, ApplicationRole, string>
{
    public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
        : base(options)
    {
    }

    protected override void OnModelCreating(ModelBuilder builder)
    {
        base.OnModelCreating(builder);
        // Customize the ASP.NET Identity model and override the defaults if needed.
        // For example, you can rename the ASP.NET Identity table names and more.
        // Add your customizations after calling base.OnModelCreating(builder);
    }
}

Шаг 5: ОбновитеOnPostAsyncв Areas/Identity/Pages/Account/Login.cshtml.cs для реализации потока проверки подлинности. Метод выглядит следующим образом:

      public async Task<IActionResult> OnPostAsync(string returnUrl = null)
{
    returnUrl ??= Url.Content("~/");

    if (ModelState.IsValid)
    {
        // Step 1: Authenticate an user against AD
        // If YES: Go to next step
        // If NO: Terminate the process
        var adLoginResult = ADHelper.ADLogin(Input.UserName, Input.Password);
        if (!adLoginResult)
        {
            ModelState.AddModelError(string.Empty, "Invalid login attempt.");
            return Page();
        }

        // Step 2: Check if the user exists in our Identity Db
        // If YES: Proceed to SignIn the user
        // If NO: Either terminate the process OR create this user in our Identity Db and THEN proceed to SignIn the user
        // I'm going with OR scenario this time
        var user = await _userManager.FindByNameAsync(Input.UserName);
        if (user == null)
        {
            var identityResult = await _userManager.CreateAsync(new ApplicationUser
            {
                UserName = Input.UserName,
            }, Input.Password);

            if (identityResult != IdentityResult.Success)
            {
                ModelState.AddModelError(string.Empty, "The user was authenticated against AD successfully, but failed to be inserted into Application's Identity database.");
                foreach (IdentityError error in identityResult.Errors)
                {
                    ModelState.AddModelError(string.Empty, error.Description);
                }

                return Page();
            }
        }

        // Step 3: SignIn the user using AD credentials
        // This doesn't count login failures towards account lockout
        // To enable password failures to trigger account lockout, set lockoutOnFailure: true
        var result = await _signInManager.PasswordSignInAsync(Input.UserName, Input.Password, Input.RememberMe, lockoutOnFailure: false);
        if (result.Succeeded)
        {
            _logger.LogInformation("User logged in.");
            return LocalRedirect(returnUrl);
        }
        if (result.RequiresTwoFactor)
        {
            return RedirectToPage("./LoginWith2fa", new { ReturnUrl = returnUrl, RememberMe = Input.RememberMe });
        }
        if (result.IsLockedOut)
        {
            _logger.LogWarning("User account locked out.");
            return RedirectToPage("./Lockout");
        }
        else
        {
            ModelState.AddModelError(string.Empty, "Invalid login attempt.");
            return Page();
        }
    }

    // If we got this far, something failed, redisplay form
    return Page();
}

В принципе, мы закончили здесь.

Шаг 6: Теперь, если пользователь-администратор хочет назначить роли вновь добавленным пользователям, просто перейдите на страницу «Управление пользователями» и назначьте соответствующие роли. Довольно легко, правда?

Шаг 7: Если вы хотите управлять ролями (добавлять, редактировать, удалять), просто перейдите на страницу управления/ролями .

Заключение

Эта настройка гарантирует, что пользователи аутентифицируются с помощью Active Directory и авторизуются с использованием ролей в базе данных удостоверений.

Полный исходный код

мой проектhttps://github.com/affableashish/blazor-server-auth/tree/feature/AddADAuthentication

Вы знаете, как реализовать кастом System.Web.Security.MembershipProvider? Вы должны быть в состоянии использовать это (переопределить ValidateUser) в сочетании с System.DirectoryServices.AccountManagement.PrincipalContext.ValidateCredentials() аутентифицироваться по активному каталогу.

пытаться: var pc = new PrincipalContext(ContextType.Domain, "example.com", "DC=example,DC=com"); pc.ValidateCredentials(username, password);