Finding the Private Key File of Certificates

At work, I have created multiple tools which we used to analyse and fix issues related to certificates that we use with Office Communications Server and their respective private key files. To summarize, a user/service who wants to use the certificate for authentication needs to have read access on the private key. If it doesn’t, you’ll typically see a strange error which many people don’t relate to missing ACLs on the private key file.

Now these days, you don’t need to write such tools anymore. PowerShell allows you to pretty much do everything you need in this area. Let’s look at the following PS script (let’s call it FindPrivateKey.ps1) which accepts a parameter of type System.Security.Cryptography.X509Certificates.X509Certificate2, i.e. a reference to the certificate you want to analyze.

param(
    [Parameter(Mandatory=$true)]
    [System.Security.Cryptography.X509Certificates.X509Certificate2]
    $Certificate
)

echo "Looking for private key file of certificate"
echo $Certificate
echo ""
echo "The private key file is '$($Certificate.PrivateKey.CspKeyContainerInfo.UniqueKeyContainerName)'"
echo ""

$file = ls $env:userprofile -Filter $Certificate.PrivateKey.CspKeyContainerInfo.UniqueKeyContainerName -Recurse -Force -EA SilentlyContinue
echo "It is located at '$($file.FullName)'."

I guess I do not need to mention that you can easily find the certificate you’re interested in by running something like

pushd cert:\CurrentUser\My
ls
$cert = gci 0DAC31905AEB722D8561BFAF3F3BFD2F551AA197
popd
.\FindPrivateKey.ps1 $cert

where ‘0DAC31905AEB722D8561BFAF3F3BFD2F551AA197’ is simply the thumbprint of the certificate we’re interested in. From here on it should be easy to check what ACLs the file has (run $file.GetAccessControl()) and to modify them (run $file.SetAccessControl()).

Windows Authentication through Forms

Let’s assume you have a web site which is exposed to the internet (i.e. to the public) but the site itself works with windows accounts internally. If you have problems visualizing this scenario, think about a web mail interface for a mail server system which offers mail services for user accounts in active directory. Users inside your corporate network can use integrated Windows authentication to access this site, so they don’t really have a problem. But when they want to access the interface from outside the corporate network, or from a machine/device which doesn’t understand integrated Windows authentication, you’ll realize that it doesn’t just work like that.

So you’ll want to do authentication based on the credentials the user enters in a simple form on a web page. The challenge now is that the rest of the site (i.e. apart from the one form which does the initial authentication) will likely still work with the HttpContext.Current.User property to determine which user is currently authenticated and provide actions and data based on that identity, because you don’t want to re-implement the entire logic. The good news is, you can do that! The bad news is, you don’t just get it for free. But that’s why you came here, and that’s why I’ll try to help you with this.

Let’s first arrange a few things on the web site facing the public — you don’t necessarily need to change this on your internal site. First, the login page will need to be accessible for anyone, i.e. anonymous access must be turned on on the site. Second, from ASP.net’s point of view, all the pages which require the user to be authenticated should be in/under the same directory. If you don’t want to do this, you’ll simply have to add a element for all pages requiring authentication in your web.config or do the opposite and add all pages which don’t require authentication in the same way. With the directories however, the basic structure can be as simple as

WebSiteRoot/
+--Default.aspx        Could redirect to /ActualSite/Default.aspx
+--ActualSite/
   +--Default.aspx     The actual home page with all its logic
+--Auth/
   +--Login.aspx       The login page

So all the logic you had in the site’s root directly would be under ActualSite or whatever name you chose.

Updating Web.config

Now for the above scenario with different directories, the web.config in the site’s root could look as follows. Please note that for the pages under ActualSite we are simply disabling anonymous access, while for everything else, we allow it. Also, in the authentication element we’re setting the mode to None because we’re not going to use any predefined authentication mechanism as is.

<configuration>
    <location path="ActualSite">
        <system.web>
            <authorization>
                <deny users="?"/>
            </authorization>
        </system.web>
    </location>

    <system.web>
        <authentication mode="None">
            <forms loginUrl="~/Auth/Login.aspx" defaultUrl="~/ActualSite/Default.aspx" />
        </authentication>

        <authorization>
            <allow users="*" />
        </authorization>
    </system.web>
