Thursday, September 15, 2011

How to Align Assignments in Visual Studio 2010

With Productivity Power Tools you can easily auto align assignments in blocks of code.

First, disable the “Ignore spaces in declaration statements” under Options/Text Editor/C#/Formatting/Spacing.

Then select a block of code and press Ctrl+Alt+] (right square bracket). This will automatically change this

Name = User.UserName,
GivenName = User.GivenName,
Surname = User.Surname,
SamAccountName = User.UserName,
EmailAddress = User.Email 

to this

Name           = User.UserName,
GivenName      = User.GivenName,
Surname        = User.Surname,
SamAccountName = User.UserName,
EmailAddress   = User.Email

IIS 7.5 – a breaking change in extensionless URIs handling after Windows Server 2008 R2 SP1 is applied

One of our sites use dynamic URL rewriting based on catching the 404 by IIS and executing an *.aspx webpage which then handles dynamic URLs accordingly.

This has been working for us for many, many years, starting on IIS 6 on Windows Server 2003. All request of the form

http://oursite.com/this/is/something/dynamic/foo.bar

and

http://oursite.com/this/is/something/dynamic/foo

were routed to the *.aspx page handling the 404 status code. ASP.NET has then been asked by IIS to process (respectively):

The404Page.aspx?404;http://oursite.com/this/is/something/dynamic/foo.bar

and

The404Page.aspx?404;http://oursite.com/this/is/something/dynamic/foo

When the SP1 is applied to Windows Server, IIS starts to behave differently. When a request regards a resource which contains an extension (like .bar, .jpg) the semantic is exactly the same as it has been before.

However, when a request to a resource without an extension is made(nonexisting folder like /this/is/something/dynamic/foo), IIS routes such request directly to ASP.NET like it is the ASP.NET responsible for handling extensionless uris. Instead of The404Page.aspx?404;… ASP.NET handles then

this/is/something/dynamic/foo

I presume this has something in common with the ExtensionlessUrl-Integrated-4.0 module and will inspect this further. What’s interesting is that the change affects only ASP.NET 4.0 while ASP.NET 2.0 is not affected.

It seems that it causes the trouble for other people too.

Monday, September 12, 2011

The quest for customizing ADFS sign-in web pages, part 6 – a WSTrustFeb2005 Endpoint on the Custom STS

A quick summary – we are working on a custom STS which the ADFS will federate with. In our previous entry we have created a trust relationship between the ADFS and our custom STS so users are able to see the HomeRealmDiscovery page in the ADFS and pick up a correct identity provider – the ADFS itself or the custom STS.

In our very last entry we are going to provide a WSTrustFeb2005 endpoint on our custom STS so that WCF signin requests could be created directly from the ADFS to the custom STS without the HRD page. In fact, users will see the ADFS login page with username/password text fields and we will modify the logic behind the page so that the pair username/password will be first validated against our custom STS and then against the Active Directory without user being aware of that. This will complete our quest for customizing ADFS sign-in web pages.

A WSTrustFeb2005 Endpoint

Go to our custom STS project, and create a *.svc service. Since I like to follow conventions, I created it under /services/trust/2005/UserName.svc (since we are going to perform username/password validation).

The service itself is rather straightforward as it uses a built-in factory class. It means that the *.svc doesn’t need any code behind and the sole content of the *.svc file is:

<%@ ServiceHost Language="C#" Debug="true" 
    Factory="Microsoft.IdentityModel.Protocols.WSTrust.WSTrustServiceHostFactory" 
    Service="The.Custom.STS.Code.CustomSecurityTokenServiceConfiguration"
 %>

Note that the Service attribute points to the security token service configuration class which we have created long, long ago somewhere at the begininng of our quest, when we have built a custom STS. In other words, the active WCF service uses the same STS infrastructure as the passive LoginForm/Default pages.

But we are not done yet. Go to the web.config file and create correct WCF sections:

