Skip to main content

CoreService with ADFS

This post presents a way to connect to the SDL Web 8.5 CoreService form a .NET client using ADFS federated security.

In this particular setup, the client application -- a .NET Console application -- makes a call to the ADFS Security Token Service (STS) to request a token based on username/password combination. This is a so called active authentication scenario. If the username/password combination is correct, the STS issues an encrypted SAML token and send it back to the client application. The client is only the bearer of the token and it cannot decrypt it.

The client then establishes a connection with the ADFS secured web service and passes on the SAML token. The web service decrypts the token and extracts the user principal, and perhaps additional claims (i.e. attributes of that user such as email, first, last names, etc). The service impersonates the user principal and creates a channel with the client application. All operations performed by the client in this channel are done in the name of this user principal until the channel is closed.

The Client

Let's start with the client, because this is the simple part. Ha!

I wrote a simple .NET Console application to prove the point of connecting to the CoreService and read some stuff from SDL Web 8.5.

I generated my own proxy classes, but the OOTB classes from the client DLL will work just as well.

This particular setup uses application configuration to setup specify where the STS is located, which binding, contract and endpoint to use, and the format and attributes of the token. Alternatively, this configuration can be done through code, but that is presented in a different blog post.

App.Config

The following is part of my App.config and it defines the client-side of the Federation binding to use when connecting to the ADFS enabled CoreService server:

<system.serviceModel>
  <bindings>
    <ws2007FederationHttpBinding>
      <binding name="myCoreServiceBinding" maxReceivedMessageSize="10485760">
        <security mode="TransportWithMessageCredential">
          <message issuedTokenType="http://docs.oasis-open.org/wss/oasis-wss-saml-token-profile-1.1#SAMLV2.0">
            <issuer address="https://myadfs.com/adfs/services/trust/2005/usernamemixed"
              binding="wsHttpBinding" bindingConfiguration="myIssuerBinding" />
            <issuerMetadata address="https://myadfs.com/adfs/services/trust/mex" />

            <tokenRequestParameters>
              <trust:SecondaryParameters xmlns:trust="http://docs.oasis-open.org/ws-sx/ws-trust/200512">
                <trust:KeyType xmlns:trust="http://docs.oasis-open.org/ws-sx/ws-trust/200512">http://docs.oasis-open.org/ws-sx/ws-trust/200512/SymmetricKey</trust:KeyType>
                <trust:KeySize xmlns:trust="http://docs.oasis-open.org/ws-sx/ws-trust/200512">256</trust:KeySize>
                <trust:KeyWrapAlgorithm xmlns:trust="http://docs.oasis-open.org/ws-sx/ws-trust/200512">http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p</trust:KeyWrapAlgorithm>
                <trust:EncryptWith xmlns:trust="http://docs.oasis-open.org/ws-sx/ws-trust/200512">http://www.w3.org/2001/04/xmlenc#aes256-cbc</trust:EncryptWith>
                <trust:SignWith xmlns:trust="http://docs.oasis-open.org/ws-sx/ws-trust/200512">http://www.w3.org/2000/09/xmldsig#hmac-sha1</trust:SignWith>
                <trust:CanonicalizationAlgorithm xmlns:trust="http://docs.oasis-open.org/ws-sx/ws-trust/200512">http://www.w3.org/2001/10/xml-exc-c14n#</trust:CanonicalizationAlgorithm>
                <trust:EncryptionAlgorithm xmlns:trust="http://docs.oasis-open.org/ws-sx/ws-trust/200512">http://www.w3.org/2001/04/xmlenc#aes256-cbc</trust:EncryptionAlgorithm>
              </trust:SecondaryParameters>
            </tokenRequestParameters>
          </message>
        </security>
      </binding>
    </ws2007FederationHttpBinding>

    <wsHttpBinding>
      <binding name="myIssuerBinding" transactionFlow="true">
        <security mode="TransportWithMessageCredential">
          <transport clientCredentialType="None" />
          <message clientCredentialType="UserName" establishSecurityContext="false" />
        </security>
      </binding>
    </wsHttpBinding>
  </bindings>

  <client>
    <endpoint address="https://web85.playground/webservices/CoreService201603.svc/wsFederationHttp"
      binding="ws2007FederationHttpBinding" bindingConfiguration="myCoreServiceBinding"
      contract="MyCoreService.ISessionAwareCoreService" name="coreServiceFederation" />
  </client>
</system.serviceModel>

Some things to mention above:
  • ws2007FederationHttpBinding is defined to match the same binding on the server
  • security in the binding is TransportWithMessageCredential -- meaning HTTPS and an encrypted message (SAML token) in it containing the user credentials
  • SAML token issued by the ADFS (issuerMetadata) that is configured to a particular point on the ADFS server
  • SAML token transported from ADFS using HTTPS and containing encrypted message inside it with username and claims
  • CoreService endpoint, binding, and contract to use

Client Code

