Log In With Local Accounts on Azure AD B2C
This is it! This is the post that we have been building up to! So far in this series we have taken a high-level overview of Azure AD B2C and identity management for Software-as-a-Service applications. We have created a tenant and configured it to be customized. We have totally decomposed the Starter Pack to understand how the sample Sign Up or Sign In flow works. We have built from scratch (mostly) our own custom policy that allowed users to sign up for a local user account for an application. Now that we can create users, it’s time that we complete stage 1: we will get the user to log in using their local user account and identify the user to the application using a JSON web token.
This is the sixth 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:
- Building Application Identity Solutions using Azure AD B2C
- Configure Azure AD B2C For Customization
- Understanding the B2C Directory
- Sign Up or Sign In Users With Azure AD B2C: Part 1
- Sign Up New Users Using Azure AD B2C
Revisiting the User Story
In Sign Up or Sign In Users With Azure AD B2C: Part 1, I created the user story for logging in:
As a user, I will log into Project Center in order to report my project status and update my tasks.
I then defined the acceptance criteria for the story:
- The user can successfully log in using their username and password
- An error will be shown if the username is incorrect
- An error will be shown if the password is incorrect
We will use the acceptance criteria to drive our implementation of the log in story. Just like in the last post, our goal is to implement the simplest login feature possible. We can enhance the login process later to add more features.
Designing the User Journey
To start the story, we want to think about the journey that the user will need to take to complete the story. Since we already have a sign up story, we are going to keep this story simple and only focus on logging in. We will not consider what happens if a new user gets to the login page by accident. We can add the sign up functionality later. I will also not consider resetting a forgotten password. We can look at that later as well. This story and the user journey will be simply focused around logging in the user.
Here’s the user journey that I have come up with:
- The user is presented with a login form where they will enter their username and password. When the form is submitted, a validation technical profile will be executed to validate the entered username and password against the user object in the directory.
- Generate the JSON web token containing the user’s identity information.
Sounds simple enough, but as we saw in the past couple of posts, there’s a lot of work that will go into implementing this story.
Sharing Custom Policy Definitions
If you look at the sample policies in the Starter Pack, you will notice the they follow a particular pattern:
- A base custom policy containing most of the claims schema, other building blocks, claims providers, and user journeys.
- An extension custom policy containing differences specific to the starter pack. The extension custom policy inherits from the base custom policy.
- The relying party policy that executes a user flow. The relying party policy inherits from the extension custom policy.
In the last post, I created everything into a single policy file because I wanted to start building custom policies from scratch to really learn how they work and it just made sense to keep everything in a single policy. As I am starting the log in policy, I can think that there will be some things such as some of the claims in the claims schema and the JWT generation technical profile that I can reuse in the log in policy, so I can refactor the sign up policy and extract out those definitions that I am going to share into a base policy. I will progressively refactor as it makes sense to throughout this post. But in anticipation of refactoring, I can start by creating an empty base policy:
I am next going to update my sign up policy to inherit from the base policy:
Finally, I will create the starter skeleton for my log in custom policy:
Define the Log In Policy
Just like in the previous post, I am going to start with the relying party policy definition for logging in. This policy will use the Open ID Connect protocol to accept and map HTTP requests to the relying party policy. In the log in policy, I am accepting the signInName
claim as an input claim. This will allow a client application to pass the username if the application knows it to pre-populate the form field. Just like in the sign up policy, I will be writing out the objectId
claim containing the user’s directory identifier and the B2C tenant identifier in the JSON web token that is output if the log in is successful.
Looking at the relying party policy, I see that I am using the signInName
, objectId
, tenantId
, and sub
claims. Instead of redefining them, I am going to update my sign up policy and move those claims to the base policy. I will not show that here, but they will appear in the final listing at the end of this post.
Charting the User’s Journey
The next step is to define the LogIn
user journey that will guide the user through the log in process. As I mentioned above, I see two orchestration steps in the journey. The first step will display the login form and will use the validation technical profile to authenticate the user against the directory. The second step will issue the JSON web token.
Showing the Log In Form
The ShowLogInForm
technical profile is a self-asserted technical profile that will use the api.localaccountsignin
content definition to display the log in form for a user account. The user will be prompted to enter their username and password:
The api.localaccountsignin
content definition is defined below:
The ShowLogInForm
technical profile specifies the signInName
as an input claim. If the client application knows the user’s username and sends the username in the signInName
claim to the relying party policy, then the username field in the form will be pre-populated. The form will display the username and password fields in the form.
Log In the Local User
After the user enters their username and password into the login form and submit the form back to the relying party policy, the ShowLogInForm
technical profile will execute the LogInLocalUser
technical profile as a validation technical profile to authenticate the user using their password. LogInLocalUser
is an OpenID Connect identity provider technical profile that implements the client side of the OpenID Connect protocol to request tokens from the B2C tenant.
The LogInLocalUser
technical profile will issue a POST
HTTP request to the token endpoint for the B2C tenant using the Resource Owner Password Credentials Grant. The username and password will be sent in the HTTP request to the token endpoint. The token endpoint will retrieve the user object and compare the passwords. If the passwords match, then the user is authenticated and an ID token is issued with profile information about the user embedded in it. If the user is not found or the passwords do not match, the authentication fails and the LogInLocalUser
technical profile returns a validation error to the ShowLogInForm
technical profile, which will update the UI and display an error to the user.
At this point, the user is authenticated and logged in, so the only thing left is to issue the JSON web token back to the client application, which we do in the last orchestration step.
Running the Policy
Since I refactored my sign up policy earlier and created the new base policy, I need to delete the old sign up policy and replace it with the new policy. But now that I created the base policy, the load order is important. In the Custom policies section of Identity Experience Framework in the B2C tenant console, I started the load process by deleting the B2C_1A_SIGNUP
policy. I then loaded the policies again in this order:
base.xml
signup.xml
login.xml
After everything loaded, I ran a test of the login policy to attempt to log in with users I created while testing the sign up policy
My login policy presented me with the login form prompting me to enter my username and password:
After successfully authenticating, I was redirected back to http://jwt.ms
to see my ID token:
Using Visual Studio Code, I looked at the Application Insight log for my log in test and looked at the trace log entry for the LogInLocalUser
technical profile:
I copied out the ID token that was returned by the token endpoint and replaced the token in the http://jwt.ms
website to see the token that was returned by the token endpoint:
As we add more data to the profile, we will read more from this token in later posts.
Where Have We Gone
Over the course of this series, we have gone from looking at the abstract view of Azure Azure Directory B2C to building a very simple identity management system. We took a very deep dive into the world of custom policies, decomposed a starter pack custom policy to understand how Azure Active Directory B2C authenticates users, and then we created a custom policy to sign up a user and a second policy to log in the user. While our policies are very simple, they achieved our goal and took some of the mystery away using Azure Active Directory B2C. We have taken our first steps, and we now have many different directions that we can go in. Stay tuned for more to come!