DEV Community

Christos Matskas for The 425 Show

Posted on

Customize the look and feel of your Azure AD B2C page

Azure AD B2C at its base is a username and password database that you can use to integrate in your apps and implement delegated authentication. It also allows you to add social media logins and beyond that, bring any OIDC-compatible provider to your login page. So users can choose how to sign up/sign in to your application.

Out of the box, the Azure AD B2C pages come with a default look and feel but in most cases, you'll want to optimize the user experience by customizing that look to match your application's UX.

In this blog post I'll show you 2 things:

  1. How to use custom HTML for your sign up/sign in page
  2. How to change the tab order on the page

Prerequisites

I will assume that you already have an Azure AD B2C tenant, with existing app registrations and flows. So we'll dive straight into the customization bit.

Create the new custom page

The requirement is simple. You need a canonical HTML page and somewhere on that page you need a div tag with the following format:

<div id="api">
Enter fullscreen mode Exit fullscreen mode

This is where B2C dynamically injects the login controls, including any social media buttons, if you have social media logins enabled. I have created a fully customized login form that makes use of Bootstrap. For testing my page locally, I've also added two things:

  1. a dependency to jQuery
  2. a copy of the html controls that will be injected in the actual page

NOTE: B2C injects jQuery on the rendered HTML page

Before uploading my page to Azure Storage, I'll be removing both the jQuery dependency and the dummy HTML controls since we'll be using the "real" thing :)

My full custom page (including the CSS and JS) is attached below:

<!DOCTYPE html>
<html lang="en-US">

<head>

    <title>Sign up or sign in</title>

    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta charset="utf-8">
    <meta name="locale" content="en-US">
    <meta name="ROBOTS" content="NONE, NOARCHIVE">
    <meta name="GOOGLEBOT" content="NOARCHIVE">
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=2.0, user-scalable=yes">
    <script src="https://code.jquery.com/jquery-3.5.1.min.js" integrity="sha256-9/aliU8dGd2tb6OSsuzixeV4y/faTqgFtohetphbbj0=" crossorigin="anonymous"></script>
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
    <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>

