Как глобально реализовать авторизацию на основе AD Group в веб-приложении asp.net core 2.x? - PullRequest
0 голосов
/ 01 мая 2018

Мне интересно, может ли кто-нибудь указать мне направление или пример, у которого есть законченный код, чтобы я мог получить общее представление?

Спасибо.

Обновление: У меня есть только следующий фрагмент кода в Startup.cs и убедитесь, что windowsAutication имеет значение true в файле launchSettings.json.

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc();

    services.AddMvc(config =>
    {
        var policy = new AuthorizationPolicyBuilder()
                         .RequireAuthenticatedUser()
                         //.RequireRole(@"Departmental - Information Technology - Development")   // Works
                         .RequireRole(@"*IT.Center of Excellence.Digital Workplace")              // Error
                         .Build();
        config.Filters.Add(new AuthorizeFilter(policy));
    });

}

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

Если я использую закомментированное RequireRole, оно работает, но я использую незакомментированное RequireRole, и это дает мне эту ошибку: Win32Exception: не удалось установить доверительные отношения между основным доменом и доверенным доменом.

Верхняя строка в стеке показывает: System.Security.Principal.NTAccount.TranslateToSids (IdentityReferenceCollection sourceAccounts, out of bool someFailed)

Есть идеи, почему?

Мое понимание из обновления выше

Кажется, имя группы, указанное в RequireRole, является списком рассылки электронной почты, а не группой безопасности. Если я использую какую-то другую группу AD, она работает, но с этой новой ошибкой:

InvalidOperationException: не указана схема аутентификации, и не найдено DefaultForbidScheme.

Если я добавлю схему проверки подлинности IIS по умолчанию в ConfigureServices в Startup.cs

services.AddAuthentication(IISDefaults.AuthenticationScheme);

это дает мне страницу HTTP 403: веб-сайт отказался показывать эту страницу

Итак, это окончательный код:

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc();

    services.AddAuthentication(IISDefaults.AuthenticationScheme);

    services.AddMvc(config =>
    {
        var policy = new AuthorizationPolicyBuilder()
                         .RequireAuthenticatedUser()
                         .RequireRole(@"Departmental - Information Technology - Development") // AD security group
                         .Build();
        config.Filters.Add(new AuthorizeFilter(policy));
    });

}

Поправь меня, если я неправильно понял. Спасибо.

1 Ответ

0 голосов
/ 02 мая 2018

Вариант 1: проверка подлинности Windows

Вы можете включить проверку подлинности Windows для приложений интрасети. Прочитайте документы здесь . Вы можете проверить, входит ли пользователь в роль / группу, выполнив что-то вроде this .

Прежде чем сделать это, вы можете проверить информацию о группах, к которой присоединился ваш компьютер, выполнив команду gpresult /R в командной строке. См. этот пост для получения дополнительной информации.

User.IsInRole("xxxx")  // this should return True for any group listed up there

Вам не нужно преобразовывать текущий принципал в принципал Windows, если вам не нужно получать какую-либо информацию, связанную с Windows.

Если вы хотите получить список всех групп, вам все равно нужно запросить свою AD.

предупреждение:

Иногда я вижу, что некоторые группы не отображаются в результате, используя gpresult /R на компьютере, по сравнению с методом 2. Вот почему иногда, когда вы делаете User.IsInRole(), и он возвращает false. Я до сих пор не знаю, почему это происходит.

Вариант 2. Аутентификация с помощью поиска AD

Аутентификация Windows предлагает лишь небольшую информацию о пользователе и группах AD. Иногда этого достаточно, но в большинстве случаев это не так.

Вы также можете использовать обычную проверку подлинности с помощью форм, общаться с AD и выдавать cookie. Таким образом, хотя пользователю необходимо войти в ваше приложение, используя свои учетные данные и пароль Windows, у вас есть полный контроль над информацией AD.

Вы не хотите писать все вручную. К счастью, есть библиотека Novell.Directory.Ldap.NETStandard , чтобы помочь. Вы можете найти его в NuGet.

Интерфейсы для определения того, что вам нужно от AD, а также протокол входа в систему:

namespace DL.SO.Services.Core
{
    public interface IAppUser
    {
        string Username { get; }
        string DisplayName { get; }
        string Email { get; }
        string[] Roles { get; }
    }

    public interface IAuthenticationService
    {
        IAppUser Login(string username, string password);
    }
}

Реализация AppUser:

using DL.SO.Services.Core;

namespace DL.SO.Services.Security.Ldap.Entities
{
    public class AppUser : IAppUser
    {
        public string Username { get; set; }
        public string DisplayName { get; set; }
        public string Email { get; set; }
        public string[] Roles { get; set; }
    }
}

Объект конфигурации Ldap для отображения значений из appsettings.json:

