Migration From Microsoft Azure AD B2C

Overview

This document will help you migrate your users off of Microsoft Azure AD B2C, and onto FusionAuth.

There are a number of different ways applications can be integrated with Azure AD B2C, and it would be difficult to cover them all. This guide focuses on migrating user data, including profile data and passwords. However, Azure AD B2C does not allow for password hash export. Therefore, you must perform a slow migration if you don’t want to force users to reset their passwords.

Alternatively, you can do a bulk migration and force everyone to reset their passwords. This option is discussed below, but the primary focus of this guide is enabling you to migrate your users from Azure AD B2C without requiring any password resets.

Prerequisites

This guide assumes intermediate level familiarity with Microsoft Azure, in particular AD B2C.

This guide assumes you have installed FusionAuth. If you have not, please view our installation guides and install FusionAuth before you begin. For more general migration information, please view the FusionAuth migration guide.

Planning Considerations

Slow Migration or Bulk Migration

To preserve your users’ passwords, you need to perform a slow migration. Users log in to FusionAuth with their Azure AD B2C credentials, and FusionAuth transparently migrates their data. Slow migrations in FusionAuth use Connectors, a paid feature.

If, on the other hand, resetting user passwords is acceptable, a Bulk Migration can work for you. Review that section for more details on the required steps. You may also perform a bulk migration after a slow migration has run for a while. Active users can be transparently migrated and infrequent users may not mind resetting their password.

You can learn more about the types of migrations that FusionAuth supports here.

Mapping User Attributes

The attributes of the User object in FusionAuth are well documented.

If there is an attribute in your user which cannot be directly mapped to a FusionAuth attribute, you can place it in the user.data field. This field can store arbitrary JSON values and will be indexed and searchable.

One Tenant or Many

In Azure AD B2C, users are placed in Tenants. FusionAuth also has the concept of Tenants. Both of these include data about users, applications and other configuration.

Each tenant in FusionAuth is a distinct user space. You may choose to merge multiple Azure AD B2C tenants into one FusionAuth tenant or keep them separate.

Learn more about FusionAuth tenants.

Identity Providers

In Azure AD B2C, external identity providers are managed using Identity Providers. With FusionAuth, these are also called Identity Providers.

Review the supported FusionAuth Identity Providers to ensure any you need are supported. At this time, while there is considerable overlap between the supported identity providers, there are a number of differences.

If not supported explicitly, a provider may work with an OIDC or SAML connection. Otherwise, please open a feature request.

Importing Azure AD B2C users stored in an external identity provider is more straightforward because you can use the Import API; you don’t have to perform a slow migration. Many of the steps in Bulk Migration will apply.

To retrieve the user information, use the approach documented in the Export User Data From Azure AD B2C section.

Migrating users with social logins such as Apple or Facebook requires that you have an existing user Id for that provider. What this unique user Id looks like depends on the particular social identity provider. The unique Id may be an email address, an integer, UUID, or a random string.

Configure the appropriate FusionAuth Identity Provider with the same values (client_id, etc) as the original user management system you are migrating away from.

Import users with the Import API, assigning each user with a social login a random password such as a UUID.

Your next step depends on whether the social login provider’s unique identifier is available as part of your migration data. If you have the social login provider’s unique identifier, for each user, use the Link API to create a link with the appropriate User Id, Identity Provider Id and Identity Provider User Id.

  • The User Id is the Id of the recently created FusionAuth User.
  • The Identity Provider Id is found on the corresponding Identity Provider API documentation. Look for identityProvider.id .
  • The Identity Provider User Id is the existing social provider user identifier exported or otherwise extracted from the original system.

You do not need to migrate the social network token, which may or may not be accessible. During the first login of a newly migrated user, FusionAuth finds the unique user in the social login provider based on the migrated Identity Provider User Id, and completes the login. During this process, FusionAuth stores a token on the Link, if the social provider returns one. Depending on the configuration of the social provider, users may see a prompt asking if they want to allow FusionAuth to have access to user data such as email address.

IdP Linking Strategies are available since version 1.28.0. Before that version, users were linked on email.

If you do not have the social login provider’s identifier, you need to decide if you want to transparently link the two accounts, which is easier for the end user, or if you want to ask the user to manually link the accounts, which is more accurate, but may be confusing.

To transparently link the accounts, choose a linking strategy of Link On Email or Link On Username, which will create the user if they don’t exist. However, if the user has an email address at their social provider which differs from the email address that was used to sign up for your application and which you imported to FusionAuth, then two accounts will be created.

For example, if the user has a Google account richard@gmail.com, but signed up for your application with richard@fusionauth.io, then if you use the Link On Email strategy, two different accounts will be created, since FusionAuth is trying to match on email address and they don’t. The same holds true for usernames with the Link on Username strategy.

To prompt the user to link the accounts, choose a linking strategy of Pending, which will prompt the end user to sign into FusionAuth after they sign into the social provider, authoritatively linking the two accounts.

Here’s more information about IdP Linking Strategies.

Other Entities

There are often other important entities, such as app clients or password policies, that need to be migrated. There are usually fewer of these, so an automated migration may not make sense, but plan to move this configuration somehow.

Be aware that functionality may not be the same between AD B2C and FusionAuth. This is different from user data; as long as you can somehow migrate a login identifier (a username or email) and a password hash, a user will be authenticated and successfully migrated. You can download FusionAuth before you begin a migration and build a proof of concept to learn more about the differences.

