DEV Community

loading...
The 425 Show

Customize the look and feel of your Azure AD B2C page

Christos Matskas
Program Manager in the Microsoft Identity Dev Advocacy team. Programmer, speaker and all around geek
・7 min read

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.

Discussion (0)