Create AD Users with help from Azure Automation and SharePoint Online

As a follow up to one of my earlier posts where I Create AD Users with SharePoint Online as frontend, I now wanted to share an extension of this solution where we will utilize Azure Automation with a Hybrid Worker to do the heavy lifting.
So basically, instead of having a script as a scheduled task reading the SharePoint list and perform tasks based on status, we will now have a SharePoint workflow to initiate our runbooks through an Azure Automation Webhook.
I also want to thank Anders Bengtsson for his quite similiar post that finally made me finish this one up. 🙂

OVERVIEW
The post will cover the following steps

  • Create an Azure Automation runbook with a WebHook that executes the runbook on a Hybrid Worker.
  • Create a basic SharePoint workflow in SharePoint Designer with approval steps prior creating the user.
  • End user experience.

PREREQUISITES
I assume that you already have the following in place.

CREATE THE AUTOMATION RUNBOOK AND WEBHOOK
I will use Azure PowerShell create and publish the runbook.

1. First we need to create a credential asset that will be used to write back information to our list.

 
#Login to Azure 
Login-AzureRmAccount
#Define ResourceGroup and Automation Account to be used used for with Automation cmdlets
$PSDefaultParameterValues = @{
    '*-AzureRmAutomation*:ResourceGroupName' = 'ResourceGroup'
    '*-AzureRmAutomation*:AutomationAccountName' = 'MyAutomationAccount'
}
#Create credential object with the SPO service account
$SPOCreds = Get-Credential 
#Create credential asset in Azure Automation
$SPOCredsHt = @{
    Name = 'SPOAdmin'
    Description = 'SharePoint Online Service Account for user onboarding'
    Value = $SPOCreds
}
New-AzureRmAutomationCredential @SPOCredsHt

2. Next up we need to create the actual runbook. Since we are doing everything with PowerShell, we need to save the script to a file and import it to Azure Automation. My example runbook (CreateADUserfromSPO.ps1) can be found below.

$SPORunbookHt = @{
    Path = 'C:\CreateADUserfromSPO.ps1'
    Name = 'Create-ADUsersFromSPO'
    Type = 'PowerShell'
    Published = $true
}
Import-AzureRmAutomationRunbook @SPORunbookHt 

CreateADUserfromSPO.ps1

param ( 
    [object]$WebhookData
)

#SharePoint Connection Info
$SPCredentials = Get-AutomationPSCredential -Name 'SPOAdmin'
$SPConnection = @{
    SharePointSiteURL = "https://threesixfivelab.sharepoint.com"
    Username = $SPCredentials.Username
    Password = $SPCredentials.GetNetworkCredential().Password
}
#Create an object from the webhook request
$UserData = ConvertFrom-Json -InputObject $WebhookData.RequestBody


$TargetOU = "OU=Users,OU=ACME,DC=corp,DC=acme,DC=com"
$ListName = "UserOnboarding"
$PasswordLength = "12"
$UPNSuffix = '365lab.net'
$FirstName = $UserData.FirstName
$LastName = $UserData.LastName
$Title = $UserData.Title
$Manager = $UserData.Manager

#region functions
function Convert-ToLatinCharacters {
    param(
        [string]$inputString
    )
    [Text.Encoding]::ASCII.GetString([Text.Encoding]::GetEncoding("Cyrillic").GetBytes($inputString))
}
  
function Get-JDDNFromUPN {
    param (
        [ValidateScript({Get-ADUser -Filter {UserprincipalName -eq $_}})] 
        [Parameter(Mandatory=$true)][string]$UserPrincipalName
    )
        $ADUser = Get-ADUser -Filter {UserprincipalName -eq $UserPrincipalName} -ErrorAction stop
        return $ADUser.distinguishedname
}
  
function New-JDSamAccountName {
    param (
        [Parameter(Mandatory=$true)][string]$FirstName,
        [Parameter(Mandatory=$true)][string]$LastName,
        [parameter(Mandatory=$false)][int]$FirstNameCharCount = 3,
        [parameter(Mandatory=$false)][int]$LastNameCharCount = 3
    )
    #Construct the base sAMAccountName
    $BaseSam = "{0}{1}" -f (Convert-ToLatinCharacters $FirstName).Substring(0,$FirstNameCharCount),(Convert-ToLatinCharacters $LastName).Substring(0,$LastNameCharCount)
  
    #Add a number until you find a free sAMAccountName
    if (Get-ADUser -Filter {samaccountname -eq $BaseSam} -ErrorAction SilentlyContinue) {
        $index = 0
        do {
            $index++
            $sAMAccountName = "{0}{1}" -f $BaseSam.ToLower(),$index
        } until (-not(Get-ADUser -Filter {samaccountname -eq $sAMAccountName } -ErrorAction SilentlyContinue))
    } else {
        $sAMAccountName = $BaseSam.tolower()
    }
    return $sAMAccountName
}
      
function New-JDUPNAndMail {
    param (
        [Parameter(Mandatory=$true)][string]$FirstName,
        [Parameter(Mandatory=$true)][string]$LastName,
        [Parameter(Mandatory=$true)][string]$UPNSuffix
     )
    #Construct the base userPrincipalName
    $BaseUPN = "{0}.{1}@{2}" -f (Convert-ToLatinCharacters $FirstName).replace(' ','.').tolower(),(Convert-ToLatinCharacters $LastName).replace(' ','.').tolower(),$UPNSuffix
      
    if (Get-ADUser -Filter {userprincipalname -eq $BaseUPN} -ErrorAction SilentlyContinue) {
        $index = 0
        do {
            $index++
            $UserPrincipalName = "{0}{1}@{2}" -f $BaseUPN.Split("@")[0],$index,$UPNSuffix
        } until (-not(Get-ADUser -Filter {userprincipalname -eq $UserPrincipalName} -ErrorAction SilentlyContinue))
  
    } else {
        $UserPrincipalName = $BaseUPN
    }
    return $UserPrincipalName
}
  