A partial list of what may need to be migrated for your application to work properly includes the following:

  • User Flows and Custom Policies are ways for you to customize authentication or authorization workflows with Azure AD B2C. FusionAuth has a similar concept called Lambdas. FusionAuth also has webhooks fired at certain points in the user lifecycle; in certain configurations, they can also stop a particular authentication flow.
  • Azure AD B2C does not have a standardized roles-and-permissions concept. Roles and permissions are usually handled with custom attributes or secondary data stores. FusionAuth has roles that are configured on an application by application basis and made available in a token after a successful authentication.
  • Azure AD B2C has the concept of User Flow Page Layouts where users can log in or register. FusionAuth has hosted login pages which offer a superset of the functionality of Azure AD B2C User Flow. They are customized via themes.
  • In Azure AD B2C, users log into App Registrations. In FusionAuth, Applications are a similar construct, but users are associated with them through Registrations. You can migrate both your Client Id and Client Secret from Azure AD B2C to FusionAuth.
  • Azure AD B2C sends emails on your behalf, such as forgot password notifications. FusionAuth can do so too; the templates are customizable.
  • Azure AD B2C offers the Client Credentials grant. FusionAuth offers a constrained version of this using Entity Management; this is available in all paid editions.
  • Refresh tokens allow JWTs to be refreshed without a user logging in. These can be migrated from Azure AD B2C using the Import Refresh Tokens API.
  • Azure AD B2C allows for custom attributes, but they must be configured at the Directory level. In FusionAuth, as mentioned above, custom user attributes are stored on the user.data field and are dynamic, searchable and unlimited in size. Any valid JSON value may be stored in this field.
  • Azure AD B2C supports MFA. FusionAuth also supports MFA, which may be enabled for a tenant and configured for a user at any time.

In FusionAuth, users are explicitly mapped to applications with a Registration.

Azure AD B2C, in contrast, gives users access to all Azure AD B2C applications in a Tenant by default.

Identifiers

When creating an object with the FusionAuth API, you can specify the Id. It must be a UUID.

This works for users, applications, and tenants, among others.

If you have external dependencies on an Id stored in , port the same Id over to FusionAuth.

Differences

In addition to the different names for common functionality outlined above in Other Entities , there are some fundamental differences between FusionAuth and Azure AD B2C. If your application relies on Azure AD B2C specific functionality, please review this section carefully.

  • Azure AD B2C has a pricing model that applies to monthly active users (MAUs). FusionAuth has no user limit. Instead, you are limited by the resources provided to a FusionAuth instance, such as memory, CPU and database capacity.
  • FusionAuth does not support custom scopes. There is an open feature request.

Once you’ve planned the migration of other entities, the next step is to set up FusionAuth to connect to Azure AD B2C to import users during login.

Importing Users

Because you are implementing a slow migration, it will take place over time. But you can set up a test environment to confirm it will work before deploying to production. Here are the steps we need to take.

  1. Set Up FusionAuth
  2. Set Up Azure AD B2C
  3. Set Up the Connector
  4. Log In as a Test User
  5. Verify the Import
  6. Migrate Everything Else

Set Up FusionAuth

You need to set up FusionAuth so migrated user data can be stored. As mentioned above, this guide assumes you have FusionAuth installed.

If you don’t, view our installation guides and install it before proceeding further.

Create a Test Tenant

It is best to create a separate tenant for migration testing. Tenants logically isolate configuration settings and users. If a migration goes awry or you need to redo it after tweaking settings, you can delete the test tenant and start with a clean system. To add a tenant, navigate to Tenants and choose the Add button (green plus sign).

Adding a tenant.

Give it a descriptive Name like AD B2C import test. You shouldn’t need to modify any of the other configuration options to test importing users.

Save the tenant.

The tenant creation screen.

Record the Id of the tenant, which will be a UUID. It will look something like 25c9d123-8a79-4edd-9f76-8dd9c806b0f3. You’ll use this later.

The tenant list.

Create a Test Application

Applications are anything a user can log in to. In FusionAuth there’s no differentiation between web applications, SaaS applications, APIs and native apps. To add an application, navigate to Applications and click on the Add button (the green plus sign). Give the application a descriptive name like AD B2C application.

Select your new tenant, created above, in the dropdown for the Tenant field.

Navigate to the OAuth tab and add an entry to Authorized redirect URLs . Use a dummy value such as https://example.com. Later, you’ll need to update this to be a valid redirect URL that can take the authorization code and exchange it for a token. Learn more about this in the FusionAuth OAuth documentation.

You shouldn’t need to modify any of the other configuration options to test importing users. Save the application.

The application creation screen.

Next, view the application by clicking the green magnifying glass and note the OAuth IdP login URL . You’ll be using it to test that users can log in.

Finding the login URL.

Set Up Azure AD B2C

There are three main components you need to set up to enable this migration.

The first is an App Registration, with an ROPC flow.

Secondly, you will need to set up a Microsoft Graph application to retrieve the user’s details, in order to import them to FusionAuth.

The third is an Azure Function to accept login requests from FusionAuth over TLS and perform an ROPC login request on Azure AD B2C, and return the relevant user data via the Azure AD B2C Graph API app.

FusionAuth Connectors will send user credentials to Azure AD B2C for authentication, via an Azure Function. Secure both the data in transit and the Azure Function endpoint.

In this guide, all communication is over TLS and the Azure Function requires a unique query string code. You should also review the documents on Securing Azure Functions to implement any additional security features you may require.

Also note that the Azure AD B2C ROPC endpoint is monitored by rate limiting and identity protection systems. If a dynamic threshold of failed authentications is exceeded, the identity protection system may identify a repeated IP address (i.e. the Azure Function) as an attacker. You may need to deploy Functions to multiple regions, or perhaps self-host the function on a known IP address to mitigate potential issues.

Configuring the App Registration in Azure AD B2C

FusionAuth will send login requests to Azure AD B2C to validate the users credentials, via the Azure Function. It will also retrieve the users Azure AD B2C profile from the Microsoft Graph API. It is best practice to create new App Registrations for each of the ROPC flow and Microsoft Graph connection. This guide will walk through the configurations using the Azure Portal.

