This section assumes that you have a working B2C tenant already setup you haven't, please follow the documentation on how to create an Azure Active Directory B2C tenant.
In order to migrate all users, we need to register an Application that has Directory.ReadWrite.All permission in the B2C tenant. We will also use this app to create the custom attributes needed. We give this application the permission Application.ReadWrite.All so we can automate the registration of the webapp.
Steps:
- Open the Azure portal and navigate to the B2C directory portal.azure.com/yourtenant.onmicrosoft.com
- Open the App registrations (Preview) blade in Azure Active Directory or use this link
- + New registration, add
http://localhost
as Redirect URIs > Register - Copy the Application (client) ID as you will need it later
- API Permissions > + Add a permission > Azure Active Directory Graph > Application permissions > Directory > check Directory.ReadWrite.All and Add permissions.
- API Permissions > + Add a permission > Microsoft Graph > Application permissions > Applications > check Application.ReadWrite.All and Add permissions.
- Grant admin consent so that status chanmges to "Granted"
- Certificates & secrets > + New client secret > give description and Add. Copy this *client secret value
There is a script you can run that will create the App Registration for you. It uses the standard AzureAD powershell cmdlets
.\connect-azureadb2c.ps1 -t yourtenant.onmicrosoft.com # interactive login using your Azure AD credentials
.\new-azureadb2c-appreg.ps1 -n "B2C-migration-app-name" -w $True -r @("http://localhost")
Getting Tenant info...
yourtenant.onmicrosoft.com
Creating App...
ObjectID: 7b...f3
ClientID: 89...c2
Secret: de...8RCw=
Adding RequiredResourceAccess...
Creating ServicePrincipal...
You need to Grant permission for this app i portal.azure.com under API Permissions
The script sets the applications AppId and secret as two environment variables called $env:client_id and $env:client_secret
Update the default value in script client-cred-login.ps1 and enter the copied values for the $client_id and the $client_secret parameters. Test that the client-cred-login.ps1 script works by running it in the Powershell command prompt. The output be as below and you now have a valid access token in the environment variable OAUTH_access_token that is valid for 60 minutes. The other scripts rely on this envvar.
.\client-cred-login.ps1 -t yourtenant.onmicrosoft.com -c $env:client_id -s $env:client_secret
Environment variable set $env:OAUTH_access_token
Access Token valid until YYYY-MM-DDTHH:MM:SS
This migration example uses two custom attributes, requiresMigration
and phoneNumberVerified
, both of datatype bool. The requiresMigration
is a flag that is used the first time the user signs in after the switch to B2C and indicates that we should complete the migration by updating the password. The phoneNumberVerified
attribute is brought over from AWS Cognito and if the value is true, the mobile phone number is updates as the phone number used for MFA. If it is false and unverified, we just leave the value in the B2C standard attribute mobile
.
To create the custom attributes we need to run the script create-ext-attribute.ps1 twice - once per attribute. The parameters to pass to the script are
- -t : your tenant name
- -n :
phoneNumberVerified
andrequiresMigration
for the names - -d : boolean for the datatype
.\create-ext-attribute.ps1 -t "yourtenant.onmicrosoft.com" -o "7b...f3" -n "phoneNumberVerified" -d "boolean"
extension_89...c2_phoneNumberVerified
.\create-ext-attribute.ps1 -t "yourtenant.onmicrosoft.com" -o "7b...f3" -n "requiresMigration" -d "boolean"
extension_89...c2_requiresMigration
The script above, create-ext-attribute.ps1, uses GraphAPI to create the extension attribute. You could also use the Powershell cmdlets as below
$appExt = Get-AzureADApplication -SearchString "b2c-extensions-app"
New-AzureADApplicationExtensionProperty -ObjectID $appExt.objectId -DataType "boolean" -Name "phoneNumberVerified" -TargetObjects @("User")
New-AzureADApplicationExtensionProperty -ObjectID $appExt.objectId -DataType "boolean" -Name "requiresMigration" -TargetObjects @("User")
Note two things: 1) You will not see these attributes listed in the portal as those attributes are for User Flows, not Custom Policies. 2) These attributes only will be visible on a user when they have a value. Null valued custom attributes don't show.
The migration script migrate-users-from-aws-cognito.ps1 uses AWS CLI to retrieve users from the AWS Cognito UserPool and Azure AD GraphAPI to create them in the B2C tenant. The AWS CLI has a pagination feature so we can iterate as many times as needed.
The parameters for running the migration script are:
- -t : your tenant name
- -p : AWS Cognito UserPool Id
- -c : Client ID of the App Registration created above that was used to create the custom attributes
.\client-cred-login.ps1 -t yourtenant.onmicrosoft.com -c $env:client_id -s $env:client_secret
Environment variable set $env:OAUTH_access_token
Access Token valid until YYYY-MM-DDTHH:MM:SS
# if you need to get your AWS Cognito UserPool
$pools = (aws cognito-idp list-user-pools --max-results 60) | ConvertFrom-json
.\migrate-users-from-aws-cognito.ps1 -t "yourtenant.onmicrosoft.com" -p $pools.UserPools.Id -c "89...c2"
Note that the migration script generates a random password for the users that is never outputed somewhere. This means that no one knows the temporary password the B2C users where created with and it is not possible to login without going through the password migration part in the Custom Policy.
After you have the migration script you can verify that the custom attributes were set ok via running
.\get-userbyemail.ps1 -t "yourtenant.onmicrosoft.com" -e "[email protected]"
...
userType : Member
extension_89...c2_phoneNumberVerified : True
extension_89...c2_requiresMigration : True
You can do the same with the Azure AD Powershell cmdlets
$user = Get-AzureADUser -Filter "signInNames/any(x:x/value eq '[email protected]')"
Get-AzureADUserextension -objectid $user.ObjectId
For users that have the requiresMigration
flag set to True, the Custom Policy will make a REST API call to the Azure Function to validate the userid and password. The Azure Function uses AWS Cognito API AWSCognitoIdentityProviderService.InitiateAuth to do the authentication and the Function will return HTTP status 200 when AWS Cognito could authenticate the user and 409 with the error message "Could not verify migrated user in old system." which will be shown to the user.
In portal.azure.com, create and Azure Function app and create a new HttpTrigger function. Then replace all the code in run.csx
with the code in this repo for run.csx. Update the Configuration on the Azure Function App and add
AWS_Region
your AWS region, likeeu-west-1
AWS_CognitoUserPoolAppClientId
should be the client id value of the AWS Cognito App client that has a name that ends with "Ropc" Make sure you save the configuration.
To test that the Azure Function works, copy the full URL for calling it and update script user-migrate-auth-azfunc.ps1. Run it twice; Once with a userid/password that is good and once where the password is bad and you should get output like this. Don't be alarmed that the migrationRequired flag is False - it is the value we will update the user object with when migration is complete.
.\user-migrate-auth-azfunc.ps1
@{tokenSuccess=True; migrationRequired=False}
.\user-migrate-auth-azfunc.ps1
409: @{version=1.0.0; status=400; userMessage=Could not verify migrated user in old system.}
The B2C Custom Policy files comes from the Azure AD B2C Starter Pack SocialAndLocalAccountsWithMfa
There are a number of edits you need to do before uploading the policies
In the file TrustFrameworkExtension.xml we need to add the Technical Profile AAD-Common to include the ObjectID and the ClientID of the App Registration we created above and we used to create the custom attributes. This is the way to get the Custom Policy to "see" our custom attributes. AAD-Common is defined in TrustFrameworkBase.xml and it is only additions we do in the Extension file.
<ClaimsProvider>
<DisplayName>Azure Active Directory</DisplayName>
<TechnicalProfiles>
<TechnicalProfile Id="AAD-Common">
<Metadata>
<Item Key="ApplicationObjectId">7b...f3</Item>
<Item Key="ClientId">89...c2</Item>
</Metadata>
</TechnicalProfile>
</TechnicalProfiles>
</ClaimsProvider>
The second change is the once change you allways need to do to adapt the starter pack files. In file TrustFrameworkExtension.xml we need to provide our values for the IdentityExperienceFramework and ProxyIdentityExperienceFramework as explained in the Custom Policy Get Started guide.
<TechnicalProfile Id="login-NonInteractive">
<Metadata>
<Item Key="client_id">ec43...7b7f</Item> <!-- ProxyIdentityExperienceFramework -->
<Item Key="IdTokenAudience">b40f...14e6</Item> <!-- IdentityExperienceFramework -->
</Metadata>
<InputClaims>
<InputClaim ClaimTypeReferenceId="client_id" DefaultValue="ec43...7b7f" />
<InputClaim ClaimTypeReferenceId="resource_id" PartnerClaimType="resource" DefaultValue="b40f...14e6" />
</InputClaims>
</TechnicalProfile>
The endpoint for calling the Azure Function needs to be updated. Copy the full URL and replace the ServiceURL value.
<TechnicalProfile Id="UserMigrationViaLegacyIdp">
<DisplayName>REST API call to communicate with Legacy IdP</DisplayName>
<Protocol Name="Proprietary" Handler="Web.TPEngine.Providers.RestfulProvider, Web.TPEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
<Metadata>
<Item Key="ServiceUrl">https://your-azfunc-appname.azurewebsites.net/api/ValidateUserAwsCognito?code=A...A==</Item>
<Item Key="AuthenticationType">None</Item>
<Item Key="SendClaimsIn">Body</Item>
</Metadata>
Finally, you need to do a search-and-replace in all xml policy files from yourtenant.onmicrosoft.com
to your tenant name.
Then, upload Base, Extension and SignUpOrSignin xml files to your B2C tenant.