OWIN Cookie Authentication - олицетворение для SQL Server с делегированием Kerberos - PullRequest
0 голосов
/ 29 июня 2018

После нескольких недель исследований Identity 2.0, олицетворения, делегирования и Kerberos я все еще не могу найти решение, которое позволило бы мне выдать себя за пользователя ClaimsIdentity, которого я создал с помощью OWIN в своем приложении MVC. Особенности моего сценария следующие.

Аутентификация Windows отключена + Аноним включен.
Я использую класс запуска OWIN для ручной аутентификации пользователя в нашей Active Directory. Затем я упаковываю некоторые свойства в файл cookie, который доступен в остальной части приложения. Это - ссылка, на которую я ссылался при настройке этих классов.

Startup.Auth.cs

app.UseCookieAuthentication(new CookieAuthenticationOptions
{
     AuthenticationType = MyAuthentication.ApplicationCookie,
     LoginPath = new PathString("/Login"),
     Provider = new CookieAuthenticationProvider(),
     CookieName = "SessionName",
     CookieHttpOnly = true,
     ExpireTimeSpan = TimeSpan.FromHours(double.Parse(ConfigurationManager.AppSettings["CookieLength"]))
});

AuthenticationService.cs

    using System;
    using System.DirectoryServices.AccountManagement;
    using System.DirectoryServices;
    using System.Security.Claims;
    using Microsoft.Owin.Security;
    using System.Configuration;
    using System.Collections.Generic;

    using System.Linq;

    namespace mine.Security
    {
        public class AuthenticationService
        {
            private readonly IAuthenticationManager _authenticationManager;
            private PrincipalContext _context;
            private UserPrincipal _userPrincipal;
            private ClaimsIdentity _identity;

        public AuthenticationService(IAuthenticationManager authenticationManager)
        {
            _authenticationManager = authenticationManager;
        }

        /// <summary>
        /// Check if username and password matches existing account in AD. 
        /// </summary>
        /// <param name="username"></param>
        /// <param name="password"></param>
        /// <returns></returns>
        public AuthenticationResult SignIn(String username, String password)
        {

            // connect to active directory
            _context = new PrincipalContext(ContextType.Domain,
                                            ConfigurationManager.ConnectionStrings["LdapServer"].ConnectionString,
                                            ConfigurationManager.ConnectionStrings["LdapContainer"].ConnectionString,
                                            ContextOptions.SimpleBind,
                                            ConfigurationManager.ConnectionStrings["LDAPUser"].ConnectionString,
                                            ConfigurationManager.ConnectionStrings["LDAPPass"].ConnectionString);

            // try to find if the user exists
            _userPrincipal = UserPrincipal.FindByIdentity(_context, IdentityType.SamAccountName, username);

            if (_userPrincipal == null)
            {
                return new AuthenticationResult("There was an issue authenticating you.");
            }

            // try to validate credentials
            if (!_context.ValidateCredentials(username, password))
            {
                return new AuthenticationResult("Incorrect username/password combination.");
            }

            // ensure account is not locked out
            if (_userPrincipal.IsAccountLockedOut())
            {
                return new AuthenticationResult("There was an issue authenticating you.");
            }

            // ensure account is enabled
            if (_userPrincipal.Enabled.HasValue && _userPrincipal.Enabled.Value == false)
            {
                return new AuthenticationResult("There was an issue authenticating you.");
            }

            MyContext dbcontext = new MyContext();
            var appUser = dbcontext.AppUsers.Where(a => a.ActiveDirectoryLogin.ToLower() == "domain\\" +_userPrincipal.SamAccountName.ToLower()).FirstOrDefault();
            if (appUser == null)
            {
                return new AuthenticationResult("Sorry, you have not been granted user access to the MED application.");
            }

            // pass both adprincipal and appuser model to build claims identity
            _identity = CreateIdentity(_userPrincipal, appUser);
            _authenticationManager.SignOut(MyAuthentication.ApplicationCookie);
            _authenticationManager.SignIn(new AuthenticationProperties() { IsPersistent = false }, _identity);


            return new AuthenticationResult();
        }

        /// <summary>
        /// Creates identity and packages into cookie
        /// </summary>
        /// <param name="userPrincipal"></param>
        /// <returns></returns>
        private ClaimsIdentity CreateIdentity(UserPrincipal userPrincipal, AppUser appUser)
        {

            var identity = new ClaimsIdentity(MyAuthentication.ApplicationCookie, ClaimsIdentity.DefaultNameClaimType, ClaimsIdentity.DefaultRoleClaimType);
            identity.AddClaim(new Claim("http://schemas.microsoft.com/accesscontrolservice/2010/07/claims/identityprovider", "Active Directory"));
            identity.AddClaim(new Claim(ClaimTypes.GivenName, userPrincipal.GivenName));
            identity.AddClaim(new Claim(ClaimTypes.Surname, userPrincipal.Surname));
            identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, userPrincipal.SamAccountName));
            identity.AddClaim(new Claim(ClaimTypes.Name, userPrincipal.SamAccountName));
            identity.AddClaim(new Claim(ClaimTypes.Upn, userPrincipal.UserPrincipalName));


            if (!String.IsNullOrEmpty(userPrincipal.EmailAddress))
            {
                identity.AddClaim(new Claim(ClaimTypes.Email, userPrincipal.EmailAddress));
            }

            // db claims
            if (appUser.DefaultAppOfficeId != null)
            {
                identity.AddClaim(new Claim("DefaultOffice", appUser.AppOffice.OfficeName));
            }

            if (appUser.CurrentAppOfficeId != null)
            {
                identity.AddClaim(new Claim("Office", appUser.AppOffice1.OfficeName));
            }

            var claims = new List<Claim>();
            DirectoryEntry dirEntry = (DirectoryEntry)userPrincipal.GetUnderlyingObject();

            foreach (string groupDn in dirEntry.Properties["memberOf"])
            {
                string[] parts = groupDn.Replace("CN=", "").Split(',');
                claims.Add(new Claim(ClaimTypes.Role, parts[0]));
            }

            if (claims.Count > 0)
            {
                identity.AddClaims(claims);
            }


            return identity;
        }

        /// <summary>
        /// Authentication result class
        /// </summary>
        public class AuthenticationResult
        {
            public AuthenticationResult(string errorMessage = null)
            {
                ErrorMessage = errorMessage;
            }

            public String ErrorMessage { get; private set; }
            public Boolean IsSuccess => String.IsNullOrEmpty(ErrorMessage);
        }
    }
}

