Customizing Client Logon and Home Realm Discovery Pages

 

Active Directory Federation Services (ADFS) provides most of its services invisibly to the end user. However, there are two points where an ADFS installation typically displays a user interface: first, during the initial client logon to the federation server (FS) or federation server proxy (FS-P), and second, during the home-realm discovery process.

This topic describes how to customize the ADFS user experience at these two points.

Customizing the Client Logon User Experience

Customizing Appearance

The collection of client credentials is handled by the ASP.NET file clientlogon.aspx on both the FS and FS-P. Clientlogon.aspx contains both markup and C# code to process the receipt of user credentials.

Clientlogon.aspx <head> Markup

Opportunities for customizing the markup of the clientlogon.aspx file are located in two distinct sections. The first is the HTML <head> element, as displayed here.

<head>  
<title>Credential Collection</title>  
<meta name="robots" content="noindex, nofollow" />  
<style type="text/css">  
<!--  
body { background: white; }  
p { font-family: verdana, sans-serif; font-size: 10pt; }  
h3 { font-family: trebuchet MS, verdana, sans-serif; font-size: 16pt; }  
input { font-size: 10pt; }  
-->  
</style>  
<script type="text/javascript" language="javascript">  
<!--  
function init() {document.forms[0].Username.focus();}  
-->  
</script>  
</head>  

The markup displayed earlier is very simple. It blocks search index crawlers, sets a basic title, font and color preferences, and uses JavaScript to position input focus on the username field of the form.

You can customize this header significantly to introduce your preferred look, such as using a customized cascading style sheet (CSS).

Clientlogon.aspx <body> Markup

Additionally, you can customize the appearance of the page by modifying the body of the HTML page, which contains the layout of its form elements, as shown here.

<body>  
<pre><%=Context.Request.Url.GetLeftPart(UriPartial.Path)%>  
<%=LSAuthenticationObject.Current.FormContext.CurrentAction.ToString()%></pre>  

This portion of the pages demonstrates the use of LSAuthenticationObject. LSAuthenticationObject allows a web form (such as clientlogon.aspx) to interface with the FS or FS-P on which it runs. This interaction includes determining what action is currently being performed and gathering any data needed for that action. In the previous example, the clientlogon.aspx file is simply displaying the name of the current authentication action

The body of the HTML continues with more markup.

  
<form id="CredentialCollection" method="post" runat="server">  
  
<table cellspacing="0" cellpadding="0" border="0" width="100%">  
<tr><td align="center" valign="middle"><h3>Active Directory Federation Services</h3>  
  
<table cellspacing="1" cellpadding="0" border="0" bgcolor="#DDDDDD"><tr><td>  
  
<table cellspacing="0" cellpadding="5" border="0" bgcolor="#FFFFFF">  
<tr><td><p>Username</p></td><td><asp:textbox runat="server" id="Username" width="150px" /></td></tr>  
<tr><td><p>Password</p></td><td><asp:textbox runat="server" id="Password" TextMode="Password" width="150px" /></td></tr>  
<tr><td></td><td align="right"><asp:button runat="server" OnClick="ButtonClick" id="UsernamePasswordButton" Text="Submit" /></td></tr>  
</table>  
  
</td></tr>  
</table>  

</td></tr>  
</table>  
  
</form>  
  
</body>  

In this section, you can customize the look-and-feel of the logon form elements, error responses, and the page title displayed to users.

Customizing Authentication

The clientlogon.aspx page described previously performs authentication using forms-based authentication. You can also customize this page to perform integrated authentication or Secure Sockets Layer (SSL) client certificate authentication.

Verifying Acceptable Authentication Methods

When you attempt to access an application using integrated authentication or SSL client certificate authentication, you must verify that the application is configured to accept the desired form of authentication. AcceptableAuthenticationMethods allows you to obtain a list of acceptable forms of authentication from the application.

The following code sample verifies that the application is configured to accept integrated authentication:

LSCredentialFormContext formContext = LSAuthenticationObject.Current.FormContext as LSCredentialFormContext;  
if(formContext != null)  
{  
    ArrayList acceptableCredentials = formContext.AcceptableAuthenticationMethods;  
    if(acceptableCredentials.Count > 0 && !acceptableCredentials.Contains(ClientCredentialInfo.AuthnUriWindows))  
    {  
        throw new InvalidOperationException("The requested resource does not accept integrated authentication credentials.");  
    }  
}  

The following code sample verifies that the application is configured to accept SSL client certificate authentication:

LSCredentialFormContext formContext = LSAuthenticationObject.Current.FormContext as LSCredentialFormContext;  
if(formContext != null)  
{  
   ArrayList acceptableCredentials = formContext.AcceptableAuthenticationMethods;  
   if(acceptableCredentials.Count > 0 && !acceptableCredentials.Contains(ClientCredentialInfo.AuthnUriTlsClient))  
   {  
      throw new InvalidOperationException("The requested resource does not accept TLS client authentication credentials.");  
   }  
}  

To use AcceptableAuthenticationMethods, you must include a reference to the System.Web.Security.SingleSignOn.dll assembly in your web.config file:

<configuration>  
    <configSections>  
        <sectionGroup name="system.web">  
            <section name="websso" type="System.Web.Security.SingleSignOn.WebSsoConfigurationHandler, System.Web.Security.SingleSignOn, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, Custom=null" />  
        </sectionGroup>  
    </configSections>  
...  

You must also refer to the System.Web.Security.SingleSignOn namespace in your clientlogon.aspx page:

<%@ Import Namespace="System.Web.Security.SingleSignOn" %>  

For more information about configuring an application to accept these forms of authentication, see Configure authentication methods for a federated application.

Integrated Authentication

The following version of clientlogon.aspx performs integrated authentication. The important code and related comments are highlighted.

<%@ Page language="c#" AutoEventWireup="false" ValidateRequest="false" Async="true" %>  
<%@ OutputCache Location="None" %>  
<%@ Import Namespace="System.ComponentModel" %>  
<%@ Import Namespace="System.Data" %>  
<%@ Import Namespace="System.Drawing" %>  
<%@ Import Namespace="System.Security.Principal" %>  
<%@ Import Namespace="System.Web" %>  
<%@ Import Namespace="System.Web.SessionState" %>  
<%@ Import Namespace="System.Web.UI" %>  
<%@ Import Namespace="System.Web.UI.WebControls" %>  
<%@ Import Namespace="System.Web.UI.HtmlControls" %>  
<%@ Import Namespace="System.Web.Security.SingleSignOn" %>  
  
<?xml version="1.0" encoding="iso-8859-1" ?>  
<!DOCTYPE html PUBLIC  
          "-//W3C//DTD XHTML 1.0 Transitional//EN"  
          "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">  
<html>  
<head>  
<title>Credential Collection</title>  
<meta name="robots" content="noindex, nofollow" />  
</head>  
  
<script runat="server">  
string m_errorStatus = null;  
ArrayList acceptableCredentials = null;WindowsIdentity wi = (WindowsIdentity)HttpContext.Current.User.Identity;  
  
override protected void OnInit(EventArgs e)  
{  
    this.Load += new System.EventHandler(this.Page_Load);  
    base.OnInit(e);  
}  
  
private void Page_Load(object sender, System.EventArgs e)  
{  
    // Make sure that this page is being called by the Federation  
    // Service or Federation Service Proxy for the purpose of  
    // collecting credentials.  
    LSAuthenticationObject LogonServer = LSAuthenticationObject.Current;  
    if(null==LogonServer)  
    {  
        throw new ApplicationException("This page should not be accessed directly.");  
    }  
    else if (LogonServer.FormContext.CurrentAction != LSFormAction.CollectInitialCredentials &&  
             LogonServer.FormContext.CurrentAction != LSFormAction.CollectAdditionalCredentials)  
    {  
        StringBuilder sb = new StringBuilder();  
        sb.Append("This page has been called with the wrong action.");  
        sb.Append(Environment.NewLine);  
        sb.Append("Expected Action: CollectInitialCredentials or CollectAdditionalCredentials");  
        sb.Append(Environment.NewLine);  
        sb.Append("Actual Action: ");  
        sb.Append(LogonServer.FormContext.CurrentAction.ToString());  
  
        throw new ApplicationException(sb.ToString());  
    }  
  
    // Registers an asynchronous operation with the page.    Page.AddOnPreRenderCompleteAsync(new BeginEventHandler(BeginAsyncLogon),                                     new EndEventHandler(EndAsyncLogon));  
}  
  