<style>
    @import url(https://fonts.googleapis.com/css?family=Open+Sans);

    * {
        -webkit-box-sizing: border-box;
        -moz-box-sizing: border-box;
        -ms-box-sizing: border-box;
        -o-box-sizing: border-box;
        box-sizing: border-box;
    }

    html {
        width: 100%;
        height: 100%;
        overflow: hidden;
    }

    body {
        width: 100%;
        height: 100%;
        font-family: 'Open Sans', sans-serif;
        color: rgb(224, 153, 86);
        background: #092756;
        background: -moz-radial-gradient(0% 100%, ellipse cover, rgba(104, 128, 138, .4) 10%, rgba(138, 114, 76, 0) 40%), -moz-linear-gradient(top, rgba(57, 173, 219, .25) 0%, rgba(42, 60, 87, .4) 100%), -moz-linear-gradient(-45deg, #670d10 0%, #092756 100%);
        background: -webkit-radial-gradient(0% 100%, ellipse cover, rgba(104, 128, 138, .4) 10%, rgba(138, 114, 76, 0) 40%), -webkit-linear-gradient(top, rgba(57, 173, 219, .25) 0%, rgba(42, 60, 87, .4) 100%), -webkit-linear-gradient(-45deg, #670d10 0%, #092756 100%);
        background: -o-radial-gradient(0% 100%, ellipse cover, rgba(104, 128, 138, .4) 10%, rgba(138, 114, 76, 0) 40%), -o-linear-gradient(top, rgba(57, 173, 219, .25) 0%, rgba(42, 60, 87, .4) 100%), -o-linear-gradient(-45deg, #670d10 0%, #092756 100%);
        background: -ms-radial-gradient(0% 100%, ellipse cover, rgba(104, 128, 138, .4) 10%, rgba(138, 114, 76, 0) 40%), -ms-linear-gradient(top, rgba(57, 173, 219, .25) 0%, rgba(42, 60, 87, .4) 100%), -ms-linear-gradient(-45deg, #670d10 0%, #092756 100%);
        background: -webkit-radial-gradient(0% 100%, ellipse cover, rgba(104, 128, 138, .4) 10%, rgba(138, 114, 76, 0) 40%), linear-gradient(to bottom, rgba(57, 173, 219, .25) 0%, rgba(42, 60, 87, .4) 100%), linear-gradient(135deg, #670d10 0%, #092756 100%);
        filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#3E1D6D', endColorstr='#092756', GradientType=1);
    }

    .btn-block {
        margin-bottom:10px;
    }

    .login {
        position: absolute;
        top: 30%;
        left: 50%;
        margin: -150px 0 0 -150px;
        /*width: 300px;
        height: 300px;*/
    }

    .login h1 {
        color: #fff;
        text-shadow: 0 0 10px rgba(0, 0, 0, 0.3);
        letter-spacing: 1px;
        text-align: center;
    }

    .login h2 {
        color: rgb(224, 153, 86);
        text-shadow: 0 0 10px rgba(0, 0, 0, 0.3);
        letter-spacing: 1px;
        text-align: center;
        font-size: 1.2em
    }

    label {
        color: rgb(224, 153, 86);
        text-shadow: 0 0 10px rgba(0, 0, 0, 0.3);
    }

    input {
        width: 100%;
        margin-bottom: 10px;
        background: rgba(0, 0, 0, 0.3);
        border: none;
        outline: none;
        padding: 10px;
        font-size: 13px;
        color: #fff;
        text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.3);
        border: 1px solid rgba(0, 0, 0, 0.3);
        border-radius: 4px;
        box-shadow: inset 0 -5px 45px rgba(100, 100, 100, 0.2), 0 1px 1px rgba(255, 255, 255, 0.2);
        -webkit-transition: box-shadow .5s ease;
        -moz-transition: box-shadow .5s ease;
        -o-transition: box-shadow .5s ease;
        -ms-transition: box-shadow .5s ease;
        transition: box-shadow .5s ease;
    }

    input:focus {
        box-shadow: inset 0 -5px 45px rgba(100, 100, 100, 0.4), 0 1px 1px rgba(255, 255, 255, 0.2);
    }
</style>

<body>

    <div class="login">
        <h1>Contoso High</h1>
        <h2>Login</h2>
        <hr/>
        <div id="api">
            <div class="options">
                <button class="accountButton firstButton claims-provider-selection" id="GoogleExchange">Google</button>
                <button class="accountButton claims-provider-selection" id="GitHubExchange">Github</button>
                <button class="accountButton claims-provider-selection" id="TwitterExchange">Twiter</button>
            </div>
                <div><input id="email" type="text" name="u" placeholder="Username" required="required" />
                <a id="forgotPassword" href="https://bing.com">Forgot Password</a>
                <input id="password" type="password" name="p" placeholder="Password" required="required" />
                <button id="next" type="submit" class="btn btn-primary btn-block btn-large">Let me in.</button>
                <a id="createAccount" href="https://microsoft.com">Sign up now</a><!---->
            </div>
        </div>
    </div>
    <script>"use strict"; $(document).ready(function () {
            if (navigator.userAgent.match(/IEMobile\/10\.0/)) {
                var t = document.createElement("style");
                t.appendChild(document.createTextNode("@-ms-viewport{width:auto!important}")),
                    t.appendChild(document.createTextNode("@-ms-viewport{height:auto!important}")),
                    document.getElementsByTagName("head")[0].appendChild(t)
            }
            if (navigator.userAgent.match(/MSIE 10/i)) {
                var e = $("#footer_links_container");
                $(e).css("padding-top", "100px")
            }
            var o, i = $("#background_background_image"),
                n = function () {
                    document.body.style.overflow = "hidden",
                        ($(window).width() - 500) / $(window).height() < o ? (i.height($(window).height()), i.width("auto")) : (i.width($(window).width() - 500), i.height("auto")),
                        document.body.style.overflow = ""
                };
            $("<img>").attr("src", i.attr("src")).on("load", function () {
                o = this.width / this.height, n()
            }),
                $(window).resize(function () { n() }),
                "undefined" != typeof $("#MicrosoftAccountExchange") && $("#MicrosoftAccountExchange").text("Microsoft"),
                $("*").removeAttr("placeholder")

            document.getElementById("email").tabIndex = "1";
            document.getElementById("password").tabIndex="2";
            document.getElementById("next").tabIndex="3";
            document.getElementById("createAccount").tabIndex="4";
            document.getElementById("forgotPassword").tabIndex="5";

            var socialTabIndex = 6;
            var socialButtons = document.getElementsByClassName("accountButton");
            while(socialButtons.length > 0){
                var button = socialButtons[0];
                button.classList.remove('accountButton','claims-provider-selection');
                button.classList.add('btn','btn-secondary', 'btn-block', 'btn-large');
                button.tabIndex=socialTabIndex;
                tabIndex++;
            }   
        });
    </script>
</body>

</html>
Enter fullscreen mode Exit fullscreen mode

The fully rendered page (locally) looks like this:

Alt Text

It's not the most beautiful or appeasing one, but then again, I'm not a designer!! Feel free to customize your as you see fit

You may nave noticed that this page also includes some custom JavaScript at the end. This is the code the changes the Tab order on the form so that it makes more sense. This code is obviously optional

document.getElementById("email").tabIndex = "1";
document.getElementById("password").tabIndex="2";
document.getElementById("next").tabIndex="3";
document.getElementById("createAccount").tabIndex="4";
document.getElementById("forgotPassword").tabIndex="5";
Enter fullscreen mode Exit fullscreen mode

Finally, I noticed that my buttons where getting messed up by the injected CSS so I used a little bit of code to enforce the CSS classes I wanted, including the right tab index

var socialTabIndex = 6;
var socialButtons = document.getElementsByClassName("accountButton");
while(socialButtons.length > 0){
     var button = socialButtons[0];
     button.classList.remove('accountButton','claims-provider-selection');
     button.classList.add('btn','btn-secondary', 'btn-block', 'btn-large');
     button.tabIndex=socialTabIndex;
     tabIndex++;
}
document.getElementById("next").classList.add('btn','btn-primary', 'btn-block', 'btn-large');
Enter fullscreen mode Exit fullscreen mode

We can now save this as *.cshtml and put it in a publicly accessible location so that B2C (and only B2C) can render it.

If you want some information about how the UX customization works, you can check the official docs here

Upload the custom page to Azure Storage

Azure Storage is the ideal place to store this static file. Sign in to the Azure Portal and navigate to the Storge Account you wish to use. Create a new Azure Storage Container and give it Public access level at Blob level, as per the instructions below:

Alt Text

Next, we need to configure the CORS settings to ensure that B2C and only B2C can access the blob using the right origin. Open the CORS tab on the Storage Account root and add a new setting for Blob Service as per below:

  • Allowed Origins: https://.b2clogin.com
  • Allowed Methods: GET, OPTIONS
  • Allowed Headers: *
  • Exposed Headers: *
  • Max Age: 200

Make sure to hit Save to persist the CORS changes before you bail out :)

Alt Text

Configure the Azure AD B2C flow to use the custom UX

The last step is to configure our SignIn/Signup flow to use the custom page. Navigate to your B2C tenant and select the flow you want to apply the UX changes. In the Properties tab enable JavaScript and press Save

Alt Text

Next, navigate to the Page Layouts tab. In the Unified Sign up or Sign In page, select Yes for the Use custom page content and set the Custom Page URI to our blob URI

Alt Text

If all worked as expected, after hitting Save, you should see that the Unified sign up or sign in page is set to Yes for having a Custom page assigned to it :)

NOTE > The Blob URI can be found in your Azure Storage account by navigating to the actual Blob and selecting Properties

Alt Text

To quickly check if everything is in order, we can use the Run user flow functionality straight from the B2C portal. My custom page renders like this on the "live" environment:

Alt Text

Summary

Azure AD B2C is extremely flexible and can adjust to your needs and level of complexity. As you can see, it takes only a little bit of time to customize the look and feel of your login pages, including custom behaviors, so that your users get a consistent experience when using your apps.

Top comments (9)

Collapse
 
rcls profile image
OssiDev

Thanks a ton for this guide! This is much more helpful than the Microsoft wall of text they have as a guide.

Collapse
 
christosmatskas profile image
Christos Matskas

Thanks @rcls . I appreciate the kind words.

Collapse
 
arianhojataes profile image
Arian Hojat(AES) • Edited

Hey OssiDev, i had a question... i have the 'Enforce Javascript enforcing page layout' Azure setting on.
I put my document.ready right before the body tag and Azure B2C popup seems to just convert it to text instead (like literally the text of the tag shows up in the UI).
For example here is what i have at the end of the custom page template:

<script>
        //"use strict"; doesn't seem to be needed
        $(document).ready(function () {
            ... my stuff here;
        });
</script>
</body></html>
Enter fullscreen mode Exit fullscreen mode

if i move that script tag into the head, at least it seems to run, but the template it pulls into load seems to run after the document.ready. For example, if i have any code like document.getElementById('email').focus(), it won't find the element(s). so right now I'm doing this setTimeout which works most of the time but its ugly:

<script>
    //"use strict";
    $(document).ready(function () {
        setTimeout(()=>{
            ... my stuff here;
        }, 1000);
    });
</script>
</head>
Enter fullscreen mode Exit fullscreen mode

just curious if the code you have at the end of body still works for the latest versions of the templates.
Note here at the latest versions I saw and am using:

b2c_1_signin flow -> 'Sign in page'  
    default page templated used is: idpSelector.html
    at version: **2.1.10**
b2c_1_signup flow -> 'Identity provider selection page'
    default page templated used is: idpSelector.html
    at version: **1.2.4**
b2c_1_susi flow   -> 'Unified sign up or sign in page'
    default page templated used is: unified.html
    at version: **2.1.10**
Enter fullscreen mode Exit fullscreen mode

Thanks!,
Ari

Collapse
 
topacv profile image
Vasile Topac

Hey.
Thanks for the informative article. I have a question.
How safe is it to rely on the IDs of the injected elements?
Isn't there the risk that Microsoft will change the ids, and then custom JS parsing will fail?

On a project I'm adding more logic that relies on the IDs of elements injected by AD B2C in the "api" div (example of injected elem id: "emailVerificationControl_but_verify_code"), and show/hide some buttons with JQuery, to make a more custom UI.
It all works, but I'm not sure how safe is to do this on the long run? (and can't really find this info in the official docs).

Thank you,
Vasile

Collapse
 
topacv profile image
Vasile Topac

Just to make things more clear, I don't only rely on IDs of injected inputs, but also on IDs of injected buttons.

Collapse
 
rcls profile image
OssiDev • Edited

I'd say if Microsoft goes and changes the ID's they break backwards compatibility. But the page layouts do have versioning, so if you lock it to a specific version I think you're safe.

Quote from the page layout version:

This is the version of the packaged content containing HTML, CSS and JavaScript provided by Azure AD B2C

So I'm guessing if you lock it to a specific version and not use the latest, you're safe. I don't think the layout automatically changes. Mine's locked in 2.1.5 and the latest is 2.1.6.

Thread Thread
 
christosmatskas profile image
Christos Matskas

That's right. If you pin it to a specific version, you should be OK. But you're right in thinking that things/IDs could potentially change. I would expect that this would be properly communicated ahead of time though, especially if there going to be breaking changes

Collapse
 
spatils profile image
spatils

Thanks a lot @christosmatskas for wonderful article. This article solves multiple use cases I am working on. But I have realised that azure is removing components that I added inside tag. Is there any step that I may be missing? Any help or pointers would be really appreciated.

Collapse
 
robinhood8865 profile image
RobinHood

Thanks, bro.