These instructions use the newer App Registrations (as opposed to the legacy Applications) experience.

Visit the Azure AD B2C App Registrations tab. Then follow these instructions to set up a new ROPC App Registration and Flow.

At the end, you’ll end up with an app client configuration looking similar to this:

Configuring an ROPC App Registration for migration.

Save the new configuration. On the list of app clients, you’ll see your new one.

Record the Application (client) ID. This will be a GUID like 022c5902-d8ee-43b0-ac1f-c2719b799657. Also record the ROPC flow name - this will be needed as part of the Function environment settings.

Finding the app client id.

Now you can set up the App Registration for the Microsoft Graph API access. Follow this guide, including the “Create Client Secret” section. Record both the Client Secret, and the Application (client) ID as above.

Next, you’ll need to set up an Azure Function to receive the login requests from FusionAuth and return the user’s profile. The Function will pass the credentials on to Azure AD B2C and, when the authentication succeeds, retrieve the user’s profile from Microsoft Graph, then return a FusionAuth friendly JSON object.

Configuring the Azure Function

The Azure Function receives the login request from FusionAuth, attempts to log the user in via ROPC, and then returns a FusionAuth user object on success. The Function will call the Users API after successful authentication to get additional user attributes, which will then be transformed into a FusionAuth compatible format.

When setting this up, modify the example Function code provided in this guide. Here’s the entire sample Azure Function, tested with the node 14 runtime:

Azure AD B2C Connector Function code

const graph = require("./graph");
require("isomorphic-fetch");
const querystring = require("querystring");
const jwt = require("jsonwebtoken");

module.exports = async function (context, req) {
  context.log("JavaScript HTTP trigger function processed a request.");
  const tenantName = process.env.TENANT_NAME;
  const tenantId = process.env.TENANT_ID;
  const ropcFlow = process.env.ROPC_FLOW_NAME;

  const tokenUrl = `https://${tenantName}.b2clogin.com/${tenantName}.onmicrosoft.com/${ropcFlow}/oauth2/v2.0/token`;
  const tokenRequest = {
    username: req.body.loginId,
    password: req.body.password,
    grant_type: "password",
    scope: `openid ${process.env.ROPC_CLIENT_ID}`,
    client_id: process.env.ROPC_CLIENT_ID,
    response_type: "token",
  };

  let tokenResponse = await fetch(tokenUrl, {
    method: "POST",
    body: querystring.stringify(tokenRequest),
    headers: {
      "Content-type": "application/x-www-form-urlencoded",
    },
  });

  if (tokenResponse.ok) {
    let body = await tokenResponse.json();
    const decodedToken = jwt.decode(body.access_token);
    let user = await graph.getUser(decodedToken.oid);
    let fusionUser = transformToFusionUserObject(user);
    context.res = {
      status: 200,
      body: {user: fusionUser},
    };
  } else {
    // Something not great with username
    context.res = {
      status: 404,
      body: await tokenResponse.text(),
    };
  }

  context.log(JSON.stringify(context.res));
};

function transformToFusionUserObject(azureUser) {
  let localIdentity = azureUser.identities.find(i=>i.signInType==="emailAddress"); //{|i|i["signInType"]==="emailAddress"}
  let epochTime = new Date(azureUser.createdDateTime);
  epochTime = epochTime.getTime()/1000;
  let fusionUser = {
    id: azureUser.id,
    active: azureUser.accountEnabled,
    firstName: azureUser.givenName,
    fullName: azureUser.displayName,
    lastName: azureUser.surname,
    username: azureUser.userPrincipalName,
    email: localIdentity.issuerAssignedId,
    verified: true,
    insertInstant: epochTime,
    data :{
        azure:{
            identities: azureUser.identities
        }
    }
  }
  return fusionUser;
}

You will need to modify the transformToFusionUserObject function to match the properties of the users that you want to import. This converts the JSON returned from Azure AD B2C into the JSON format FusionAuth requires. You can see samples of both below. To modify this procedure, clone or fork the repo for the full Function project, then make the changes in your local copy of the project.

The exact implementation depends on your Active Directory’s custom attributes and business logic. You could, for example, give users certain FusionAuth roles, register them for more than one application or add them to a previously created FusionAuth group.

Sample User Data Response from Azure AD B2C

