I recently worked on migration users’ accounts in an existing SQL database to azure AD B2C. I found some helpful articles from Microsoft that document different migration approaches and offer example codes on using Microsoft Graph SDK to manage the users. You can find the links to these articles and sample projects in the References section.
For the most part, I did not have much troubles with the basic CRUD operations. However, I had a bit of difficulties working with custom attributes and retrieving a user by email. In this post, I’m going to share some tips and caveats I learned. In particular, I’ll discuss:
Per the document on Microsoft Graph permissions, you need at least the following application permissions to create and update users’ profiles:
Directory.ReadWrite.All
Per the document, the Directory.ReadWrite.All
However, as you read from the document, the above permissions are not sufficient if you also want to delete users’ accounts or reset users’ passwords. For those operations, you need the following permissions:
User.ReadWrite.All
Per the document, the User.ReadWrite.All
Depending on your setup, choose between application or delegated permissions. In my app, I use the application permissions because the migration script does not involve a user. Fore more information, checkout this document.
Here is how you can add the permissions to your app via the azure portal:
Both the User.ReadWrite.All and the Directory.ReadWrite.All permissions require admin consent. If you are an admin of your tenant, once you have added the permissions, you can click on the Grant admin consent for {your tenant name} link under Configured permissions to grant the consent. If you are not an admin, reach out to someone with admin privileges, and that person can grant consent by going to the same screen under API permissions.
I have not tried to update a user’s password using the graph SDK. However, if you need to reset a user’s password, you’ll need to assign the User administrator role to your application. For more info, checkout the document.
On retrieving a user object by id, I inspected the default properties to determine which properties I could use for filtering by email. Among the properties I got back, only “mail” and “userPrincipalName” attributes appeared appropriate. However, in our directory, all the users have blank mail. It should not be a problem as I expected to be able to use the userPrincipalName attribute because in the portal, the attribute appears to hold an email.
However, the value of userPrincipalName I got back from the graph API for that user was the object id of the user, not the email.
That was a bit surprising to me. Luckily, I’m used to surprises, having working with different APIs. I thought of using alternatives such as filtering on multiple attributes like givenName and surName. Although it’s unlikely we have two persons with the same given name and surname, it’s still possible, and I need an exact result, so filtering on those attributes would not work.
After consulting my best friend Google, I found the way to accomplish filtering by email is by using the issuerAssignedId attribute.
private async Task<User> ExpectExactUserByEmail(string email) { // https://github.com/microsoftgraph/microsoft-graph-docs/issues/7282 var filter = $"identities / any(id: id / issuer eq '{_migrationAppOptions.Issuer}' and id / issuerAssignedId eq '{email}')"; return await ExpectExactUser(filter); } private async Task<User> ExpectExactUser(string filter) { var users = await _graphClient.Users.Request().Filter(filter).Select(_defaultUserSelectAttributes).GetAsync(); if (users.Count == 0) { throw new KeyNotFoundException($"Not able to find user with given filter: {filter}"); } else if (users.Count > 1) { throw new ArgumentException($"More than one users found with given filter: {filter}"); } return users[0]; }
In the above snippets, notice the filtering is on identities and use both issuer and issuerAssignedId attributes, as per this issue. I suspect this only work because we use the “emailAddress” sign in type when creating a user. In fact, when creating a user using the SDK, I had to set the Identities property of the graph user object to the following:
Identities = new ObjectIdentity[] { new ObjectIdentity() { SignInType = "emailAddress", Issuer = _migrationAppOptions.Issuer, IssuerAssignedId = adB2CUser.UserEmail } }
I had seen dated posts that suggested the Microsoft Graph SDK did not support custom attributes in Azure AD B2C, and the alternative was to use Azure Active Directory Graph API. However, azure active directory graph api is fading away, and Microsoft Graph SDK is its successor.
At the time I used it, Microsoft Graph SDK does support working with custom attributes, as I was able to follow the example codes to set and retrieve custom attributes, so you can give it a try.
One thing to pay attention is you need to use the full name of the custom attribute, which you can construct using the following format:
‘extension_{client id of b2c-extensions app with all dash removed}_{name of attribute}’
The b2c-extensions-app is automatically registered when you create an azure ad b2c tenant.
Below show the snippets I use to construct the full attribute name.
private string ExtensionAttributeFullName(string attributeName) { return $"extension_{B2CExtensionsAppClientId.Replace("-", "")}_{attributeName}"; }
When retrieving a user, you can use a $select clause to include the custom attribute in the result you get back from the graph, as the below example shows.
public async Task < User > GetUserByIdAsync(string userId) { var select = $ "id, givenName, surName, extension_2b76741733644c348db79bdc2e1002ce_MigrationSource"; return await _graphClient.Users[userId].Request().Select(select).GetAsync(); }
When creating a user, you can set the custom attributes using a dictionary and assign to the AdditionalData property of the graph user, as shown in the below snippets.
public async Task < User > MergeUser([Required] ADB2CUserDTO adB2CUser) { IDictionary < string, object > extensionInstance = new Dictionary < string, object > { { _migrationAppOptions.ExtensionAttributeMigrationSourceFullName, "My web app" } }; User user = new User() { AccountEnabled = true, DisplayName = adB2CUser.DisplayName, PasswordProfile = new PasswordProfile { ForceChangePasswordNextSignIn = true, Password = _passwordGenerator.GenerateAlphanumericPassword() }, GivenName = adB2CUser.GivenName, Surname = adB2CUser.Surname, PostalCode = adB2CUser.PostalCode, State = adB2CUser.State, StreetAddress = adB2CUser.StreetAddress, City = adB2CUser.City, MobilePhone = adB2CUser.TelephoneNumber, Country = adB2CUser.Country, Identities = new ObjectIdentity[] { new ObjectIdentity() { SignInType = "emailAddress", Issuer = _migrationAppOptions.Issuer, IssuerAssignedId = adB2CUser.UserEmail } }, AdditionalData = extensionInstance }; return await _graphClient.Users.Request().AddAsync(user); }
That’s about it for this post. As always, if you have questions, feel free to reach out.
Best practices for working with Microsoft Graph
Git repo of example codes for user migration
Permissions for user delete and password update
Microsoft graph document issue: Not able to filter users by identity
Building a fully multitenant system using Microsoft Identity Framework and SQL Row Level Security
Integrate Azure AD B2C reset password user flow in angular using oidc-client-js.
Using MSAL angular to authenticate a user against azure ADB2C via authorization code flow with Proof Key for Code Exchange.
Using Azure Application Insights for centralized logging
Building multitenant application – Part 3: Authentication
Building multitenant application – Part 1: Multitenant database using Row Level Security
Migration from Oracle to azure SQL caveat – Azure SQL does not support time zone settings
Migrating from Oracle to Azure SQL caveat – prepared statement set string causes implicit conversion