function New-JDADUser {
    [CmdletBinding(SupportsShouldProcess=$true)]
    param (
        [Parameter(Mandatory=$true)][string]$FirstName,
        [Parameter(Mandatory=$true)][string]$LastName,
        [Parameter(Mandatory=$true)][string]$UserPrincipalName,
        [Parameter(Mandatory=$true)][string]$sAMAccountName,
        [Parameter(Mandatory=$true)][string]$Title,
        [Parameter(Mandatory=$true)][string]$OU,
        [Parameter(Mandatory=$true)][string]$Manager,
        [Parameter(Mandatory=$true)][int]$PasswordLength = 12
    )
     #Generate a password
     $Password = [System.Web.Security.Membership]::GeneratePassword($PasswordLength,2)
  
     #Construct the user HT
     $ADUserHt = @{
        GivenName = $FirstName
        SurName = $LastName
        ChangePasswordAtLogon = $true
        EmailAddress = $UserPrincipalName
        UserPrincipalName = $UserPrincipalName
        sAMAccountName = $sAMAccountName
        Title = $Title
        Name = "$FirstName $LastName ($sAMAccountName)"
        Displayname = "$FirstName $LastName"
        Manager = $Manager
        Path = $OU
        AccountPassword = (ConvertTo-SecureString -String $Password -AsPlainText -Force)
        Enabled = $true
        OtherAttribute = @{proxyAddresses = "SMTP:$UserPrincipalName"}
     }
     try {
        #Create the user and return a custom object
        New-ADUser @ADUserHt -ErrorAction Stop 
        Write-Verbose "Successfully created the user $($ADUserHt.Name)"
        [pscustomobject] @{
            sAMAccountName = $ADUserHt.sAMAccountName 
            UserPrincipalName = $ADUserHt.UserPrincipalName 
            Password = $Password
        }
     } catch {
        Write-Warning "Error creating the user $($ADUserHt.Name) `r`n$_"
     }
}
#endregion functions
    try {
        $sAMAccountName = New-JDSamAccountName -FirstName $Firstname -LastName $LastName
        $UPNandMail = New-JDUPNAndMail -FirstName $Firstname -LastName $LastName -UPNSuffix $UPNSuffix
        $ManagerDN = Get-JDDNFromUPN -UserPrincipalName $Manager
  
        #Create the user in Active Directory
        $NewAdUserHt = @{
            FirstName = $Firstname
            LastName = $LastName
            Manager = $ManagerDN
            sAMAccountName = $sAMAccountName
            UserPrincipalName = $UPNandMail
            Title = $Title
            OU = $TargetOU
            PasswordLength = $PasswordLength
        }
        Write-Output $NewAdUserHt
        $ADUser = New-JDADUser @NewAdUserHt -ErrorAction Stop
        $UpdateHt = @{
            Status = 'Created'
            UserName = $ADUser.sAMAccountName 
            EmailAddress = $ADUser.UserPrincipalName 
        }
        Update-SPOListItem -SPOConnection $SPConnection -ListItemID $UserData.ID -ListFieldsValues $UpdateHt -ListName $ListName
    } catch {
         $UpdateHt = @{
            Status = 'Error'
        }
        Update-SPOListItem -SPOConnection $SPConnection -ListItemID $UserData.ID -ListFieldsValues $UpdateHt -ListName $ListName
        Write-Warning $_
    }

3. Last step is to create the Webhook that will be used when triggering the runbook from the SharePoint workflow. As notified when running the cmdlet, save the WebhookURI, you will need it later and cannot see it more than on Webhook creation. I have set the Webhook expirytime to 2 years, change it according to your needs.

$SPOWebHookHt = @{
    Name = 'SPOWorkflow'
    RunbookName = 'Create-ADUsersFromSPO'
    IsEnabled = $true
    ExpiryTime = (Get-Date).AddDays(730)
}
New-AzureRmAutomationWebhook @SPOWebHookHt

2016-01-09_15-37-15spoworkflow

CONFIGURE THE SHAREPOINT WORKFLOW
1. Open SharePoint Designer and open your website containing the custom list.
2. Add a new List Workflow to your site and associate the workflow with your custom list.
2016-01-09_15-58-35
3. Click on the workflow and edit the Start Options to kick of the workflow when a new item has been created.
2016-01-09_16-01-54
2016-01-09_16-02-31
4. Edit and customize the workflow to fit your needs. In my case, I will have one stage for manager approval and one stage for executing the automation runbook.

Manager approval stage
a. Create a “Start a task process action” to submit the request to the manager.
2016-01-09_16-31-25
b. Create a condition to Execute the runbook if the request was approved.
2016-01-09_16-44-58

Execute Azure Automation Runbook stage
a. First build a dictionary with the data you want to pass to the automation webhook.
2016-01-09_17-04-39
b. Insert a “Call HTTP Web Service” action and configure it to POST the earlier created dictionary to the Webhook url you saved creating the Webhook.
2016-01-09_17-09-36
c. Our last step before ending the workflow will be to write back the response from the webhook to the SharePoint list item. This makes it easier for us to track if the task was properly submitted to Azure Automation. 2016-01-09_17-15-46
5. Check the workflow for errors before saving and publishing. This is my finished workflow before publishing it.
2016-01-09_17-25-17

IN ACTION
First, we create a new item in the SharePoint list to start the process.2016-01-09_19-04-18
2016-01-09_19-05-38

The manager approval process sends an email to the new users manager as we stated in the workflow.
2016-01-09_19-06-22
2016-01-09_19-07-47

