Monday, December 30, 2019

Removing AD-Mastered Users From an Okta Group

When associates leave our company, their Active Directory accounts are automatically disabled, which in turn causes their Okta account to be deactivated. These former associates still need access to Workday, to get their paystubs and to retrieve their W-2 tax documents the following year. To facilitate access to Workday, their Okta accounts are reactivated (which turns them into Okta-mastered accounts) and added to a special Okta group that assigns them to the Workday integration.

Being a retailer, we hire a lot of temporary associates, particularly around the holidays, and quite a few of those are rehires from the previous season. And when a former associate is rehired, although a new AD account is created (because the old one has been deleted by this time), the new account usually has the same username and Workday number that they had previously. This is no big deal, but I recently discovered that most of these new AD accounts, once imported into Okta, were being automatically relinked to their old Okta accounts, the ones that were supposedly now Okta-mastered. And that's also not a big deal, since it requires no admin intervention due to name conflicts. The one negative is that these relinked accounts remain members of that special Okta group that assigns them to Workday. This doesn't cause a problem for the user, but since they also have an AD group membership that assigns them to Workday, membership in the Okta group is redundant. And it just annoys me, so I decided to write a script to remove them from the Okta group, since that's supposed to be for former associates only.

The following powershell script retrieves the entire list of users from the Okta group, then filters that list down to only the user profiles that have Active Directory as their credential provider. Those users are then deleted from the Okta group.