</configuration>

But of course that’s not all yet. You’ll see that if now you wanted to look at the actual site, you’ll get a 401 because you’re not authenticated. So let’s take a look at that.

Validating credentials

Next, let’s create the login page which the user will use to enter his credentials. Fortunately, ASP.net offers the Login control which does almost everything we need. So add that one to your login page plus add an event handler for the OnAuthenticate event of that control. Alternatively, you can also derive your own control from the Login control and override the OnAuthenticate method. This event handler is where we’ll do our custom logic to check that the credentials really map to an existing Windows user. Below’s the code which does that.

protected override void OnAuthenticate(AuthenticateEventArgs args)
{
    string[] parts = UserName.Split('\\');
    string password = Password;
    string username;
    string domain = null;

    args.Authenticated = false;

    if (parts.Length == 1)
    {
        username = parts[0];
    }
    else if (parts.Length == 2)
    {
        domain = parts[0];
        username = parts[1];
    }
    else
    {
        return;
    }

    if (WebAuthenticationModule.AuthenticateUser(username, domain, password))
    {
        string userData = String.Format("{0}\n{1}\n{2}",
            username, domain, password);

        FormsAuthenticationTicket ticket = new FormsAuthenticationTicket(
            2                           /* version */,
            username,
            DateTime.Now                /* issueDate */,
            DateTime.Now.AddMinutes(30) /* expiration */,
            true                        /* isPersistent */,
            userData,
            FormsAuthentication.FormsCookiePath);

        HttpCookie ticketCookie = new HttpCookie(
            FormsAuthentication.FormsCookieName,
            FormsAuthentication.Encrypt(ticket));

        Context.Response.Cookies.Add(ticketCookie);

        Context.Response.Redirect(
            FormsAuthentication.GetRedirectUrl(username, false), false);
    }
}

The AuthenticateUser method from WebAuthenticationModule is a wrapper on the LogonUser function from the Win32 API which will return true if the user could be logged on. So if the credentials are valid, we’re going to pass them in to the FormsAuthenticationTicket in the UserData property so later on, we’ll be able to use them again. At least, we don’t want the consumers of the site have to enter credentials for every request their making, right? Also, we’re encrypting the entire ticket because we’re going to send it over the wire. The Encrypt method from the FormsAuthentication class does this. However, you’ll have to make sure that the protection attribute of the forms element in the web.config is set to All which actually is the default (but it can be inherited, so watch out!).

What you see is that we’re heavily using the functionality offered by the FormsAuthentication class and related classes to handle the tickets, encryption, settings, etc. This is not mandatory but it helps a lot. Plus it’s better anyway than coming with your own ticketing and encryption mechanisms; unless you have a degree in maths and/or cryptography, chances are that that’s not so secure as you think it is.

Authenticating users

Then, we need to authenticate the user for all the requests he makes after providing the credentials. Thus, we need to add some custom logic to the AuthenticateRequest event of the HttpApplication. There’s multiple ways to do that:

  • Add a file called global.asax to your site’s root
  • Create and register a new HttpModule by implementing the System.Web.IHttpModule interface and adding an entry in the httpModules section in your root’s web.config

Personally, I like the approach with the custom module more, but adding this stuff to the global.asax can be done a little bit faster. In either case, make sure you can handle the AuthenticateRequest event. My code proposal for the handler is given below. I’m intentionally omitting most error handling code here.

private static void OnAuthenticateRequest(object sender, EventArgs args)
{
    HttpApplication application = sender as HttpApplication;

    HttpContext context = application.Context;
    HttpRequest request = context.Request;
    HttpResponse response = context.Response;

    if (!request.IsAuthenticated &amp;&amp;
        !context.SkipAuthorization)
    {
        if (request.CurrentExecutionFilePath.Equals(FormsAuthentication.LoginUrl,
                                                    StringComparison.OrdinalIgnoreCase) ||
            request.CurrentExecutionFilePath.EndsWith(".axd"))
        {
            context.SkipAuthorization = true;
        }
        else
        {
            HttpCookie cookie = request.Cookies.Get(FormsAuthentication.FormsCookieName);
            FormsAuthenticationTicket ticket = FormsAuthentication.Decrypt(cookie.Value);

            if (!ticket.Expired)
            {
                IntPtr hToken = LogonUserFromTicket(ticket);

                WindowsIdentity identity = new WindowsIdentity(
                    hToken, "Win/Forms", WindowsAccountType.Normal, true);

                context.User = new WindowsPrincipal(identity);

                if (FormsAuthentication.SlidingExpiration)
                {
                    ticket = FormsAuthentication.RenewTicketIfOld(ticket);
                    cookie.Value = FormsAuthentication.Encrypt(ticket);
                    response.Cookies.Set(cookie);
                }

                return;
            }

            FormsAuthentication.RedirectToLoginPage();
        }
    }
}