After the manager has approved the request, the workflow will kick off the runbook, create the user in Active Directory and write back the UserName/EmailAddress and status of the newly created user to the list. In this example, a password is generated but not distributed to anyone.
2016-01-09_19-12-56
2016-01-09_19-15-032016-01-09_20-08-01
2016-01-09_19-14-18

My hope is that you with this post have got some inspiration on what you can achieve with Azure Automation in combination with SharePoint (Online or On-Prem). Sky is the limit!

/Johan

Advertisement

Recursively enumerate Azure AD Group members with PowerShell

Many of the scripts used to assign licenses for Azure AD / Office 365 users are utilizing groups to assign the licenses. On the topic I have received quite a lot requests on nested group support, which is not possible with Get-MsolGroupMember as of now.

To solve this problem, I have built a simple function canned Get-JDMSOLGroupMember. It checks if a group consists of other groups, and loops through them until all group members of all groups have been enumerated. In the end it will sort the members so that no duplicate member records are returned. It will only return users, not groups.

See the screenshot below on how it works:
2016-01-07_23-46-04

Get-JDMsolGroupMember

function Get-JDMsolGroupMember { 
<#
.SYNOPSIS
    The function enumerates Azure AD Group members with the support for nested groups.
.EXAMPLE
    Get-JDMsolGroupMember 6d34ab03-301c-4f3a-8436-98f873ec121a
.EXAMPLE
    Get-JDMsolGroupMember -ObjectId  6d34ab03-301c-4f3a-8436-98f873ec121a -Recursive
.EXAMPLE
    Get-MsolGroup -SearchString "Office 365 E5" | Get-JDMsolGroupMember -Recursive
.NOTES
    Author   : Johan Dahlbom, johan[at]dahlbom.eu
    Blog     : 365lab.net 
    The script are provided “AS IS” with no guarantees, no warranties, and it confer no rights.
#>

    param(
        [CmdletBinding(SupportsShouldProcess=$true)]
        [Parameter(Mandatory=$true, ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true,Position=0)]
        [ValidateScript({Get-MsolGroup -ObjectId $_})]
        $ObjectId,
        [switch]$Recursive
    )
    begin {
        $MSOLAccountSku = Get-MsolAccountSku -ErrorAction Ignore -WarningAction Ignore
        if (-not($MSOLAccountSku)) {
            throw "Not connected to Azure AD, run Connect-MsolService"
        }
    } 
    process {
        Write-Verbose -Message "Enumerating group members in group $ObjectId"
        $UserMembers = Get-MsolGroupMember -GroupObjectId $ObjectId -MemberObjectTypes User -All
        if ($PSBoundParameters['Recursive']) {
            $GroupsMembers = Get-MsolGroupMember -GroupObjectId $ObjectId -MemberObjectTypes Group -All
            if ($GroupsMembers) {
                Write-Verbose -Message "$ObjectId have $($GroupsMembers.count) group(s) as members, enumerating..."
                $GroupsMembers | ForEach-Object -Process {
                    Write-Verbose "Enumerating nested group $($_.Displayname) ($($_.ObjectId))"
                    $UserMembers += Get-JDMsolGroupMember -Recursive -ObjectId $_.ObjectId 
                }
            }
        }
        Write-Output ($UserMembers | Sort-Object -Property EmailAddress -Unique) 
        
    }
    end {
    
    }
}

As usual, let me know if you find issues or have any other suggestions.

Enjoy!

/Johan

License your Office 365/Azure AD users with Azure Automation

A while ago I wrote a post regarding schedule your Office 365 automation jobs in Azure Automation. At that time, the Azure AD PowerShell module did not support Azure Automation due to the Online Services Sign in Assistant dependency.

The preview Azure AD PowerShell module released in september do however use ADAL for authentication which solves this problem.
This is great news for all currently running licensing scripts in regular servers as scheduled tasks!

Since many probably will move from their scheduled tasks to Automation for their licensing scripts, I thought it was a good idea sharing on how to make the switch. I will use the “new” Azure portal and assume that you already have an Automation account.

1. Since the MSOnline module is not included by default in Azure Automation you need to download and install the module on your computer. When that is done you need to add the module folders (MSOnline and MSOnlineExtended) to a zip file each. The easiest way to do that is of course with PowerShell 🙂
The following code assumes that you have installed the modules in the default location and the zip files will be put on your desktop.

Add-Type -AssemblyName 'system.io.compression.filesystem'

Get-ChildItem -Path $PSHOME\Modules\MSonline* | ForEach-Object -Process {
    $Source = $_.FullName
    $Destination = '{0}\Desktop\{1}.zip' -f $env:USERPROFILE,$_.Name  
    [Io.compression.zipfile]::CreateFromDirectory($Source, $Destination) 
}

2. Upload the zipped modules files as assets to your automation account.
2015-11-28_17-38-18
2015-11-28_17-43-302015-11-28_17-42-44
3. Now add your license service account as an asset to your automation account.
If you haven’t created one you can use the below PowerShell lines to do it. Note that the account only needs to have the “User management administrator” role.

#Create the service account
New-MsolUser -UserPrincipalName licenserobot@acme333.onmicrosoft.com `
             -DisplayName "LicenseRobot" `
             -FirstName "License" `
             -LastName "Robot" `
             -Password 'password' `
             -ForceChangePassword $false `
             -PasswordNeverExpires $true 