namespace DL.SO.Services.Security.Ldap
{
    public class LdapConfig
    {
        public string Url { get; set; }
        public string BindDn { get; set; }
        public string Username { get; set; }
        public string Password { get; set; }
        public string SearchBase { get; set; }
        public string SearchFilter { get; set; }
    }
}

Реализация LdapAuthenticationService:

using Microsoft.Extensions.Options;
using Novell.Directory.Ldap;
using System;
using System.Linq;
using System.Text.RegularExpressions;
using DL.SO.Services.Core;
using DL.SO.Services.Security.Ldap.Entities;

namespace DL.SO.Services.Security.Ldap
{
    public class LdapAuthenticationService : IAuthenticationService
    {
        private const string MemberOfAttribute = "memberOf";
        private const string DisplayNameAttribute = "displayName";
        private const string SAMAccountNameAttribute = "sAMAccountName";
        private const string MailAttribute = "mail";

        private readonly LdapConfig _config;
        private readonly LdapConnection _connection;

        public LdapAuthenticationService(IOptions<LdapConfig> configAccessor)
        {
            _config = configAccessor.Value;
            _connection = new LdapConnection();
        }

        public IAppUser Login(string username, string password)
        {
            _connection.Connect(_config.Url, LdapConnection.DEFAULT_PORT);
            _connection.Bind(_config.Username, _config.Password);

            var searchFilter = String.Format(_config.SearchFilter, username);
            var result = _connection.Search(
                _config.SearchBase,
                LdapConnection.SCOPE_SUB, 
                searchFilter,
                new[] { 
                    MemberOfAttribute, 
                    DisplayNameAttribute, 
                    SAMAccountNameAttribute, 
                    MailAttribute 
                }, 
                false
            );

            try
            {
                var user = result.next();
                if (user != null)
                {
                    _connection.Bind(user.DN, password);
                    if (_connection.Bound)
                    {
                        var accountNameAttr = user.getAttribute(SAMAccountNameAttribute);
                        if (accountNameAttr == null)
                        {
                            throw new Exception("Your account is missing the account name.");
                        }

                        var displayNameAttr = user.getAttribute(DisplayNameAttribute);
                        if (displayNameAttr == null)
                        {
                            throw new Exception("Your account is missing the display name.");
                        }

                        var emailAttr = user.getAttribute(MailAttribute);
                        if (emailAttr == null)
                        {
                            throw new Exception("Your account is missing an email.");
                        }

                        var memberAttr = user.getAttribute(MemberOfAttribute);
                        if (memberAttr == null)
                        {
                            throw new Exception("Your account is missing roles.");
                        }

                        return new AppUser
                        {
                            DisplayName = displayNameAttr.StringValue,
                            Username = accountNameAttr.StringValue,
                            Email = emailAttr.StringValue,
                            Roles = memberAttr.StringValueArray
                                .Select(x => GetGroup(x))
                                .Where(x => x != null)
                                .Distinct()
                                .ToArray()
                        };
                    }
                }
            }
            finally
            {
                _connection.Disconnect();
            }

            return null;
        }

        private string GetGroup(string value)
        {
            Match match = Regex.Match(value, "^CN=([^,]*)");
            if (!match.Success)
            {
                return null;
            }

            return match.Groups[1].Value;
        }
    }
}

Конфигурация в appsettings.json (только пример):

{
    "ldap": {
       "url": "[YOUR_COMPANY].loc",
       "bindDn": "CN=Users,DC=[YOUR_COMPANY],DC=loc",
       "username": "[YOUR_COMPANY_ADMIN]",
       "password": "xxx",
       "searchBase": "DC=[YOUR_COMPANY],DC=loc",
       "searchFilter": "(&(objectClass=user)(objectClass=person)(sAMAccountName={0}))"
    },
    "cookies": {
        "cookieName": "cookie-name-you-want-for-your-app",
        "loginPath": "/account/login",
        "logoutPath": "/account/logout",
        "accessDeniedPath": "/account/accessDenied",
        "returnUrlParameter": "returnUrl"
    }
}

Настройка аутентификации (а также авторизации) для приложения:

namespace DL.SO.Web.UI
{
    public class Startup
    {
        private readonly IHostingEnvironment _currentEnvironment;
        public IConfiguration Configuration { get; private set; }

        public Startup(IConfiguration configuration, IHostingEnvironment env)
        {
            _currentEnvironment = env;
            Configuration = configuration;
        }