{
  "@odata.context": "https://graph.microsoft.com/beta/$metadata#users",
  "value":
  [
      {
          "id": "2e255143-5592-4233-a5b9-15e4ba56dfbe",
          "deletedDateTime": null,
          "accountEnabled": true,
          "ageGroup": null,
          "businessPhones":
          [],
          "city": null,
          "createdDateTime": "2022-08-05T21:05:58Z",
          "creationType": null,
          "companyName": null,
          "consentProvidedForMinor": null,
          "country": null,
          "department": null,
          "displayName": "Erlich Bachman",
          "employeeId": null,
          "employeeHireDate": null,
          "employeeLeaveDateTime": null,
          "employeeType": null,
          "faxNumber": null,
          "givenName": "Erlich",
          "imAddresses":
          [],
          "infoCatalogs":
          [],
          "isManagementRestricted": null,
          "isResourceAccount": null,
          "jobTitle": null,
          "legalAgeGroupClassification": null,
          "mail": null,
          "mailNickname": "erlich_bachman.com#EXT#",
          "mobilePhone": null,
          "onPremisesDistinguishedName": null,
          "officeLocation": null,
          "onPremisesDomainName": null,
          "onPremisesImmutableId": null,
          "onPremisesLastSyncDateTime": null,
          "onPremisesSecurityIdentifier": null,
          "onPremisesSamAccountName": null,
          "onPremisesSyncEnabled": null,
          "onPremisesUserPrincipalName": null,
          "otherMails":
          [
              "erlich@aviato.com"
          ],
          "passwordPolicies": null,
          "postalCode": null,
          "preferredDataLocation": null,
          "preferredLanguage": null,
          "proxyAddresses":
          [],
          "refreshTokensValidFromDateTime": "2022-08-05T21:05:58Z",
          "securityIdentifier": "S-1-12-1-774197571-1110660498-3826629029-3202307770",
          "showInAddressList": null,
          "signInSessionsValidFromDateTime": "2022-08-05T21:05:58Z",
          "state": null,
          "streetAddress": null,
          "surname": "Bachman",
          "usageLocation": null,
          "userPrincipalName": "erlich_bachman.com#EXT#@fusiontut.onmicrosoft.com",
          "externalUserConvertedOn": null,
          "externalUserState": null,
          "externalUserStateChangeDateTime": null,
          "userType": "Member",
          "employeeOrgData": null,
          "passwordProfile": null,
          "assignedLicenses":
          [],
          "assignedPlans":
          [],
          "authorizationInfo":
          {
              "certificateUserIds":
              []
          },
          "deviceKeys":
          [],
          "identities":
          [
              {
                  "signInType": "federated",
                  "issuer": "MicrosoftAccount",
                  "issuerAssignedId": null
              },
              {
                  "signInType": "userPrincipalName",
                  "issuer": "fusiontut.onmicrosoft.com",
                  "issuerAssignedId": "erlich_bachman.com#EXT#@fusiontut.onmicrosoft.com"
              }
          ],
          "onPremisesExtensionAttributes":
          {
              "extensionAttribute1": null,
              "extensionAttribute2": null,
              "extensionAttribute3": null,
              "extensionAttribute4": null,
              "extensionAttribute5": null,
              "extensionAttribute6": null,
              "extensionAttribute7": null,
              "extensionAttribute8": null,
              "extensionAttribute9": null,
              "extensionAttribute10": null,
              "extensionAttribute11": null,
              "extensionAttribute12": null,
              "extensionAttribute13": null,
              "extensionAttribute14": null,
              "extensionAttribute15": null
          },
          "onPremisesProvisioningErrors":
          [],
          "provisionedPlans":
          []
      }
  ]
}

The transformToFusionUserObject will transform the above into a FusionAuth compatible format, as displayed below:

Sample Successful Login JSON

{
  "user": {
    "id": "702eebc9-ef84-489f-b7d3-6f67388ffdd3",
    "active": true,
    "firstName": "Erlich",
    "fullName": "Erlich Bachman",
    "lastName": "Bachman",
    "username": "702eebc9-ef84-489f-b7d3-6f67388ffdd3@fusiontut.onmicrosoft.com",
    "email": "erlich.bachman@piedpiper.com",
    "verified": true,
    "insertInstant": 1661188751,
    "data": {
      "azure": {
        "identities": [
          {
            "signInType": "emailAddress",
            "issuer": "fusiontut.onmicrosoft.com",
            "issuerAssignedId": "erlich.bachman@piedpiper.com"
          },
          {
            "signInType": "userPrincipalName",
            "issuer": "fusiontut.onmicrosoft.com",
            "issuerAssignedId": "702eebc9-ef84-489f-b7d3-6f67388ffdd3@fusiontut.onmicrosoft.com"
          }
        ]
      }
    }
  }
}

Once you have the custom function logic updated, it is time to deploy the Azure Function.

Deploying the Azure Function

Azure Functions can be coded within the Azure portal directly, or coded on your local machine and deployed as a package using https://docs.microsoft.com/en-us/azure/azure-functions/functions-develop-vs-code?tabs=csharp[Visual Studio Code, with Azure Extensions]. As the function has dependent modules, you will need to follow these instructions. Clone the function GitHub repo, and follow the instructions in the link to deploy to your Azure Function.

After deploying the Function, set the following Function environment variables, also known as Application Settings, to those that you recorded when setting up the ROPC app:

Azure Connector Function environment variables to set

TENANT_NAME:          // The name of your AD B2C Tenant,
ROPC_FLOW_NAME:       // The name of the ROPC flow setup above
ROPC_CLIENT_ID:       // The Client ID of the ROPC App Registration
GRAPH_CLIENT_ID:      // The Microsoft Graph API App Registration clientId, a.k.a Application id
GRAPH_CLIENT_SECRET:  // The Microsoft Graph API App Registration client secret

Testing The Deployed API

Once you have deployed the Function, copy the Function URL with the access code from the Azure Portal. You can do this by clicking the “Get Function URL” button on the top bar of the Function overview page. The URL should look something like this: https://fusionAuth.azurewebsites.net/api/RopcProxy?code=s0ndU863xdbXsFO4dLZAJQXLzyTU789iaUJ43uLAtIkXm_AzFuFWP1zg==.

Test the login process with the lambda. You can use curl and an account whose username and password you know to do so.

Testing the API and Azure Function with Curl

curl -X POST \
'https://fusionAuth.azurewebsites.net/api/RopcProxy?code=s0ndU863xdbXsFO4dLZAJQXLzyTU789iaUJ43uLAtIkXm_AzFuFWP1zg=='  -H 'Content-type: application/json' -d '{
  "loginId": "test@example.com",
  "password": "password"
}'

If the user is authenticated, you should receive a response similar, though differing based on how you modified the Function code, to this:

Successful Results of Testing the API and Azure Function

