Office 365 – Migrating Users Tenant-to-Tenant – Part Five – Domain Migration

By | July 22, 2020

This post continues the documentation of a single project – migration of a bunch of users between two 365 tenants. This is Part Five which documents the removal of the domain from objects, then the de-feedarating and removal from the source tenant

This is the start of our outage window. In the reference case we had a single weekend starting on Thursday Evening, this work really needs to get done that evening to allow time for the various other tasks. It’s gonna be a long night.

Part One – Introduction and Approach
Part Two – Identifying Users and Objects
Part Three – Setup of New Tenant
Part Four – Data Migration with Quest-On-Demand
Part Five – Domain Migration
Part Six – Destination Tenant User Setup

All the scripts for this project are available at https://github.com/jmattmacd/O365-Tenant_to_tenant

As you will might recall the situation here was that we had two domains, one federated one not. Both are already running with AD Connect with PHS on the source. On the target they will be using AD Connect with PTA. Our AD Connect instance contains

Because we still need data and possibly some account access my approach was to switch every object using one of our two scoped domains to use a new temporary migration domain. Its cheap as chips to register a new domain and the licenses had a month left to run so there’s no downside to this method.

The sequence for this operation is then:

  • Stop AD Connect from the source forest and changing user UPNs to new temporary domain name
  • Switch any in-scope user aliases to temporary domain name
  • Switch all in-scope group SMTP Addresses to the temporary domain name
  • Switch all in-scope Shared Mailbox addresses to use the temporary domain name
  • De-federate the federated domain
  • Remove the domains from the source tenant
  • Register the domains in the target tenant

A note about mail delivery – in this case the mail delivery mechanism for the two target domains went via an external smarthost to an On-Prem Exchange infrastructure. This meant that mail control was a simple matter of stopping the connector up to Office 365 then switching to the new tenant once the addresses were populated. In most configurations the mail will run the other way without a smarthost messing up exchanges protection mechanisms (one more security company use the phrase “all your eggs in one basket when trying to sell there shoddy-ass basket….I digress…). In those case you will need to drop the domain DNS records ttl to as low as you can and switch over the mail route that way, some mail is likely to get NDRed while you are doing this.

Stopping AD Connect and changing User UPNs on Source

In a nice clean lab environment you might have an AD Connect server serving a single tenant and single AD Domain. This step is incredibly easy in that case – you just go through the wizard and remove AD Connect from the tenant, the On premise objects remain as is and the Azure AD cloud side account become un-synced cloud only accounts. Great.

Obviously the owners of the tens of thousands of accounts in this case we have syncing from other domains would be somewhat irked should I go this route so I have two choices – Rip out the immutable IDs on the cloud side forcing them to be cloud only and AD Connect to fail or remove the On-Prem domain objects from synchronisation which will delete the cloud side version then i can just script a restore. I went with this option, I’ve got an outage window and its easier.

