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.")