#Add the user as an "User Account Administrator"
Add-MsolRoleMember -RoleName "User Account Administrator" `
                   -RoleMemberEmailAddress "licenserobot@acme333.onmicrosoft.com"

2015-11-28_18-06-16
4. Create your runbook and import the script. In this case I am using a slight modified version of this script, which you will find if you expand the script below below. This version are using the GroupObjectGuid to identify the groups instead of the DisplayName. Since the script is using Write-Output and Write-Warning for logging, Azure Automation will automatically handle basic logging.

LicenseAzureADUsers

#Connect to Azure AD
$Credentials = Get-AutomationPSCredential -Name 'LicenseRobot'
	
Connect-MsolService -Credential $Credentials

$Licenses = @{
                 'E1' = @{ 
                          LicenseSKU = 'tenant:STANDARDPACK'
                          Group = 'cb41a390-4312-4ecf-896e-086f64652690'
                          DisabledPlans = "SHAREPOINTSTANDARD"
                        }                        
                
                 'E3' = @{ 
                          LicenseSKU = 'tenant:ENTERPRISEPACK'
                          Group = '4549bd70-232a-4b71-8afe-4d431e5464ae'
                          DisabledPlans = "SHAREPOINTENTERPRISE","SHAREPOINTWAC"
                        }
                 'EMS' = @{
                         LicenseSKU = 'tenant:EMS'
                         Group = '9abc1736-049b-4c97-9fb6-8b6b02f9e7d5'
                 }
            
            }
    
$UsageLocation = 'SE'
    
#Get all currently licensed users and put them in a custom object
$LicensedUserDetails = Get-MsolUser -All -Synchronized | Where-Object {$_.IsLicensed -eq 'True'} | ForEach-Object {
    [pscustomobject]@{
                UserPrincipalName = $_.UserPrincipalName
                License = $_.Licenses.AccountSkuId
                DisabledPlans = $_.Licenses.Servicestatus | Where-Object {$_.Provisioningstatus -contains "Disabled"}
    }
}
   
#Create array for users to change or delete
$UsersToChangeOrDelete = @()
    
foreach ($license in $Licenses.Keys) {
      
    #Get current group name and ObjectID from Hashtable
    $GroupID = $Licenses[$license].Group
    $AccountSKU = Get-MsolAccountSku | Where-Object {$_.AccountSKUID -eq $Licenses[$license].LicenseSKU}
    $DisabledPlans =  $licenses[$license].DisabledPlans
    Write-Output "Checking for unlicensed $license users in group $GroupID..."
    #Get all members of the group in current scope
    $GroupMembers = ''
    $GroupMembers = (Get-MsolGroupMember -GroupObjectId $GroupID -All).EmailAddress
    #Get all already licensed users in current scope
    $ActiveUsers = ($LicensedUserDetails | Where-Object {$_.License -eq $licenses[$license].LicenseSKU}).UserPrincipalName 
    $UsersToHandle = ''
    $UsersToAdd = ''

    if ($GroupMembers) {  
        
        if ($ActiveUsers) {  
            #Compare $Groupmembers and $Activeusers
            #Users which are in the group but not licensed, will be added
            #Users licensed, but not, will be evaluated for deletion or change of license 
            $UsersToHandle = Compare-Object -ReferenceObject $GroupMembers -DifferenceObject $ActiveUsers -ErrorAction SilentlyContinue -WarningAction SilentlyContinue
            $UsersToAdd = ($UsersToHandle | Where-Object {$_.SideIndicator -eq '<='}).InputObject
            $UsersToChangeOrDelete += ($UsersToHandle | Where-Object {$_.SideIndicator -eq '=>'}).InputObject  
        } else {  
            #No licenses currently assigned for the license in scope, assign licenses to all group members.
            $UsersToAdd = $GroupMembers
        }
    
    } else {  
        Write-Warning  "Group $GroupID is empty - will process removal or move of all users with license $($AccountSKU.AccountSkuId)"
        #If no users are a member in the group, add them for deletion or change of license.
        $UsersToChangeOrDelete += $ActiveUsers
    }
            
    if ($UsersToAdd -match "[.]") {
  
        foreach ($User in $UsersToAdd){
    
            #Process all users for license assignment, if not already licensed with the SKU in order.  
            $MsolUser = Get-MsolUser -UserPrincipalName $User
           
            #Assign licenses for users
            if ($MsolUser.Licenses.AccountSkuId -notcontains $AccountSku.AccountSkuId) {
                try {  
                    #Assign UsageLocation and License.
                    $LicenseAssignmentHt = @{
                        UserPrincipalname = $User
                        AddLicenses = $AccountSKU.AccountSkuId
                    }
                    #Set custom license options to not enable SharePoint / OneDrive by Default
                    
                    if ($DisabledPlans) {
                        $LicenseOptions = New-MsolLicenseOptions -AccountSkuId $AccountSKU.AccountSkuId -DisabledPlans $DisabledPlans
                        $LicenseAssignmentHt["LicenseOptions"] = $LicenseOptions
                    }
                    Set-MsolUser -UserPrincipalName $user -UsageLocation $UsageLocation -ErrorAction Stop -WarningAction Stop
                    Set-MsolUserLicense @LicenseAssignmentHt -ErrorAction Stop -WarningAction Stop
                    Write-Output "SUCCESS: Licensed $User with $license"
                } catch {  
                    Write-Warning "Error when licensing $User$_"
    
                }
            
            }  
        }
    }
}
    
#Process users for change or deletion
if ($UsersToChangeOrDelete -ne $null) {
        foreach ($User in $UsersToChangeOrDelete) {
          if ($user -ne $null) {
    
            #Fetch users old license for later usage
            $OldLicense = ($LicensedUserDetails | Where-Object {$_.UserPrincipalName -eq $User}).License
                 
             #Loop through to check if the user group assignment has been changed, and put the old and the new license in a custom object.
             #Only one license group per user is currently supported.
             $ChangeLicense = $Licenses.Keys | ForEach-Object {
                  $GroupID = $Licenses[$_].Group
                  if (Get-MsolGroupMember -All -GroupObjectId $GroupID | Where-Object {$_.EmailAddress -eq $User}) {
                     [pscustomobject]@{
                        OldLicense = $OldLicense
                        NewLicense = $Licenses[$_].LicenseSKU
                     }
                  } 
    
              }
    
              if ($ChangeLicense) {
                    #The user were assigned to another group, switch license to the new one.
                    try {  
                          Set-MsolUserLicense -UserPrincipalName $User -RemoveLicenses $ChangeLicense.OldLicense -AddLicenses $ChangeLicense.NewLicense -ErrorAction Stop -WarningAction Stop
                          Write-Output "SUCCESS: Changed license for user $User from $($ChangeLicense.OldLicense) to $($ChangeLicense.NewLicense)"
                    } catch {  
                          Write-Warning "Error when changing license on $User`r`n$_"
                    }
                      
              } else {  
                               
                    #The user is no longer a member of any license group, remove license
                    Write-Warning "$User is not a member of any group, license will be removed... "
                    try {  
                          Set-MsolUserLicense -UserPrincipalName $User -RemoveLicenses $OldLicense -ErrorAction Stop -WarningAction Stop
                          Write-Output "SUCCESS: Removed $OldLicense for $User"
                    } catch {  
                          Write-Warning "Error when removing license on user`r`n$_"
                    }
              }
         }
    }
}

2016-01-03_14-33-11
2016-01-03_15-29-22
5. Verify that the runbook is working as intended by starting it in the test pane. 2016-01-03_14-54-40
Make sure to solve eventual errors before you publish and schedule the runbook. The most common error is login errors for the automation service account.
6. Publish and schedule the runbook. In my case I will run it one time every hour.
2016-01-03_15-21-34
2016-01-03_15-19-28

You are now all set and can hopefully disable a scheduled task in your on premises environment. If thinking about running this in production, keep in mind that the PowerShell module is still in preview (I haven’t experienced any problems, but…).

Let me know if you have any questions or suggestions, and happy automation!

/Johan

From Password Sync to ADFS – Reset the state of Azure AD Connect

Setting up your identity bridge using Azure AD Connect requires you to choose authentication/user sign in methods for your users as in the screenshot below.
(A detailed guide on the entire installation can be found here.)
2015-09-27_15-20-37

If you for example have chosen to use Password Synchronization and later want to reconfigure your solution to get SSO with ADFS, you don’t have the possibility to change authentication mode in the AAD Connect wizard after having it configured once.
2015-09-27_15-09-59
2015-09-27_18-33-33

In this case, we could of course just configure our ADFS servers from outside AAD Connect to achieve our goal with SSO.

If we really want to use AAD Connect, we have to “reset the state” of AAD Connect. How this is done is described in KB3008643. The KB do however refer to an xml file which should be present in %LocalAppData%. Trying to do a “state reset” a while ago, I didn’t manage to find the file there.
I did however find a file named PersistedState.xml in C:\ProgramData\AADConnect. 2015-09-27_18-51-51
Hopefully the KB article will be updated to reflect the reality soon.

Renaming the PersistedState.xml file will make the wizard run as it did in the initial state, where you have the possibility to change the authentication settings. Keep in mind that you will need to reconfigure all other settings again. If you are unsure about the settings, make sure to make a proper backup of your synchronization settings prior to doing any changes.
2015-09-27_18-58-52

Hope this helps if running in to this issue and let me know if you have any questions!

/Johan

Exchange Online (Archive) offboarding – “Multiple objects with Guid were found”

Quite a few customers that are using Exchange Online Archiving in an Exchange Hybrid environment, where they have their primary mailboxes on-premises and the archive mailboxes in Exchange Online. This makes a very flexible solution and a perfect start to utilize but not go all in to the cloud.

Just as in a “regular” hybrid environment, you have the possibility to on and offboard the archive mailboxes from and to the on-premises Exchange server. While testing offboarding of an archive mailbox a while ago, I ran in to an interesting error related to the ExchangeGuid attribute.

The exact error message trying to start the moverequest was as follows:
2015-08-19_07-54-19
“Multiple objects with Guid 1b2eaa95-0d64-4469-9fb2-d8f9be3e28ce were found”

Multiple objects with the same Guid, could that really be true? Investigating further confirmed the error message. All users in Exchange Online/Office365 had the same value in the ExchangeGuid attribute, even if they didn’t have an archive mailbox.
2015-08-18_12-44-21

SOLUTION
The following kb describes a similiar problem in a hybrid environment, but the exact solution could not be applied in my scenario since the RemoteMailbox-cmdlets isn’t working in EOA scenarios. It do however state that in order for a successful move, the ExchangeGuid should be the same on-premises as in the cloud.

After doing the change on one user as below the move started and completed successfully.

#Connect to Exchange Online and Exchange On Premises with prefixes
$cred = Get-Credential
$OnPremSess = New-PSSession -ConfigurationName Microsoft.Exchange `
                            -Authentication Kerberos `
                            -ConnectionUri http://exchangeserver/powershell
$O365Sess = New-PSSession -ConfigurationName Microsoft.Exchange `
                          -Authentication Basic `
                          -ConnectionUri https://outlook.office365.com/powershell-liveid/ `
                          -AllowRedirection:$true `
                          -Credential $cred
Import-PSSession $O365Sess -Prefix Cloud -AllowClobber
Import-PSSession $OnPremSess -Prefix OnPrem -AllowClobber

#Get the On-Premises mailbox
$Mailbox = Get-OnPremMailbox -Identity johan@365lab.net
#Get the Exchange Online MailUser
$MailUser = Get-CloudMailUser -Identity johan@365lab.net
#Change the ExchangeGuid to match the on-premises Guid
$MailUser | Set-CloudMailUser -ExchangeGuid $Mailbox.ExchangeGuid

Since it worked on one user, I then decided to to the change on all users that had EOA activated, as below:

#Get all Mailboxes with EOA Activated
$Mailboxes = Get-OnPremMailbox -Filter {(Archivestate -eq "HostedProvisioned")}
foreach ($Mailbox in $Mailboxes) {
    $CloudUser = Get-CloudMailUser -Identity $mailbox.userprincipalname
    if ($CloudUser.ExchangeGuid -ne $mailbox.ExchangeGuid) {
        $clouduser | Set-CloudMailUser -ExchangeGuid $mailbox.ExchangeGuid
    }
}

I still have a MS support case open regarding how the attribute ended up the same on all users and will update the post when we have identified the root cause.

Let me know if you have questions!

/Johan

Creating AD users with help from SharePoint Online and PowerShell

Creating users in different systems is a common task that in many cases can be quite challenging. It gets even more challenging having migrated email to Office 365, decommisioned the Exchange Servers, still keeping the local Active Directory. Using Directory Synchronization/Password sync, as of today, creating users in the correct way in your local Active Directory is not optional. By the correct way, I mean a user with proper UserPrincipalName, proxyAddresses, mail etc, which usually is where we get problems.

We do of course have lots of tools, like FIM/MIM and others to help us do this in an automated and predictable way. But if we don’t have that, what are the options?

In this post, I will give you an example on how to create a “poor mans solution” on this topic, using tools that you already have if using Office 365. SharePoint Online and PowerShell. If you have System Center Orchestrator in your environment, the integration with SharePoint Online can be handled by an integration pack instead.

OVERVIEW
Overview on that the post will cover:

  • Read list items from an already existing list in SharePoint Online.
  • Create an AD user in a predictable way based on input from SharePoint Online.
  • Send an email to the new users manager with the account information.
  • Report back status to SharePoint  with status and username/email address to the SharePoint list item.

GETTING STARTED
First of all, you need to create a SharePoint list with the columns that you want to use as input for your new users. In my example I have called the list “OnBoarding” and created  the columns as below. The status column will in my case be extra important since I will use that to determine if the user has been created or not. (there are other ways to do that of course).

2015-05-14_13-33-52 2015-05-14_15-07-11

Next up is preparing our “automation server” with the right tools. We will use the Client Side Object Model (CSOM) in SharePoint to integrate with SharePoint Online. For that we need the SharePoint Server 2013 Client Components SDK, which can be downloaded from here.  You also need to have the ActiveDirectory PowerShell module on the machine running the script.

CREATING THE USER / RUNNING THE SCRIPT
First of all, we need input data in the SharePoint list for the user to create. 

2015-05-14_15-50-072015-05-14_15-50-41

Then configure the script to fit your AD Settings, SPO credentials, addresses and the column name mappings in the $SPListItemColumns hashtable. Note that spaces will be replaced by ‘_x0020_’, so for example ‘Last Name’ will be ‘Last_x0020_Name’.
2015-05-14_19-35-10

Apart from the above, the example script consists of the simple functions described below:

  • Convert-ToLatinCharacters – will get rid of all the non-unicode characters and some more creating the username. (Thanks to Johan Akerstrom)
  • Get-JDDNFromUPN – returns distinguishedname of a userprincipalname, to support adding a manager to the AD User.
  • New-JDSamAccountName – Generates a samaccountname for the user, based on firstname + lastname (default 3+3 characters). Will handle duplicates.
  • New-JDUPNAndMail – Generates a userprincipalname and emailaddress based on firstname.lastname. Will also handle duplicates but assumes that UPN and Email are the same in your environment.
  • New-JDADUser  Creates the AD user and returns sAMAccountName, UserPrincipalName and the Password. Password generated by the System.Web GeneratePassword method (there are of course other and better methods)
  • Send-JDWelcomeEmail – Basic function to send a “Welcome Email” to the manager with the account information. Lots of room for design improvement in the email sent out, but it works. The email will contain the users password in clear text, so be careful with the recipient.

CreateADUsersFromSPO.ps1

<#
.SYNOPSIS
    The scripts creates and active directory users based on input from a SharePoint list in SharePoint Online.
    For more details, read at 365lab.net
.EXAMPLE
   .\CreateADUsersFromSPO.ps1 -Verbose
.NOTES
    File Name: CreateADUsersFromSPO.ps1
    Author   : Johan Dahlbom, johan[at]dahlbom.eu
    Blog     : 365lab.net 
    The script are provided “AS IS” with no guarantees, no warranties, and they confer no rights.
    Requires PowerShell Version 3.0!
#>
[CmdletBinding(SupportsShouldProcess=$true)]
param(
    $UPNSuffix = "365lab.net",
    $OrganizationalUnit = "OU=Users,OU=365lab Inc.,DC=ad,DC=365lab,DC=net",
    $PasswordLength = 12
)
#Import the Required Assemblys from the Client components SDK (https://www.microsoft.com/en-us/download/details.aspx?id=35585)
[System.Reflection.Assembly]::LoadWithPartialName("Microsoft.SharePoint.Client") | Out-Null
[System.Reflection.Assembly]::LoadWithPartialName("Microsoft.SharePoint.Client.Runtime") | Out-Null
[System.Reflection.Assembly]::LoadWithPartialName(“System.Web”) | Out-Null
 
#Import Active Directory Module
Import-Module ActiveDirectory -Verbose:$false
 
#Define SharePoint Online Credentials with proper permissions
$User = 'admin@tenant.onmicrosoft.com'
$Pass = ConvertTo-SecureString 'password' -AsPlainText -Force
#SharePoint online address and list name
$SPOUrl = "https://tenant.sharepoint.com/"
$SPOList = "UserOnboarding"
#Column Name mapping in SharePoint 
$SPListItemColumns = @{
        FirstName = "First_x0020_Name"
        LastName = "Last_x0020_Name"
        Title = "Title"
        Manager = "Manager"
        Status = "Status"
        Mail = "EmailAddress"
}

#region functions
function Convert-ToLatinCharacters {
    param(
        [string]$inputString
    )
    [Text.Encoding]::ASCII.GetString([Text.Encoding]::GetEncoding("Cyrillic").GetBytes($inputString))
}
 
function Get-JDDNFromUPN {
    param (
        [ValidateScript({Get-ADUser -Filter {UserprincipalName -eq $_}})] 
        [Parameter(Mandatory=$true)][string]$UserPrincipalName
    )
        $ADUser = Get-ADUser -Filter {UserprincipalName -eq $UserPrincipalName} -ErrorAction stop
        return $ADUser.distinguishedname
}
 
function New-JDSamAccountName {
    param (
        [Parameter(Mandatory=$true)][string]$FirstName,
        [Parameter(Mandatory=$true)][string]$LastName,
        [parameter(Mandatory=$false)][int]$FirstNameCharCount = 3,
        [parameter(Mandatory=$false)][int]$LastNameCharCount = 3
    )
    #Construct the base sAMAccountName
    $BaseSam = "{0}{1}" -f (Convert-ToLatinCharacters $FirstName).Substring(0,$FirstNameCharCount),(Convert-ToLatinCharacters $LastName).Substring(0,$LastNameCharCount)
 
    #Add a number until you find a free sAMAccountName
    if (Get-ADUser -Filter {samaccountname -eq $BaseSam} -ErrorAction SilentlyContinue) {
        $index = 0
        do {
            $index++
            $sAMAccountName = "{0}{1}" -f $BaseSam.ToLower(),$index
        } until (-not(Get-ADUser -Filter {samaccountname -eq $sAMAccountName } -ErrorAction SilentlyContinue))
    } else {
        $sAMAccountName = $BaseSam.tolower()
    }
    return $sAMAccountName
}
     
function New-JDUPNAndMail {
    param (
        [Parameter(Mandatory=$true)][string]$FirstName,
        [Parameter(Mandatory=$true)][string]$LastName,
        [Parameter(Mandatory=$true)][string]$UPNSuffix
     )
    #Construct the base userPrincipalName
    $BaseUPN = "{0}.{1}@{2}" -f (Convert-ToLatinCharacters $FirstName).replace(' ','.').tolower(),(Convert-ToLatinCharacters $LastName).replace(' ','.').tolower(),$UPNSuffix
     
    if (Get-ADUser -Filter {userprincipalname -eq $BaseUPN} -ErrorAction SilentlyContinue) {
        $index = 0
        do {
            $index++
            $UserPrincipalName = "{0}{1}@{2}" -f $BaseUPN.Split("@")[0],$index,$UPNSuffix
        } until (-not(Get-ADUser -Filter {userprincipalname -eq $UserPrincipalName} -ErrorAction SilentlyContinue))
 
    } else {
        $UserPrincipalName = $BaseUPN
    }
    return $UserPrincipalName
}
 
function New-JDADUser {
    [CmdletBinding(SupportsShouldProcess=$true)]
    param (
        [Parameter(Mandatory=$true)][string]$FirstName,
        [Parameter(Mandatory=$true)][string]$LastName,
        [Parameter(Mandatory=$true)][string]$UserPrincipalName,
        [Parameter(Mandatory=$true)][string]$sAMAccountName,
        [Parameter(Mandatory=$true)][string]$Title,
        [Parameter(Mandatory=$true)][string]$OU,
        [Parameter(Mandatory=$true)][string]$Manager,
        [Parameter(Mandatory=$true)][int]$PasswordLength = 12
    )
     #Generate a password
     $Password = [System.Web.Security.Membership]::GeneratePassword($PasswordLength,2)
 
     #Construct the user HT
     $ADUserHt = @{
        GivenName = $FirstName
        SurName = $LastName
        ChangePasswordAtLogon = $true
        EmailAddress = $UserPrincipalName
        UserPrincipalName = $UserPrincipalName
        sAMAccountName = $sAMAccountName
        Title = $Title
        Name = "$FirstName $LastName ($sAMAccountName)"
        Displayname = "$FirstName $LastName"
        Manager = $Manager
        Path = $OU
        AccountPassword = (ConvertTo-SecureString -String $Password -AsPlainText -Force)
        Enabled = $true
        OtherAttribute = @{proxyAddresses = "SMTP:$UserPrincipalName"}
     }
     try {
        #Create the user and return a custom object
        New-ADUser @ADUserHt -ErrorAction Stop 
        Write-Verbose "Successfully created the user $($ADUserHt.Name)"
        [pscustomobject] @{
            sAMAccountName = $ADUserHt.sAMAccountName 
            UserPrincipalName = $ADUserHt.UserPrincipalName 
            Password = $Password
        }
     } catch {
        Write-Warning "Error creating the user $($ADUserHt.Name) `r`n$_"
     }
}
 
function Send-JDWelcomeEmail {
    param (
        [Parameter(Mandatory=$true)][string]$Recipient,
        [Parameter(Mandatory=$false)][string]$Sender = "useronboarding@365lab.net",
        [Parameter(Mandatory=$true)][string]$uUserPrincipalName,
        [Parameter(Mandatory=$true)][string]$usAMAccountName,
        [Parameter(Mandatory=$true)][string]$uPassword
    )
    $message = @"
    Hi,
 
    A user with the following account details was created in Active Directory:
    Username: $usAMAccountName
    EmailAddress: $uUserPrincipalName
    Password: $uPassword (needs to be changed at first logon)
 
    Best Regards
    365lab IT department
"@
    $MailMessageHt = @{
        From = $Sender
        To = $Recipient
        Body = $message
        SmtpServer = "365lab-net.mail.protection.outlook.com"
        Subject = "User $uUserPrincipalName was created"
    }
    Send-MailMessage @MailMessageHt
}
#endregion functions
 
#Connect to SharePoint Online
$Context = New-Object Microsoft.SharePoint.Client.ClientContext($SPOUrl)
$Context.Credentials = New-Object Microsoft.SharePoint.Client.SharePointOnlineCredentials($User, $Pass)
#Get the list
$List = $Context.Web.Lists.GetByTitle($SPOList)
$Query = [Microsoft.SharePoint.Client.CamlQuery]::CreateAllItemsQuery(100) 
$Items = $List.GetItems($Query)
$Context.Load($Items)
$Context.ExecuteQuery()
 
#Loop through list items
foreach($item in $Items) {
    #Get list item for later reference
    $ListItem = [Microsoft.SharePoint.Client.ListItem]$listItem = $List.GetItemById($Item.FieldValues["ID"])
    if ($item.FieldValues[$SPListItemColumns.Status] -eq "New") {
        Write-Verbose "Processing list item $Firstname $LastName with ID=$($item.FieldValues["ID"])"
 
        #Put the fieldvalues in variables.
        $FirstName = $item.FieldValues[$SPListItemColumns.FirstName]
        $LastName = $item.FieldValues[$SPListItemColumns.LastName]
        $Title = $item.FieldValues[$SPListItemColumns.Title]
        $Manager = $item.FieldValues[$SPListItemColumns.Manager].LookupValue
        #Generate Sam, UserPrincipalName/Email and the managers DN
        try {
            $sAMAccountName = New-JDSamAccountName -FirstName $Firstname -LastName $LastName
            $UPNandMail = New-JDUPNAndMail -FirstName $Firstname -LastName $LastName -UPNSuffix $UPNSuffix
            $ManagerDN = Get-JDDNFromUPN -UserPrincipalName $Manager
 
            #Create the user in Active Directory
            $NewAdUserHt = @{
                FirstName = $Firstname
                LastName = $LastName
                Manager = $ManagerDN
                sAMAccountName = $sAMAccountName
                UserPrincipalName = $UPNandMail
                Title = $Title
                OU = $OrganizationalUnit
                PasswordLength = $PasswordLength
            }
            $ADUser = New-JDADUser @NewAdUserHt -ErrorAction Stop
            Write-Verbose "Created the AD user $sAMAccountName with UPN $UPNandMail"
 
            #Update EmailAddress in the SharePoint List
            $ListItem[$SPListItemColumns.Mail] = $ADUser.UserPrincipalName
            $ListItem[$SPListItemColumns.Status] = "Completed"
            Write-Verbose "Updated the SharePoint list item with status=Completed"
 
            #Send information to manager
            Send-JDWelcomeEmail -Recipient $Manager -uUserPrincipalName $UPNandMail -usAMAccountName $sAMAccountName -uPassword $ADUser.Password
            Write-Verbose "Sent account information to $Manager"
        } catch {
            Write-Warning $_
        }
        #Execute the changes
        $ListItem.Update()
        $Context.ExecuteQuery()
    }
}

Having understood all the functions and settings, we are now ready to run the script!
2015-05-14_20-33-46
The script executed successfully, which means we should now be able to add the script as a scheduled task to automate the task even further.

RESULTS
We now have a properly created AD user according to my organizations needs, and the manager has got an email with the account information needed. The only thing left now is waiting for the directory synchronization and licensing scripts to kick in, and the user will be all set.

2015-05-14_21-09-06
Created AD user

2015-05-14_20-07-25
Updated SharePoint list item

2015-05-14_20-06-31
Information email sent to manager

Hopefully this has given you some useful information regarding how you can create your simple self service tool with some help from things you already have. Let me know if you have any feedback!

/Johan

Get in control of your Office 365 usage with PowerShell reporting!

Having deployed different Office 365 workloads often makes you want to get some statistics on how the services are used.
You can find quite a few reports for most of the services in the reporting part of the admin UI. 2015-04-18_20-57-28
The most common ones used are the inactive users and mailboxes reports, but there are also reports on how users are utilizing Lync Online, OneDrive for Business etc.

What not so many people know about is that all these reports and some more are available through Exchange Online PowerShell as well.
2015-04-18_21-25-05
As seen above, we have 61 cmdlets related to reporting in EXO PowerShell, some of them quite interesting. For example, Get-LicenseVsUsageSummaryReport, that will give you a brief overview of how many active users you have compared to the amount of assigned licenses. Detailed information about the cmdlets can be found here.

2015-04-26_16-21-31
Active users per workload in comparison to the amount of licenses assigned to the tenant.

Except the report above, two reports that many of my customers schedule and send out as an email to to admins, are Get-StaleMailboxReport and Get-StaleMailboxDetailReport. Those reports can in many cases help us avoid having to use Get-MailboxStatistics that has been the only option earlier.

Happy reporting!

/Johan

Unable to Connect to Skype for Business using old BPOS account

Recently I had to make a change in the Skype for Business Online environment of a customer. When I was trying to connect with Remote PowerShell I got an error message saying that it was “Unable to query AutoDiscover URL”.

lyncdiscover_missing

It turned out that this customer was once using BPOS, and the domain used at that time was tenant.emea.microsoftonline.com. This domain is not supported to use with Skype for Business, since important DNS records are missing.

Luckily there are two solutions for this problem:

Solution 1

Connect using the -OverrideAdminDomain option, as described in KB2909536.

$cssession = New-CsOnlineSession `
        –Credentials $cred `
        –OverrideAdminDomain 'tenant.onmicrosoft.com'

This will force the cmdlet to look for DNS records at the tenant.onmicrosoft.com domain

Solution 2

This solution requires you to change the user name of the admin account to a supported domain (or create a new account). Renaming accounts can be tricky, but it is worth the job, since the emea.microsoftonline.com domain support is deprecated.

Result

By using a new admin account I was able to connect.

lyncdiscover_working

/ Andreas