Azure Active Directory B2C - DevEx
Azure Active Directory B2C - DevEx
Azure Active Directory B2C - DevEx

Our experience with Azure AD B2C – 2. Developer Experience

Backend

Backend

Backend

Delve into the technical realm! Explore how developers engage and optimize user interactions through Azure Active Directory B2C.

15. 1. 2024

In this series of articles, we'll dive into questions like:

  • What was our experience with Azure Active Directory B2C identity as service?

  • Why we won’t be using it any time soon.

  • Issues with custom frontend and Microsoft flows for authentication.

  • Horrific complexity of custom policies in XML.

  • User unfriendly release of custom policies to Azure.

Developer Experience in AD B2C?

What about Microsoft’s developer tools, coding language, and general approach?

And now we get to the crème de la crème of this blog post: User Flows and Custom Policies are tools, systems, and frontend for users to sign up, sign in, or manage their profile –– like reset password or change information.

User Flows

User flows are very basic and barebones building blocks for creating user experience. You can follow Microsoft’s wizard to create a user flow, which works pretty well.

Azure AD B2C User Flow

But the issues creep up when you realize you are pretty much stuck with Microsoft’s frontend design, which sometimes makes no sense or is not user-friendly. Therefore, our opinion is that it’s hard to make complex and custom User Flows. This means you can’t really use User Flows for production deployment. User Flows might only be valid for prototyping or for very simple solutions that require no exotic information from the user (like national identity numbers, driver’s license numbers, etc.).

Azure AD B2C User Flows

You can choose from a few designs Microsoft made. You can customize your Azure AD B2C pages with a banner logo, background image, and background color by using Microsoft Entra ID Company Branding. But in the end, you don't have much of a choice:

Azure AD B2C Customization

To see how this looks for the end user, you can go to part 4 of this series.

Custom Policies

When your task is to create something more tangible, you have to use custom policies. Right off the bat, you are greeted by a confusing and unreadable XML file that contains… all basic Microsoft policies. This is a snippet from an almost 1300-lines-long file:

...
  	  <ClaimType Id="isActiveMFASession">
        <DisplayName>isActiveMFASession</DisplayName>
        <DataType>boolean</DataType>
        <UserHelpText>Parameter provided by the MFA session management to indicate that the user has an active MFA session.</UserHelpText>
      </ClaimType>

      <!-- SECTION III: Additional claims that can be collected from the users, stored in the directory, and sent in the token. Add additional claims here. -->

      <ClaimType Id="givenName">
        <DisplayName>Given Name</DisplayName>
        <DataType>string</DataType>
        <DefaultPartnerClaimTypes>
          <Protocol Name="OAuth2" PartnerClaimType="given_name" />
          <Protocol Name="OpenIdConnect" PartnerClaimType="given_name" />
          <Protocol Name="SAML2" PartnerClaimType="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname" />
        </DefaultPartnerClaimTypes>
        <UserHelpText>Your given name (also known as first name).</UserHelpText>
        <UserInputType>TextBox</UserInputType>
      </ClaimType>

      <ClaimType Id="surname">
        <DisplayName>Surname</DisplayName>
        <DataType>string</DataType>
        <DefaultPartnerClaimTypes>
          <Protocol Name="OAuth2" PartnerClaimType="family_name" />
          <Protocol Name="OpenIdConnect" PartnerClaimType="family_name" />
          <Protocol Name="SAML2" PartnerClaimType="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname" />
        </DefaultPartnerClaimTypes>
        <UserHelpText>Your surname (also known as family name or last name).</UserHelpText>
        <UserInputType>TextBox</UserInputType>
      </ClaimType>

You are asked to use these files to create your custom policies:

  • TrustFrameworkBase.xml

  • TrustFrameworkExtensions.xml

  • TrustFrameworkLocalization.xml

You don’t necessarily need them, but developing your custom policy is next to impossible without Microsoft’s base code.

