Customize the Home Realm Discovery page to ask for UPN right away

DISCLAIMER: This post is a POC written for ADFS on Windows Server 2012 R2

When you have more than one Claim Provider Trust, this is the default user experience:

The Piaudonn Yoga is in fact my local Active Directory. The other one, Cabane, is another ADFS deployment that I am trusting. If you don't want to have to list all your trusts, or just rather have the user type its UPN for login, you can associate UPN suffixes to a claim provider trust. Here is the PowerShell cmdLet:

As soon as this association is created, here is the new user experience:

And if you click on Other Organization, you will be asked to enter your UPN to determine where we are going to redirect you:

So if I type joe@cabane.com, I'll be redirected to the trusted claim provider sign-in page:

Wouldn't it be great if the first page we get was this field where we type our UPN? If the UPN is local, then we are redirected to the local ADFS sign-in page. And if it is the UPN of a trusted claim provider, we are redirected to the corresponding sign-in page. This article is an attempt to do so.

Spoiler Alert

Before even starting I have to warn you that the story does not end super well. I don't want to give false hope, so what we are doing might not fit your requirement. But it also just might :) So let's dig into it.

Preface

All the modifications we are going to do are in the onload.js JavaScript of your webtheme. It means you need a custom webtheme before even starting. The following article explains how to modify the JavaScript:

Please, carefully read the Things to know before you start section of the above article regarding JavaScript customization. This blog also has some examples of onload.js customizations you can review if you're not familiar with this kind of customization:

In a nutshell, here is an example of cmdLet you could use to create a new webtheme and export the onload.js script to be able to modify it:

New-AdfsWebTheme -Name Yoga -SourceName Default

mkdir C:\ExportYoga

Export-AdfsWebTheme -Name Yoga -DirectoryPath C:\ExportYoga

You will find the onload.js file in the C:\ExportYoga\script folder. When you modify the file, to re-import it into the webtheme, the cmdLet is:

Set-AdfsWebTheme -TargetName Yoga -AdditionalFileResource @{Uri="/adfs/portal/script/onload.js";Path="c:\ExportYoga\script\onload.js"}

Now that you have the basics, we can go ahead with some fancy modifications!

Changing the default behavior

When you added the OrganizationalAccountSuffix with the cmdLet aforementioned, the Home Realm Discovery page has changed. If you look at the JavaScript executed for this new page, there is a function named showEmailInput. This function is called when the user is clicking on the "Other organization" tile or text. We can start by calling this function right away when the HRD page is loading:

 //Check if we are displaying the hrd page
var myCheckHRD = document.getElementById('hrdArea') ;
if (myCheckHRD) {
    //Directly show the email input form
    HRD.showEmailInput();
}

We started by making sure we are currently displaying the HRD page. The onload.js is loaded for every page the user is seeing. It is very important to check before modifying anything that the objects we are trying to update are actually in the page. If there are not, it will generate JavaScript error and could even cause the execution of the code to stop. This modification will redirect us directly the to the UPN (email...) form:

Several modifications to do on this page.

A is useless. If we click on it, it will display the page we don't want to display. > To be removed

B is inaccurate. > To be removed

C is also inaccurate. We are going to use the same form for both accounts from Active Directory and accounts from a trusted claim provider. Hence the text should be more neutral. > To be updated

For the first one, we just replace the current content by nothing. The second is actually in the same tag as the first. So setting A to nothing also sets B to nothing. For the third, we just update the content:

      //Remove the image and the login description message
     document.getElementsByClassName('groupMargin')[1].innerHTML = "" ;
     document.getElementsByClassName('groupMargin')[2].innerHTML = "Enter your credentials:" ;

Note that we are using an array of getElementsByClassName('groupMargin') because those sections do not have an identifier. Here is the result, when we display the HRD page, we are automatically redirected to:

This looks already better! At this point, the problem is that if you enter local credentials (from the local Active Directory), this will not work.

And we cannot associate an organizational suffix to the local Active Directory:

Override the submitEmail function

