Validating User Passwords for Azure AD B2C

Michael Collins
Neudesic Innovation
12 min readFeb 19, 2022

In this series, I have shown how to create an Azure Active Directory B2C tenant with a basic sign up and log in process using a username and password. I have also shown how to add user authentication to web and mobile applications using the Microsoft Authentication Library. Now I’m going to return back to my custom policies and build those out a bit more to add new features. In this post, I’m going to look at validating passwords using the haveibeenpwned password database.

This is the eleventh post in a series on Azure Active Directory B2C and how to use Azure Active Directory B2C to create an identity management system for a Software-as-a-Service application. If you are new to this series, I recommend reading the earlier posts in the series:

  1. Building Application Identity Solutions using Azure AD B2C
  2. Configure Azure AD B2C For Customization
  3. Understanding the B2C Directory
  4. Sign Up or Sign In Users With Azure AD B2C: Part 1
  5. Sign Up New Users Using Azure AD B2C
  6. Log In With Local Accounts on Azure AD B2C
  7. Combining Sign Up and Sign In Into a Single Policy Using Azure AD B2C
  8. Using Azure AD B2C to Authenticate Web App Users
  9. Using Azure AD B2C to Authenticate iOS App Users
  10. Using Azure AD B2C to Authenticate Android App Users

What is “pwned”?

A pwned password is a password that has been compromised or exposed in a data breach. A pwned password does not necessarily mean that one of your accounts was hacked. Sometimes companies or websites accidentally expose data files containing this information and the data file becomes public knowledge. A pwned password is out there though. People know about it. For the purpose of this article, a pwned password is not a password that you want users to be using to log into their account.

For more information, see the haveibeenpwned FAQ.

How Do We Detect Pwned Passwords?

The Have I Been Pwned website maintains a database of millions of passwords that have been compromised from public data breaches. Fortunately for our needs, we do not need to download the entire database; instead we will use a freely available API that will allow us to securely query and detect if the password exists in the database.

I will be implementing the following algorithm:

  1. The user’s password is captured in the sign up form.
  2. The custom policy will invoke an Azure Function and will pass the password as input.
  3. The Azure Function will calculate the SHA-1 hash of the password.
  4. The first 5 hexadecimal characters (the prefix) from the SHA-1 hash will be sent as a query to the Have I Been Pwned API.
  5. The Have I Been Pwned API will use the prefix to find matching hashes for the password. The API will return a stream of text with one hash suffix per line and the number of times that password has been found in data breaches, separated by a colon (:).
  6. I’ll parse the result list into an array of records. The records appear to be returned in sorted order by suffix, so I do not need to sort them myself.
  7. I’ll use a binary search algorithm to determine if the suffix appears in the list. If the suffix appears, the password is rejected. Otherwise, the password succeeds and is valid.

Integrating Password Validation with Azure AD B2C

Earlier in this series, we have explored creating custom policies using their XML-based domain language. We described relying party policies that launch user journeys and issue tokens containing identity and authorization data for users. So far, the web UI was provided for us by B2C and we only used services that are a part of B2C to complete the user journeys and relying party policies. However, we’re not just limited to what’s in the Azure Active Directory B2C’s box.

Azure Active Directory B2C supports a REST technical profile that we can use in our user journeys. A technical profile is an action that gets executed during the user journey either to perform an action or validate data. A technical profile is similar to a function in other languages. We can use the REST technical profile to pause the user journey and to make a call to a REST API that lives outside of the Azure Active Directory B2C ecosystem to perform a custom action.

When implementing custom behavior for an Azure Active Directory B2C, Azure Functions are the perfect host. They have a low barrier to entry and can be developed, deployed, and tested quickly. There is no server to configure or create. You just deliver your code and it runs. Azure Functions can also expose HTTP endpoints which makes it really simple to build REST APIs that can be consumed by Azure Active Directory P2C policies.

Build the Azure Function

I am going to start by building my Azure Function to call the Have I Been Pwned API and determine if the user’s password has been compromised. I chose C# and .NET 6 to implement my Azure Function. My Azure Function will be triggered by incoming HTTP requests and will return an HTTP response.

My function starts by reading the HTTP request to get the password that needs to be validated. It next uses the SHA-1 algorithm to hash the password and retrieve the 40 character hexadecimal string containing the hash of the password. I extract the 5-character prefix from the hash and use the prefix to query the Have I Been Pwned database. I receive back an array of tuples containing the suffixes and the number of times each password has been compromised. I search through the list to match the password that I have with a password in the list and return the result indicating if the password was found or not.

