OWIN Cookie身份验证-使用Kerberos委派模拟到SQL Server

时间:2018-06-29 12:48:31

标签: c# asp.net-mvc owin kerberos wif

在对Identity 2.0,模拟,委派和Kerberos进行了数周的研究之后,我仍然找不到能够让我模拟使用OWIN在MVC应用程序中创建的ClaimsIdentity用户的解决方案。我的情况的具体情况如下。

Windows身份验证已禁用+启用了匿名。
我正在使用OWIN启动类来根据我们的Active Directory手动验证用户身份。然后,我将一些属性打包到cookie中,该cookie在整个应用程序的其余部分中都可用。 This是我在设置这些类时引用的链接。

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的上下文下进行连接。

  • 我已经为Kerberos设置了SPN,并且我知道它可以工作。这个程序是 以前使用Kerberos委派进行Windows身份验证,并且可以正常工作。
  • 应用程序池在具有委派权限的SPN中使用的服务帐户下运行。
  • 我创建的Identity对象几乎只在应用程序上下文中使用。我的意思是,我将从活动目录中获取所有必要的属性,但是将从数据库中创建两个属性。此身份不会直接映射到sql表或任何其他数据源。

有人可以帮我指出一个示例,在对SQL Server数据库进行数据库查询时,我可以模拟ClaimsIdentity对象吗?

3 个答案:

答案 0 :(得分:4)

也许我误解了这个问题,但是:

要使用Windows身份验证建立SQL Server连接,连接字符串必须使用“集成安全性”,这意味着它将使用进行连接的当前安全性上下文。通常,这将是您的AppPool用户,在您的情况下是服务帐户。据我所知,you can't propagate your impersonation to the AppPool thread automatically using Kerberos auth。这是我发现的报价:

  

在IIS中,仅基本身份验证使用安全令牌登录用户   通过网络流到远程SQL Server。默认情况下,   与标识一起使用的其他IIS安全模式   配置元素设置将不会产生可以   对远程SQL Server进行身份验证。

因此,如果要模拟其他用户,则必须在要模拟的用户主体下启动一个新线程。这样,集成安全连接将使用该用户的Windows Auth连接到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);

编辑:

关于您如何使此结构“全局”化的评论,假设您可以在身份验证处理程序中访问 HttpContext ,则可以执行以下操作:

var principal = new ClaimsPrincipal(_identity);

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

因此,从理论上讲,IIS的辅助线程现在应在经过身份验证的用户(模拟)下运行。到SQL Server的信任连接应该是可能的。我说理论上是因为我自己还没有尝试过。但是最坏的情况是,您可以从HttpContext中获取声明,以启动一个单独的线程,如上面的示例所示。但是,如果这本身起作用,您甚至不必像我最初提到的那样启动新线程。

答案 1 :(得分:1)

[已解决的更新2-1-19] ,我写了一篇博客文章,详细介绍了此过程,here.

可用

我能够通过执行以下操作来完成此任务。我创建了一个类来使这些方法可重用。在该类中,我使用了System.IdentityModel.SelectorsSystem.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");
            }

        }
    }
}

显然,它绝对不是完美的,但是它允许我对活动目录使用Owin Cookie身份验证,并生成Kerberos票证,从而允许在经过身份验证的用户的上下文下连接到SQL数据库。

答案 2 :(得分:-1)

我想您缺少IIS中的配置点,您需要允许IIS将该用户上下文传递给您,这不是默认设置。

在尝试“修复”代码之前,请先查看this document。如果这样做没有帮助,请告诉我们您的设置,仅靠代码可能无法解决问题。