The client code is quite simple, because the handling of the entire communication with the ADFS, token issuing, and endpoint configuration is done via the App.config and .NET will use its in-built APIs to leverage all that.

using (var factory = new ChannelFactory<ISessionAwareCoreService>("coreServiceFederation"))
{
    factory.Credentials.UserName.UserName = username;
    factory.Credentials.UserName.Password = password;
    ISessionAwareCoreService coreService = factory.CreateChannel();

    Console.WriteLine("API Version: {0}", coreService.GetApiVersion());

    UserData user = coreService.GetCurrentUser();
    Console.WriteLine("User: {0} | {1} | {2}", user.Title, user.Description, user.Id);

    IdentifiableObjectData[] publications = coreService.GetSystemWideList(new PublicationsFilterData());
    Console.WriteLine("Found {0} Publications:", publications.Length);
    publications.Cast<PublicationData>().ToList().
        ForEach(x => Console.WriteLine("\t{0} | {1}", x.Title, x.Id));
}

The code creates a System.ServiceModel.ChannelFactory class that wraps an ISessionAwareCoreService proxy. It connects to the endpoint coreServiceFederation configured in the App.config and it passes in the username/password.

Once the factory method CreateChannel is called, the proxy is ready to be used.

The Server

In order for the CoreService, and other WCF services on the SDL Web 8.5 CM server to work with ADFS, we need to modify quite a few things in several web.config files under the SDL Web website.

Note: Before making the changes below, I enabled HTTPS and SSO on the CM server, by running the appropriate PowerShell scripts.

In file [SDLWebHome]\web\Web.config, add the following section. It defines the Relying Party identifier of the SDL Web application and the thumbprint of the STS signing certificate. This thumbprint is used just for the server (our CM server) to validate that the token has indeed been signed by the ADFS STS issuer.

<system.identityModel>
  <identityConfiguration>
    <claimsAuthenticationManager type="My.ClaimsAuthenticationManager"/>

    <securityTokenHandlers>
      <securityTokenHandlerConfiguration>
        <audienceUris>
          <add value="https://web85.playground/webservices/CoreService201603.svc/wsFederationHttp"/>
        </audienceUris>

        <issuerNameRegistry type="System.IdentityModel.Tokens.ConfigurationBasedIssuerNameRegistry, System.IdentityModel, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
          <trustedIssuers>
            <add name="my.adfs.com Signing" thumbprint="5d 32 11 c3 67 ac 3f 85 d3 1a 66 64 f1 a5 54 2c a2 b3 f3" />
          </trustedIssuers>
        </issuerNameRegistry>
      </securityTokenHandlerConfiguration>
    </securityTokenHandlers>

    <certificateValidation certificateValidationMode="None"/>
  </identityConfiguration>
</system.identityModel>

In file [SDLWebHome]\webservices\Web.config, add or modify the following sections to look like the ones below. Some of these sections already exist and must be modified:

<system.serviceModel>
  <bindings>
    <ws2007FederationHttpBinding>
      <binding name="CoreService_wsFederationHttpBinding" transactionFlow="true" maxReceivedMessageSize="10485760">
        <security mode="TransportWithMessageCredential">

          <message negotiateServiceCredential="false" issuedKeyType="SymmetricKey"
            issuedTokenType="http://docs.oasis-open.org/wss/oasis-wss-saml-token-profile-1.1#SAMLV2.0"/>

        </security>
      </binding>
    </ws2007FederationHttpBinding>
  </bindings>

  <services>
    <service behaviorConfiguration="Tridion.ContentManager.ServiceHost.IISHost.CoreServiceBehavior"
      name="Tridion.ContentManager.ServiceHost.IISHost.CoreService201603">

      <endpoint address="wsFederationHttp" binding="ws2007FederationHttpBinding"
        bindingConfiguration="CoreService_wsFederationHttpBinding" name="wsFederationHttp"
        bindingNamespace="http://www.sdltridion.com/ContentManager/CoreService/201603"
          contract="Tridion.ContentManager.CoreService.ISessionAwareCoreService201603">
        <identity>
          <dns value="web85.playground"/>
        </identity>
      </endpoint>
    </service>
  </services>

  <behaviors>
    <serviceBehaviors>
      <behavior name="Tridion.ContentManager.ServiceHost.IISHost.CoreServiceBehavior">
        <serviceCredentials useIdentityConfiguration="true">
          <serviceCertificate storeLocation="LocalMachine" storeName="My" x509FindType="FindByThumbprint"
            findValue="71 11 13 3e 44 69 46 12 a5 b5 c3 d8 1c 99 8a 7a 57 63 08 94"/>
        </serviceCredentials>

        <serviceAuthorization principalPermissionMode="Always"/>
      </behavior>
    </serviceBehaviors>
  </behaviors>
</system.serviceModel>