The request to my Azure Function will be a simple JSON object containing a password field:

The password is sent in the clear in the body of the text, but the HTTP request will be sent using HTTPS, so the value should be safe from prying eyes. I read the request and deserialize the JSON into a Request object:

The next step is to hash the password using the SHA-1 hashing algorithm. SHA-1 will produce a 20-byte value which I then encode as a 40-character hexadecimal string:

From the hashed password, I take the first 5 characters to use as the password prefix and I send the prefix to the Have I Been Pwned API to retrieve all of the suffixes that may include the password that I am searching for:

The Have I Been Pwned API will return a text response with one record per line. The fields are delimited by a colon(:). The first field is the suffix for a matching password with the same prefix. The second field has the number of times that the password has been exposed in data breaches. I parse the result stream and place the values into an array of Tuple objects.

For the final step, I take the list of Tuple objects and search through them. I extract the suffix from the password hash and use a binary search algorithm to find the suffix in the list (I am making the assumption, which is currently the case, that the suffixes are returned by the Have I Been Pwned API in sorted order):

If the suffix is found in the list, I extract the count from the Tuple. I then send back an error response in the body of an HTTP 409 error response to let Azure Active Directory B2C know that the password validation failed. The error response object is defined as:

If the suffix is not found in the list, I return an HTTP 200 response to let Azure Active Directory B2C know that the password is valid and can be used to identify the user.

Test the Function Locally

After implementing the password validation function, I ran it locally using the Azure Functions Tools to debug the function and to make sure that it worked. When I ran the Functions host locally, my function was hosted at http://localhost:7071/api/ValidatePassword.

I used an HTTP testing tool (I like Paw), I sent a test request to my Azure Function:

The Function generated and returned this response:

The error message shows how many times the password has been detected in data breaches.

When I tested a more difficult password, the password was not found in the list and I received the HTTP 200 response.

Host the Azure Function in Azure

Now that the Azure Function is built, I need to create a host for it in Azure:

  1. Open Azure Portal and navigate to the main subscription directory. This is not the directory for the Azure Active Directory B2C tenant.
  2. Create a new resource and select Function App for the resource type.
  3. For the Resource Group field, I chose an existing resource group that I created earlier in this series to group all of my application’s resources together.
  4. For the Function App name field, this field is used to set the DNS name for your function app, so you will need to choose something unique.
  5. For the Publish field, choose Code.
  6. For the Runtime stack field, choose .NET.
  7. For the Version field, choose 6.
  8. For the Region field, choose a region that makes sense for you. I chose US West 2.
  9. Click the Review + create button.

Azure will begin creating the services that will support your Azure Function, including the hosting environment, a storage account, and an Application Insights service for analyzing log data.

Publish the Function to Azure

Now that the Functions app service has been created in Azure, I can deploy my function to the app. I’m on a MacBook Pro and using JetBrains Rider for development. I can publish directly from the IDE. You can also publish to Azure if you are using Visual Studio Code or Visual Studio on Windows.

Within Rider, I will do the following:

  1. Right-click on my project. A context menu will appear and I can choose Publish. From the drop-down list, I will choose to publish to Azure.
  2. On the Edit Configuration screen, I will choose the app that I created in the previous section as the application that I want to publish to.
  3. I will click on the Run button to save the publishing configuration and begin the process of deploying my function to Azure.

Rider will compile my Function application and will create the ZIP package that will then be uploaded to Azure and deployed in the Functions app service.

Register the API Key with Azure AD B2C

If you are new to Azure Functions, many times Azure Functions are deployed with authorization enabled. This is done to limit who can call the Azure Function directly. In order to call an Azure Function that requires authorization, callers will need to pass an API key to the function that the Azure Functions app service host can verify. The Azure Function that I defined has authorization enabled.

To allow Azure Active Directory B2C to invoke the function, I will create a new policy key and will store the API key value in the policy key. I will later reference this policy key when adding the Azure Function call to my custom policy.

First, I need to get the API key that I can use to invoke the Azure Function:

  1. Open the Azure Portal and navigate to the Function App.
  2. In the left navigation menu, click on the App keys link.
  3. Either create a new host key or use the default key.
  4. Click on the Value field to show the key.
  5. Copy the key to the clipboard and save for use in the next step.