{
  "user": {
    "id": "702eebc9-ef84-489f-b7d3-6f67388ffdd3",
    "active": true,
    "firstName": "Erlich",
    "fullName": "Erlich Bachman",
    "lastName": "Bachman",
    "username": "702eebc9-ef84-489f-b7d3-6f67388ffdd3@fusiontut.onmicrosoft.com",
    "email": "erlich.bachman@piedpiper.com",
    "verified": true,
    "insertInstant": 1661188751,
    "data": {
      "azure": {
        "identities": [
          {
            "signInType": "emailAddress",
            "issuer": "fusiontut.onmicrosoft.com",
            "issuerAssignedId": "erlich.bachman@piedpiper.com"
          },
          {
            "signInType": "userPrincipalName",
            "issuer": "fusiontut.onmicrosoft.com",
            "issuerAssignedId": "702eebc9-ef84-489f-b7d3-6f67388ffdd3@fusiontut.onmicrosoft.com"
          }
        ]
      }
    }
  }
}

If the authorization header or account credentials are incorrect, a 404 HTTP status code is returned, along with a message in the body. You can view the status code by running curl with the -v switch.

Set Up the Connector

Now you need to set up a Connector to use the Azure AD B2C API you created. You should set up a different Connector for each Azure AD B2C Tenant.

Log back into the FusionAuth administrative user interface if needed.

FusionAuth Reactor logo

This feature is only available in paid plans. Please visit our pricing page to learn more.

Connectors are a feature limited to paid editions, so you must ensure you have a valid reactor license. Learn more about activating reactor.

Next:

  • Configure the Connector with the API URL and authorization header
  • Configure the Tenant to use the Connector

Configure a Connector

Create and configure the Connector. Navigate to Settings -> Connectors and add a new Generic Connector.

Configure the Connector:

  • Add a name like Azure AD B2C migration.
  • Set the Authentication URL to the value of the Function URL endpoint created above.
  • You don’t need to set any headers, as Azure Functions have a code in the querystring.

At the end, you should have a screen like this:

Configuring a Generic Connector.

Save the Connector. Next, configure your tenant to use this Connector.

Configuring the Tenant

Navigate to your tenant settings: Tenants -> Azure import tenant -> Connectors.

Click the Add policy button to set up a new Connector policy.

Connector policies for this Tenant.

Set the Connector field value to the name of the Connector created previously. Make sure that the Migrate user field is enabled. You can leave the Domains field with the value of *, which will apply this Connector to every user.

After configuration, the Policy entry form should look similar to this:

Add Connector policy.

Save it.

Next, ensure this Connector Policy is in the correct order by using the arrows in the administrative user interface to put it at the top. With this configuration, all users are checked against this Connector the first time they are seen. If they log in, they’ll be migrated to the FusionAuth user database.

Azure AD B2C Connector policy added and in list.

Log In With a Test User

To test that users will be migrated, log in as a test user via the FusionAuth interface.

When you set up the test application, you recorded the OAuth IdP login URL .

Finding the login URL.

Copy this URL and open it in a new incognito browser window. (If you don’t use an incognito window, the admin user session will interfere with the test.) You should see the login screen:

The login page.

Enter credentials for a Azure AD B2C user account; it can be the same one you used to test the API with curl and log in. The user will be transparently migrated over to FusionAuth.

If the user was not migrated or the login was unsuccessful, you can troubleshoot. In the administrative user interface, enable the Debug enabled field in the Connector configuration by navigating to Settings -> Connectors and editing the Generic Connector you added.

After enabling enhanced debug messages, try to log in again with the test user. In the administrative user interface, navigate to System -> Event Log and look for useful messages.

After a successful test login, the user will be redirected to a URL like https://example.com/?code=FlZF97WIYLNxt4SGD_22qvpRh4fZ6kg_N89ZbBAy1E4&locale=fr&userState=Authenticated. This occurs because you haven’t set up a web application to handle the authorization code redirect yet.

That is an important next step but is beyond the scope of this document. Consult the 5 minute setup guide for an example of how to do this.

Let’s check that the import succeeded in another way: by viewing the user in the administrative user interface.

Verify the Migration

Next, log in to the FusionAuth administrative user interface. Review the user entries to ensure the data was correctly imported.

List imported users.

You can manage the user by clicking on the Manage button (black button) to the right of the Created date in the list to review the details of the imported user’s profile.

If you have a test user whose password you know, open an incognito window and log in to ensure the hash migration was successful. You recorded the URL to log in to the example application in Create a Test Application .

The user login screen.

After the test login, the user will be redirected to a URL like https://example.com/?code=FlZF97WIYLNxt4SGD_22qvpRh4fZ6kg_N89ZbBAy1E4&locale=fr&userState=Authenticated. This happens because you haven't set up a web application to handle the authorization code redirect.

That is an important next step but is beyond the scope of this document. Consult the 5 minute setup guide

for an example of how to do this.

At this point, you’ve successfully migrated a user from Azure AD B2C into FusionAuth. Any further changes for this user will occur against the FusionAuth database; this includes profile and password changes.

Clean Up Your Test Environment

After you are done testing, deploy these same configuration changes to production.

Depending on your architecture, you can choose to migrate users into the default tenant or a new tenant of the production instance. Whichever you choose, configure the Connector policy of the destination tenant.

If you aren’t keeping users in the test tenant, delete it. This is also useful if you want to start over because you need to tweak a setting such as the default application registration. In either case, delete the tenant you created.

This will remove all the users and other configuration for this tenant, giving you a fresh start. To delete a tenant, navigate to Tenants and choose the red trash can icon corresponding to the tenant to be deleted.

Deleting a tenant.

Confirm your desire to delete the tenant. Depending on how many users exist in that tenant, this may take some time. If it is easier, you may also delete migrated users one at a time using the administrative user interface.

Migrate Everything Else

You now have your users migrated, or a plan to do so. Congratulations! What is next?