Эта часть работает нормально. Тем не менее, я должен иметь возможность выдавать себя за ClaimsIdentity при совершении вызовов в базу данных, потому что база данных имеет настройку безопасности на уровне ролей. Мне нужно, чтобы соединение было установлено в контексте ClaimsIdentity до конца сеанса этого пользователя.

  • Я установил SPN для Kerberos и знаю, что он работает. Это приложение было ранее аутентификация Windows с делегированием Kerberos, и она работала правильно.
  • Пул приложений работает под учетной записью службы, используемой в имени участника-службы, у которого есть разрешения на делегирование.
  • Объект Identity, который я создал, в основном используется только в контексте приложения. Под этим я подразумеваю, что я получаю все необходимые свойства в основном из Active Directory, но из базы данных будет создано два. Этот идентификатор не отображается непосредственно в таблицу SQL или любой другой источник данных.

Может ли кто-нибудь помочь мне привести пример, где я могу олицетворять объект ClaimsIdentity при выполнении запросов базы данных к базе данных SQL Server?

Ответы [ 3 ]

0 голосов
/ 12 июля 2018

Я полагаю, вам не хватает точки конфигурации в IIS. Вам нужно разрешить IIS передать вам этот пользовательский контекст, это не значение по умолчанию.

Взгляните на этот документ , прежде чем пытаться "исправить" свой код. Если это не поможет, сообщите нам и сообщите нам свои настройки, сам по себе код может не сработать.

0 голосов
/ 12 июля 2018

[решено обновление 2-1-19] Я написал пост в блоге, подробно описывающий этот процесс, и он доступен здесь.

Мне удалось сделать это, выполнив следующее. Я создал класс, чтобы сделать эти методы многоразовыми. В этом классе я использовал библиотеку System.IdentityModel.Selectors и System.IdentityModel.Tokens для генерации KeberosReceiverSecurityToken и сохранил ее в памяти.

public class KerberosTokenCacher
{
    public KerberosTokenCacher()
    {

    }

    public KerberosReceiverSecurityToken WriteToCache(string contextUsername, string contextPassword)
    {
        KerberosSecurityTokenProvider provider =
                        new KerberosSecurityTokenProvider("YOURSPN",
                        TokenImpersonationLevel.Impersonation,
                        new NetworkCredential(contextUsername.ToLower(), contextPassword, "yourdomain"));

        KerberosRequestorSecurityToken requestorToken = provider.GetToken(TimeSpan.FromMinutes(double.Parse(ConfigurationManager.AppSettings["KerberosTokenExpiration"]))) as KerberosRequestorSecurityToken;
        KerberosReceiverSecurityToken receiverToken = new KerberosReceiverSecurityToken(requestorToken.GetRequest());

        IAppCache appCache = new CachingService();
        KerberosReceiverSecurityToken tokenFactory() => receiverToken;

        return appCache.GetOrAdd(contextUsername.ToLower(), tokenFactory); // this will either add the token or get the token if it exists

    }