<system.serviceModel>
    <services>
        <service name="Microsoft.IdentityModel.Protocols.WSTrust.WSTrustServiceContract"
                 behaviorConfiguration="ServiceBehavior">
            <endpoint address="Sts" binding="ws2007HttpBinding"
                      contract="Microsoft.IdentityModel.Protocols.WSTrust.IWSTrustFeb2005SyncContract"
                      bindingConfiguration="wsTrustFeb2005Configuration" />
            <host>
                <baseAddresses>
                    <add baseAddress="https://customsts.yourdomain.com/services/trust/2005/UserName.svc"/>
                </baseAddresses>
            </host>
            <endpoint address="mex" binding="mexHttpBinding" contract="IMetadataExchange" />
        </service>
    </services>
    <bindings>
        <ws2007HttpBinding>
            <binding name="wsTrustFeb2005Configuration">
                <security mode="TransportWithMessageCredential">
                    <message clientCredentialType="UserName" establishSecurityContext="false"/>
                </security>
            </binding>
        </ws2007HttpBinding>
    </bindings>
    <behaviors>
        <serviceBehaviors>
            <behavior name="ServiceBehavior">
                <serviceMetadata httpGetEnabled="true" />
                <serviceDebug includeExceptionDetailInFaults="false" />
            </behavior>
        </serviceBehaviors>
    </behaviors>
    <serviceHostingEnvironment multipleSiteBindingsEnabled="true" />
</system.serviceModel>

Note that we are using a built-in contract interface (IWSTrustFeb2005SyncContract) and we create an explicit ws2007HttpBinding binding with security mode set to TransportWithMessageCredential and clientCredentialType set to UserName. This is the only correct combination and what’s important is that it works only when the service is exposed on the secure channel (SSL).

This is enough to test the service, just point your browser to the https://customsts/services/trust/2005/UserName.svc and check if it displays correct information page.

A custom token handler

By default the service we have just built will try to accept tokens for Windows users. However, we’d like to have a custom validation logic so for example we could validate users agains the SQL database.

For this, we need a custom token handler which could examine SAML request tokens, extract usernames/passwords from there and create claims accordingly.

This is surprizingly straightforward:

public class CustomUserNameSecurityTokenHandler : UserNameSecurityTokenHandler
 {
     public override bool CanValidateToken
     {
         get
         {
             return true;
         }
     }
 
     public override ClaimsIdentityCollection ValidateToken( 
         System.IdentityModel.Tokens.SecurityToken token )
     {
         UserNameSecurityToken userNameToken = token as UserNameSecurityToken;
         if ( userNameToken == null )
             throw new ArgumentException( "The security token is not a valid username token." );
 
         if ( userNameToken.Password == userNameToken.UserName.Length.ToString() )
         {
             IClaimsIdentity identity = new ClaimsIdentity();
 
             identity.Claims.Add( new Claim( ClaimTypes.Name, userNameToken.UserName ) );
             identity.Claims.Add( new Claim( ClaimTypes.Role, "CustomTokenHandlerRole1" ) );
             identity.Claims.Add( new Claim( ClaimTypes.Role, "CustomTokenHandlerRole2" ) );
 
             return new ClaimsIdentityCollection( new IClaimsIdentity[] { identity } );
         }
 
         throw new InvalidOperationException( "Username/password is incorrect in STS." );
     }
 }

Note that my simple token handler just checks whether the password matches the length of the username (so foo/3 or foobar/6 are valid credentials) but this is the exact place you can plug any validation logic.

We only need to inform WIF to use this custom token handler instead of the default one. We do this in web.config in a proper section:

<microsoft.identityModel>
    <service>
        <securityTokenHandlers>
            <remove type="Microsoft.IdentityModel.Tokens.WindowsUserNameSecurityTokenHandler, 
                          Microsoft.IdentityModel, 
                          Version=3.5.0.0, Culture=neutral, 
                          PublishKeyToken=31BF3856AD364E35"/>
            <add type="The.Custom.STS.CustomUserNameSecurityTokenHandler" />
        </securityTokenHandlers>
    </service>
</microsoft.identityModel>

This new token handler will not interfere with the passive authentication scenario, where your browser asks for the Default.aspx page of the STS. Instead, it is only used during the active authentication scenario, where WCF requests are handled by the STS.

Passing through claims

The custom token handler we have created will now works correctly, however our WCF service still points to our CustomSecurityTokenService class where we create claims manually (the GetOutputClaimsIdentity method).

Because our custom token handler now creates its own claims, we have to modify the STS logic so that WCF claims could be passed through.

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" ) );
 
     // Pass through any existing claims (here: passed from the custom token handler)
     foreach ( var claim in principal.Identities[0].Claims )
         outputIdentity.Claims.Add( new Claim( claim.ClaimType, claim.Value ) );
 
     return outputIdentity;
 }
Modifying the ADFS login page