You need to migrate additional configurations, as mentioned in Other Entities . Since the type of configuration varies, it is hard to provide a full list of how to import these items, but the general pattern will be:

  • Identify corresponding FusionAuth functionality.
  • Configure it in your FusionAuth instance, either manually or by scripting it using the client libraries or API.
  • Update your application configuration to use the new FusionAuth functionality.

Make sure you assign your users to the appropriate FusionAuth applications. You can do this either:

  • As part of your import process by adding registrations at import time.
  • After users have been migrated with the Registrations API.

You’ll also need to modify and test each of your applications, whether custom, open source, or commercial, to ensure:

  • Users can successfully log in.
  • The authorization code redirect is handled correctly.
  • Users receive appropriate permissions and roles based on the JWT.
  • The look and feel of the hosted login pages matches each application’s look and feel.

If your application uses a standard OAuth, SAML or OIDC library to communicate with , the transition should be relatively painless.

If you are using the Azure Client Credentials Flow, which is still in public preview, and need to migrate Client Credentials grants, create Entities in FusionAuth corresponding to each Azure AD B2C app client that is configured to use the Client Credentials grant. The scopes within FusionAuth are currently constrained to a certain format, though there’s an open link to GitHub issue to address it. Learn more about Entity Management.

You can migrate each of your Azure AD B2C app clients to a FusionAuth application using the Application API, which allows you to set your Client Id and Client Secret to be the same as they were in Azure AD B2C. Maintaining these values will minimize the impact on any applications using the OAuth Authorization Code grant or Implicit grant. Ensure you add the application to the correct tenant and allow the required grants.

Estimate the Slow Migration Timeline

When using a slow migration, you can estimate how many accounts will be migrated in a given period of time.

You can calculate a timeline by knowing the following:

  • How frequently the average user logs in
  • What the distribution of your users’ login behavior is
  • What your migration goal is

Determining these values precisely is beyond the scope of this guide. However, a good rule of thumb is to determine how often your average user logs in to your application. Then you can use this formula to find out when a certain percentage of users have migrated: S = [(1 - (1 - P)^D)*100]%.

Breaking that equation down, you have:

  • P is the probability of any single user authenticating in a given day. So if your users log in once a week on a business day, it is 0.2 (one out of five days). If they log in once a year, it is 0.0027 (one out of 365 days).
  • D is the number of days of the migration period.
  • S is the percentage of users who have migrated.

For P = 0.2 and D = 10, S = 89%. Therefore, if your users log in one out of every five days, in ten days almost nine out of ten will be migrated to FusionAuth.

For P = 0.0027 and D = 370, S = 63.7%; if your users log in once a year on average, in about a year, almost two thirds of accounts will be migrated to FusionAuth.

Using the above calculations should help you estimate the duration of a migration.

After this period of time, you may want to bulk migrate the rest of the users, or treat them as inactive and not migrate them. Plan to disable the Connector and remove the tenant’s Connector Policy after the slow migration is complete.

Learn more about general slow migration considerations.

Additional Support

If you need support in your migration beyond that provided in this guide, you may:

Bulk Migration

As mentioned above, a bulk migration of Azure AD B2C users requires all imported users to reset their passwords, since Azure AD B2C password hashes are inaccessible. This is typically a poor choice as it negatively affects users. However, if a slow migration won’t work because of timing or other reasons, you can move all user information other than passwords from Azure AD B2C to FusionAuth, and then send password reset emails.

The benefits of a bulk migration are:

  • You can move your users all at once.
  • You no longer have a dependency on Azure AD B2C .

The downsides of a bulk import:

  • You must require all users to reset their password. You can do this in bulk via API calls or you can have users reset their passwords when they try to login.

To bulk migrate users, the following steps are needed:

  • Set up FusionAuth
  • Extract all user data from Azure AD B2C
  • Reformat the data into FusionAuth compatible JSON
  • Import the users using the Import User API
  • Reset all user passwords

Let’s look at each of these steps in more detail.

Set Up FusionAuth For a Bulk Migration

You need to set up FusionAuth, including a tenant and API key.

It is best to create a separate tenant for migration testing. Tenants logically isolate configuration settings and users. If a migration goes awry or you need to redo it after tweaking settings, you can delete the test tenant and start with a clean system. To add a tenant, navigate to Tenants and choose the Add button (green plus sign).

Adding a tenant.

Give it a descriptive Name like AD B2C import test. You shouldn’t need to modify any of the other configuration options to test importing users.

Save the tenant.

The tenant creation screen.

Record the Id of the tenant, which will be a UUID. It will look something like 25c9d123-8a79-4edd-9f76-8dd9c806b0f3. You’ll use this later.

The tenant list.

The next step is to create an API key. This will be used by the import script. To do so, navigate to Settings -> API Keys in the administrative user interface.

Adding an API key

This key needs to have the permission to run a bulk import of users. In the spirit of the principle of least privilege, give it the permission to POST to the /api/user/import endpoint. Also, give the key permissions to POST to the API endpoint /api/user/forgot-password. Record the API key string, as you’ll use it below.

Setting API key permissions

Export User Data From Azure AD B2C

You need to export whatever data you can from Azure AD B2C before you can import it to FusionAuth.

The Azure AD B2C UI provides for bulk exporting of users. However, at the time of this guide, user email addresses are not available through the bulk export. This guide uses the Graph API to export user data, which does include email addresses.

To export the data via the Graph API, you need to authenticate with the API, and then call the List method to return the data.

To authenticate with Azure AD B2C, you first need to register an application with Azure AD B2C, which will give you an app ID and secret. Follow this guide to create an application.

We’ve created a node console tool to authenticate with the Graph API and export the users. You can clone or fork it from the GitHub repository. Then update the .env file with your Azure AD B2C tenant and app ID and secret.

Bulk Export tool environment variables to set