So let’s go through the important things here. First we check (lines 9/10) if the request is already authenticated or authorization is to be skipped completely. If either of those is true, we’re out of the picture already. Else it’s our job to do the authentication. Lines 12-17 are used to prevent authentication on the login page as well as the *.axd handlers, which are typically used to return resources like scripts for ASP.net components. Lines 20-42 do the actual authentication: we first retrieve the cookie from the previous credential validation and decrypt it to get the ticket. If the ticket has not expired, we log the user on (the call to LogonUserFromTicket is again only a wrapper for the LogonUser function from the Win32 API; it uses the data from the UserData property of the ticket) to get the logon token which we’ll pass in to the WindowsIdentity constructor to get the WindowsIdentity object all the code in ActualSite will use to determine which user is making the request. Then of course we need to update the context with the new identity. If sliding expiration is turned on in the web.config, we renew the ticket. And if the ticket has expired, of course we don’t authenticate the user but instead we redirect him to the login page.

Cleanup

Finally, we still have a tiny problem here. We have used the LogonUser function, but according to its documentation, we should call the CloseHandle function from the Win32 API. For the method AuthenticateUser I have already done that; there we don’t need the token/handle anymore when we have verified the credentials. But what about the authentication we’re doing in AuthenticateRequest? We’re setting the newly created WindowsIdentity (which is here used to represent the user token) to the current HttpContext because we’ll need it there to ultimately handle the request. But once the request handler is done, we don’t need it anymore. Luckily, there’s also an event for this purpose. It’s the last event that there is and it’s called EndRequest. So let’s add the following code in the handler for that event.

private static void OnEndRequest(object sender, EventArgs args)
{
    HttpApplication application = sender as HttpApplication;

    HttpContext context = application.Context;

    if (null != context.User)
    {
        WindowsIdentity identity = context.User.Identity as WindowsIdentity;

        if (null != identity)
        {
            NativeAuth.CloseHandle(identity.Token);
        }
    }
}

Basically all it does, if the request was given a WindowsIdentity, it’ll call the CloseHandle function from the Win32 API on that identity’s token handle. That should do the trick and we shouldn’t leak handles anymore.

Summary

I have shown here how you can make use of the forms authentication mechanisms which come with ASP.net to do Windows authentication behind the scenes. This can be very useful in cases when not all users have access to systems which know how to do Windows authentication.

Please note that I do not claim that this solution is going to work for every challenge you may be facing. The solution shown here is neither claimed to be complete nor suitable for every web application.

Troubleshooting Authentication Issues with RGS Agent Tab

Sometimes – especially in lab environments – you’ll see issues around user authentication with the RGS Agent Tab of Office Communications Server 2007 R2. This post should help you in determining what could be the issue and how to work around it.

First of all, when the OCS 2007 R2 WebComponents get installed on a machine, by default Integrated Windows Authenticated (IWA) for the RGS parts of WebComponents are enabled. We don’t require IWA, but this is the recommended setting; anything but Anonymous Authentication should work. If Anonymous Authentication is set for the RGS virtual directory in IIS, you’ll find a warning in NT event log about that. In that case, you should turn back on authentication for the virtual directory.

Another problem I’ve seen a couple of times was as follows: Agent A’s credentials are used to sign in with Office Communicator, but the Agent Tab in OC shows the RGS Agent Group memberships of Agent B, or it shows that the “Current User is not an Agent”. In this case, you should start tracing the RgsClientsLib component and either wait until the Agent Tab in OC refreshes automatically (this should happen within 30 – 60 seconds) or you can open the tab URL in IE; it’s typically something like https://pool-1.contoso.com/Rgs/Clients/Tab.aspx. Then, stop tracing and check out the captured traces for RgsClientsLib. You should now find something along the lines of

