Search code examples
c#asp.net-mvcowinkerberoswif

OWIN Cookie Authentication - Impersonation to SQL Server with Kerberos Delegation


After weeks of research on Identity 2.0, impersonation, delegation and Kerberos, I am still unable to find a solution that will allow me to impersonate the ClaimsIdentity user I have created using OWIN in my MVC application. The specifics to my scenario are as follows.

Windows Authentication is disabled + Anonymous enabled.
I am using an OWIN startup class to manually authenticate the user against our Active Directory. Then I pack up some of the properties into a cookie which is available throughout the rest of the application. This is the link I referenced when setting up these classes.

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);
        }
    }
}

That part seems to work just fine. However, I am required to be able to impersonate the ClaimsIdentity when making calls to the database because the database has role level security setup on it. I need the connection to be done under the context of the ClaimsIdentity for the remainder of that user's session.

  • I have set up the SPN for Kerberos and I know it works. This app was previously windows auth with the Kerberos delegation and it worked properly.
  • The app pool is running under the service account used in the SPN which does have delegation permissions.
  • The Identity object I created is pretty much only being used within the application context. What I mean by that is I am getting all necessary properties from mostly Active directory, but there will be two that will be created from the database. This identity is not mapped directly to an sql table or any other data source.

Can someone help point me towards an example where I am able to impersonate the ClaimsIdentity object when making the database queries to the SQL Server database?


Solution

  • [SOLVED Update 2-1-19] I have written a blog post detailing this process and it is available here.

    I was able to accomplish this by doing the following. I created a class to make these methods reusable. In that class i used the System.IdentityModel.Selectors and System.IdentityModel.Tokens library to generate a KeberosReceiverSecurityToken and stored it in memory.

    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());
            }
        }
    
    }
    

    Now when users login using my AuthenticationService, I create the ticket and store it in memory. When they log out I do the reverse and remove the ticket from cache. The final part (which i am still looking for a better way to accomplish this), I added some code to the constructor of my dbcontext class.

    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");
                }
    
            }
        }
    }
    

    Obviously its definitely not perfect, but it allows me to use Owin Cookie Authentication against active directory and have a Kerberos ticket generated allowing the connection to the SQL database to be under the context of the user who was authenticated.