    public KerberosReceiverSecurityToken ReadFromCache(string contextUsername)
    {
        IAppCache appCache = new CachingService();
        KerberosReceiverSecurityToken token = appCache.Get<KerberosReceiverSecurityToken>(contextUsername.ToLower());

        return token;
    }

    public void DeleteFromCache(string contextUsername)
    {
        IAppCache appCache = new CachingService();
        KerberosReceiverSecurityToken token = appCache.Get<KerberosReceiverSecurityToken>(contextUsername.ToLower());

        if(token != null)
        {
            appCache.Remove(contextUsername.ToLower());
        }
    }

}

Теперь, когда пользователи входят в систему, используя мой AuthenticationService, я создаю тикет и сохраняю его в памяти. Когда они выходят из системы, я делаю наоборот и удаляю билет из кеша. В заключительной части (которую я все еще ищу для лучшего способа сделать это) я добавил некоторый код в конструктор моего класса dbcontext.

public MyContext(bool impersonate = true): base("name=MyContext")
{
    if (impersonate)
    {
        var currentUsername = HttpContext.Current.GetOwinContext().Authentication.User?.Identity?.Name;

        if (!string.IsNullOrEmpty(currentUsername)){

            KerberosTokenCacher kerberosTokenCacher = new KerberosTokenCacher();
            KerberosReceiverSecurityToken token = kerberosTokenCacher.ReadFromCache(currentUsername);

            if (token != null)
            {
                token.WindowsIdentity.Impersonate();
            }
            else
            {
                // token has expired or cache has expired so you must log in again
                HttpContext.Current.Response.Redirect("Login/Logoff");
            }

        }
    }
}

Очевидно, что он определенно не идеален, но он позволяет мне использовать Autin Cookie Authentication для активного каталога и генерировать билет Kerberos, позволяющий подключаться к базе данных SQL в контексте аутентифицированного пользователя.

0 голосов
/ 09 июля 2018

Может быть, я неправильно понимаю вопрос, но:

Для подключения к серверу SQL с использованием аутентификации Windows строка подключения должна использовать «Интегрированную безопасность», что означает, что она будет использовать текущий контекст безопасности, который устанавливает соединение. Обычно это будет ваш пользователь AppPool, который в вашем случае является учетной записью службы. Насколько я знаю, вы не можете автоматически передавать свое олицетворение в поток AppPool, используя Kerberos auth . Вот цитата, которую я нашел:

В IIS только базовая аутентификация регистрирует пользователей с токеном безопасности. который течет по сети на удаленный сервер SQL. По умолчанию, другие режимы безопасности IIS, используемые вместе с удостоверением Настройки элемента конфигурации не приведут к токену, который может аутентифицироваться на удаленном сервере SQL.

Так что, если вы хотите выдать себя за других пользователей, вам нужно будет создать новый поток под принципалом пользователя, которого вы олицетворяете. Таким образом, соединение Integrated Security будет использовать аутентификацию Windows этого пользователя для подключения к SQL Server.

Я не уверен, как именно это сделать, но вот что-то, что может подтолкнуть вас в правильном направлении:

public void NewThreadToRunSQLQueries(object claimsIdentity) {
    if (claimsIdentity as ClaimsIdentity == null) {
        throw new ArgumentNullException("claimsIdentity");
    }

    ClaimsIdentity claimsIdentity = (ClaimsIdentity)claimsIdentity;
    var claimsIdentitylst = new ClaimsIdentityCollection(new List<IClaimsIdentity> { claimsIdentity });
    IClaimsPrincipal claimsPrincipal = new ClaimsPrincipal(claimsIdentitylst);
    Thread.CurrentPrincipal = claimsPrincipal; //Set current thread principal

    using(SqlConnection connection = new SqlConnection("Server=myServerAddress;Database=myDataBase;Integrated Security=True;")) 
    {
        connection.Open(); //Open connection under impersonated user account
        //Run SQL Queries
    }
}

Thread thread = new Thread(NewThreadToRunSQLQueries);
thread.Start(_identity);

Edit:

Что касается вашего комментария о том, как сделать эту структуру "глобальной", при условии, что у вас есть доступ к HttpContext в вашем обработчике аутентификации, вы можете сделать это:

var principal = new ClaimsPrincipal(_identity);

Thread.CurrentPrincipal = principal;
if (HttpContext.Current != null)
{
     HttpContext.Current.User = principal;
}

Таким образом, теоретически рабочий поток из IIS теперь должен выполняться под аутентифицированным пользователем (олицетворение). И надежные подключения к SQL Server должны быть возможны. Я говорю теоретически, потому что сам не пробовал. Но в худшем случае вы можете получить заявку от HttpContext, чтобы запустить отдельный поток, как в моем примере выше. Но если это работает само по себе, вам даже не придется начинать новый поток, как я уже упоминал.

...