So – first go into AD connect, open the connector and drop all the OUs for the affected domain out of the sync (we will clean up AD Connect more fully later). Run off a pair off a sync and all your replicated object will go into a deleted state. You can then use the below script to fish them back out of the recycle bin while also changing the UPNs to the new temporary domain. Note it also changes the passwords and blocks logon. This is important – without it even though the domain name is different some users will auto logon and not notice regardless of comms: [12-Undelete_Users_SwitchUPN.ps1]
(($DomainName1 and $DomainName2 and $TempDomain require setting)

# Connect-MSOLService
$DomainName1 = "*@thisdomain.com"
$DomainName2 = "*@thatdomain.com"
$TempDomainName = "tempdomain.com"


$deletedusers = Get-MsolUser -All |where-object {$_.UserPrincipalName -like $DomainName1 -or $_.UserPrincipalName -like $DomainName2}
$outfile = ".\12-output-ChangedUPNs.csv"
$content = "OldUPN,NewUPN"
Add-COntent -Path $outfile $content


$i=0
ForEach ($user in $deletedusers) {
    $i++
    Write-Host "Recovering User" $i "of" $deletedusers.count "-" $user.UserPrincipalName
    $newUPN = ($user.UserPrincipalName.Replace($DomainName1.Replace("*@",""),$TempDomainName)).Replace($DomainName2.Replace("*@",""),$TempDomainName)
    Restore-MSOLUser -UserPrincipalName $user.UserPrincipalName -NewUserPrincipalName $newUPN
    Set-msoluser -UserPrincipalName $newUPN -ImmutableId "$null"
    Write-Host "Recovering User" $i "of" $deletedusers.count "-" $user.UserPrincipalName
    Write-Host "Recovered as" $newUPN
    $content = $user.UserPrincipalName+","+$newUPN
    Add-Content -Path $outfile $content
}
$recoveredusers = get-msoluser -DomainName $TempDomainName

$j=0
ForEach ($recovereduser in $recoveredusers) {
$j++
    Write-Host $j.ToString() $recovereduser.UserPrincipalName
    $password = Set-MsolUserPassword -UserPrincipalName $recovereduser.UserPrincipalName -NewPassword "P#ZweVn^TX6Dg4kr"
    Set-MsolUser  -UserPrincipalName $recovereduser.UserPrincipalName -BlockCredential $true
}

This outputs .\12-output-ChangedUPNs.csv which lists the old and new UPNs for each user.

Remove User Aliases

We next want to quickly check none of our users still have a proxy address in either of the two domains we are going to migrate. This is a simple matter of iterating all of the users we have just recovered and removing any proxy address which matches our target : [13-Remove_User_Proxy_Addresses.ps1]
($DomainName1, $DomainName2 and $TempDomainName will need to be set)

# Connect-MSOLService
# Connect-ExchangeOnline
$DomainName1 = "*@thisdomain.com"
$DomainName2 = "*@thatdomain.com"
$TempDomainName = "tempdomain.com"


$users = get-msoluser -All -DomainName $TempDomainName
$i=0
ForEach ($user in $users) {
    $i++
    Write-Host $i "of" $users.count "-" $user.UserPrincipalName
    ForEach ($proxyaddress in $user.ProxyAddresses) {
        If ($proxyaddress -clike "smtp*" -and ($proxyaddress -like $DomainName1 -or $proxyaddress -like $DomainName2)) {
            $proxyaddress
            Set-Mailbox -Identity $User.UserPrincipalName -EmailAddresses @{Remove=$proxyaddress}      
        }
    }
}

Distribution Groups on Source

Distribution groups can have multiple addresses which we want to preserve the prefix for for each. We therefore iterate through each to identify any whose primary address matches our domains for migration and set it’s primary address to the temp domain. We then iterate through again to reset all of the subdomains. This seems a huge inefficiency but running both at once seems to kick off errors about the group not having a primary with some regularity and ends up taking longer. [14-DistroGroup_SMTP_Address_Updates.ps1]
($DomainName1, $DomainName2 and $TempDomainName will need to be set)

# Connect-exchangeonline
$DomainName1 = "*@thisdomain.com"
$DomainName2 = "*@thatdomain.com"
$TempDomainName = "tempdomain.com"

$groups =  Get-DistributionGroup -ResultSize Unlimited 


ForEach($group in $groups) {
    If($group.PrimarySmtpAddress -like $DomainName1 -or $group.PrimarySMTPAddress -like $DomainName2) {
        $newprimary = ($group.PrimarySmtpAddress.Replace($DomainName2.Replace("*@",""),$TempDomainName)).Replace($DomainName1.Replace("*@",""),$TempDomainName)
        Write-Host "Re-writing Primary Address " $Group.DisplayName "-" $group.PrimarySmtpAddress "-" $newprimary
        get-distributiongroup $group.DisplayName | Set-DistributionGroup -PrimarySmtpAddress $newprimary    
    }
}

$groups =  Get-DistributionGroup -ResultSize Unlimited 

ForEach($group in $groups) {
    ForEach ($emailaddress in $group.EmailAddresses) {
        If($emailaddress -like $DomainName1 -or $group.PrimarySMTPAddress -like $DomainName2) {
            $newaddress = ($emailaddress.Replace($DomainName2.Replace("*@",""),$TempDomainName)).Replace($DomainName1.Replace("*@",""),$TempDomainName)
            write-host "-Re-writing Secondary Address "$emailaddress "-" $newaddress
            get-distributiongroup $group.DisplayName | Set-DistributionGroup -EmailAddresses @{Add=$newaddress}
            get-distributiongroup $group.DisplayName | Set-DistributionGroup -EmailAddresses @{Remove=$emailaddress}
        }
    }
}

Unified Groups On Source

This could be considered very close to the same operation as for Distributions Lists, but is in fact the exact same operation (but against Unified Groups instead of Distribution Groups): [15-UnifiedGroup_SMTP_Address_Updates.ps1]
($DomainName1, $DomainName2 and $TempDomainName will need to be set)

# Connect-exchangeonline
$DomainName1 = "*@thisdomain.com"
$DomainName2 = "*@thatdomain.com"
$TempDomainName = "tempdomain.com"
$groups =  Get-UnifiedGroup -ResultSize Unlimited 


ForEach($group in $groups) {
    If($group.PrimarySmtpAddress -like $DomainName1 -or $group.PrimarySMTPAddress -like $DomainName2) {
        $newprimary = ($group.PrimarySmtpAddress.Replace($DomainName2.Replace("*@",""),$TempDomainName)).Replace($DomainName1.Replace("*@",""),$TempDomainName)
        Write-Host "Re-writing Primary Address " $Group.DisplayName "-" $group.PrimarySmtpAddress "-" $newprimary
        get-unifiedgroup $group.DisplayName | Set-UnifiedGroup -PrimarySmtpAddress $newprimary    
    }
}

$groups =  Get-UnifiedGroup -ResultSize Unlimited 


ForEach($group in $groups) {
    ForEach ($emailaddress in $group.EmailAddresses) {
        If($emailaddress -like $DomainName1 -or $group.PrimarySMTPAddress -like $DomainName2) {
            $newaddress = ($emailaddress.Replace($DomainName2.Replace("*@",""),$TempDomainName)).Replace($DomainName1.Replace("*@",""),$TempDomainName)
            write-host "-Re-writing Secondary Address "$emailaddress "-" $newaddress
            get-unifiedgroup $group.DisplayName | Set-UnifiedGroup -EmailAddresses @{Add=$newaddress}
            get-unifiedgroup $group.DisplayName | Set-UnifiedGroup -EmailAddresses @{Remove=$emailaddress}
        }
    }
}

Shared Mailboxes on Source

For shared mailboxes we are just going to run through all the shared mailboxes and set any addresses using an in scope for migration domain to use the temporary domainname: [16-Shared_Mailbox_Address_Switching.ps1]
($DomainName1, $DomainName2 and $TempDomainName will need to be set)

# connect-exchangeonline
$DomainName1 = "*@thisdomain.com"
$DomainName2 = "*@thatdomain.com"
$TempDomainName = "tempdomain.com"

#$sharedmbxs = get-mailbox -RecipientTypeDetails shared -ResultSize unlimited
$sharedmbxs = get-mailbox "testfr-shared"

$i=0
ForEach($mbx in $sharedmbxs) {
    $i++
    Write-Host "Checking Mailbox" $i "of" $sharedmbxs.Count "-" $mbx.DisplayName
    $mbx.emailaddresses
    :exitloop ForEach($EmailAddress in $mbx.EmailAddresses) {
        If ($EmailAddress -like $DomainName1 -or $EmailAddress -like $DomainName2) {
            $emailaddresstring = ($mbx.EmailAddresses.Replace($domainname1.Replace("*@",""),$TempDomainName)).Replace($DomainName1.Replace("*@",""),$TempDomainName)
            $emailaddresstring
            Set-Mailbox -Identity $mbx.DisplayName -EmailAddresses $emailaddresstring
            break exitloop
        }
    }
}

De-federate and drop the domain

Okay, pay off time for the hard work. Microsoft say this step can take up to 72 hours, which is a kick in the teeth for your outage window. No guarantees but the de-federation took about 5 minutes and the domain drops about 30-60 minutes as we have made sure there aren’t any objects using the domain names.

De-federation can be done “cleanly” -ie from the adfs side using Convert-Msoldomaintostandard or purely from the ad side with Set-MsolDomainAuthentication. Best option and the one I go with is Convert. The best place to run the command from is powershell ON A/THE ADFS server. This gets around numerous issues authenticating with and traversing to your adfs server, you will need domain credentials for the ADFS server.
($ADFSServerFQDN and $DomainName1 will require inputting. Only a single domain was federated in my case)

# Connect-MSOLService
$ADFSServerFQDN = "ADFS01.thisdomain.com"
$DomainName1 = "thisdomain.com"

Set-MSOLADFSContext -Computer $ADFSServerFQDN

Convert-MsolDomainToStandard -DomainName $DomainName1 -SkipUserConversion:$true -PasswordFile .\userpasswords.txt 

This removes the Domain Federation. Now we can go into portal.azure.com, browse to Azure Active Directory | Custom Domain Names then select our domain names and hit delete. It will walk through a wizard calling out any objects we missed that still have the domain attached (there should be none) then start dropping the domain.

Once released you can add them to your new tenant immediately.

Loading

Leave a Reply

Your email address will not be published. Required fields are marked *