Monday, April 29, 2013

Project Server 2013 PSI from the web context. Unauthorized issue

There are number of changes in SharePoint 2013 and Project Server 2013. One of them is that the Claims Authentication becomes default authentication mechanism now.

Project Server 2013 is claims aware, so all out of the box functionality works fine with claims authentication. But things get worse when you try to use Project Server ClientOM or PSI services from the web context where the claims authentication used. Result - "Unauthorized" message.

This post is about to show some workarounds to cope with the issue.

The scenario

PSI services are ok to be called from console app, but fail when they are called from the web part in the web application with claims authentication set up. Current user Windows identity is known in the first scenario, which is ok. But in the second scenario current identity is not the identity of logged on user ("iusr" by default). Below are 4 approaches to make PSI working from the web context.

Approach #1. Use Windows classic authentication

Pros: The easiest way; no code changes required.

Cons: Not recommended way. Microsoft highly recommends using claims authentication for all new solutions. The use of classic mode authentication, also known as Windows classic authentication, is discouraged in SharePoint 2013 and you can only create or configure web applications for classic mode authentication with Windows PowerShell cmdlets. (See Create web applications that use classic mode authentication in SharePoint 2013).

Approach #2. Use Application Pool account

This approach is applicable when your web application uses the domain account as the application pool account. In this case you can use "Service account" pattern. For example you can make PSI calls using the application pool account. The code:

SPSecurity.RunWithElevatedPrivileges(delegate()
{
    //PSI call
});

This code impersonates current thread under the application pool identity, so any call within the supplied delegate will be done under the application pool account. Do not forget to grant appropriate permissions to pool account (For example, you can add it to Project Server administrators group). This trick will not work as expected if NETWORK SERVICE account is used as an application pool account. Also, external system will not have access to identity of the original user of web page. It will think that application pool made a call.

Pros: It works :)

Cons: Code changes required; service won't be aware of the actual caller - Project Server impersonation will have to be used; application pool needs to be configured with domain account.

Approach #3. Use UpnLogon method of "Claims to Windows Token Service"

The most complex one. It requires you to manually call the UpnLogon method of “Claims to Windows Token Service”. First, you need to get your current user claims identity. It can be done as follows:

if (Thread.CurrentPrincipal.Identity is ClaimsIdentity)
{
    var identity = (ClaimsIdentity)Thread.CurrentPrincipal.Identity;
}

Than you need to extract UPN-Claim from the identity. It indicates a Kerberos-style user principal name (UPN), for example, user@realm. Only one claim can be of the UPN type. Additional UPNs can be configured as a custom claim types. the code:

string upn = null;
foreach (Claim claim in identity.Claims)
{
   if (StringComparer.Ordinal.Equals(System.IdentityModel.Claims.ClaimTypes.Upn, claim.ClaimType))
   {
       upn = claim.Value;
   }
}

After you got the UPN-Claim, you can get the windows user identity from this claim with help of “Claims to Windows Token Service”. By default, this service is disabled. You need to enable it in SharePoint central administration (See Claims-based identity and concepts in SharePoint 2013). Steps:

1. Go to Central Administration -> System Settings -> Manger services on server and ensure that “Claims to Windows Token Service” state is “Started”.

2. Edit “C:\Program Files\Windows Identity Foundation\v3.5\c2wtshost.exe” and ensure that your application pool account is in the list of allowed callers. The best way to do this is to add your application pool account to WSS_WPG security group. Your “allowedCallers” section would look like this:

<allowedCallers>
    <clear />
    <add value="WSS_WPG" />
</allowedCallers>

More info can be found at Claims to Windows Token Service (C2WTS).

Now you can do the call to “Claims to Windows Token Service”. The code:

WindowsIdentity windowsIdentity = null;
SPSecurity.RunWithElevatedPrivileges(delegate()
{
     windowsIdentity = S4UClient.UpnLogon(upn);
});

Now you can perform the impersonation and actual call:

using (windowsIdentity.Impersonate())
{
    // PSI call
}

Pros: Recommended way to work with legacy systems; this approach does not work if there is no user with given UPN in the domain where “Claims 2 Windows Token Service” is running.

Cons: The manual configuration required; code changes required.

Approach #4. Enable the mapToWindows

There is also fourth solution of this problem. If your application always requires impersonation-level Windows security tokens, then you may choose to enable the mapToWindows property on samlSecurityTokenRequirement on the <securityTokenHandlers> element. You need to enable Claims 2 Windows Token Service” as described above.

Pros: no code change required

Cons: PSI services are not configured to use securityTokenHandlers. Not sure it this can be done for PSI in particular.

Read more...