Customize the look and feel of your Azure AD B2C page 
Published: 2021-01-07 18:54:40

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">
```
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>
```
The fully rendered page (locally) looks like this:

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/jtxmyr1jclg9hv46isz9.png)

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";
```
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');
```

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](https://docs.microsoft.com/en-us/azure/active-directory-b2c/customize-ui-with-html?pivots=b2c-user-flow)

## 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](https://dev-to-uploads.s3.amazonaws.com/i/9aeehoylb5x6ombdfra9.png)

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://<YourB2CTenantName>.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](https://dev-to-uploads.s3.amazonaws.com/i/yz16ztvo5yhtota5p9b0.png)

##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](https://dev-to-uploads.s3.amazonaws.com/i/3b6ajejvv160sgclw242.png)

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](https://dev-to-uploads.s3.amazonaws.com/i/kq1nyj985lg50xm5x9b7.png)

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](https://dev-to-uploads.s3.amazonaws.com/i/q4eyw1xzurmkbmwtogda.png)

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](https://dev-to-uploads.s3.amazonaws.com/i/dghxv6r5qbmrjchigv7b.png)

##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.