[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
$api_token = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
$groupid = "xxxxxxxxxxxxxxxxxxxx"
$uri = "https://YOURORG.okta.com/api/v1/groups/$groupid/users?limit=1000"
Do {
    $webrequest = Invoke-WebRequest -Headers @{"Authorization" = "SSWS $api_token"} -Method GET -Uri $uri
    $link = $webrequest.Headers.Link.Split("<").Split(">")
    $uri = $link[3]
    $psobjects = $webrequest | ConvertFrom-Json
    $alum += $psobjects
} while ($webrequest.Headers.Link.EndsWith('rel="next"'))
$alumAD = @($alum | where-object {$_.credentials.provider.type -like "ACTIVE_DIRECTORY"})
if ($alumAD.count -gt 0) {
    foreach ($user in $alumAD) {
        $uri = "https://YOURORG.okta.com/api/v1/groups/$groupid/users/$($user.id)"
        $deleterequest = Invoke-WebRequest -Headers @{"Authorization" = "SSWS $api_token"} -Method DELETE -Uri $uri
    }
}

Sunday, December 29, 2019

Automating the Creation of Linked Mailboxes

Our regional IT teams have been delegated rights to create new user accounts in their own OUs. However, access to the on-premises Exchange org is limited to one or two org admins, as well as recipient admin rights for our Service Desk team.

Since there can sometimes be a significant delay between the creation of a new user account and the creation of that user's mailbox (mostly due to different timezones), I developed the following script to both reduce the load on the Service Desk and accelerate the creation of new mailboxes, which ultimately gives the regional IT teams more control over the provisioning process for their users.

The script is fairly specific to our environment, but hopefully the concepts will help others to create similar automation for their own systems. It currently runs as an hourly scheduled task. The regional IT teams simply need to add the string, "MailboxPlease" to the info attribute (on the Telephones tab) of the new user's account for the script to pick it up and create the linked mailbox.

<#
.SYNOPSIS
    NewLinkedMailbox - Creates a linked mailbox for accounts with MailboxPlease in the info attribute
    Created by Mike Koch on October 29, 2019
.DESCRIPTION
    Queries USERDOMAIN for user accounts that have 'MailboxPlease' in the 'info' attribute
        - this attribute is populated by the regional IT teams, which have been delegated rights to create user accounts in their own OUs
    Creates a matching account in the RESOURCEDOMAIN, in the appropriate region OU
    Sets the Company attribute on the mailbox account, to ensure that the proper email address policy is applied
    Creates the linked mailbox, then copies the primary smtp address back to the user's account in USERDOMAIN
    Clears 'MailboxPlease' from the info attribute
.NOTES
    Assumes the account running this script has sufficient rights to do the following:
        1. Read user properties in USERDOMAIN
        2. Create user accounts in RESOURCEDOMAIN
        3. Create linked mailboxes in the on-premises Exchange org
        4. Write user properties in USERDOMAIN (to write the primary smtp address back to the user's account)
#>
[CmdletBinding()]
Param()

$USERDOMAINDC = "dc1.USERDOMAIN.local"
$RESOURCEDOMAINDC = "dc1.RESOURCEDOMAIN.local"

###
# Query USERDOMAIN for enabled accounts containing "MailboxPlease" in the info attribute
###
$mbxrequests = @(Get-ADUser -Filter { enabled -eq $true -AND info -like "*MailboxPlease*" } -Server $USERDOMAINDC -SearchBase "ou=All Users,dc=USERDOMAIN,dc=local" -Properties givenname, sn, title, description, department, office, company, manager)

if (!$mbxrequests) {
    Write-Verbose "No mailbox requests detected."
}
else {
    foreach ($mbx in $mbxrequests) {
        Write-Verbose "Attempting mailbox creation for $($mbx.Name)..."
        $resAccountParms = @{ }
        $resAccountParms.Add("displayName", $mbx.Name)
        $resAccountParms.Add("userprincipalname", "$($mbx.SamAccountName)@COMPANYNAME.com")
        switch -Wildcard ($mbx.DistinguishedName) {
            "*Australia Associates*" { 
                $path = "ou=Australia,ou=LinkedMailboxAccounts,dc=RSEOURCEDOMAIN,dc=local"
                $resAccountParms.Add("company", "COMPANYNAME Australia") # required to trigger custom email address policy
            }
            "*Canada Associates*" { 
                $path = "ou=Canada,ou=LinkedMailboxAccounts,dc=RESOURCEDOMAIN,dc=local"
                $resAccountParms.Add("company", "COMPANYNAME Canada") # required to trigger custom email address policy
            }
            "*France Associates*" { 
                $path = "ou=France,ou=LinkedMailboxAccounts,dc=RESOURCEDOMAIN,dc=local"
                $resAccountParms.Add("company", "COMPANYNAME France") # required to trigger custom email address policy
            }
            "*Germany Associates*" { 
                $path = "ou=Germany,ou=LinkedMailboxAccounts,dc=RESOURCEDOMAIN,dc=local"
                if ($mbx.company) { $resAccountParms.Add("company", $mbx.company)}
            }
            "*Ireland Associates*" { 
                $path = "ou=Ireland,ou=LinkedMailboxAccounts,dc=RESOURCEDOMAIN,dc=local"
                if ($mbx.company) { $resAccountParms.Add("company", $mbx.company)}
            }
            "*Italy Associates*" {
                $path = "ou=Italy,ou=LinkedMailboxAccounts,dc=RESOURCEDOMAIN,dc=local"
                if ($mbx.company) { $resAccountParms.Add("company", $mbx.company)}
            }
            Default {
                $path = "ou=LinkedMailboxAccounts,dc=RESOURCEDOMAIN,dc=local"
                if ($mbx.company) { $resAccountParms.Add("company", $mbx.company)}
            }
        }
        if ($mbx.givenname) { $resAccountParms.Add("givenName", $mbx.GivenName) }
        if ($mbx.sn) { $resAccountParms.Add("sn", $mbx.sn) }
        if ($mbx.Department) { $resAccountParms.Add("department", $mbx.Department) }
        if ($mbx.description) { $resAccountParms.Add("description", $mbx.description) }
        if ($mbx.Title) { $resAccountParms.Add("title", $mbx.Title) }
        if ($mbx.office) { $resAccountParms.Add("physicalDeliveryOfficeName", $mbx.office) }
        $resAccountParms.Add("extensionAttribute1", "migrate.me")  # triggers migration script

        ### Let's see if we can locate this user's manager's mailbox account in RESOURCEDOMAIN
        if ($mbx.Manager) {
            $mgrsam = (Get-ADUser $mbx.Manager -Server $USERDOMAINDC).SamAccountName
            $resAccountParms.Add("Manager", (Get-ADUser $mgrsam -Server $RESOURCEDOMAINDC).DistinguishedName)
        }

        ### Make sure this samaccountname doesn't exist in RESOURCEDOMAIN
        if (!(Get-ADUser -Filter "samaccountname -eq '$($mbx.SamAccountName)'" -Server $RESOURCEDOMAINDC)) {
            Write-Verbose "Creating RESOURCEDOMAIN account for $($mbx.Name)..."
            New-ADUser -Name $mbx.Name -SamAccountName $mbx.SamAccountName -Enabled $FALSE -Path $path -Server $RESOURCEDOMAINDC -OtherAttributes $resAccountParms
            $resacct = Get-ADUser $mbx.SamAccountName -Server $RESOURCEDOMAINDC
            if ($resacct) {
                Write-Verbose "Creating linked mailbox for $($mbx.Name)..."
                $Session = New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri http://OnPremExchangeServer/powershell/ -Authentication Kerberos -AllowRedirection
                Import-PSSession $Session -CommandName Enable-Mailbox
                Enable-Mailbox -Identity $resacct.DistinguishedName -DomainController $RESOURCEDOMAINDC -Alias $resacct.SamAccountName -LinkedMasterAccount $mbx.DistinguishedName -LinkedDomainController $USERDOMAINDC
                Get-PSSession | Remove-PSSession

                ### copy email address to the USERDOMAIN account
                $email = (Get-ADUser $mbx.SamAccountName -Server $RESOURCEDOMAINDC -Properties mail).mail
                Set-ADUser $mbx.SamAccountName -Server $USERDOMAINDC -EmailAddress $email -Clear info
            } else {
                Write-Verbose "Creation of RESOURCEDOMAIN account failed $($mbx.SamAccountName)"
            }
        }
        else {
            Write-Verbose "Samaccountname already exists in RESOURCEDOMAIN ($($mbx.SamAccountName))."
        }
    }
}
Write-Verbose "Finished."

Automate the Migration of Linked & Shared Mailboxes

Our current environment consists of Exchange 2010 on-premises and Exchange Online, in hybrid mode with centralized mail flow enabled, and Azure AD Connect synchronizing everything. We have two forests, each with user accounts (result of a merger and a CIO who didn't want to rock the boat and force everyone in one forest to migrate). Both forests had Exchange 2003 at the time, but when we upgraded to 2007 we consolidated down to one Exchange org. Users in the same forest as Exchange have normal user mailboxes, while users in the other forest require linked mailboxes. And it's been that way for more than 10 years. Office 365 and Exchange Online came along a few years ago, but we just got around to migrating all user mailboxes last year.

Although new mailboxes for users in the forest with Exchange can easily be created in Exchange Online by running the Enable-RemoteMailbox command, the users in the other forest still have to be created on-premises as linked mailboxes, and then migrated to Exchange Online. We've also had some challenges with creating shared mailboxes, so those get created on-premises as user mailboxes, then converted to shared mailboxes and THEN migrated to Exchange Online.

The powershell script you see below is my solution to automating the migration of those linked and shared mailboxes. This script runs every hour as a scheduled task, queries the on-premises Exchange for new linked or shared mailboxes, and if any are found, each is submitted in its own migration batch. The only issue I encountered was one of timing - the migration batches would fail if they got submitted before Azure AD Connect had synchronized the on-premises objects up into Exchange Online. I mistakenly thought that the "-StartAfter" parameter of New-MigrationBatch command would allow me to delay the start of the migration. Turns out it only delays the actual data movement, but Exchange Online was starting the object-matching prep work as soon as the batch was submitted. To solve that problem, I added a bit of logic to delay submission of the migration batch until at least one hour after the whenMailboxCreated timestamp, which gives Azure AD Connect plenty of time to get everything in place, and I've had no failures since.

<#
.SYNOPSIS
    MigrateMailboxes - Migrates linked and shared mailboxes to Exchange Online
    Created by Mike Koch on December 20, 2019
.DESCRIPTION
    Remote powershell to on-premises Exchange
        Query Exchange for linked and/or shared mailboxes to migrate
    Remote powershell to Exchange Online
        Submit migration batch request with CSV file
.NOTES
    DEPENDENCIES
    1. Functions-PSStoredCredentials.ps1 - http://practical365.com/blog/saving-credentials-for-office-365-powershell-scripts-and-scheduled-tasks
        - contains functions to store and retrieve encrypted credentials from the local file system
        - required so that script can run unattended
    
    ASSUMPTIONS
    1. Assumes the account running this script has admin rights in the on-premises Exchange environment, as well as Account Operator
        rights in the linked domain.
    
    TO-DO
    1. Integrate my linked mailbox creation script, to result in one script that handles everything, easier to maintain
    #>

[CmdletBinding()]
Param()

$linkedDC = "dc1.userdomain.local"  # needed only to add linked mailbox users to O365 licensing groups

## IMPORTANT: encrypted credentials can only be retrieved by the same account that was used to encrypt them
## AND must be on the same machine where they were encrypted
. "C:\Scripts\Functions-PSStoredCredentials.ps1"
$cred = Get-StoredCredential -UserName globaladmin@yourcompany.onmicrosoft.com

##### Connect to on-premises Exchange, import only the commands we plan to use
$Session = New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri http://OnPremExchangeServer/powershell/ -Authentication Kerberos -AllowRedirection
Import-PSSession $Session -CommandName Get-Mailbox, Set-Mailbox

##### Build a list of mailboxes to migrate
## Some older shared mailboxes stay on-premises, so we'll set a date variable to limit our query to recently created mailboxes
$SharedMailboxThreshold = (Get-Date).AddDays(-30)
## A separate script creates linked mailboxes and sets extensionAttribute1 to "migrate.me"
$MailboxesToMigrate = @(Get-Mailbox | Where-Object {$_.RecipientTypeDetails -like "LinkedMailbox" -AND $_.CustomAttribute1 -like "migrate.me"})
## Returns recently created shared mailboxes that are not already being migrated (see line 90, below)
$MailboxesToMigrate += @(Get-Mailbox | Where-Object {$_.RecipientTypeDetails -like "SharedMailbox" -AND $_.whenMailboxCreated -gt $SharedMailboxThreshold -AND $_.CustomAttribute1 -notlike "migration in progress"})

if ($MailboxesToMigrate.count -gt 0) {
    ##### Initiate remote powershell connection to Exchange Online, import only the commands needed to submit a migration batch
    $exo = New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri https://ps.outlook.com/powershell/ -Credential $cred -Authentication Basic -AllowRedirection
    Import-PSSession $exo -CommandName Get-MigrationEndPoint, New-MigrationBatch
    $MigrationEndpointOnPrem = Get-MigrationEndpoint -Identity owa.on-prem-endpoint.com
    
    foreach ($mbx in $MailboxesToMigrate) {
        ## don't try to migrate a mailbox until it's at least one hour old
        ## this ensures that Azure AD Connect has had enough time to replicate the object to Azure and Exchange Online
        if ((New-TimeSpan -Start $mbx.whenMailboxCreated -End (Get-Date).AddHours(-1)) -gt 0) {
            $mbx | Select-Object @{Name="EmailAddress";Expression={$_.primarysmtpaddress}} | Export-Csv "c:\temp\mbx.csv" -NoTypeInformation

            ## use the mailbox name as the migration batch name, but make sure it doesn't exceed the 64-char limit
            ## very unlikely, but costs almost nothing to do
            $batchname = "$($mbx.displayName)"
            if ($batchname.Length -gt 60) {
                $batchname = $batchname.Substring(0,60)
            }

            ###### Submit the migration batch
            Write-Verbose "Submitting migration batch request..."
            New-MigrationBatch -Name $batchname `
                -SourceEndpoint $MigrationEndpointOnPrem.Identity `
                -TargetDeliveryDomain yourcompany.mail.onmicrosoft.com `
                -CSVData ([System.IO.File]::ReadAllBytes("c:\temp\mbx.csv")) `
                -NotificationEmails "EmailAdmin@yourcompany.com" `
                -AutoStart `
                -AutoComplete

            switch ($mbx.RecipientTypeDetails) {
                "LinkedMailbox" {
                    # clear the migrate.me string from customattribute1
                    Set-Mailbox $mbx.primarySmtpAddress -CustomAttribute1 $null
                    ## we want to assign an EXO license to the user account in the linked domain (not the mailbox account)
                    ## LinkedMasterAccount contains the owner's account, in domain/username format
                    ## we just need to grab the username portion, which is the samaccountname in the linked domain
                    $sam = $mbx.LinkedMasterAccount.split("\")[1]
                    ##### Assign Office Pro Plus and Exchange Online feature licenses to the mailbox owner
                    ## Assumes use of group-based licensing, which requires an Azure AD Premium subscription
                    Add-ADGroupMember -Identity "O365 Exchange Online (E5)" -Members $sam -Server $linkedDC
                    Add-ADGroupMember -Identity "O365 Office Pro Plus (E5)" -Members $sam -Server $linkedDC
                }
                "SharedMailbox" {
                    ## set extensionAttribute1 to avoid adding this mailbox to future migrations (see line 44, above)
                    Set-Mailbox $mbx.primarySmtpAddress -CustomAttribute1 "migration in progress"
                }
                Default {}
            }
        }
    }
    Get-PSSession | Remove-PSSession
}

Write-Verbose "Finished."

Friday, December 27, 2019

Unable to Recreate a Recently Deleted O365 Group

A colleague recently came to me with a problem. One of our "teams" had somehow managed to delete the dynamic Office 365 group that their Team was based on. He was attempting to recreate the group, but each attempt failed with the following error.


New-AzureADMSGroup : Error occurred while executing NewMSGroup
Code: Request_BadRequest
Message: Another object with the same value for property mailNickname already exists.


To this point, I haven't worked with O365 groups very much (or at all), so it was an interesting problem to dig into.

Since the error mentions the 'mailNickname' attribute and I know that's an attribute of the O365 group, it was pretty obvious that the group was still out there somewhere, interfering with the creation of the new group. So, it seemed obvious that the group was only soft-deleted. I just had to figure out how to find it and delete it permanently.

And before anyone points it out, yes, I know that we could have just restored the old group - and that's probably what we should have done. But part of me was curious to confirm that getting rid of the old group would eliminate the error we were getting when we tries to create the new group.

Initially, I was unable to locate the deleted group in the Azure AD portal. All of the team names start with "FLD", and while searching for that string did reveal some deleted groups, the one I was looking for was not among them. Nor was it among the active groups (had to check). Hmmm.

After a bit of googling, I learned of the Get-AzureADMSDeletedGroup command, part of the AzureAD powershell module. That command, with no arguments, returns a list of all soft-deleted groups, and it was on that list that I found my missing group. The default output of Get-AzureADMSDeletedGroup shows the Id, DisplayName, and Description of the group, and this is when I discovered that my colleague had not been consistent with our O365 team group naming. I happened to find the group I was looking for by looking for the unique number in the name. While the object name (which was not in the output from Get-AzureADMSDeletedGroup) may have started with "FLD", the DisplayName did not. But both the name and the DisplayName had that same unique number, which allowed me to locate the group, which also revealed that he DisplayName started with "District", not the expected "FLD".

At this point, I could have deleted the group by using the Remove-AzureADMSDeletedDirectoryObject command, which takes the group's unique ID string. However, now that I located the group in powershell, I wanted to find it in the Azure AD console, too. Based on what I saw in powershell, I searched the deleted groups using "District", and there it was! Right there in the Azure console, I selected the group and permanently deleted it.

Switching back to powershell, I again tried to pull up the details of the group, but this time that failed because the object had been deleted. So then I entered the command that my colleague had tried to run, to recreate the group, and that also succeeded. Problem solved.

New-AzureADMSGroup `
   -DisplayName "FLD DIST 0806" `
   -Description "District 0806 (dynamic)" `
   -MailEnabled $True `
   -SecurityEnabled $True `
   -MailNickname "FLDDIST0806DYN" `
   -GroupTypes "DynamicMembership", "Unified" `
   -MembershipRule "(User.extensionAttribute13 -eq ""0806"")" `
   -MembershipRuleProcessingState "On" `
   -Visibility "Private"

Bye.

Wednesday, November 6, 2019

How to Fix a Linked Mailbox if Exchange Online has Already Created a Mailbox in the Cloud

Problem: In our hybrid Exchange 2010 / Exchange Online environment, a user somehow ends up with a mailbox in both Exchange orgs. After some background on our environment, I'll explain how this problem occurred, and the solution I developed to resolve it.

Background: DOMAIN1 is the domestic (U.S.) domain, and has both users and the on-premises Exchange 2010 servers. DOMAIN2 exists due to a merger, is in a separate forest, but there's a two-way trust between the forests ('cause we're all one big happy family). Azure AD Connect imports accounts from both forests, then exports accounts to a single O365 E5 tenant. Licensing for the O365 features is group-based, using global security groups that are mapped to license options in Azure AD. Not all employees are provisioned with a mailbox, so the Exchange feature license is controlled by its own AD security group.

When all mailboxes were in Exchange 2010, each DOMAIN1 user was provisioned with a regular User Mailbox. Users in DOMAIN2 had to be provisioned with a Linked Mailbox. For those in the audience that haven't experienced Exchange in a multi-forest environment before, it's important to understand that only objects in the same forest as the Exchange org can be mail-enabled. In order to provision a mailbox for a user in another forest, you have to create a Linked Mailbox, which involves creating a new, disabled user account in DOMAIN1, populate all the necessary Exchange attributes on that account, and then give the user in DOMAIN2 permission to use the DOMAIN1 user mailbox as if it were his/her own. The New-Mailbox command, available in both the Exchange Admin Shell and in the Exchange Admin Console, makes creating linked mailboxes a breeze. But it's very important to understand that, in order to have a mailbox in Exchange, every user in DOMAIN2 will end up having TWO accounts - their normal login account in DOMAIN2 and the disabled mailbox account in DOMAIN1. No Exchange attributes get written to the account in DOMAIN2. In fact, provisioning a linked mailbox makes no changes to the account in DOMAIN2 at all. If you review every attribute of the DOMAIN2 account, there is absolutely no indication that this account has a mailbox.

As we began our journey to O365 and Exchange Online, it was nice to discover that, for DOMAIN2 users with linked mailboxes, as Azure AD Connect imports their accounts from both forests, it will recognize and intelligently merge their primary user account data from DOMAIN2 with the Exchange attributes from their account in DOMAIN1, and then export that combined information to create a single account in Azure/O365 containing all the necessary attributes.

Those last two paragraphs may seem to have gone a bit off-topic, but I described all of that just to help you understand that this is still how we're provisioning mailboxes today, even though everyone has been migrated to Exchange Online. So, this is how we currently provision mailboxes for new hires:

  • For new users in DOMAIN1, we simply run the Enable-RemoteMailbox command in the on-premises Exchange org, which stamps the account with all the necessary attributes. The account is also added to the AD security group that is used to assign the Exchange Online feature license, so once the account syncs up into Azure and Exchange, a mailbox will be created and the feature license assigned, automatically. Simple and easy, works every time.
  • For users in DOMAIN2, we can't just enable a remote mailbox since Exchange can't see the user accounts in DOMAIN2, so as before, we have to create a linked mailbox in the on-premises Exchange org, wait for Azure AD Connect to merge those accounts and export the changes to Azure, and then migrate the linked mailbox to Exchange Online. The account in DOMAIN2 also needs to be added to the AD security group that is used to assign the Exchange Online feature license, and it is the timing of this license assignment for linked mailbox users that can cause the issue that is the subject of this article.


Scenario: A help desk technician creates a new user account in DOMAIN2 and immediately adds that account to the O365 Exchange Online security group in Active Directory. Within 30 minutes, that account is imported and synced up into Azure AD by Azure AD Connect, and no further action is taken for several hours or longer. Since the account has no Exchange attributes but has been assigned an Exchange feature license, Exchange Online just goes right ahead and creates a mailbox for the user. Later (sometimes much later), the help desk receives a request to create a mailbox for this user, and so they proceed (correctly) to create a new linked mailbox in the on-premises Exchange org. The on-premises Exchange server allows the mailbox to be created, since it has no knowledge of the mailbox that Exchange Online had already created.

Side effects: As described previously, in creating the linked mailbox, a disabled DOMAIN1 account was created and populated with all the usual Exchange attributes. Azure AD Connect correctly sees this account, sees that it is linked to the DOMAIN2 account, correctly merges the two accounts together and exports the now-modified account data up into Azure AD. The user's Exchange Online mailbox is updated with all the Exchange attributes that were stamped on the user's DOMAIN1 account during the linked mailbox creation, so the Exchange Online mailbox now has all the proper details of a full-blown company mailbox. But...

What's the problem? Well, we still have that linked mailbox that was created back in the on-premises Exchange environment. And that's a big problem because both the linked mailbox and the cloud mailbox have the same SMTP addresses on them (with a few exceptions), and if we leave both of them as-is, any mail received by the on-premises Exchange org will be delivered to the on-premises mailbox (because that's the only one that the on-premises Exchange server knows about), and any mail received by Exchange Online will be delivered to the Exchange Online mailbox (ditto) - and the user, who is probably only aware of the Exchange Online mailbox, will have no idea why they're getting some emails but not others.

So, how do we fix this? In the recent past, I would simply delete the mistakenly-created Exchange Online mailbox and then migrate the linked mailbox, which didn't always go smoothly. Plus, that's a lot of work, so I started looking for a simpler, more reliable alternative. Since we ultimately want to keep the Exchange Online mailbox (which may have been receiving important mail by now) and it's already configured correctly, thanks to Azure AD Connect, I realized that all I really needed to do was to get rid of the linked mailbox and make the on-premises Exchange org aware of the Exchange Online mailbox's presence. The solution I came up with is to modify the Exchange properties on the disabled DOMAIN1 account, by either adding, modifying, or deleting the attributes that control how the on-premises Exchange org "sees" that user object.

This solution requires quite a few attribute changes, and after modifying five or six accounts by bringing up another account and comparing attribute values, it quickly became clear that the manual process was far too labor-intensive and prone to mistakes. It's too easy to forget which attributes need to be modified, and copying values from one account to another is just so boring and slow. So now I've collected all of those attributes into an easily repeatable powershell script. We'll use the Set-ADUser command to make our changes all at once, but we need to do a bit of set up before we can run it.

Caveat: Please consider the attributes and values you see below as specific to my company's Exchange environment. While it is quite likely that your org uses many of the same attributes and possibly even the same attribute values, I strongly urge you to compare these with those on your user accounts and make adjustments as needed before you attempt to run this in your environment. I can only confirm that what follows below seems to work well for our environment.

There's only one attribute that is user-specific, and that's the targetAddress, which is what the on-premises Exchange org uses to route the user's mail up to their Exchange Online mailbox. So our first step is to set a variable to the samaccountname of the user's disabled DOMAIN1 account. We'll use that to retrieve this account's mailNickname (which was set when the linked mailbox was created), then build the targetAddress.

$aduser = ""

$target = "SMTP:" + (get-aduser $aduser -Properties mailnickname).mailnickname + "@COMPANY.mail.onmicrosoft.com"

Some attributes don't exist on this user's account yet, so we'll have to add them. These attributes have the same values for all remote mailboxes, so we'll create a hash table with all the proper values hard-coded right in. Note that the $target variable is used in this hash table, to set the targetAddress.

$addhash = @{ `
    msExchAddressBookFlags=1; `
    msExchArchiveQuota=52428800; `
    msExchArchiveWarnQuota=47185920; `
    msExchBypassAudit=$false; `
    msExchMailboxAuditEnable=$false; `
    msExchMailboxAuditLogAgeLimit=7776000; `
    msExchMDBRulesQuota=64; `
    msExchModerationFlags=6; `
    msExchProvisioningFlags=0; `
    msExchRemoteRecipientType=4; `
    msExchTransportRecipientSettingsFlags=0; `
    targetAddress=$target
}

A couple more attributes already exist but need to be changed, again with values that are common to all remote user mailboxes, so we'll create another hash table for those.

$replhash = @{ `
    msExchRecipientDisplayType=-2147483642; `
    msExchRecipientTypeDetails=2147483648
}

Finally, there are half a dozen attributes that just need to disappear (be cleared), so we'll create a variable containing an array of those attribute names.

$clearargs = `
    'homeMDB', `
    'homeMTA', `
    'mDBUseDefaults', `
    'msExchMailboxSecurityDescriptor', `
    'msExchHomeServerName', `
    'msExchRBACPolicyLink'

That's it for the prep work, so now we can run the command.

Set-ADUser $aduser `
    -Add $addhash `
    -Replace $replhash `
    -Clear $clearargs

If your account does not have rights to make these changes in Active Directory, you can specify alternate credentials:

Set-ADUser $aduser `
    -Add $addhash `
    -Replace $replhash `
    -Clear $clearargs `
    -Credential (Get-Credential)

Assuming the changes apply without error, there will be no output. And if you go back into the on-premises Exchange console, you will see that the linked mailbox object has disappeared, replaced with the familiar Remote User Mailbox object. And once all those changes have synced up into Azure and Exchange Online, everything will work as expected.

Troubleshooting: If something goes wrong and you don't see the Remote User Mailbox object in the Exchange console… first, try refreshing the listing again (remember, it'll be in the Mail Contact node). Exchange could be using a different domain controller than the one that processed your Set-ADUser command, so allow a few minutes for AD replication then refresh the list again. If all else fails, pull up the user's DOMAIN1 account side-by-side with another, working DOMAIN1 account, go to the Attribute Editor tab of both accounts and compare the settings. They won't all match, so just concentrate on the attributes listed in the above hashtables.

Ciao!