CLIENT_ID          // The client id,
TENANT_ID           // The the tenant id eg. guide.onmicrosoft.com
CLIENT_SECRET       // The client secret

Now you can run the tool, which will connect and export all the tenant users to a JSON file called users.json. It should look similar to this:

list-users results

{
  "@odata.context": "https://graph.microsoft.com/beta/$metadata#users",
  "value":
  [
      {
          "id": "862caa6a-86a6-4f3c-a0c0-8fc5d817fc56",
          "deletedDateTime": null,
          "accountEnabled": true,
          "ageGroup": null,
          "businessPhones":
          [],
          "city": null,
          "createdDateTime": "2022-08-05T21:40:48Z",
          "creationType": "LocalAccount",
          "companyName": null,
          "consentProvidedForMinor": null,
          "country": "South Africa",
          "department": null,
          "displayName": "Erlich Bachman",
          "employeeId": null,
          "employeeHireDate": null,
          "employeeLeaveDateTime": null,
          "employeeType": null,
          "faxNumber": null,
          "givenName": "Erlich",
          "imAddresses":
          [],
          "infoCatalogs":
          [],
          "isManagementRestricted": null,
          "isResourceAccount": null,
          "jobTitle": null,
          "legalAgeGroupClassification": null,
          "mail": null,
          "mailNickname": "862caa6a-86a6-4f3c-a0c0-8fc5d817fc56",
          "mobilePhone": null,
          "onPremisesDistinguishedName": null,
          "officeLocation": null,
          "onPremisesDomainName": null,
          "onPremisesImmutableId": null,
          "onPremisesLastSyncDateTime": null,
          "onPremisesSecurityIdentifier": null,
          "onPremisesSamAccountName": null,
          "onPremisesSyncEnabled": null,
          "onPremisesUserPrincipalName": null,
          "otherMails":
          [],
          "passwordPolicies": "DisablePasswordExpiration",
          "postalCode": "2109",
          "preferredDataLocation": null,
          "preferredLanguage": null,
          "proxyAddresses":
          [],
          "refreshTokensValidFromDateTime": "2022-08-05T21:40:46Z",
          "securityIdentifier": "S-1-12-1-1680648810-1329366694-3314532512-1258035160",
          "showInAddressList": null,
          "signInSessionsValidFromDateTime": "2022-08-05T21:40:46Z",
          "state": null,
          "streetAddress": null,
          "surname": "Bachman",
          "usageLocation": null,
          "userPrincipalName": "862caa6a-86a6-4f3c-a0c0-8fc5d817fc56@fusiontut.onmicrosoft.com",
          "externalUserConvertedOn": null,
          "externalUserState": null,
          "externalUserStateChangeDateTime": null,
          "userType": "Member",
          "employeeOrgData": null,
          "passwordProfile": null,
          "assignedLicenses":
          [],
          "assignedPlans":
          [],
          "authorizationInfo":
          {
              "certificateUserIds":
              []
          },
          "deviceKeys":
          [],
          "identities":
          [
              {
                  "signInType": "emailAddress",
                  "issuer": "fusiontut.onmicrosoft.com",
                  "issuerAssignedId": "erlich_bachman@fusiontut.onmicrosoft.com"
              },
              {
                  "signInType": "userPrincipalName",
                  "issuer": "fusiontut.onmicrosoft.com",
                  "issuerAssignedId": "862caa6a-86a6-4f3c-a0c0-8fc5d817fc56@fusiontut.onmicrosoft.com"
              }
          ],
          "onPremisesExtensionAttributes":
          {
              "extensionAttribute1": null,
              "extensionAttribute2": null,
              "extensionAttribute3": null,
              "extensionAttribute4": null,
              "extensionAttribute5": null,
              "extensionAttribute6": null,
              "extensionAttribute7": null,
              "extensionAttribute8": null,
              "extensionAttribute9": null,
              "extensionAttribute10": null,
              "extensionAttribute11": null,
              "extensionAttribute12": null,
              "extensionAttribute13": null,
              "extensionAttribute14": null,
              "extensionAttribute15": null
          },
          "onPremisesProvisioningErrors":
          [],
          "provisionedPlans":
          []
      }
  ]
}

You are now ready to transform your data, and import it to FusionAuth. We’ve created a Ruby script to help you do this.

Get the Script

FusionAuth provides an import script under a permissive open source license. It requires ruby (tested with ruby 2.7). To get the script, clone the git repository:

Getting the import scripts

git clone https://github.com/FusionAuth/fusionauth-import-scripts

Navigate to the azureadb2c directory:

cd fusionauth-import-scripts/azureadb2c

Install Needed Gems

The following gems must be available to the import script:

  • date
  • json
  • optargs
  • securerandom
  • fusionauth_client

Most likely all of these will be on your system already, except the fusionauth_client gem.

If you have bundler installed, run bundle install in the azureadb2c directory. Otherwise install the needed gems in some other way.

Use the Script

You can see the usage guide of the script by running it with the -h option:

Running the import script with the help command line switch

ruby ./import.rb -h

The output will be similar to this:

The help output of the import.rb script

Usage: import.rb [options]
    -l, --link-social-accounts       Link social accounts, if present, after import. This operation is slower than an import.
    -r APPLICATION_IDS,              A comma separated list of existing applications Ids. All users will be registered for these applications.
        --register-users
    -o, --only-link-social-accounts  Link social accounts with no import.
    -u, --users-file USERS_FILE      The exported JSON user data file from Azure AD B2C. Defaults to users.json.
    -f FUSIONAUTH_URL,               The location of the FusionAuth instance. Defaults to http://localhost:9011.
        --fusionauth-url
    -k, --fusionauth-api-key API_KEY The FusionAuth API key.
    -t TENANT_ID,                    The FusionAuth tenant id. Required if more than one tenant exists.
        --fusionauth-tenant-id
    -h, --help                       Prints this help.