Afterward, you can start developing your custom policies. This is Microsoft’s example of password reset policy:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<TrustFrameworkPolicy
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xmlns:xsd="http://www.w3.org/2001/XMLSchema"
  xmlns="http://schemas.microsoft.com/online/cpim/schemas/2013/06"
  PolicySchemaVersion="0.3.0.0"
  TenantId="yourtenant.onmicrosoft.com"
  PolicyId="B2C_1A_PasswordReset"
  PublicPolicyUri="http://yourtenant.onmicrosoft.com/B2C_1A_PasswordReset">

  <BasePolicy>
    <TenantId>yourtenant.onmicrosoft.com</TenantId>
    <PolicyId>B2C_1A_TrustFrameworkExtensions</PolicyId>
  </BasePolicy>

  <RelyingParty>
    <DefaultUserJourney ReferenceId="PasswordReset" />
    <TechnicalProfile Id="PolicyProfile">
      <DisplayName>PolicyProfile</DisplayName>
      <Protocol Name="OpenIdConnect" />
      <OutputClaims>
        <OutputClaim ClaimTypeReferenceId="email" />
        <OutputClaim ClaimTypeReferenceId="objectId" PartnerClaimType="sub"/>
        <OutputClaim ClaimTypeReferenceId="tenantId" AlwaysUseDefaultValue="true" DefaultValue="{Policy:TenantObjectId}" />
      </OutputClaims>
      <SubjectNamingInfo ClaimType="sub" />
    </TechnicalProfile>
  </RelyingParty>
</TrustFrameworkPolicy>

Not only are you asked to write the policy in XML, but you are also asked to know many custom and very specific XML tags that initially look confusing. Which, frankly, sucks even with sophisticated XML editors or VS Code extensions – fortunately, Microsoft provides Azure AD B2C extension for VS Code, which helps a lot. After a while, it boils down to UserJorneysTechnicalProfiles, and a whole bunch of name configuration copy-pasting. Other tags are usually just reused when you create new policy or one-off settings that doesn’t need to change that much.

You can learn more about the XML tags in part 3 of this series.

How do you query and manage users created by this process, and how can you access custom data?

While the application is doing its thing, you might want to access the registered users into your Active Directory to help them with registration or get a statistic about usage. The problem is that accessing meaningful data from the Azure Portal – Active Directory is impossible. You can delete or create users in the active directory. You can also access or export basic data about the user. But nothing else. If you want to see your custom claims, you are out of luck.

The only meaningful way to do more complex tasks with your users is to use Microsoft Graph API. We can’t do MS Graph API justice here because it would need its own blog post, but it works splendidly and an immaculate architecture. It essentially allows queries similar to OData System Query Options like $filter or $select.

GET <https://graph.microsoft.com/v1.0/me/messages?filter=emailAddress> eq '[email protected]'

And it works very well for accessing AD B2C data. Unfortunately, it is a little constrained in some aspects. As of now, AD B2C does not support advanced query capabilities on directory objects. This means that there is no support for $count$search query parametersm and Not (not), Not equals (ne), and Ends with (endsWith) operators in $filter query parameter. Currently, there exist two major versions of Microsoft Graph (v1.0 and beta), and we had to use the beta version because v1.0 didn’t allow some more complex filters.

Getting user data in .NET

Microsoft made excellent libraries for .NET that can help you contact Azure resources. The usage is very simple and straightforward:

var graphClient = new GraphServiceClient(...);

var result = await graphClient.Users
            .Request()
            .Filter(filter)
            .GetAsync(cancellationToken);

One of the most significant drawbacks is that user claims/data from external applications (like when using Custom Policies) save their data into weird properties on the user objects, and they can look something like this:

"extension_1cdaac2e207c4dbf9e7d2d9666c2df76_phoneNumber": "123456789"

"extension_{registered application id}_phoneNumber"

This is okay if you use only one AD B2C, but we needed one for every environment. Therefore, the code had to be more complex since typical deserialization libraries wouldn’t work here.

Getting all users at once in .NET

Even though it’s not recommended, Microsoft built a way to get all users in the Active Directory with a straightforward query.

// Setup
var graphClient = new GraphServiceClient(...);
var users = new List<User>();

// Create query
var result = await graphClient
    .Users
    .Request()
    .GetAsync(cancellationToken);

var pageIterator = PageIterator<User>
    .CreatePageIterator(graphClient, result, u =>
    {
        users.Add(u);
        return true; //continue iterating;
    });

// Iterate
await pageIterator.IterateAsync(cancellationToken);

return users;

This simplifies exports quite a bit and is very useful when you need to perform an operation on the users that cannot be done via MS Graph API, like search by property that you need to parse before you check for equality (like you would do in LINQ). It’s reasonably fast.