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

Posted

技术标签:

【中文标题】OWIN Cookie 身份验证 - 使用 Kerberos 委派模拟 SQL Server【英文标题】:OWIN Cookie Authentication - Impersonation to SQL Server with Kerberos Delegation 【发布时间】:2018-06-29 12:48:31 【问题描述】:

在对 Identity 2.0、模拟、委派和 Kerberos 进行了数周的研究之后,我仍然无法找到一种解决方案来模拟我在 MVC 应用程序中使用 OWIN 创建的 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 对象几乎只在应用程序上下文中使用。我的意思是我主要从 Active Directory 中获取所有必要的属性,但是将从数据库中创建两个。此标识不直接映射到 sql 表或任何其他数据源。

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

【问题讨论】:

【参考方案1】:

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

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

在 IIS 中,只有基本身份验证使用安全令牌登录用户 通过网络流向远程 SQL 服务器。默认情况下, 与身份结合使用的其他 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 中获取声明以启动一个单独的线程,就像我上面的示例一样。但如果这本身有效,您甚至不必像我最初提到的那样启动一个新线程。

【讨论】:

所以我没有使用 Windows 身份验证。我正在使用 OWIN 和身份验证服务手动检查 AD 以验证凭据。从那里我尝试使用 Kerberostokenprovider 或其他库之类的东西来帮助我模拟我从该身份验证服务构建的声明身份。您提到的链接我遇到过,但我不确定这是否适用于我尝试这样做的方式。但是可以说 thread.start(_identity) 用于模拟,你能指出一种方法来包装我的数据上下文以全局处理吗? 我不知道您在 ASP.NET 管道中注入自定义身份验证的确切位置,但通常您可以从处理程序访问 HttpContext。因此,您可以创建一个新的ClaimPrincipal 对象并将其分配给上下文,如下所示:HttpContext.Current.User = principal;。这就是您还可以在控制器中使用其他东西,例如 [Authorize] 属性,因为整个 ASP.NET 应用程序现在将知道经过身份验证的用户。我将在上面的答案中添加一个示例。【参考方案2】:

[已解决更新 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 数据库。

【讨论】:

【参考方案3】:

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

在您尝试“修复”您的代码之前,请查看this document。如果这无助于让我们知道并告诉我们您的设置,那么仅靠代码可能无法解决问题。

【讨论】:

或许这个链接也有帮助,blogs.msdn.microsoft.com/surajdixit/2018/02/07/… 伙计们,在问题中他明确表示他已关闭 Windows 身份验证并且无法打开它。但是你们都在分享文章,在第一步中它的字面意思是“打开 Windows 身份验证” 缺乏自托管,没有办法绕过windows server的工作方式。您需要将用户令牌传递给运行托管应用程序的进程。在请求被匿名化后,您需要找到一个 0day 漏洞来获取用户并在该用户的代码中执行。有很多黑客喜欢在“用户”上下文中的服务器上执行。

以上是关于OWIN Cookie 身份验证 - 使用 Kerberos 委派模拟 SQL Server的主要内容,如果未能解决你的问题,请参考以下文章

在 OWIN 托管的 SignalR 实现中接受 ASP.NET 表单身份验证 cookie?

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

IsPersistent 在 OWIN Cookie 身份验证中的工作原理

在与一个 IIS 应用程序连接的两个域之间共享 OWIN 身份验证 Cookie

是否可以配置 OWIN Cookie 身份验证以防止某些 URL 影响滑动过期?

OWIN - Authentication.SignOut() 似乎没有删除 cookie