Monday, September 12, 2011

The quest for customizing ADFS sign-in web pages, part 5 – ADFS as the R-STS

A quick summary – we are working on a custom STS which the ADFS will federate with. In our previous blog entry we have federated our custom RP application with the ADFS. What we have so far is then a fully functional custom STS, the ADFS and a sample RP application to test the federation.

In this part of the tutorial we are going to modify the custom STS so that the ADFS could be federated with it. This means that the RP application which is federated with the ADFS will not need to be reconfigured at all but the ADFS will offer a new way to login with the custom STS serving as the Identity Provider STS (IP-STS).

Several things must happen correctly to create the federation relation between ADFS the custom STS.

SAML and WS-FederationPassive Endpoints

Although our custom STS has already been implemented to publish the metadata, the ADFS will not accept it as the IP-STS until the SAML and WS-FederationPassive endpoints are provided in the metadata. It is possible to configure all the endpoints manually in the ADFS configuration console but it’s easier to modify the metadata generation code with two simple descriptors (ipsso, spsso below):

/// <summary>
/// Metadane usługi federacyjnej
/// </summary>
public static MetadataBase GetFederationMetadata()
{
    string endpointId = UriHelper.ResolveServerUrl( "~/Default.aspx" );
 
    ....
 
    // Add endpoints
    string activeSTSUrl   = 
        UriHelper.ResolveServerUrl( "~/Default.aspx" );
    string activeMetadata = 
        UriHelper.ResolveServerUrl( "~/FederationMetadata/2007-06/FederationMetadata.ashx" );
    EndpointAddress endpointAddress = new EndpointAddress(
                                            new Uri( activeSTSUrl ),
                                            null,
                                            null,
                                            GetMetadataReader( activeMetadata ), null );
    stsRole.SecurityTokenServiceEndpoints.Add( endpointAddress );
    stsRole.PassiveRequestorEndpoints.Add( endpointAddress );
 
    // spsso, ipsso bindings
    ServiceProviderSingleSignOnDescriptor spsso = new ServiceProviderSingleSignOnDescriptor();
    spsso.ProtocolsSupported.Add( Saml20Protocol );
    spsso.Keys.Add( signingKey );
    spsso.SingleLogoutServices.Add( new ProtocolEndpoint( Common.HttpPost, new Uri( activeSTSUrl ) ) );
    spsso.SingleLogoutServices.Add( new ProtocolEndpoint( Common.HttpRedirect, new Uri( activeSTSUrl ) ) );
    spsso.AssertionConsumerService.Add( 0, new IndexedProtocolEndpoint( 0, Common.HttpPost, new Uri( activeSTSUrl ) ) );
    spsso.AssertionConsumerService.Add( 1, new IndexedProtocolEndpoint( 1, Common.HttpRedirect, new Uri( activeSTSUrl ) ) );
    metadata.RoleDescriptors.Add( spsso );
    IdentityProviderSingleSignOnDescriptor ipsso = new IdentityProviderSingleSignOnDescriptor();
    ipsso.ProtocolsSupported.Add( Saml20Protocol );
    ipsso.Keys.Add( signingKey );
    ipsso.SingleSignOnServices.Add( new ProtocolEndpoint( Common.HttpPost, new Uri( activeSTSUrl ) ) );
    ipsso.SingleSignOnServices.Add( new ProtocolEndpoint( Common.HttpRedirect, new Uri( activeSTSUrl ) ) );
    ipsso.SingleLogoutServices.Add( new ProtocolEndpoint( Common.HttpPost, new Uri( activeSTSUrl ) ) );
    ipsso.SingleLogoutServices.Add( new ProtocolEndpoint( Common.HttpRedirect, new Uri( activeSTSUrl ) ) );
    metadata.RoleDescriptors.Add( ipsso );
 
    // offer two claims
    stsRole.ClaimTypesOffered.Add( new DisplayClaim( ClaimTypes.Name ) );
    stsRole.ClaimTypesOffered.Add( new DisplayClaim( ClaimTypes.Role ) );
 
    return metadata;
}

with following consts

public static readonly Uri HttpPost = 
   new Uri( "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" );
public static readonly Uri HttpRedirect = 
   new Uri( "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" );
public static readonly Uri Saml20Protocol = 
   new Uri( "urn:oasis:names:tc:SAML:2.0:protocol" );

I have found 0 (zero!) examples of using these classes in the internet and have figured most things by inspecting the metadata of the ADFS itself and trying to implement similar structures in the custom STS metadata. Fortunately, ADFS can be federated with another ADFS rather easily which was the reason to dig deeper into the ADFS metadata to find any stuff specific to STS-to-STS federation.

Matching the EntityID and the IssuerName

The metadata declares the EntityID of the STS. On the other hand, the certificate used to sign the SAML token declares the name of the certificate issuer.

For some reason, these two must match otherwise the ADFS will not accept claims from your STS. I’ve blogged about one of the ways to handle this. Another solution would be to declare one string const in your code and access it from the two places in the code.

First, in the metadata:

/// <summary>
 /// Metadane usługi federacyjnej
 /// </summary>
 public static MetadataBase GetFederationMetadata()
 {
     string endpointId = UriHelper.ResolveServerUrl( "~/Default.aspx" );
     EntityDescriptor metadata = new EntityDescriptor();
     // I am using a const, defined elsewhere
     metadata.EntityId = 
       new EntityId( CustomSecurityTokenServiceConfiguration.TheCustomSTSID );
 
 

And then in the custom STS configuration class:

/// <summary>
 /// A custom SecurityTokenServiceConfiguration implementation.
 /// </summary>
 public class CustomSecurityTokenServiceConfiguration : 
     SecurityTokenServiceConfiguration
 {
     public const string TheCustomSTSID = "The.Custom.STS";
 
     ...
 
     /// <summary>
     /// CustomSecurityTokenServiceConfiguration constructor.
     /// </summary>
     public CustomSecurityTokenServiceConfiguration()
         // the same const used     
         : base( TheCustomSTSID,
                 new X509SigningCredentials( 
                     CertificateUtil.GetCertificate( "the.custom.sts.pfx", "12345678" ) )
                )
     {
         this.SecurityTokenService = typeof( CustomSecurityTokenService );
     }
 }

Correcting the ReplyToAddress

When creating the relying requests to our custom STS, the ADFS will introduce itself using its identifier (scope.AppliesToAddress). The problem is that in the default configuration, the identifier points to the URL which is invalid and will not accept response messages from your STS.

For example, in my test environment, the ADFS is available at https://fs.adfs.pl/adfs/ls/ however the ID it passes to my custom STS is http://fs.adfs.pl/adfs/services/trust.

(Note the http:// protocol while ADFS is available at https://!)

There are two ways to correct this. First, you can go to ADFS, open it’s configuration panel and change its identifier.

Second, in your custom STS you can create a mapping between IDs and valid URLs of requesting parties. The mapping could have a default clause which would just copy the identifier as the return address (as it does that by default in our implementation!). For example:

/// <summary>
/// A custom SecurityTokenService implementation.
/// </summary>
public class CustomSecurityTokenService : SecurityTokenService
{
    ...
 
    protected override Scope GetScope( 
        IClaimsPrincipal principal, 
        RequestSecurityToken request )
    {
        ValidateAppliesTo( request.AppliesTo );
 
        Scope scope = new Scope( 
            request.AppliesTo.Uri.OriginalString, 
            SecurityTokenServiceConfiguration.SigningCredentials );
        scope.TokenEncryptionRequired = false;
 
        // default clause
        scope.ReplyToAddress = scope.AppliesToAddress;
        // mapping
        foreach ( var key in ConfigurationManager.AppSettings.AllKeys )
            if ( scope.AppliesToAddress == key )
                scope.ReplyToAddress = ConfigurationManager.AppSettings[key];
 
        return scope;
    }

with

<appSettings>
    <add key="http://fs.adfs.pl/adfs/services/trust" 
         value="https://fs.adfs.pl/adfs/ls/"/>
</appSettings>
Creating the Custom STS trust relation in ADFS

After all these preparations, creating the trust relationship between ADFS the custom STS is easy.

First navigate to the custom STS metadata (https://customsts.yourdomain.com/FederationMetadata/2007-06/FederationMetadata.ashx) and save it to an XML file.

Then go to ADFS configuration and create a new trust relationship. Import the provider metadata from the XML file. Create two simple claim rules which just pass Name and Role through (the ADFS should not touch them as they are created in the custom STS).

Then go to your example RP application and try to login. Instead of the ADFS login page, you should see the HomeRealmDiscovery page as ADFS is now unsure whether you want to login with your AD identity or via the trusted claims provider (custom STS). The default HRD page contains a combobox with two selections however you can easily modify this page in ADFS (it’s stored as *.aspx file).

When things fail

Couple of tips when you fail at some point.

If you do not see the HRD page in ADFS but the trust relationship has been created, it probably means that the SAML/WSFederationPassive endpoints in the custom STS are created incorrectly. Go back to the 1st section of this tutorial.

If the custom STS redirects back to wrong address, it probably means that the mapping between ADFS ID and ADFS address is incorrect in the custom STS. Go back two sections.

If the redirect hits a correct ADFS page but ADFS shows a generic error it could mean that the custom STS ID does not match the issuer name of the certificate. Go back three sections.

We are close to the very last tutorial in which we will create a WSTrustFeb2005 service endpoint in our custom STS and modify the login logic of the ADFS.

5 comments:

fdr said...

When trying to add Claims Provider trust, I get error when importing federation metadata xml: "Error message: ID6013: The signature verification failed." I created certificate using Portecle as described. Any idea what might be causing this?

Wiktor Zychla said...

The most possible cause is that when you SAVE the metadata to the XML file, you ALTER the XML somehow before you pass it to the ADFS. For example, you open it and reformat or manually insert some tags. This will make the metadata invalid according to the signature.

fdr said...

Thanks Wiktor, you were right on this one. My editor touched the xml as I saved it from my sts.

John W said...

Stuck on this step. Two questions:

#1 - I set up the claims provider trust in adfs and everything looks OK on the adfs side. I can see the endpoints listed (one ws-federation passive (redirect), two SAML single sign on (POST, redirect) and two SAML Logout (POST, redirect). ws-federation passive is set as the default. The default page of my custom sts is listed under each url. But the home realm discovery page doesn't show - I just see the normal ADFS login page. Went back through and checked all the code and everything looks OK. Any ideas?

#2 - Also curious why we have to export the federationmetadata as an xml file. Why not just use the dynamic ashx url when adding the claims provider trust in ADFS? I tried both ways, same result.

Lars Stoustrup said...

Hi Wiktor! Even though your ADFS pages aren't new at this point, this is supremely useful. I have created a custom STS myself (lots of pain and grief) and I was completely halted by the http:///adfs/services/trust identifier issue. Your fix for that was just what I needed.