In short, the configurations specify:
  • binding to used for federation
    • the security mode
    • type of message with its encryption type, key, token type, SAML2
  • service
    • maps a behavior configuration to a binding to a contract
  • behavior
    • configures certificate to use to decrypt SAML token. This certificate must be installed on the CM server and contain a private key. This is the same certificate (with public key) that is configured in the ADFS Relying Party encryption tab
    • principalPermissionMode instructs to place the newly created user principal in the thread, thus impersonating the user from the SAML token

I implemented one custom .NET class in this setup, namely MyClaimsAuthenticationManager. This class is responsible with choosing between the original or newly impersonated user. If we have a user in the HttpContext, which is our authenticated Federation user, then use this one (return it); otherwise, use the original user:

public class MyClaimsAuthenticationManager : ClaimsAuthenticationManager
{
    public override ClaimsPrincipal Authenticate(string resourceName, ClaimsPrincipal incomingPrincipal)
    {
        ClaimsPrincipal user = HttpContext.Current.User as ClaimsPrincipal;

        if (user != null && user.Identity.IsAuthenticated && user.Identity.AuthenticationType == "Federation")
        {
            return new ClaimsPrincipal(user);
        }
        else
        {
            return incomingPrincipal;
        }
    }
}

Conclusion

This "simple" crazy setup is almost nowhere documented. The entire .NET service model is one black box with no clear examples or documentation. I spent hours trying to figure out what each setting is doing and how it changes the behavior of the service.

In order to get logging of what goes on in .NET API, one must enable diagnostics and go through megabytes of useless .svclog entries just to find a stack-trace.

One thing to note: the federation approach does not work on a scaled-out scenario. If there are more CM servers behind a load-balancer, the setup of the communication channel fails. Apparently the chatty nature of the handshake between client and server is to blame. During the handshake, the client must talk to the same server node. In my tests, even enabling session stickiness was not sufficient, as the cookie is only used after the handshake hash completed. I didn't spend much time on this -- maybe it is possible to solve that by using some session manager or session replication in IIS?

Anyway, this is a cumbersome and complex setup with many moving parts and cryptic error messages. It is hard to debug and to understand which component talks to which and the slightest change is configuration will have the setup break in a completely seemingly unrelated area.

As an alternative, I suggest using a simpler approach, such as enhancing a basicHttp endpoint to authenticate against an ADFS server. This offers more interoperability with other non .NET clients (e.g. Java, PowerShell, or even AJAX), and a much simpler setup. Also it provides a way of securing old fashioned dinosaur .asmx web services against ADFS... But more about that, in a follow-up blog post.



Comments

Popular posts from this blog

Running sp_updatestats on AWS RDS database

Part of the maintenance tasks that I perform on a MSSQL Content Manager database is to run stored procedure sp_updatestats . exec sp_updatestats However, that is not supported on an AWS RDS instance. The error message below indicates that only the sa  account can perform this: Msg 15247 , Level 16 , State 1 , Procedure sp_updatestats, Line 15 [Batch Start Line 0 ] User does not have permission to perform this action. Instead there are several posts that suggest using UPDATE STATISTICS instead: https://dba.stackexchange.com/questions/145982/sp-updatestats-vs-update-statistics I stumbled upon the following post from 2008 (!!!), https://social.msdn.microsoft.com/Forums/sqlserver/en-US/186e3db0-fe37-4c31-b017-8e7c24d19697/spupdatestats-fails-to-run-with-permission-error-under-dbopriveleged-user , which describes a way to wrap the call to sp_updatestats and execute it under a different user: create procedure dbo.sp_updstats with execute as 'dbo' as...

I Have Gone Dark

Maybe it's the Holidays, but my mood has gone pretty dark. That is, regarding the look and feel of my computer and Tridion CME, of course. What I did was to dim the lights on the operating system, so I installed Placebo themes for Windows 7 . I went for the Ashtray look -- great name :) My VM looks now like this: But, once you change the theme on Windows, you should 'match' the theme of your applications. Some skin easily, some not. The Office suite has an in-built scheme, which can be set to Black , but it doesn't actually dim the ribbon tool bars -- it looks quite weird. Yahoo Messenger is skinnable, but you can't change the big white panels where you actually 'chat'. Skype is not skinnable at all. For Chrome, there are plenty of grey themes. Now i'm using Pro Grey . But then I got into changing the theme of websites. While very few offer skinnable interfaces (as GMail does), I had to find a way to darken the websites... Enter Stylish -- a pl...

REL Standard Tag Library

The RSTL is a library of REL tags providing standard functionality such as iterating collections, conditionals, imports, assignments, XML XSLT transformations, formatting dates, etc. RSTL distributable is available on my Google Code page under  REL Standard Tag Library . Always use the latest JAR . This post describes each RSTL tag in the library explaining its functionality, attributes and providing examples. For understanding the way expressions are evaluated, please read my post about the  Expression Language used by REL Standard Tag Library . <c:choose> / <c:when> / <c:otherwise> Syntax:     <c:choose>         <c:when test="expr1">             Do something         </c:when>         <c:when test="expr2">             Do something else         </c:when...