簡體   English   中英

OWIN Cookie身份驗證 - 使用Kerberos委派模擬SQL Server

[英]OWIN Cookie Authentication - Impersonation to SQL Server with Kerberos Delegation

在對Identity 2.0,模擬,委派和Kerberos進行了數周的研究之后,我仍然無法找到允許我冒充我在MVC應用程序中使用OWIN創建的ClaimsIdentity用戶的解決方案。 我的方案的細節如下。

Windows身份驗證已禁用+已啟用匿名。
我正在使用OWIN啟動類來手動驗證用戶對我們的Active Directory。 然后我將一些屬性打包到一個cookie中,該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的上下文中為該用戶會話的剩余部分完成連接。

  • 我已經為Kerberos設置了SPN,我知道它有效。 這個應用程序以前是使用Kerberos委派的Windows身份驗證,它工作正常。
  • 應用程序池在SPN中使用的服務帳戶下運行,該服務帳戶具有委派權限。
  • 我創建的Identity對象幾乎只在應用程序上下文中使用。 我的意思是我從大多數Active目錄中獲取所有必需的屬性,但是將有兩個將從數據庫中創建。 此標識不直接映射到sql表或任何其他數據源。

有人可以幫我指出一個例子,我可以在對SQL Server數據庫進行數據庫查詢時冒充ClaimsIdentity對象嗎?

也許我誤解了這個問題,但是:

對於使用Windows身份驗證進行的SQL服務器連接,連接字符串必須使用“集成安全性”,這意味着它將使用正在建立連接的當前安全上下文。 通常情況下,這將是您的AppPool用戶,在您的情況下是服務帳戶。 據我所知, 您無法使用Kerberos身份驗證自動將您的模擬傳播到AppPool線程 這是我發現的引用:

在IIS中,只有基本身份驗證使用通過網絡流向遠程SQL服務器的安全令牌來登錄用戶。 默認情況下,與身份配置元素設置一起使用的其他IIS安全模式不會生成可以對遠程SQL Server進行身份驗證的令牌。

因此,如果您想冒充其他用戶,則必須在您模擬的用戶的主體下啟動新的線程。 這樣,集成安全連接將使用該用戶的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);

編輯:

關於如何使這個結構“全局”的評論,假設您可以訪問身份驗證處理程序中的HttpContext ,您可以這樣做:

var principal = new ClaimsPrincipal(_identity);

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

因此理論上,IIS中的工作線程現在應該在經過身份驗證的用戶(模擬)下運行。 應該可以實現與SQL Server的可信連接。 我在理論上說,因為我自己沒有嘗試過。 但最糟糕的情況是你可以從HttpContext中獲取聲明來啟動一個單獨的線程,就像我上面的例子中一樣。 但如果這本身就有效,你甚至不必像我最初提到的那樣開始一個新線程。

[求助更新2-1-19]我寫了一篇博文,詳細介紹了這個過程,可以在這里找到。

通過執行以下操作,我能夠實現此目的。 我創建了一個類來使這些方法可重用。 在該類中,我使用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數據庫在經過身份驗證的用戶的上下文中。

我猜你錯過了IIS中的配置點,你需要允許IIS將該用戶上下文傳遞給你,這不是默認設置。

在嘗試“修復”代碼之前,請先查看此文檔 如果這沒有幫助讓我們知道並告訴我們您的設置,單獨的代碼可能無法解決問題。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM