Skip to main content

Introduction

Dozens of articles (several of which we have authored ourselves) deal with customizing NetScaler portal themes. This is yet another, but one with a few interesting tricks. We have previously covered adding text, links, and images to various areas of the AAA-TM or Citrix Gateway logon pages with the RfWebUI theme. We have also written several articles that create specific actions using the advanced authentication nFactor “custom labels” functionality made possible with the RfWebUI-based portal themes.

In this article, we will cover a few scenarios using custom AAA-TM login schemas and custom labels we’ll invoke in the theme’s script.js file (located in /var/netscaler/logon/themes/<your_custom_theme>/ and auto-propagate as with all theme files to secondary nodes in an HA pair) such as:

  • Adding and styling text without the use of rewrite policies and CSS file customizations (CTX285214 or our rewrite article)
  • Dynamically retrieving text to render on the login page ( rotating random messages)
  • Inserting a video into the login page
  • Inserting client IP and location details
  • Inserting weather based on IP location

One advantage of using this method is the ability to render unique elements on different login schemas within an authentication flow. If we simply used the following classes (which we can also style within script.js if we wish to avoid editing CSS), they may appear throughout the authentication flow, which may be undesirable.

$('.customAuthHeader').html("Example one - top of login screen");
$('.customAuthTop').html("Example two - above login box");
$('.customAuthBottom').html("Example three - below login box");
$('.customAuthFooter').html("Example four - bottom of login screen");

All of these examples were performed on NetScaler 14.1 b25.56 but should work fine on 13.1 as well. They rely on RfWebUI-based portal themes (the only supported portal theme type on the NetScaler currently; all others are deprecated).

In addition to the script.js file, edits may be made to login schema XML files used in your given use case. These are found at this location on the NetScaler and auto-propagate to secondary nodes in an HA pair: /nsconfig/loginschema/LoginSchema

For a refresh on NetScaler nFactor components, this cheat sheet is very useful.

To paraphrase NetScaler nFactor documentation, we can create specific “requirements” entries of different types and labels within a login schema XML file (which directs the portal theme on what to render on the screen for both browser and Citrix client access methods). These elements allow us to introduce JavaScript code we define in the theme’s script.js file and HTML code. This opens many opportunities for customization, as per the numerous NetScaler and community articles available online.

A Quick Note On Login Schema Requirement Types

The NetScaler has a standard set of “types” it accepts and validates login schema contents against when binding a login schema in the admin UI. For example, many custom labels are found to use types that include “nsg-custom-label” and “nsg-custom-cred”. While you may specify these and bind them to a login schema profile without error, this is a bit limiting if you wish, for example, to insert different custom labels within a portal theme, either on the same login schema or different ones.

In our examples, we will use nsg-custom-label1 and nsg-custom-label2 in the login schema to illustrate adding multiple different JavaScript functions that a login schema can invoke. Please note that when binding these login schemas in the UI, an error message as below will be generated. It does not prevent the actual binding of the schema, however, and is indicative of NetScaler’s limitation in validating custom labels outside of its defaults vs. scanning script.js files in any custom theme on the appliance to check for mentions of a non-standard label.

This is being mentioned to avoid confusion and concern because the use of non-standard type labels is essential to allow for multiple functions within a theme to be defined and do work.

Equally important, if the Login Schema has a requirement type that does not correspond to something in the script.js file of the theme, the page will not render correctly. Upon checking the console of the browser’s dev mode, the following might be displayed. So be sure the “Requirements” align when adding a custom type.

Stylizing Text

In the following example, we want to add text to the login page and style it so it stands out more. By just adding in a new “Requirement” into the desired location (in this case, under the credentials fields) the text will inherit the format defined in the theme.

Note: The <Credential>…</Credential> lines below are not required but can be kept in for proper structure.

If we change the ‘type’ following the text in the login schema XML file, we can change the text slightly, but it renders it as an h1 if we inspect the page element (not really ideal), and to further style this, we’d need to edit the theme.css file.

Note: The <Credential>…</Credential> lines below are not required but can be kept in for proper structure.

To avoid styling the text using a rewrite in combination with a theme.css entry as per the earlier mentioned KB article, we will go about switching this over to a custom label config. First, we will omit the text from the login schema and place the Requirement text within a suitable location within our login schema XML file. In this case, under the credentials and submit button. Note, you can use just “nsg-custom-label” if this is all you plan to add to the theme, but add a 1 to the name or use another unique name if you intend to add more later, or add the next one you add as a non-standard type such as “ng-custom-label1”. The choice is yours. We must ensure whatever we use matches what we add into script.js.

<Requirement>
<Label>
<Text></Text>
<Type>nsg-custom-label1</Type>
</Label>
</Requirement>

Now, edit your custom theme’s RfWebUI script.js file (we assume the theme is already bound to the AAA-TM / Gateway vServers) and add the following code. Update the text to your needs, the styling, and adjust the “nsg-custom-label1” if necessary to match whatever you’ve chosen from the prior step.

 // Custom Label Handler for Notice
CTXS.ExtensionAPI.addCustomAuthLabelHandler({
getLabelTypeName: function () { return "nsg-custom-label1"; },
getLabelTypeMarkup: function (requirements) {
return $('<div style="font-size: 16px; font-weight: bold; color: red; text-align: left;">By logging into this system, you declare your eternal allegiance to Acme Corporation</div>');
},
// Instruction to parse the label as if it was a standard type
parseAsType: function () {
return "plain";
}
});

With the correct syntax in place and the label types matching in both files, we have our newly stylized text.

Dynamically Retrieve Text

If your organization often rotates messages and you do not want to continuously log into the NetScaler to modify the script.js file, you can host a text file on a public site and have the text retrieved dynamically at page load. This modified code will do the trick:

// Custom Label Handler for Notice
CTXS.ExtensionAPI.addCustomAuthLabelHandler({
    getLabelTypeName: function () { return "nsg-custom-label1"; },
    getLabelTypeMarkup: function (requirements) {
        // Create a placeholder div
        const placeholder = $('<div id="custom-text" style="font-size: 16px; font-weight: bold; color: red; text-align: left;">Loading text...</div>');

        // Fetch the external text file and update the div content
        $.get('https://example.com/path-to-your-text-file.txt', function(data) {
            placeholder.html(data);
        }).fail(function() {
            placeholder.html('Failed to load text.');
        });

What about, for “reasons,” the organization wished to have a random message displayed, such as an inspirational quote of the day? No problem, just tweak the JavaScript code to randomly retrieve one line item per page load.

// Custom Label Handler for Notice
CTXS.ExtensionAPI.addCustomAuthLabelHandler({
    getLabelTypeName: function () { return "nsg-custom-label1"; },
    getLabelTypeMarkup: function (requirements) {
        // Create a placeholder div
        const placeholder = $('<div id="custom-text" style="font-size: 16px; font-weight: bold; color: red; text-align: left;">Loading quote...</div>');

        // Fetch the external text file
        $.get('https://example.com/path-to-your-text-file.txt', function(data) {
            // Split the file content by line
            const lines = data.split('\n').filter(line => line.trim() !== '');

            // Randomly select a line
            const randomLine = lines[Math.floor(Math.random() * lines.length)];

            // Update the placeholder with the selected line
            placeholder.html(randomLine);
        }).fail(function() {
            placeholder.html('Failed to load quote.');
        });

        return placeholder;
    },
    // Instruction to parse the label as if it was a standard type
    parseAsType: function () {
        return "plain";
    }
});

Randomly Display a Meme

Following on with the random quote concept, we can leverage the text file concept with per-line entries per image file. As JavaScript cannot dynamically read the contents of a folder, we would need a server-side PHP script to do that, so let’s not go down that garden path.

Here is a sample images.txt file:

https://www.example.com/images/image1.jpg
https://www.example.com/images/image2.jpg
https://www.example.com/images/image3.jpg
https://www.example.com/images/image4.jpg

And here is the JavaScript code to randomly pull an image path from the images.txt file:

// Custom Label Handler for Notice
CTXS.ExtensionAPI.addCustomAuthLabelHandler({
    getLabelTypeName: function () { return "nsg-custom-label1"; },
    getLabelTypeMarkup: function (requirements) {
        // Create a placeholder div
        const placeholder = $('<div id="custom-image" style="text-align: center;"></div>');

        // Fetch the text file with image paths
        $.get('https://www.example.com/path-to-your/images.txt', function(data) {
            // Split the file content by line to get an array of image paths
            const imagePaths = data.split('\n').filter(line => line.trim() !== '');

            // Randomly select an image path
            const randomImagePath = imagePaths[Math.floor(Math.random() * imagePaths.length)];

            // Create an image element and set its source
            const imgElement = $('<img>').attr('src', randomImagePath).attr('alt', 'Random Image').css({
                'max-width': '100%',
                'height': 'auto'
            });

            // Add the image to the placeholder
            placeholder.append(imgElement);
        }).fail(function() {
            placeholder.html('Failed to load images.');
        });

        return placeholder;
    },
    // Instruction to parse the label as if it was a standard type
    parseAsType: function () {
        return "plain";
    }
});

Inserting a Video

For something slightly more practical, we can inject a video into the page, ideal for user training and self-help resources. In this example, we will add another “Requirements” item (not necessary however) to the login schema directly underneath the first one we added, with type “nsg-custom-label2”.  In the example below, we have appended the script.js file with the new code in addition to the first code.

Here are the example login schema “Requirements” XML snippets we would insert into an existing login schema XML file (change the IDs to suit your needs). Note that in this case, they are consecutive to each other, but depending on the login schema layout, you may wish to place them elsewhere such as below the Submit button or something.

<Requirement>
<Label>
<Text></Text>
<Type>nsg-custom-label1</Type>
</Label>
</Requirement>
<Requirement>
<Label>
<Text></Text>
<Type>nsg-custom-label2</Type>
</Label>
</Requirement>

If you do edit your login schema XML file to have multiple custom “Requirements” as per this example, please remember to re-bind it to the profile so it loads the update into memory of the appliance.

 // Custom Label Handler for Notice
CTXS.ExtensionAPI.addCustomAuthLabelHandler({
getLabelTypeName: function () { return "nsg-custom-label1"; },
getLabelTypeMarkup: function (requirements) {
return $('<div style="font-size: 16px; font-weight: bold; color: red; text-align: left;">By logging into this system, you declare your eternal allegiance to Acme Corporation</div>');
},
// Instruction to parse the label as if it was a standard type
parseAsType: function () {
return "plain";
}
});

 // Custom Label Handler for Notice
CTXS.ExtensionAPI.addCustomAuthLabelHandler({
getLabelTypeName: function () { return "nsg-custom-label2"; },
getLabelTypeMarkup: function (requirements) {
return $('<iframe width="560" height="315" src="https://www.youtube-nocookie.com/embed/1EevEHh58B0?si=5ye1YmkZJb0KbC-S&amp;controls=0" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>');
},
// Instruction to parse the label as if it was a standard type
parseAsType: function () {
return "plain";
}
});

Inserting Client IP and Geolocation Info

This next one will use the same logic of appending the script.js file with an additional custom label. It will use the nsg-custom-label2 type to link the Requirement to the label that invokes the JavaScript code. This scenario will inject the client’s IP, city, country, and ISP onto the login page. Displaying this information to end-users may be useful for IT support personnel.

Note that there are numerous IP databases online, you can find one that suits the organization’s needs. For the example below, we are using a proxy server that will require the end-user to visit https://cors-anywhere.herokuapp.com/corsdemo and click the “Request temporary access to the demo server” button before testing (loading the page). In a real-world scenario, an administrator would register for one of the many IP database services and use an API key to retrieve the details to overcome these limitations.

The following script.js assumes both the nsg-custom-label1 and nsg-custom-label2 are present in the login schema, but it can be modified as required to only use one of them.

 // Custom Label Handler for Notice
CTXS.ExtensionAPI.addCustomAuthLabelHandler({
getLabelTypeName: function () { return "nsg-custom-label1"; },
getLabelTypeMarkup: function (requirements) {
return $('<div style="font-size: 16px; font-weight: bold; color: red; text-align: left;">By logging into this system, you declare your eternal allegiance to Acme Corporation</div>');
},
// Instruction to parse the label as if it was a standard type
parseAsType: function () {
return "plain";
}
});

        // Function to get the client's IP address
        function getClientIP(callback) {
            $.getJSON('https://api.ipify.org?format=json', function(data) {
                callback(data.ip);
            });
        }

        // Function to get geolocation data from IP
        function getGeolocation(ip, callback) {
            $.getJSON(`https://ipwhois.app/json/${ip}`, function(data) {
                callback(data);
            });
        }

        // Function to get geolocation data from IP using proxy
        function getGeolocation(ip, callback) {
            const proxyUrl = 'https://cors-anywhere.herokuapp.com/';
            const targetUrl = `https://ipwhois.app/json/${ip}`;
            $.getJSON(proxyUrl + targetUrl, function(data) {
                callback(data);
            }).fail(function(jqXHR, textStatus, errorThrown) {
                console.error("Error fetching IP info:", textStatus, errorThrown);
                callback(null);
            });
        }

        // Custom Label Handler for Notice
        CTXS.ExtensionAPI.addCustomAuthLabelHandler({
            getLabelTypeName: function () { return "nsg-custom-label2"; },
            getLabelTypeMarkup: function (requirements) {
                // Create a placeholder div
                const placeholder = $('<div id="custom-label" style="font-size: 16px; font-weight: bold; color: red; text-align: left;">Loading...</div>');

                // Fetch the client's IP and then geolocation data
                getClientIP(function(ip) {
                    getGeolocation(ip, function(info) {
                        if (info && info.success) {
                            const { ip, city, country, isp } = info;
                            placeholder.html(`Your IP address is: ${ip}, City: ${city}, Country: ${country}, ISP: ${isp}`);
                        } else {
                            placeholder.html('Failed to retrieve location information.');
                        }
                    });
                });

                return placeholder;
            },
            // Instruction to parse the label as if it was a standard type
            parseAsType: function () {
                return "plain";
            }
        });

Inserting Weather Based on IP Location

This is another gimmick but could enhance user experience. It relies on a proxy once again and uses a free weather site that does not require an API key. In both cases, for real-world use, administrators would want to gain access to more suitable services that do not require a proxy workaround to tag a security header. but for our example, it does the trick. Bear in mind that, as with the prior example, using the CORS proxy requires first visiting the site in the end-user’s browser first to gain temporary access to the server.

// Function to get the client's IP address
function getClientIP(callback) {
    $.getJSON('https://api.ipify.org?format=json', function(data) {
        callback(data.ip);
    });
}

// Function to get geolocation data from IP using proxy
function getGeolocation(ip, callback) {
    const proxyUrl = 'https://cors-anywhere.herokuapp.com/';
    const targetUrl = `https://ipwhois.app/json/${ip}`;
    $.getJSON(proxyUrl + targetUrl, function(data) {
        callback(data);
    }).fail(function(jqXHR, textStatus, errorThrown) {
        console.error("Error fetching IP info:", textStatus, errorThrown);
        callback(null);
    });
}

// Function to get weather data from wttr.in
function getWeather(city, callback) {
    const weatherUrl = `https://wttr.in/${city}?format=%C+%t`;
    $.get(weatherUrl, function(data) {
        callback(data);
    }).fail(function(jqXHR, textStatus, errorThrown) {
        console.error("Error fetching weather info:", textStatus, errorThrown);
        callback(null);
    });
}

// Custom Label Handler for Notice
CTXS.ExtensionAPI.addCustomAuthLabelHandler({
    getLabelTypeName: function () { return "nsg-custom-label2"; },
    getLabelTypeMarkup: function (requirements) {
        // Create a placeholder div
        const placeholder = $('<div id="custom-label" style="font-size: 16px; font-weight: bold; color: red; text-align: left;">Loading weather information...</div>');

        // Fetch the client's IP and then geolocation data
        getClientIP(function(ip) {
            getGeolocation(ip, function(info) {
                if (info && info.success) {
                    const { city } = info;
                    getWeather(city, function(weather) {
                        if (weather) {
                            placeholder.html(`The current weather in ${city} is: ${weather}`);
                        } else {
                            placeholder.html('Failed to retrieve weather information.');
                        }
                    });
                } else {
                    placeholder.html('Failed to retrieve location information.');
                }
            });
        });

        return placeholder;
    },
    // Instruction to parse the label as if it was a standard type
    parseAsType: function () {
        return "plain";
    }
});

Bonus – Blinking Text

For those missing the 1990s website aesthetic or useful application of blinking text to draw attention to important information, the following code will add a blinking treatment to our initial notice text.

// Custom Label Handler for Notice
CTXS.ExtensionAPI.addCustomAuthLabelHandler({
    getLabelTypeName: function () { return "nsg-custom-label1"; },
    getLabelTypeMarkup: function (requirements) {
        // Create a blinking text style
        const blinkStyle = `
            @keyframes blink {
                0% { opacity: 1; }
                50% { opacity: 0; }
                100% { opacity: 1; }
            }
            .blinking {
                font-size: 16px;
                font-weight: bold;
                color: red;
                text-align: left;
                animation: blink 1s infinite;
            }
        `;

        // Add the style to the document
        const styleSheet = document.createElement("style");
        styleSheet.type = "text/css";
        styleSheet.innerText = blinkStyle;
        document.head.appendChild(styleSheet);

        // Create the blinking text div
        return $('<div class="blinking">By logging into this system, you declare your eternal allegiance to Acme Corporation</div>');
    },
    // Instruction to parse the label as if it was a standard type
    parseAsType: function () {
        return "plain";
    }
});

Conclusion

These examples illustrate some of the versatility of the NetScaler RfWebUI portal theme and nFactor extensibility capabilities using custom labels. In reality, these are simple examples, but it is our hope that they will spark some creativity to devise interesting and useful ways to extend the customization and utility of NetScaler AAA-TM and Citrix Gateway login pages.

If the page renders with a “Failed to load …” message, it is prudent to inspect the page in your browser’s debug tool, visit the console tab (in Chrome’s case), and look at the error generated to find the root cause and fix it.

 

  • Michael Shuster
    Michael Shuster

    Michael is Ferroque's founder and a noted Citrix authority, overseeing operations and service delivery while keeping a hand in the technical cookie jar. He is a passionate advocate for end-user infrastructure technology, with a rich history designing and engineering solutions on Citrix, NetScaler, VMware, and Microsoft tech stacks.

Subscribe
Notify of
guest

0 Comments
Inline Feedbacks
View all comments

Redefine Your Approach to Technology and Innovation

Schedule a call to discover how customized solutions crafted for your success can drive exceptional outcomes, with Ferroque as your strategic ally.