The ADFS login page can now be modified to use our WCF service. Go to ADFS installation, find the FormsSignIn.aspx.cs file and modify it to:

protected void SubmitButton_Click( object sender, EventArgs e )
 {
     try
     {
         SignInWithTokenFromOtherSTS( UsernameTextBox.Text, PasswordTextBox.Text );
     }
     catch 
     {     
         try
         {
             SignIn( UsernameTextBox.Text, PasswordTextBox.Text );
         }
         catch ( AuthenticationFailedException ex )
         {
             HandleError( ex.Message );
         }
     }
 }
 
 private void SignInWithTokenFromOtherSTS( string UserName, string Password )
 {
         const string OtherSTSAddress = 
            "https://customsts.yourdomain.com/services/trust/2005/UserName.svc/Sts";
         const string YourStsAddress  = 
            "http://fs.adfs.pl/adfs/services/trust";
 
         EndpointAddress endpointAddress = new EndpointAddress( OtherSTSAddress );
         UserNameWSTrustBinding binding = 
            new UserNameWSTrustBinding( SecurityMode.TransportWithMessageCredential );
 
         WSTrustChannelFactory factory = new WSTrustChannelFactory( binding, endpointAddress );
         factory.Credentials.UserName.UserName = UserName;
         factory.Credentials.UserName.Password = Password;
         factory.TrustVersion = System.ServiceModel.Security.TrustVersion.WSTrustFeb2005;
 
         WSTrustChannel channel = (WSTrustChannel)factory.CreateChannel();
 
         RequestSecurityToken rst = new RequestSecurityToken(
             WSTrustFeb2005Constants.RequestTypes.Issue,
             WSTrustFeb2005Constants.KeyTypes.Bearer );
         rst.AppliesTo = new EndpointAddress( YourStsAddress );
 
         SecurityToken token = channel.Issue( rst );
 
         SignIn( token );
 }

Note that instead of using the built-in SignIn( string UserName, string Password ) method (which would check the credentials against the active directory) we are providing a custom sign in method which creates a WCF request to the WSTrustFeb2005 service in behalf of http://fs.adfs/adfs/services/trust (which is the ID of the ADFS, not it’s address! If you pass the address, the ADFS will refuse to accept the token because the token would not be issued for the ADFS).

When we get the token back from the service, we pass it to another built-in method which this time accepts the token instead of the username/password pair (SignIn( token )). Note that this would not work without the explicit trust relationship in the ADFS to our custom STS we have created in the previous blog entry. This time the ADFS would complain that the claims provider is not trusted.

Also note that this is the most difficult step in our tutorial as we are finally putting all blocks together. My advice is to test the SignInWithTokenFromOtherSTS method separately, from within a custom console application and when it finally works (you get the token and not the exception from the WCF service) you can move on to testing this code from within the ADFS.

I’ve spent at least a day at this point, still getting exceptions from the WCF service and when it finally started to work (I got tokens from the WCF service) making it work with the ADFS was easy.

Modifying the ADFS HRD page

The only remaining issue is that because of our trust relationship between the ADFS and our custom STS, the ADFS still shows the HomeRealmDiscovery page! This is rather unfortunate because we’d like it to show the default page using our enhanced logic. And the passive login using the loginpage of our custom STS should be now disabled.

To do so, open the HomeRealmDiscovery.aspx.cs in the ADFS installation and modify the Page_Init so that it immediately picks the home realm:

public partial class HomeRealmDiscovery : 
   Microsoft.IdentityServer.Web.UI.HomeRealmDiscoveryPage
{
    protected void Page_Init( object sender, EventArgs e )
    {
        PassiveIdentityProvidersDropDownList.DataSource = base.ClaimsProviders;
        PassiveIdentityProvidersDropDownList.DataBind();
        
        // Added line. Pick up the home realm immediately 
        // (first one from the list which is the ADFS)
        SelectHomeRealm( PassiveIdentityProvidersDropDownList.SelectedItem.Value );
    }    
 
    protected void PassiveSignInButton_Click( object sender, EventArgs e )
    {
        SelectHomeRealm( PassiveIdentityProvidersDropDownList.SelectedItem.Value );
    }
}

Enjoy

Our quest is complete. The default ADFS login page can now accept any validation logic, you can for example validate users against a database or accept the email/password pair against the AD.

If you need the complete source code of this tutorial, please contact me directly.