Customize the look and feel of your Azure AD B2C page
Christos Matskas
Posted on January 7, 2021
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:
- How to use custom HTML for your sign up/sign in page
- 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:
- a dependency to jQuery
- 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:
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
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:
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 :)
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
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
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
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:
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.
Posted on January 7, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
November 30, 2024