Authentication type: [Negotiate]
Authenticated user: [CONTOSO\AgentB]
Authenticated user's SID: [S-1-5-21-2278291046-1170081271-1450921830-1285]
Authenticated user's SID maps to: [efa2cabd-462c-49e4-a021-4dd71bd97ce4]

Please note that I left out the less important information like timestamps etc. here. What you see is that instead of AgentA, AgentB is being authenticated. Usually, this happens when the credentials you pass in to OC are different from the credentials you used to log in to Windows. OC uses the IE engine to render the tabs and thus also leaves the authentication for IE. Then, IE performs the authentication based on the “User Authentication” / “Logon” settings for the zone the Agent Tab is in. The default setting for the “Local Intranet Zone” in IE is to automatically try loggin on with the current user’s credentials – i.e. AgentB’s credentials in this case, because AgentB is the currently logged on (Windows) user. Only if authentication for this user fails, IE is going to prompt you for a different set of credentials. To change this behavior, you can set the security settings in IE accordingly:

IeSecurityZoneUserAuth

Setting it to “Prompt for user name and password” will always prompt you for sites in the intranet zone. Once you’ve done that, exit OC and start it again. Now you should be prompted for the credentials to the Agent Tab and you can provide AgentA’s credentials. You then should see the correct list of groups AgentA is a member of.

Fun with JSON and WCF, Part II

Following the web app I mentioned in Fun with JSON and WCF (Part I), I ran into another issue with WCF hosted in IIS and serving the callers through JSON objects. My application uses integrated windows authentication to authenticate the users and grant / deny access based on the given credentials. Therefore, I have turned off anonymous access for the entire virtual directory the application is running in and turned on integrated windows authentication. Now when invoking the JSON service, I get the following exception.

[NotSupportedException: Security settings for this service require 'Anonymous' Authentication but it is not enabled for the IIS application that hosts this service.]
   System.ServiceModel.Channels.HttpChannelListener.ApplyHostedContext(VirtualPathExtension virtualPathExtension, Boolean isMetadataListener) +11453217
   System.ServiceModel.Activation.VirtualPathExtension.ApplyHostedContext(TransportChannelListener listener, BindingContext context) +75
   System.ServiceModel.Channels.HttpTransportBindingElement.BuildChannelListener(BindingContext context) +119
   System.ServiceModel.Channels.BindingContext.BuildInnerChannelListener() +66
   System.ServiceModel.Channels.MessageEncodingBindingElement.InternalBuildChannelListener(BindingContext context) +67
   System.ServiceModel.Channels.WebMessageEncodingBindingElement.BuildChannelListener(BindingContext context) +47
   System.ServiceModel.Channels.BindingContext.BuildInnerChannelListener() +66
[...]

This indicates that according to the configuration of the service binding, anonymous access is to be allowed however IIS does not allow it. Apart from the fact that I don’t understand in the first place, why the service would care about this (if it was the other way around, I’d understand), fixing it is simple. It again requires changes in the Web.config, like follows.

<configuration>
    <!-- ... -->
    <system.serviceModel>
        <behaviors>
            <!-- ... -->
        </behaviors>
        <serviceHostingEnvironment aspNetCompatibilityEnabled="true" />
        <services>
            <service behaviorConfiguration="MyServiceTypeBehavior" name="MyService">
                <endpoint address="" behaviorConfiguration="MyServiceAspNetAjaxBehavior"
                          binding="webHttpBinding" bindingConfiguration="ServiceAuth"
                          contract="MyService" />
            </service>
        </services>
        <bindings>
            <webHttpBinding>
                <binding name="ServiceAuth">
                    <security mode="TransportCredentialOnly">
                        <transport clientCredentialType="Windows"/>
                    </security>
                </binding>
            </webHttpBinding>
        </bindings>
    </system.serviceModel>
</configuration>

The bindingConfiguration attribute on line 11 refers to the new webHttpBinding definition from lines 17 to 21. Client authentication is there specified to be integrated windows authentication.