Now that I have the API key, I can create a policy key and store the API key in it:

  1. Open Azure Portal and switch to the directory for my Azure Active Directory B2C tenant.
  2. Open the Azure Active Directory B2C console.
  3. In the left navigation bar, click the Identity Experience Framework link.
  4. In the left navigation bar, click the Policy keys link.
  5. Click the Add button.
  6. In the Create a key panel, change the Options field to Manual.
  7. In the name, enter a descriptive name like PasswordValidationFunctionKey.
  8. In the Secret field, paste the API key.
  9. For the Key Usage field, set it to Signature.
  10. Click the Create button.

Invoke the Azure Function from a Technical Profile

I am now at the point that I am able to create the technical profile that will invoke my Azure Function to validate passwords, and then to add that technical profile to the sign up flows for my policies. The first thing that I will need to do is to obtain the URL where my Azure Function is hosted:

  1. Open Azure Portal and switch to your main subscription.
  2. Navigate to the Function app.
  3. In the left navigation menu, click the Functions link.
  4. Click on the ValidatePassword function in the list of functions (there should be only one if you are following along).
  5. At the top of the page, click on the Get Function Url button. This will display a popup with the URL.
  6. Copy the main part of the URL. You do not need the query string containing the code.

⚠️ NOTE: I am making the assumption that you have been following along in this series and have custom policies similar to what I have built. If you are doing this with your own policies, you may need to adapt.

I opened up my custom policy XML files in Visual Studio Code next to create the technical profile. Looking at where I left off, I am going to add the new technical profile to the base.xml file so that it is shared by signup.xml and signup_or_login.xml. At the bottom of the <ClaimsProviders> element, I added a new claim provider:

This technical profile implements a REST call to an external web API. The ServiceUrl metadata item contains the URL where the API is hosted. The SendClaimsIn metadata item instructs the REST provider to take the input claims and store them in a JSON blob in the body of the request. The AuthenticationType metadata item instructs the REST provider to read the API key from the B2C_1A_PasswordValidationFunctionKey policy key and pass the API key in the x-functions-key HTTP header of the request.

The <InputClaims> section will be turned into a simple JSON object and sent in the body of the HTTP request. The new password for the user is stored in the newPassword claim. I will map the newPassword claim to the password claim because the Azure Function looks for the password to validate in the password field in the JSON request.

Add Password Validation to Sign Up

The final step is to add password validation to the user’s sign up flow. I currently have two sign up flows. The first flow is independent in signup.xml and the second one is combined with the login flow in the signup_or_login.xml policy. Fortunately, I don’t need to modify either of those and I can make the change in the base.xml policy file.

In base.xml, the sign up for both flows is handled by the ShowSignUpForm technical profile. If I want to validate the password before the user is created, I can add the password validation call as a <ValidationTechnicalProfile>. Validation technical profiles are called when a self-asserted form is submitted to validate the values that were provided by the user, and to actually create the user. I can add the call to the ValidatePassword technical profile immediately before the call to CreateLocalUser, therefore is the password is invalid, the user will not be created.

In base.xml, I updated the ShowSignUpForm technical profile to invoke the ValidatePassword technical profile:

Save the changes and upload the new base.xml custom policy to your Azure Active Directory B2C tenant.

Test Password Validation

I am going to begin by testing the password validation in my sign up custom policy. When I run the custom policy in the Azure Active Directory B2C tenant, I am first taken to my sign up form:

The sign up form

When I enter a username and the password Password1, I receive the error telling me how many times the password has been discovered in a data breach:

The error message shows that the password has been compromised 220,490 times

When I enter a valid password that has not been breached, then my new user account is created.

When I run the combined sign up or log in policy, I should see the same result when I sign up as a new user.

Where Have We Gone?

In this article, I have demonstrated how to extend a custom policy in Azure Active Directory B2C using an Azure Function invoked as a REST API. Using the Restful technical profile in Azure Active Directory B2C, we can integrate with other systems or enhance our custom policies and user flows with custom behavior. I have also demonstrated how to use an Azure Function to do data validation of a field, in this case the password for the new user account.

--

--

Michael Collins
Neudesic Innovation

Senior Director of Application Innovation at Neudesic; Software developer; confused father