IAsyncResult BeginAsyncLogon(object sender, EventArgs args, AsyncCallback cb, object state){    // Verify that Windows integrated authentication is acceptable.    LSCredentialFormContext formContext = LSAuthenticationObject.Current.FormContext as LSCredentialFormContext;    if (formContext != null)    {       ArrayList acceptableCredentials = formContext.AcceptableAuthenticationMethods;       if(acceptableCredentials.Count > 0 && !acceptableCredentials.Contains(ClientCredentialInfo.AuthnUriWindows))       {          throw new InvalidOperationException("The requested resource does not accept integrated authentication credentials.");       }    }    // Begin asynchronous work handler.    try     {        return LSAuthenticationObject.BeginLogonClient(wi, cb, state);    }     catch(Exception ex)     {         m_errorStatus = ex.Message;        return null;     }}  
  
void EndAsyncLogon(IAsyncResult ar)  
{  
    try   
    {  
        LSAuthenticationObject.EndLogonClient(ar);  
    }   
    catch(Exception ex)   
    {   
        m_errorStatus = ex.Message;   
    }  
}  
</script>  
  
<body>  
  
<pre><%=Context.Request.Url.GetLeftPart(UriPartial.Path)%>  
<%=LSAuthenticationObject.Current.FormContext.CurrentAction.ToString()%>  
<%=wi.Name%> (<%=wi.AuthenticationType%>)  
</pre>  
  
<% if(m_errorStatus != null)   
   {  
%>     <p style="color:red">There was an error processing your credentials: <%=m_errorStatus%></p>  
<% } %>  
  
</body>  
</html>  

SSL Client Certificate Authentication

The following version of clientlogon.aspx performs SSL client certificate authentication. The important code and related comments are highlighted.

<%@ Page language="c#" AutoEventWireup="false" ValidateRequest="false"  Async="true" %>  
<%@ OutputCache Location="None" %>  
<%@ Import Namespace="System.ComponentModel" %>  
<%@ Import Namespace="System.Data" %>  
<%@ Import Namespace="System.Drawing" %>  
<%@ Import Namespace="System.Web" %>  
<%@ Import Namespace="System.Web.SessionState" %>  
<%@ Import Namespace="System.Web.UI" %>  
<%@ Import Namespace="System.Web.UI.WebControls" %>  
<%@ Import Namespace="System.Web.UI.HtmlControls" %>  
<%@ Import Namespace="System.Web.Security.SingleSignOn" %>  
  
<?xml version="1.0" encoding="iso-8859-1" ?>  
<!DOCTYPE html PUBLIC  
          "-//W3C//DTD XHTML 1.0 Transitional//EN"  
          "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">  
<html>  
<head>  
<title>Credential Collection</title>  
<meta name="robots" content="noindex, nofollow" />  
</head>  
  
<script runat="server">  
string m_errorStatus = null;  
ArrayList acceptableCredentials = null;HttpClientCertificate cert = HttpContext.Current.Request.ClientCertificate;  
  
override protected void OnInit(EventArgs e)  
{  
    this.Load += new System.EventHandler(this.Page_Load);  
    base.OnInit(e);  
}  
  
private void Page_Load(object sender, System.EventArgs e)  
{  
    // Make sure that this page is being called by the Federation  
    // Service or Federation Service Proxy for the purpose of  
    // collecting credentials.  
    LSAuthenticationObject LogonServer = LSAuthenticationObject.Current;  
    if(null==LogonServer)  
    {  
        throw new ApplicationException("This page should not be accessed directly.");  
    }  
    else if (LogonServer.FormContext.CurrentAction != LSFormAction.CollectInitialCredentials &&  
             LogonServer.FormContext.CurrentAction != LSFormAction.CollectAdditionalCredentials)  
    {  
        StringBuilder sb = new StringBuilder();  
        sb.Append("This page has been called with the wrong action.");  
        sb.Append(Environment.NewLine);  
        sb.Append("Expected Action: CollectInitialCredentials or CollectAdditionalCredentials");  
        sb.Append(Environment.NewLine);  
        sb.Append("Actual Action: ");  
        sb.Append(LogonServer.FormContext.CurrentAction.ToString());  
  
        throw new ApplicationException(sb.ToString());  
    }  
  
    // Register an asynchronous operation with the page.    Page.AddOnPreRenderCompleteAsync(new BeginEventHandler(BeginAsyncLogon),                                     new EndEventHandler(EndAsyncLogon));  
}  
  
IAsyncResult BeginAsyncLogon(object sender, EventArgs args, AsyncCallback cb, object state){    // Verify that SSL client authentication is acceptable.    LSCredentialFormContext formContext = LSAuthenticationObject.Current.FormContext as LSCredentialFormContext;    if (formContext != null)    {       ArrayList acceptableCredentials = formContext.AcceptableAuthenticationMethods;       if(acceptableCredentials.Count > 0 && !acceptableCredentials.Contains(ClientCredentialInfo.AuthnUriTlsClient))       {          throw new InvalidOperationException("The requested resource does not accept TLS client authentication credentials.");       }    }    // Begin asynchronous work handler.    ClientCredentialInfo creds = ClientCredentialInfo.CreateCertificateCredential(cert);    try     {        return LSAuthenticationObject.BeginLogonClient(creds, cb, state);    }     catch(Exception ex)     {         m_errorStatus = ex.Message;        return null;     }}  
  
void EndAsyncLogon(IAsyncResult ar)  
{  
    try   
    {  
        LSAuthenticationObject.EndLogonClient(ar);  
    }   
    catch(Exception ex)   
    {   
        m_errorStatus = ex.Message;   
    }  
}  
</script>  
  
<body>  
  
<pre><%=Context.Request.Url.GetLeftPart(UriPartial.Path)%>  
<%=LSAuthenticationObject.Current.FormContext.CurrentAction.ToString()%>  
<I><%=cert.Issuer%><S><%=cert.Subject%>  
</pre>  
  
<% if(m_errorStatus != null)   
   {  
%>     <p style="color:red">There was an error processing your credentials: <%=m_errorStatus%></p>  
<% } %>  
  
</body>  
</html>  

Customizing the Home Realm Discovery User Experience

The ASP.NET file DiscoverClientRealm.aspx on the FS and FS-P generates the user interface that gives users the opportunity to indicate their home realm during the initial logon process.

DiscoverClientRealm.aspx <head> Markup

The <head> element in the home realm discovery page primarily consists of ASP.NET code, as displayed here. However, there are opportunities to change the page title, define cascading style sheets, and define other visual elements.

<head>  
<title>Client Realm Discovery</title>  
<meta name="robots" content="noindex, nofollow" />  
  
<script runat="server">  
override protected void OnInit(EventArgs e)  
{  
    this.RealmSubmissionButton.Click += new System.EventHandler(this.ButtonClick);  
    this.Load += new System.EventHandler(this.Page_Load);  
    base.OnInit(e);  
}  
  
private void Page_Load(object sender, System.EventArgs e)  
{  
    LSAuthenticationObject LogonServer = LSAuthenticationObject.Current;  
    if(null==LogonServer)  
    {  
        throw new ApplicationException("This page should not be accessed directly.");  
    }  
    else if (LogonServer.FormContext.CurrentAction != LSFormAction.DiscoverClientRealm)  
    {  
        StringBuilder sb = new StringBuilder();  
        sb.Append("This page has not been called with the correct action.");  
        sb.Append(Environment.NewLine);  
        sb.Append("Expected Action: DiscoverClientRealm");  
        sb.Append(Environment.NewLine);  
        sb.Append("Actual Action: ");  
        sb.Append(LogonServer.FormContext.CurrentAction.ToString());  
  
        throw new ApplicationException(sb.ToString());  
    }  
    else if(LogonServer.FormContext.IsClientNonInteractive)  
    {  
        throw new ApplicationException("Home realm unknown for a non-interactive client. Non-interactive clients must indicate their homerealm with a cookie or query string parameter.");  
    }  
  
    if (!IsPostBack)  
    {  
        LSDiscoveryFormContext dc = (LSDiscoveryFormContext)LogonServer.FormContext;  
        RealmList.DataSource = dc.DiscoveryTable;  
        RealmList.DataTextField = LSDiscoveryFormContext.DisplayNameColumn;  
        RealmList.DataValueField = LSDiscoveryFormContext.UriColumn;  
        RealmList.DataBind();  
    }  
}  
  
private void ButtonClick(object sender, System.EventArgs e)  
{  
    // redirect to the Account logon server  
    LSAuthenticationObject.Current.RedirectToAccountFederationPartner(RealmList.SelectedItem.Value);  
}  
</script>  
</head>  

DiscoverClientRealm.aspx <body> Markup

Perhaps the widest set of opportunities for customizing the look of the home realm discovery page is in the <body> element of the DiscoverClientRealm.aspx file. Here you can customize the text that instructs the user to select their home realm and change the layout and appearance of the form elements that they use to do this. An example of this is shown here.

<body>  
  
<pre><%=Context.Request.Url.GetLeftPart(UriPartial.Path)%>  
<%=LSAuthenticationObject.Current.FormContext.CurrentAction.ToString()%></pre>  
  
<form id="DiscoverClientRealm" runat="server" method="post">  
<table>  
<tr><td>Choose your home realm.</td></tr>  
<tr><td><asp:dropdownlist id="RealmList" runat="server" Height="34px" Width="243px" /></td></tr>  
<tr><td><asp:button id="RealmSubmissionButton" runat="server" Text="Submit" /></td></tr>  
</table>  

</form>  
</body>  

Automatic Home Realm Discovery

You can also specify the home realm for an AD FS enabled application instead of asking the user to select it. To do so, add the whr query parameter to the URL of an AD FS enabled application. The whr query parameter has the following form:

whr= <Federation service URI of home realm>

An example of a URL with the home realm specified using the whr parameter is:

https://mydomain/myadfsapp/?whr=urn:federation:contoso  

Modifying the Lifetime of the User's Home Realm Selection

When the user selects a home realm, the user's selection persists for 30 days or until the user deletes his or her cookies. You can let the user's selection persist for a duration other than 30 days by modifying the RealmCookieLifetimeInDays entry in the trust policy. We recommend using a script to modify the trust policy.

The following VBScript modifies the home realm selection cookie lifetime that is written by the resource Federation Server to a persistent cookie. This script requires CScript, which can be set as the default script engine by typing cscript /H:CScript at a command prompt.

' VBScript source code  
  
Option Explicit  
  
Dim tpf ' Trust policy factory  
Dim tp ' TrustPolicy  
Dim filename ' Trust policy filename  
Dim lifetime ' Realm lifetime in days  
Dim args  
  
Set args=WScript.Arguments  
  
if args.Count < 2 then  
    WScript.Echo("Must specify trust policy file and realm cookie lifetime in days")  
    WScript.Quit  
end if  
  
filename = args.Item(0)  
lifetime = CInt(args.Item(1))  
  
'  
' Create factory  
'  
set tpf = CreateObject("System.Web.Security.SingleSignOn.TrustPolicyFactory")  
  
'  
' Create the TrustPolicy  
'  
set tp = tpf.Load(filename, False) ' filename, don't initialize certificates  
  
'  
' Hosted realm attributes  
'  
tp.RealmCookieLifetimeInDays = lifetime  
  
tp.Write(filename)  
  
WScript.Echo(filename & " has been updated for a realm cookie lifetime of " & lifetime & " days.")