        public void ConfigureServices(IServiceCollection services)
        { 
            // Authentication service
            services.Configure<LdapConfig>(this.Configuration.GetSection("ldap"));
            services.AddScoped<IAuthenticationService, LdapAuthenticationService>();

            // MVC
            services.AddMvc(config =>
            {
                // Requiring authenticated users on the site globally
                var policy = new AuthorizationPolicyBuilder()
                    .RequireAuthenticatedUser()

                    // You can chain more requirements here
                    // .RequireRole(...) OR
                    // .RequireClaim(...) OR
                    // .Requirements.Add(...)         

                    .Build();
                config.Filters.Add(new AuthorizeFilter(policy));
            });

            services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();

            // Authentication
            var cookiesConfig = this.Configuration.GetSection("cookies")
                .Get<CookiesConfig>();
            services.AddAuthentication(
                CookieAuthenticationDefaults.AuthenticationScheme)
                .AddCookie(options =>
                {
                    options.Cookie.Name = cookiesConfig.CookieName;
                    options.LoginPath = cookiesConfig.LoginPath;
                    options.LogoutPath = cookiesConfig.LogoutPath;
                    options.AccessDeniedPath = cookiesConfig.AccessDeniedPath;
                    options.ReturnUrlParameter = cookiesConfig.ReturnUrlParameter;
                });

            // Setup more authorization policies as an example.
            // You can use them to protected more strict areas. Otherwise
            // you don't need them.
            services.AddAuthorization(options =>
            {
                options.AddPolicy("AdminOnly", 
                    policy => policy.RequireClaim(ClaimTypes.Role, "[ADMIN_ROLE_OF_YOUR_COMPANY]"));

                // More on Microsoft documentation
                // https://docs.microsoft.com/en-us/aspnet/core/security/authorization/policies?view=aspnetcore-2.1
            });
        }

        public void Configure(IApplicationBuilder app)
        {
            app.UseAuthentication();
            app.UseMvc(...);
        }  
    }
}

Как аутентифицировать пользователей с помощью сервиса аутентификации:

namespace DL.SO.Web.UI.Controllers
{
    public class AccountController : Controller
    {
        private readonly IAuthenticationService _authService;

        public AccountController(IAuthenticationService authService)
        {
            _authService = authService;
        }

        [AllowAnonymous]
        [HttpPost]
        public async Task<IActionResult> Login(LoginViewModel model)
        {
            if (ModelState.IsValid)
            {
                try
                {
                    var user = _authService.Login(model.Username, model.Password);

                    // If the user is authenticated, store its claims to cookie
                    if (user != null)
                    {
                        var userClaims = new List<Claim>
                        {
                            new Claim(ClaimTypes.Name, user.Username),
                            new Claim(CustomClaimTypes.DisplayName, user.DisplayName),
                            new Claim(ClaimTypes.Email, user.Email)
                        };

                        // Roles
                        foreach (var role in user.Roles)
                        {
                            userClaims.Add(new Claim(ClaimTypes.Role, role));
                        }

                        var principal = new ClaimsPrincipal(
                            new ClaimsIdentity(userClaims, _authService.GetType().Name)
                        );

                        await HttpContext.SignInAsync(                            
                          CookieAuthenticationDefaults.AuthenticationScheme, 
                            principal,
                            new AuthenticationProperties
                            {
                                IsPersistent = model.RememberMe
                            }
                        );

                        return Redirect(Url.IsLocalUrl(model.ReturnUrl)
                            ? model.ReturnUrl
                            : "/");
                    }

                    ModelState.AddModelError("", @"Your username or password
                        is incorrect. Please try again.");
                }
                catch (Exception ex)
                {
                    ModelState.AddModelError("", ex.Message);
                }
            }
            return View(model);
        }
    }
}

Как читать информацию, хранящуюся в формуле изобретения:

public class TopNavbarViewComponent : ViewComponent
{
    private readonly IHttpContextAccessor _httpContextAccessor;

    public TopNavbarViewComponent(IHttpContextAccessor httpContextAccessor)
    {
        _httpContextAccessor = httpContextAccessor;
    }

    public async Task<IViewComponentResult> InvokeAsync()
    {
        string loggedInUsername = _httpContextAccessor.HttpContext.User.Identity.Name;

        string loggedInUserDisplayName = _httpContextAccessor.HttpContext.User.GetDisplayName();

       ...
       return View(vm);
    }
}

Метод расширения для ClaimsPrincipal:

namespace DL.SO.Framework.Mvc.Extensions
{
    public static class ClaimsPrincipalExtensions
    {
        public static Claim GetClaim(this ClaimsPrincipal user, string claimType)
        {
            return user.Claims
                .SingleOrDefault(c => c.Type == claimType);
        }

        public static string GetDisplayName(this ClaimsPrincipal user)
        {
            var claim = GetClaim(user, CustomClaimTypes.DisplayName);

            return claim?.Value;
        }

        public static string GetEmail(this ClaimsPrincipal user)
        {
            var claim = GetClaim(user, ClaimTypes.Email);

            return claim?.Value;
        }
    }
}

Как использовать политику авторизации:

namespace DL.SO.Web.UI.Areas.Admin.Controllers
{
    [Area("admin")]
    [Authorize(Policy = "AdminOnly")]
    public abstract class AdminControllerBase : Controller {}
}

Бонус

Вы можете загрузить AD Explorer от Microsoft , чтобы вы могли визуализировать AD своей компании.

Opps. Я планировал просто что-то выдать для начала, но в итоге написал очень длинный пост.

...