For this script to work correctly, set the following switches, unless the defaults work for you:

  • -u should point to the location of the user export file you obtained from running the API export, unless the default works.
  • -f must point to your FusionAuth instance. If you are testing locally, it will probably be http://localhost:9011.
  • -k needs to be set to the value of the API key created above.
  • -t should be set to the Id of the testing tenant created above.

The -o and -l switches will attempt to create links for any social users (where the user authenticated via Google or another social provider) found in the user’s data file.

If you are loading social users, you must create the social providers in FusionAuth beforehand, or the links will fail. Additionally, creating a link is not currently optimized in the same way that loading users is. So it may make sense to import all the users in one pass (omitting the -l switch). Then, after the users are imported, create the links using the -o switch in a second pass.

The social account linking functionality will only work with FusionAuth versions above or equal to 1.28. The fusionauth_client library must be >= 1.28 too.

Running the script should produce an output like this:

Import script output

$ ruby ./import.rb -f http://localhost:9011 -k '...'
FusionAuth Importer : Azure AD B2C
 > User file: user-data.json
 > Call FusionAuth to import users
 > Import success
Duplicate users 0
Import complete. 2 users imported.

Enhancing the Script

You may also want to migrate additional data. Currently, the following attributes are migrated:

  • user_id
  • email
  • email_verified
  • username
  • insertInstant
  • registrations, if supplied

The migrated user will have the Azure AD B2C original user identities stored on the user.data object. If you have additional user attributes to migrate, review and modify the map_user method.

You may also want to assign Roles, or associate users with Groups, by creating the appropriate JSON data structures in the import call. These are documented in the Import User API docs. This will require modifying the import.rb code.

Verify the Import

Next, log in to the FusionAuth administrative user interface. Review the user entries to ensure the data was correctly imported.

List imported users.

The Final Destination of Imported Users

After you are done testing, you can choose to import users into the default tenant or a new tenant. Whichever you choose, make sure to update the -t switch to the correct value before running the import for the final time.

If you aren’t keeping users in the test tenant, delete it.

If you need to start over because the import failed or you need to tweak a setting, delete the tenant you created. This will remove all the users and other configurations for this tenant, giving you a fresh start. To do so, navigate to Tenants and choose the Delete button (red trash can icon).

Deleting a tenant.

Confirm your desire to delete the tenant. Depending on how many users you have imported, this may take some time.

Now that you have all the users imported, you must reset their passwords.

Reset Passwords

Users may reset their password using the Forgot Password link on the login page, or you can use the API calls documented below to do it.

This script will send an email to every imported user. Ensure your SMTP server, configured in Tenants -> Your Tenant -> Email, is able to handle the volume.

Review the Change Password template to ensure the messaging is correct. Here is more information on modifying the template.

Retrieve the loginId from the Azure AD B2C JSON export files and place it in a single file. The loginId will either be the username or the email address. Update the script below to use the API key created in Set Up FusionAuth For a Bulk Migration step.

The script below will call the forgot password API for every user. It sleeps periodically to avoid overloading the SMTP server.

Example User Password Reset

#!/bin/bash
API_KEY=...
LOGIN_ID_FILE=...
FA_HOST=...

for loginId in `cat $LOGIN_ID_FILE`; do
  sleep $[( $RANDOM % 10 > 8)] # sleeps 1 second ~10% of the time
  RES=`curl --max-time 600 \
       -s -w "%{http_code}" \
       -H "Authorization: $API_KEY" \
       -H "Content-type: application/json" \
       -XPOST \
       $FA_HOST/api/user/forgot-password \
       -d '{"loginId": "'$loginId'","sendForgotPasswordEmail": true}'`
  if [ "$RES" -ne "200" ]; then
    echo "Error: $RES";
    exit 1;
  fi
done

At the end of this process, you have imported the user data and enabled users to reset their passwords to a known value.

What To Do Next

You now have your users migrated, or a plan to do so. Congratulations! What is next?

You need to migrate additional configurations, as mentioned in Other Entities . Since the type of configuration varies, it is hard to provide a full list of how to import these items, but the general pattern will be:

  • Identify corresponding FusionAuth functionality.
  • Configure it in your FusionAuth instance, either manually or by scripting it using the client libraries or API.
  • Update your application configuration to use the new FusionAuth functionality.

Make sure you assign your users to the appropriate FusionAuth applications. You can do this either:

  • As part of your import process by adding registrations at import time.
  • After users have been migrated with the Registrations API.

You’ll also need to modify and test each of your applications, whether custom, open source, or commercial, to ensure:

  • Users can successfully log in.
  • The authorization code redirect is handled correctly.
  • Users receive appropriate permissions and roles based on the JWT.
  • The look and feel of the hosted login pages matches each application’s look and feel.

If your application uses a standard OAuth, SAML or OIDC library to communicate with , the transition should be relatively painless.

If you are using the Azure Client Credentials Flow, which is still in public preview, and need to migrate Client Credentials grants, create Entities in FusionAuth corresponding to each Azure AD B2C app client that is configured to use the Client Credentials grant. The scopes within FusionAuth are currently constrained to a certain format, though there’s an open link to GitHub issue to address it. Learn more about Entity Management.

You can migrate each of your Azure AD B2C app clients to a FusionAuth application using the Application API, which allows you to set your Client Id and Client Secret to be the same as they were in Azure AD B2C. Maintaining these values will minimize the impact on any applications using the OAuth Authorization Code grant or Implicit grant. Ensure you add the application to the correct tenant and allow the required grants.

Additional Support

If you need support in your migration beyond that provided in this guide, you may: