Wednesday, August 31, 2011

The quest for customizing ADFS sign-in web pages, part 2 – a custom STS

The quest for customizing ADFS sign-in web pages starts with writing a custom STS. The plan is as follows:

  1. Write a custom STS
  2. Test the STS with any RP application
  3. Federate ADFS with the STS

The aim of this post is the first task from this list.

Writing a custom STS

The custom STS basically consist of two web pages – the first one should be responsible for authentication and the second for dealing with WS-Federation login requests and SAML responses.

The basic structure

Which authentication method you choose is up to you. What’s important is to be able to authenticate a user only once and then store the information somewhere so that when consecutive requests from other RPs hit your STS, the user does not have to authenticate again. For example then, the well known Forms Authentication is a good choice for a custom STS.

Create two web pages, LoginPage.aspx and Default.aspx. Leave Default.aspx empty for now and make the LoginPage.aspx an actual login page – set authentication to Forms in web.config, add the <authorization> section so that all unauthenticated requests are redirected to Login.aspx and plug a MembershipProvider of your choice to the pipeline.

After you are done, let’s move to the next step.

Federation metadata

To become an STS, your application should provide a metadata for RP applications. Metadata makes the federation easier, you do not input all settings manually but you can use some authomated tools like the fedutil.exe (=”Add STS Reference” from VS).

In your project, create a folder FederationMetadata and a subfolder 2007-06 in it. This is only a convention but we will follow it. In the subfolder, create FederationMetadata.ashx handler – we will create the metadata dynamically. Also create a local web.config file with authorization set to “allow users=*” – you’d like your metadata to be available for everyone, not only for users who first log into your STS.

The metadata handler code is a copy-paste from one of the WIF SDK examples (remember to first add references to “Microsoft.IdentityModel” and “System.IdentityModel”):

public class FederationMetadata : IHttpHandler
 {
     public void ProcessRequest( HttpContext context )
     {
         MetadataBase metadata = Common.GetFederationMetadata();
         MetadataSerializer serializer = new MetadataSerializer();
         serializer.WriteMetadata( context.Response.OutputStream, metadata );
         context.Response.ContentType = "text/xml";
     }
 
     public bool IsReusable
     {
         get
         {
             return true;
         }
     }
 }

As you can see, we delegate the metadata creation to another class, Common, and then simply serialize the metadata and write to the output stream. Metadata provide the information about the STS itself, like it’s endpoints, the signing certificate or the list of offered claims. Most of these are straightforward except the certificate – you have to create it and then import to your application.

I prefer to use Portecle to create certificates, just run the application, create new PKCS#12 keystore, generate a new RSA key pair and save the keystore to a .pfx file (you’ll have to provide a password like “12345678”).

Now we write some code to import certificates from *.pfx files dynamically (without manually importing certificates to the keystore). Since we’ll import the certificate from the assembly resources, remember to add it to your project and set it to be an embedded resource.

public class CertificateUtil
{
    static Dictionary<string, X509Certificate2> _certificates = 
        new Dictionary<string, X509Certificate2>();
 
    public static X509Certificate2 GetCertificate( 
        string CertResourceName, 
        string CertPwd )
    {
        if ( !_certificates.ContainsKey( CertResourceName ) )
        {
            foreach ( string name in 
                typeof( CertificateUtil ).Assembly.GetManifestResourceNames() )
                if ( name.EndsWith( CertResourceName ) )
                {
                    Stream certificateStream = 
                        typeof( CertificateUtil ).Assembly.GetManifestResourceStream( name );
 
                    using ( BinaryReader br = new BinaryReader( certificateStream ) )
                        _certificates[CertResourceName] = 
                            new X509Certificate2( br.ReadBytes( (int)certificateStream.Length ), 
                                CertPwd, X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.Exportable );
                }
        }
 
        if ( _certificates.ContainsKey( CertResourceName ) )
            return _certificates[CertResourceName];
 
        throw new Exception( "No certificate was found in application's resources" );
    }
}

Now back to our Metadata generation class:

