After writing an earlier post, I did some more research online and realized there was a relatively simple way to provide a standardized look and feel to all of the default Azure AD B2C pages: design a single layout which has an empty div element with the id=”api” tag, and let the AD B2C system insert whatever html it wants to there.
Here’s how I implemented this for my Ride Monitor site:
<!DOCTYPE html>
<html>
<head>
<title>Sign in</title>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css" rel="stylesheet" type="text/css" />
<link href="https://ridemonitor.blob.core.windows.net/authentication/auth.css" rel="stylesheet" type="text/css" />
</head>
<body>
<div class="container unified_container">
<div class="row rider-header">
<h1 class="col-md-12 col-sm-12">
Ride Monitor
</h1>
</div>
<div class="row">
<div class="col-md-6 col-sm-6">
<img alt="RideMonitor Rider" class="img-responsive" src="https://ridemonitor.blob.core.windows.net/authentication/rider.jpg" />
<p>
a system for monitoring motorcycle rides
</p>
</div>
<div class="col-md-6 col-sm-6">
<div id="api" data-name="Unified">
</div>
</div>
</div>
</div>
</body>
</html>
I changed some of the referenced blob names to make them consistent with this more generic approach.
The tedious part of this exercise was that you have to update each and every page within the Azure AD B2C portal separately. There’s no option to say “just use this one file for everything”. That means you have to go through each and every policy you’ve set up, and each and every one of its custom pages, and tell the system to use this one blob file. Time consuming but not hard.
I also took the time to insert some more stuff from a css file I found on github that was intended to style a server-based Open Identity site (recall that where in an Open Identity site, the site itself has to serve all of the necessary pages, Azure AD B2C fills in the “api” div with content and generates all the pages for you).
Here’s the resulting css file:
.rider-header h1 {
color: darkred;
}
.rider-footer p {
margin-top: 10px;
}
@media(max-width:480px) {
.divider h2 {
margin: 10px 0 !important;
}
}
::-moz-placeholder {
color: transparent;
}
::-moz-placeholder {
color: transparent;
}
:-ms-input-placeholder {
color: transparent;
}
::-webkit-input-placeholder {
color: transparent;
}
#api ul {
list-style-type: none;
padding-left: 0;
}
#api[data-name='IdpSelections'] ul {
text-align: center;
}
#api[data-name='Phonefactor'] .buttons button#cancel {
width: 32%;
}
#api[data-name='SelfAsserted'] > div:first-child {
display: none;
}
#GoogleExchange {
background-color: #C64A29;
background-image: url("https://ridemonitor.blob.core.windows.net/authentication/googleplus.png");
background-repeat: no-repeat;
background-size: 50px;
margin: 10px 0;
padding-left: 55px;
}
#GoogleExchange:hover {
background-color: #C14325;
}
#MicrosoftAccountExchange {
background-color: #28B1E6;
background-image: url("https://ridemonitor.blob.core.windows.net/authentication/microsoft.png");
background-repeat: no-repeat;
background-size: 50px;
padding-left: 55px;
}
#MicrosoftAccountExchange:hover {
background-color: #189DCF;
}
.accountButton {
border: none;
border-radius: 2px;
color: whitesmoke;
font-size: larger;
height: 45px;
width: 284px;
}
.accountButton:hover {
border: none
}
label, .password-label {
margin-top: 10px;
}
.attrEntry, .phoneEntry {
margin-bottom: 15px;
padding-top: 0;
}
.attrEntry input, .attrEntry select, .phoneEntry input, .phoneEntry select, #codeVerification input {
background-color: #fff !important;
background-image: none !important;
border: 1px solid #ccc !important;
border-radius: 2px !important;
box-shadow: inset 0 1px 1px rgba(0,0,0,.075) !important;
color: #555;
display: block;
font-size: 14px;
height: 40px;
line-height: 1.42857143;
padding: 6px 12px;
transition: border-color ease-in-out .15s,box-shadow ease-in-out .15s !important;
width: 100% !important;
-o-transition: border-color ease-in-out .15s,box-shadow ease-in-out .15s !important;
-webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.075) !important;
-webkit-transition: border-color ease-in-out .15s,-webkit-box-shadow ease-in-out .15s !important;
}
.attrEntry input:invalid, .phoneEntry input:invalid, #codeVerification input:invalid {
border-color: inherit;
}
.attrEntry.validate input:invalid, .phoneEntry.validate input:invalid, #codeVerification.validate input:invalid {
border-color: #a94442 !important;
box-shadow: inset 0 1px 1px rgba(0,0,0,.075) !important;
-webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.075) !important;
}
.attrEntry #email_intro {
display: none !important;
}
.attrEntry .error.itemLevel, .attrEntry .helpText, .phoneEntry .error.itemLevel, .phoneEntry .helpText, #codeVerification .error.itemLevel, #codeVerification .helpText {
display: none;
}
.attrEntry .tiny, .phoneEntry .tiny, #codeVerification .tiny {
display: none;
}
.buttons button {
background-image: none;
border: 1px solid transparent;
border-radius: 4px;
cursor: pointer;
display: inline-block;
font-size: 14px;
font-weight: 400;
height: inherit;
line-height: 1.42857143;
margin: 0;
padding: 6px 12px;
text-align: center;
touch-action: manipulation;
user-select: none;
vertical-align: middle;
white-space: nowrap;
width: inherit;
-moz-user-select: none;
-ms-touch-action: manipulation;
-ms-user-select: none;
-webkit-user-select: none;
}
.buttons {
margin: 20px 0;
}
.buttons button#cancel, .buttons button#email_ver_but_edit, .buttons button#email_ver_but_resend {
background-color: #fff;
border-color: #ccc;
color: #333;
}
.buttons button#cancel {
font-size: 18px;
height: 60px;
width: 49.9%;
}
@media(max-width:767px) {
.buttons button#cancel {
height: 50px;
width: 48.9%;
}
}
.buttons button:hover#cancel, .buttons button:hover#email_ver_but_edit, .buttons button:hover#email_ver_but_resend {
background-color: #e6e6e6;
background-image: none;
border-color: #adadad;
color: #333;
}
.buttons button#continue {
background-color: #5cb85c;
border-bottom: 5px solid #449d44;
border-color: #4cae4c;
color: #fff;
font-size: 18px;
height: 60px;
width: 49%;
}
@media(max-width:767px) {
.buttons button#continue {
height: 50px;
}
}
.buttons button:hover#continue {
background-color: #449d44;
background-image: none;
border-color: #398439;
color: #fff;
}
.buttons button[disabled]#continue, .buttons button[disabled]:hover#continue {
background-color: #5cb85c;
background-image: none;
border-color: #4cae4c;
color: #fff;
}
.buttons button#email_ver_but_resend, .buttons button#email_ver_but_verify {
margin-top: 5px;
}
.buttons button#email_ver_but_send, .buttons button#email_ver_but_verify {
background-color: #337ab7;
border-color: #2e6da4;
color: #fff;
}
.buttons button:hover#email_ver_but_send, .buttons button:hover#email_ver_but_verify {
background-color: #286090;
background-image: none;
border-color: #204d74;
color: #fff;
}
.buttons button#verifyCode, .buttons button#verifyPhone {
background-color: #5cb85c;
border-bottom: 5px solid #449d44;
border-color: #4cae4c;
color: #fff;
font-size: 18px;
height: 60px;
margin-right: 9px;
width: 32%;
}
@media(max-width:767px) {
.buttons button#verifyCode, .buttons button#verifyPhone {
height: 50px;
}
}
.create p {
text-align: center;
}
.divider {
margin: 0 auto;
position: relative;
text-align: center;
text-shadow: 0 1px 0 #fff;
}
.divider h2 {
color: #ccc;
line-height: 20px;
margin: 20px 0;
text-align: center;
text-transform: lowercase;
}
.divider h2:after, .divider h2:before {
content: "";
height: 1px;
position: absolute;
top: 10px;
width: 40%;
}
.divider h2:after {
background: rgb(126,126,126);
background: linear-gradient(left, rgba(126,126,126,1) 0%, rgba(255,255,255,1) 100%);
background: -moz-linear-gradient(left, rgba(126,126,126,1) 0%, rgba(255,255,255,1) 100%);
background: -ms-linear-gradient(left, rgba(126,126,126,1) 0%, rgba(255,255,255,1) 100%);
background: -o-linear-gradient(left, rgba(126,126,126,1) 0%, rgba(255,255,255,1) 100%);
background: -webkit-linear-gradient(left, rgba(126,126,126,1) 0%, rgba(255,255,255,1) 100%);
right: 0;
}
.divider h2:before {
background: rgb(126,126,126);
background: linear-gradient(right, rgba(126,126,126,1) 0%, rgba(255,255,255,1) 100%);
background: -moz-linear-gradient(right, rgba(126,126,126,1) 0%, rgba(255,255,255,1) 100%);
background: -ms-linear-gradient(right, rgba(126,126,126,1) 0%, rgba(255,255,255,1) 100%);
background: -o-linear-gradient(right, rgba(126,126,126,1) 0%, rgba(255,255,255,1) 100%);
background: -webkit-linear-gradient(right, rgba(126,126,126,1) 0%, rgba(255,255,255,1) 100%);
left: 0;
}
.entry .buttons button {
background-color: #337ab7;
border-bottom: 5px solid #204d74;
border-radius: 2px;
font-size: 18px;
line-height: 1.3333333;
padding: 10px 16px;
color: #fff;
height: 60px;
width: 100%;
}
.entry .buttons button:hover {
background-color: #286090;
border-color: #204d74;
color: #fff;
}
.error.itemLevel p {
color: #a94442;
}
.error.itemLevel p:before {
content: "\e101";
display: inline-block;
font-family: 'Glyphicons Halflings';
font-style: normal;
font-weight: 400;
line-height: 1;
margin-right: 3px;
position: relative;
top: 1px;
-webkit-font-smoothing: antialiased;
}
.highlightError {
border-color: #a94442 !important;
box-shadow: inset 0 1px 1px rgba(0,0,0,.075) !important;
outline: 0;
-webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.075) !important;
}
.image-center {
display: block;
margin-left: auto;
margin-right: auto;
text-align: center;
vertical-align: middle;
}
.intro {
display: none;
}
.localAccount .divider {
display: none;
}
.login-logo {
height: 140px;
margin-top: -90px;
}
.options {
display: block;
margin-left: auto;
margin-right: auto;
padding-bottom: 10px;
text-align: center;
vertical-align: middle;
}
.phonefactor_container, .self_asserted_container, .unified_container {
padding-top: 80px;
}
.phoneNumber .type {
display: inline-block;
font-weight: bold;
margin-bottom: 5px;
max-width: 100%;
}
.phoneNumbers {
margin-top: 20px;
}
.social {
margin-top: 30px;
}
.verify {
margin-top: 5px;
padding-top: 0 !important;
}
.working {
bottom: 0;
display: none;
height: 2em;
left: 0;
margin: auto;
overflow: show;
position: fixed;
right: 0;
top: 0;
width: 2em;
z-index: 999;
}
.working:before {
background-color: rgba(0,0,0,0.3);
content: '';
display: block;
height: 100%;
left: 0;
position: fixed;
top: 0;
width: 100%;
}
.working:not(:required) {
background-color: transparent;
border: 0;
color: transparent;
font: 0/0 a;
text-shadow: none;
}
.working:not(:required):after {
animation: spinner 1500ms infinite linear;
border-radius: 0.5em;
box-shadow: rgba(0, 0, 0, 0.75) 1.5em 0 0 0, rgba(0, 0, 0, 0.75) 1.1em 1.1em 0 0, rgba(0, 0, 0, 0.75) 0 1.5em 0 0, rgba(0, 0, 0, 0.75) -1.1em 1.1em 0 0, rgba(0, 0, 0, 0.75) -1.5em 0 0 0, rgba(0, 0, 0, 0.75) -1.1em -1.1em 0 0, rgba(0, 0, 0, 0.75) 0 -1.5em 0 0, rgba(0, 0, 0, 0.75) 1.1em -1.1em 0 0;
content: '';
display: block;
height: 1em;
font-size: 10px;
margin-top: -0.5em;
width: 1em;
-moz-animation: spinner 1500ms infinite linear;
-ms-animation: spinner 1500ms infinite linear;
-o-animation: spinner 1500ms infinite linear;
-webkit-animation: spinner 1500ms infinite linear;
-webkit-box-shadow: rgba(0, 0, 0, 0.75) 1.5em 0 0 0, rgba(0, 0, 0, 0.75) 1.1em 1.1em 0 0, rgba(0, 0, 0, 0.75) 0 1.5em 0 0, rgba(0, 0, 0, 0.75) -1.1em 1.1em 0 0, rgba(0, 0, 0, 0.5) -1.5em 0 0 0, rgba(0, 0, 0, 0.5) -1.1em -1.1em 0 0, rgba(0, 0, 0, 0.75) 0 -1.5em 0 0, rgba(0, 0, 0, 0.75) 1.1em -1.1em 0 0;
}
@keyframes spinner {
0% {
transform: rotate(0deg);
-moz-transform: rotate(0deg);
-ms-transform: rotate(0deg);
-o-transform: rotate(0deg);
-webkit-transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
-moz-transform: rotate(360deg);
-ms-transform: rotate(360deg);
-o-transform: rotate(360deg);
-webkit-transform: rotate(360deg);
}
}
@-moz-keyframes spinner {
0% {
transform: rotate(0deg);
-moz-transform: rotate(0deg);
-ms-transform: rotate(0deg);
-o-transform: rotate(0deg);
-webkit-transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
-moz-transform: rotate(360deg);
-ms-transform: rotate(360deg);
-o-transform: rotate(360deg);
-webkit-transform: rotate(360deg);
}
}
@-o-keyframes spinner {
0% {
transform: rotate(0deg);
-moz-transform: rotate(0deg);
-ms-transform: rotate(0deg);
-o-transform: rotate(0deg);
-webkit-transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
-moz-transform: rotate(360deg);
-ms-transform: rotate(360deg);
-o-transform: rotate(360deg);
-webkit-transform: rotate(360deg);
}
}
@-webkit-keyframes spinner {
0% {
transform: rotate(0deg);
-moz-transform: rotate(0deg);
-ms-transform: rotate(0deg);
-o-transform: rotate(0deg);
-webkit-transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
-moz-transform: rotate(360deg);
-ms-transform: rotate(360deg);
-o-transform: rotate(360deg);
-webkit-transform: rotate(360deg);
}
}
I confess that I don’t understand all of what this css is doing… but adding it made the resulting pages look a lot better, and more “professional”. You can find the source code for this css, and some helpful starting points for the html, over here on github.
Here are some of the results:
It’s still annoying that you can’t easily reorder the way the supplemental fields are displayed. Who ever heard of putting State/Province and Postal Code between Last Name and First Name? But I can live with that, for now.
It turns out there is a way to reorder the supplemental fields: hover the mouse over the left hand margin of the field row in the Azure portal’s UI customization view blade, and drag and drop the field where you want it to be. You do this in the sign-up page blade, which is the one which lists the fields that are displayed:
The three vertical dots, which appear when you hover over the left hand edge of a field row, are the handle you use to drag and drop the row to a different position in the list.
There is a way to take control of exactly how the authentication pages are displayed, what they show, and what sequence they show it in. But that requires using an Azure AD B2C feature which is in public preview…and, given how difficult it is to deal with Azure stuff even when it’s “official”, I don’t want to mess around with it. Those who are not faint of heart can read about it here.