[英]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的上下文中為該用戶會話的剩余部分完成連接。
有人可以幫我指出一個例子,我可以在對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.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");
}
}
}
}
顯然它絕對不是完美的,但它允許我對活動目錄使用Owin Cookie身份驗證並生成Kerberos票證,允許連接到SQL數據庫在經過身份驗證的用戶的上下文中。
我猜你錯過了IIS中的配置點,你需要允許IIS將該用戶上下文傳遞給你,這不是默認設置。
在嘗試“修復”代碼之前,請先查看此文檔 。 如果這沒有幫助讓我們知道並告訴我們您的設置,單獨的代碼可能無法解決問題。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.