/// <summary>
 /// Summary description for Common
 /// </summary>
 public static class Common
 {
     /// <summary>
     /// Metadane usługi federacyjnej
     /// </summary>
     public static MetadataBase GetFederationMetadata()
     {
         string endpointId = UriHelper.ResolveServerUrl( "~/Default.aspx" );
         EntityDescriptor metadata = new EntityDescriptor();
         metadata.EntityId = new EntityId( endpointId );
 
         // Define the signing key
         X509Certificate2 cert = 
             CertificateUtil.GetCertificate( "the.custom.sts.pfx", "12345678" );
         metadata.SigningCredentials = new X509SigningCredentials( cert );
 
         // Create role descriptor for security token service
         SecurityTokenServiceDescriptor stsRole = new SecurityTokenServiceDescriptor();
         stsRole.ProtocolsSupported.Add( new Uri( WSFederationMetadataConstants.Namespace ) );
         metadata.RoleDescriptors.Add( stsRole );
 
         // Add a contact name
         ContactPerson person = new ContactPerson( ContactType.Administrative );
         person.GivenName = "contactName";
         stsRole.Contacts.Add( person );
 
         // Include key identifier for signing key in metadata
         SecurityKeyIdentifierClause clause = new X509RawDataKeyIdentifierClause( cert );
         SecurityKeyIdentifier ski = new SecurityKeyIdentifier( clause );
         KeyDescriptor signingKey = new KeyDescriptor( ski );
         signingKey.Use = KeyType.Signing;
         stsRole.Keys.Add( signingKey );
 
         // 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 );
 
         // offer two claims
         stsRole.ClaimTypesOffered.Add( new DisplayClaim( ClaimTypes.Name ) );
         stsRole.ClaimTypesOffered.Add( new DisplayClaim( ClaimTypes.Role ) );
 
         return metadata;
     }
 
     /// <summary>
     /// Create a reader to provide simulated Metadata endpoint configuration element
     /// </summary>
     /// <param name="activeSTSUrl">The active endpoint URL.</param>
     static XmlDictionaryReader GetMetadataReader( string activeSTSUrl )
     {
         MetadataSet metadata = new MetadataSet();
         MetadataReference mexReferece = 
             new MetadataReference( 
                 new EndpointAddress( activeSTSUrl + "/mex" ), 
                 AddressingVersion.WSAddressing10 );
         MetadataSection refSection = 
             new MetadataSection( MetadataSection.MetadataExchangeDialect, null, mexReferece );
         metadata.MetadataSections.Add( refSection );
 
         byte[] metadataSectionBytes;
         StringBuilder stringBuilder = new StringBuilder();
         using ( StringWriter stringWriter = new StringWriter( stringBuilder ) )
         {
             using ( XmlTextWriter textWriter = new XmlTextWriter( stringWriter ) )
             {
                 metadata.WriteTo( textWriter );
                 textWriter.Flush();
                 stringWriter.Flush();
                 metadataSectionBytes = 
                     stringWriter.Encoding.GetBytes( stringBuilder.ToString() );
             }
         }
 
         return XmlDictionaryReader.CreateTextReader( 
             metadataSectionBytes, 
             XmlDictionaryReaderQuotas.Max );
     }

Few things here to notice.

First, we import the certificate from the *.pfx file and we have to provide this “12345678” password to import both keys. Then we use a little helper by Rick Strahl to be able to resolve uris. Then we describe the endpoints and offered claims. Our metadata is ready, you can open your browser and point it to http://whatever.your.address.is/FederationMetadata/2007-06/FederationMetadata.ashx.

After you are done, let’s move to the next step.

Handling WS-Federation requests and responses

To handle actucal WS-Federation requests from RP applications you need a custom token service. Let’s start from the token service configuration:

/// <summary>
/// A custom SecurityTokenServiceConfiguration implementation.
/// </summary>
public class CustomSecurityTokenServiceConfiguration : 
    SecurityTokenServiceConfiguration
{
    static readonly object syncRoot = new object();
    const string CustomSTSKey = "CustomSTS.key";
 
    public static CustomSecurityTokenServiceConfiguration Current
    {
        get
        {
            HttpApplicationState httpAppState = HttpContext.Current.Application;
 
            CustomSecurityTokenServiceConfiguration customConfiguration = 
                httpAppState.Get( CustomSTSKey ) 
                as CustomSecurityTokenServiceConfiguration;
 
            if ( customConfiguration == null )
            {
                lock ( syncRoot )
                {
                    customConfiguration =
                        httpAppState.Get( CustomSTSKey ) 
                        as CustomSecurityTokenServiceConfiguration;
 
                    if ( customConfiguration == null )
                    {
                        customConfiguration = 
                            new CustomSecurityTokenServiceConfiguration();
                        httpAppState.Add( CustomSTSKey, customConfiguration );
                    }
                }
            }
 
            return customConfiguration;
        }
    }
 
    /// <summary>
    /// CustomSecurityTokenServiceConfiguration constructor.
    /// </summary>
    public CustomSecurityTokenServiceConfiguration()
        : base( "The.Custom.STS",
                new X509SigningCredentials( 
                    CertificateUtil.GetCertificate( "the.custom.sts.pfx", "12345678" ) )
               )
    {
        this.SecurityTokenService = typeof( CustomSecurityTokenService );
    }
}

And the token service itself:

/// <summary>
 /// A custom SecurityTokenService implementation.
 /// </summary>
 public class CustomSecurityTokenService : SecurityTokenService
 {
     public CustomSecurityTokenService( SecurityTokenServiceConfiguration configuration )
         : base( configuration )
     {
     }
 
     void ValidateAppliesTo( EndpointAddress appliesTo )
     {
         if ( appliesTo == null )
         {
             throw new ArgumentNullException( "appliesTo" );
         }
     }
 
     protected override Scope GetScope( 
         IClaimsPrincipal principal, 
         RequestSecurityToken request )
     {
         ValidateAppliesTo( request.AppliesTo );
 
         Scope scope = new Scope( 
             request.AppliesTo.Uri.OriginalString, 
             SecurityTokenServiceConfiguration.SigningCredentials );
         scope.TokenEncryptionRequired = false;
 
         scope.ReplyToAddress = scope.AppliesToAddress;
         //scope.ReplyToAddress = request.ReplyTo;
 
         return scope;
     }
 
 
     protected override IClaimsIdentity GetOutputClaimsIdentity( 
         IClaimsPrincipal principal, 
         RequestSecurityToken request, Scope scope )
     {
         if ( null == principal )
         {
             throw new ArgumentNullException( "principal" );
         }
 
         ClaimsIdentity outputIdentity = new ClaimsIdentity();
 
         // Name
         outputIdentity.Claims.Add( new Claim( ClaimTypes.Name, principal.Identity.Name ) );
 
         // Roles (should be dynamic)
         outputIdentity.Claims.Add( new Claim( ClaimTypes.Role, "Role1" ) );
         outputIdentity.Claims.Add( new Claim( ClaimTypes.Role, "Role2" ) );
 
         return outputIdentity;
     }
 }

As you can see, the configuration uses the certificate we’ve created eariler (yet again we have to provide the password) and the token service transforms the existing principal (which is the Forms Identity because of the way we’ve chosen users are authenticated) to the ClaimsIdentity – the object can contain arbitrary claims, including the Name, Roles, Givenname, Surname, Email etc.

The remaining thing is the code of the WS-Federation request/response processor which in our case is the Default.aspx page:

public partial class Default : System.Web.UI.Page
{
    /// <summary>
    /// Performs WS-Federation Passive Protocol processing. 
    /// </summary>
    protected void Page_PreRender( object sender, EventArgs e )
    {
        string action = Request.QueryString[WSFederationConstants.Parameters.Action];
 
        try
        {
            if ( action == WSFederationConstants.Actions.SignIn )
            {
                // Process signin request.
                SignInRequestMessage requestMessage = (SignInRequestMessage)WSFederationMessage.CreateFromUri( Request.Url );
                if ( User != null && User.Identity != null && User.Identity.IsAuthenticated )
                {
                    SecurityTokenService sts = 
                        new CustomSecurityTokenService( CustomSecurityTokenServiceConfiguration.Current );
                    SignInResponseMessage responseMessage = 
                        FederatedPassiveSecurityTokenServiceOperations.ProcessSignInRequest( 
                            requestMessage, 
                            User, 
                            sts );
                    FederatedPassiveSecurityTokenServiceOperations.ProcessSignInResponse( 
                        responseMessage, Response );
                }
                else
                {
                    throw new UnauthorizedAccessException();
                }
            }
            else if ( action == WSFederationConstants.Actions.SignOut )
            {
                // Process signout request.
                SignOutRequestMessage requestMessage = 
                    (SignOutRequestMessage)WSFederationMessage.CreateFromUri( Request.Url );
                FederatedPassiveSecurityTokenServiceOperations.ProcessSignOutRequest( 
                    requestMessage, 
                    User,
                    requestMessage.Reply, 
                    Response );
            }            
        }
        catch ( ThreadAbortException ) { }
        catch ( Exception exception )
        {
            throw new Exception( "An unexpected error occurred when processing the request. See inner exception for details.", exception );
        }
    }
}

Note two sections here, the first one handles signin requests and it processes these request with respect to our custom security token handler. The second one handles sign out requests, in this case a single request from a single RP application but it’s fairly easy to enhance the code to handle the signout of the whole environment.

This completes the Custom STS. In the next blog entry we will create an RP application federated with our STS.

4 comments:

Monish said...

Wiktor,

Is it possible to instantiate a SecureTokenService instance which enables us to perform federated operations directly against ADFS? I would like to perform these operations programmatically from the relying party, rather than being redirected to ADFS's login page. So rather than instantiate a custom STS, I would like to do something along the lines of:

SecurityTokenService sts = new ADFSSecurityTokenService(); SignInResponseMessage responseMessage = FederatedPassiveSecurityTokenServiceOperations.ProcessSignInRequest( requestMessage, User, sts ); FederatedPassiveSecurityTokenServiceOperations.ProcessSignInResponse( responseMessage, Response );

KIMZHU said...

Hi,

I am following your code, the property "TokenIssuerName" looks missing in CustomSecurityTokenServiceConfiguration.
So that I got an error "ID2083: IssuerName cannot be null or empty. Create SecurityTokenServiceConfiguration with a valid TokenIssuerName or override SecurityTokenService.GetIssuerName to provide it. "

KIMZHU said...

After I solved "TokenIssuerName" issue, if I add a custom claim type, I will get the error below.

ID4254: The AttributeValueXsiType of a SAML Attribute must be a string of the form 'prefix#suffix', where prefix and suffix are non-empty strings.
Parameter name: value

add Custom Claim type in GetOutputClaimsIdentity

e.g.
Claim c2 = new Claim("http://myclaimtyp.org/claims/DriverId", "12121", System.IdentityModel.Claims.Rights.PossessProperty);

KIMZHU said...

Finally, I solved those problems, but new issue happen.

ThreadAbortException break the code after "FederatedPassiveSecurityTokenServiceOperations.ProcessSignOutRequest"