To enable the redirection to the local sign-in page, we will override the existing submitEmail function to add a redirection if the user is typing a login finishing by @ad.piaudonn.com (the UPN suffix of the local Active Directory). Besides, you don't want the user to type the logon again, so you will pass along what the user already typed to the local sign-in page. This is what it will look like:

      var myURL = window.location.href ;
     //Override of the submitEmail function
     HRD.submitEmail = function () {
           var u = new InputUtil() ;
           var e = new HRDErrors() ;
           var email = document.getElementById(HRD.emailInput);
           //Detect if the user typed the AD suffix
           if (email.value.toLowerCase().match('(@ad.piaudonn.com)$')) {
                //Calculate the URL to redirect the user to the AD form
                if ( myURL.indexOf("?") != -1 ) {
                    var myRedirectURL = myURL + "&RedirectToIdentityProvider=http%3a%2f%2fadfs.piaudonn.com%2fadfs%2fservices%2ftrust&userName=" + email.value ;
                } else {
                    var myRedirectURL = myURL + "?RedirectToIdentityProvider=http%3a%2f%2fadfs.piaudonn.com%2fadfs%2fservices%2ftrust&userName=" + email.value ;
                }
                window.location.href = myRedirectURL ;
                return false ;
           }
           if (!email.value || !email.value.match('[@]')) {
                u.setError(email, e.invalidSuffix) ;
                return false ;
           }   
           return true ;
     };

Note this will also publicly disclose your UPN on the source code of the page. This shouldn't be a big deal since most of UPN nowadays are matching the email address, so it's quite public already.

I spare you the subtle details, we are getting the current URL of the page, and we add the RedirectToIdentityProvider parameter to it with the identifier of the local ADFS farm. If you don't know what it is, you can see it in the ADFS administration console:

You also need to escape the colon and slashes with %3a and %2f. So https://adfs.piaudonn.com/adfs/services/trust becomes http%3a%2f%2fadfs.piaudonn.com%2fadfs%2fservices%2ftrust. We also pass the parameter username with what the user just typed. We also need to check if the current URL of the page has a ?. If not, we will have to append it as well. Here is the result, we are redirected to the local sign-in page and the username field is pre filled with...

Weirdly, we end up with a comma in front of the username. The issue here is that the form is already passing the username parameter with an empty string. You can pass 2 times the same parameter but it becomes an array (hence the ","). So we are renaming the existing username parameter to usernameold. Then the browser is not confused anymore.

             //Check if the URL has an empty username
                if ( myURL.indexOf("username=") != -1 ) {
                     //Discard the old name
                     myURL = myURL.replace("username=","userNameOld=") ;
                }

Now the username field has the right value!

It looks rather good, what are you talking about in your spoiler alert?

The limitation of this JavaScript tweak is that we cannot pass along the username that the user just typed to the trusted claim provider page. So if the user is typing joe@ad.piaudonn.com he is redirected to the local sign-in page with the username pre entered, however if he types joe@cabane.com he gets redirected to the trusted claim provider sign-in page with the username field empty. It needs to be typed twice...  If you still want to use that trick, here is the full JavaScript code:

 //Check if we are displaying the hrd page
var myCheckHRD = document.getElementById('hrdArea') ;
if ( myCheckHRD ) {
     //Directly show the email input form
     HRD.showEmailInput();
     //Remove the image and the login description message
     document.getElementsByClassName('groupMargin')[1].innerHTML = "" ;
     document.getElementsByClassName('groupMargin')[2].innerHTML = "Enter your credentials:" ;
     //Get the current URL
     var myURL = window.location.href ;
     //Override of the submitEmail function
     HRD.submitEmail = function () {
           var u = new InputUtil() ;
           var e = new HRDErrors() ;
           var email = document.getElementById(HRD.emailInput);
           //Detect if the user typed the AD suffix
           if (email.value.toLowerCase().match('(@ad.piaudonn.com)$')) {
                //Calculate the URL to redirect the user to the AD form
             //Check if the URL has an empty username
                if ( myURL.indexOf("username=") != -1 ) {
                     //Discard the old name
                     myURL = myURL.replace("username=","userNameOld=") ;
                }
                if ( myURL.indexOf("?") != -1 ) {
                    var myRedirectURL = myURL + "&RedirectToIdentityProvider=http%3a%2f%2fadfs.piaudonn.com%2fadfs%2fservices%2ftrust&userName=" + email.value ;
                } else {
                    var myRedirectURL = myURL + "?RedirectToIdentityProvider=http%3a%2f%2fadfs.piaudonn.com%2fadfs%2fservices%2ftrust&userName=" + email.value ;
                }
                window.location.href = myRedirectURL ;
                return false ;
           }
           if (!email.value || !email.value.match('[@]')) {
                u.setError(email, e.invalidSuffix) ;
                return false ;
           }   
           return true ;
     };
}

Another approach?

This is an example of an approach you could also consider. Instead of having the user typing its entire UPN, we could just ask to enter the domain and redirect accordingly. In this case, when the user is redirected to the local sign-in page, it will be requested to type the entire username. If the user typed the UPN suffix of the trusted claim provider, at this point it will be asked to enter the entire username. So the username isn't typed twice... Besides, if those pages are just requiring the user to type his samaccount name (like described here), it will first enter just the domain name, validate and then enter just the samaccountname.

We could also do something hybrid for local users. If the user typed his entire UPN and the user is local, then we redirect him to the local sign-in page with the complete username already pre-filled. If the user just type the UPN suffix, then it is redirected to the local sign-in page if the UPN suffix was the local one or to the trusted sign-in page if it was the UPN suffix of the trusted sign-in page. This might be a bit confusing for the user since we have to make it clear on the HRD UPN field that we are expecting only a domain name if we are coming from a trusted provider... Something like this:

To enable this scenario, we could use the following JavaScript:

 //Check if we are displaying the hrd page
var myCheckHRD = document.getElementById('hrdArea') ;
if ( myCheckHRD ) {
     //Directly show the email input form
     HRD.showEmailInput();
     //Remove the image and the login description message
     document.getElementsByClassName('groupMargin')[1].innerHTML = "" ;
     document.getElementsByClassName('groupMargin')[2].innerHTML = "Enter your organization's domain name.
If you are a user from Yoga Corporation, you can also enter your complete UPN." ;
     document.getElementById(HRD.emailInput).placeholder = "example.com" ;
     //Get the current URL
     var myURL = window.location.href ;
     //Override of the submitEmail function
     HRD.submitEmail = function () {
           var u = new InputUtil() ;
           var e = new HRDErrors() ;
           var data = document.getElementById(HRD.emailInput);
           //Detect if the user typed the AD suffix
           if (data.value.toLowerCase().match('(@ad.piaudonn.com)$')) {
                //Calculate the URL to redirect the user to the AD form
              //Check if the URL has an empty username
                if ( myURL.indexOf("username=") != -1 ) {
                     //Discard the old name
                     myURL = myURL.replace("username=","userNameOld=") ;
                }
                if ( myURL.indexOf("?") != -1 ) {
                    var myRedirectURL = myURL + "&RedirectToIdentityProvider=http%3a%2f%2fadfs.piaudonn.com%2fadfs%2fservices%2ftrust&userName=" + data.value ;
                } else {
                    var myRedirectURL = myURL + "?RedirectToIdentityProvider=http%3a%2f%2fadfs.piaudonn.com%2fadfs%2fservices%2ftrust&userName=" + data.value ;
                }
                window.location.href = myRedirectURL ;
                return false ;
           }
      //If it is only the local domain name redirect without the username
           if (data.value.toLowerCase().match('(ad.piaudonn.com)$')) {
                //Calculate the URL to redirect the user to the AD form
                if ( myURL.indexOf("?") != -1 ) {
                    var myRedirectURL = myURL + "&RedirectToIdentityProvider=http%3a%2f%2fadfs.piaudonn.com%2fadfs%2fservices%2ftrust" ;
                } else {
                    var myRedirectURL = myURL + "?RedirectToIdentityProvider=http%3a%2f%2fadfs.piaudonn.com%2fadfs%2fservices%2ftrust" ;
                }
                window.location.href = myRedirectURL ;
                return false ;
           }
      //Else we are adding the @ in front of the data typed by the user to enable redirection
     document.getElementById(HRD.emailInput).value = "@" + data.value ;
           return true ;
     };
}

I use the wording UPN throughout this article and the GUI, maybe it might be less geeky to use the word email... It's up to you!

Better ideas?

Please share them with us in the comments section!