Using PowerShell to automate Office 365 license assignment

The move to Office 365 almost always requires changes to existing operational processes. One of the processes that inevitably requires an update is the provisioning process and the extent of these changes will differ from organization to organization and depend on the maturity of your identity lifecycle management process. In many environments, license assignment can be easily automated using a scheduled task and PowerShell so I wanted to put together a post that provides an outline on how this can be done.

Before getting into it, I just want to add a little disclaimer to this post – I love PowerShell and because I love PowerShell, I like to use it, but this doesn’t mean it is always fit for purpose. Each environment is different so I would urge you to consider all options before implementing a full blown PowerShell provisioning process because you may already own better tools for the job (FIM/MIM, etc). These tools often take a while to implement when done properly, so PowerShell could also be a great stop-gap solution. This post is intended to provide a foundation that helps you put together your own process and should not necessarily be implemented “as-is”.

With that out of the way, there are some requirements to think of as well. The server executing the script will need the following:

  • The ability to connect to Azure AD via remote PowerShell which requires the Azure AD Module –  Click here for more info
  • Remote Server Administration Tools – RSAT
  • A certificate to encrypt and decrypt your service account passwords. This certificate can be from an internal CA – See this post for more info
  • Service accounts with the relevant permissions
  • Relay permission on your Exchange server – Used for send report emails

The scenario I will be addressing in this post is to automate mailbox provisioning and license assignment in a hybrid deployment. All new mailboxes get provisioned as remote mailboxes directly in Office 365 and users are assigned the relevant Office 365 license. Each user account has an entry in the ‘extensionAttribute1’ attribute which determines the license they will be assigned, eg. E3 or Exchange Plan 2. We make use of 2 security groups during this process so users can be created in any OU as long as that OU is being synchronized to Azure AD. These groups are:

  • O365_Provision – Starts the provisioning process. New accounts are added to this group once they have been created in Active Directory
  • O365_License – Used by the script to keep track of users who still need to have licenses assigned

At a high level the workflow looks something like this:

Lets start by looking at the variables and functions we need. Here you can define you license SKUs, service account credentials, etc:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
# Modules
Import-Module ActiveDirectory
# Variables - Edit these
#########################
$ErrorActionPreference = 'Stop'
$ExchangeServer = 'you_exchange_server'
$FromAddress = 'Provisioning Service
<provisioning@yourdomain.com>'
$ToAddress = 'you@yourdomain.com'
$ADUsername = 'YourDomain\service_acc'
$RoutingDomain = 'yourtenant.mail.onmicrosoft.com'
$ADEncryptedPwd = ''
$MSOLUsername = 'service_acc@yourtenant.onmicrosoft.com'
$MSOLEncryptedPwd = ''
$Cert = Get-ChildItem Cert:\LocalMachine\My | Where-Object {$_.Subject -like 'CN=Provisioning Service*'}
$E3SKU = 'yourtenant:ENTERPRISEPACK'
$EP2SKU = 'yourtenant:EXCHANGEENTERPRISE'
#Email Styling
$EmailBody = @"
<html>
<head>
<style>
	.table {
		border:1px solid #F0F0F0;
        border-collapse: collapse;
		padding:10px;
	}
	.table th {
		border:1px solid #F0F0F0;
		padding:10px;
		background:#F0F0F0;
	}
	.table td {
		border:1px solid #F0F0F0;
		padding:10px;
	}
</style>
</head>
<body>
Hello,
This is an automated report from the Office 365 Provisioning Service. The following user accounts have been successfully provisioned in Office 365:
<table class="table">
<thead>
<tr>
<th>Username</th>
<th>Email Address</th>
<th>License Assigned</th>
</tr>
</thead>
<tbody>
"@
$EmailBodyClosure = @"
Regards,
Provisioning Service
</body>
 </html>
"@
###########################
# Functions
# Function to create report email
function Send-Report{
    $Msg = New-Object Net.Mail.MailMessage
    $Smtp = New-Object Net.Mail.SmtpClient($ExchangeServer)
    $Msg.From = $FromAddress
    $Msg.To.Add($ToAddress)
    $Msg.Subject = $EmailSubject
    $Msg.Body = $EmailBody
    $Msg.IsBodyHTML = $true
    $Smtp.Send($Msg)
    }
# Function for Exchange Connection
function Connect-Exchange{
    $ADEncryptedBytes = [System.Convert]::FromBase64String($ADEncryptedPwd)
    $ADDecryptedBytes = $Cert.PrivateKey.Decrypt($ADEncryptedBytes, $true)
    $ADDecryptedPwd = [system.text.encoding]::UTF8.GetString($ADDecryptedBytes) | ConvertTo-SecureString -AsPlainText -Force
    $ADCredentials = New-Object System.Management.Automation.PSCredential ($ADUsername,$ADDecryptedPwd)
    $ExchSession = New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri http://$ExchangeServer/PowerShell/ -Authentication Kerberos -Credential $ADCredentials
    Import-PSSession $ExchSession
    }
# Function for MSOL Connection
function Connect-MSOL{
    $MSOLEncryptedBytes = [System.Convert]::FromBase64String($MSOLEncryptedPwd)
    $MSOLDecryptedBytes = $Cert.PrivateKey.Decrypt($MSOLEncryptedBytes, $true)
    $MSOLDecryptedPwd = [system.text.encoding]::UTF8.GetString($MSOLDecryptedBytes) | ConvertTo-SecureString -AsPlainText -Force
    $MSOLCredentials = New-Object System.Management.Automation.PSCredential ($MSOLUsername,$MSOLDecryptedPwd)
    Connect-MSOLService -Credential $MSOLCredentials
    }

Next we have the ‘licensing phase’ – This phase also generates the email report because a user is considered to be fully provisioned once they have a license assigned. We can also catch any errors and generate an error report email for those.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
# Licensing Phase - Check if any users need to have licenses assigned
$NeedLicense = Get-AdGroupMember -Identity O365_License
If ($NeedLicense) {
        $HasMbxArray = @()
        Connect-MSOL
        Foreach ($User in $NeedLicense) {
            $UserInfo = Get-ADUser $User.SamAccountName -Properties *
            $Username = $UserInfo.SamAccountName
            $UserEmail = $UserInfo.Mail
            $UserLic = $UserInfo.extensionAttribute1
            $UserLoc = $UserInfo.c
            $UPN = $UserInfo.UserPrincipalName
            $MsolUser = Get-MsolUser -UserPrincipalName $UPN
            $HasLic = $MsolUser.IsLicensed
                If ($MsolUser -and $UserLic -and $UserLoc) {
                    Try {
						If ($HasLic) {
	                    $ExistingLic = $MsolUser.Licenses.AccountSkuId
	                    Set-MsolUserLicense -UserPrincipalName $UPN -RemoveLicenses $ExistingLic
	                    }
	                    If ($UserLic -eq 'Exchange 2') {
	                    Set-MsolUser -UserPrincipalName $UPN -UsageLocation $UserLoc
	                    Set-MsolUserLicense -UserPrincipalName $UPN -AddLicenses $EP2SKU
                            Remove-AdGroupMember -Identity O365_License -Members $Username -Confirm:$False
	                    }
	                    ElseIf ($UserLic -eq 'E3') {
	                    Set-MsolUser -UserPrincipalName $UPN -UsageLocation $UserLoc
	                    Set-MsolUserLicense -UserPrincipalName $UPN -AddLicenses $E3SKU
                            Remove-AdGroupMember -Identity O365_License -Members $Username -Confirm:$False
                       }
					 }
					Catch {
        			$EmailSubject = 'Office 365 Provisioning Error'
        			$EmailBody = @"
<html>
<head>
</head>
<body>
Hello,
This is an automated report from the Office 365 Provisioning Service. The following errors occurred when attempting to provision users in Office 365:
$Error
Additional Diagnostic Info:
Username: $Username
Email Address: $UserEmail
License Assigned: $UserLic
Usage Location: $UserLoc
Regards,
Provisioning Service
</body>
</html>
"@
Send-Report
			        }
$EmailBody += '<tr>'
$EmailBody += "<td>$Username</td>"
$EmailBody += "<td>$UserEmail</td>"
$EmailBody += "<td>$UserLic</td>"
$EmailBody += '</tr>'
}
}
$Licenses = Get-MsolAccountSku
$E3Consumed = $Licenses[0].ConsumedUnits
$E3Total = $Licenses[0].ActiveUnits
$E3Remaining = $E3Total - $E3Consumed
$ExP2Consumed = $Licenses[1].ConsumedUnits
$ExP2Total = $Licenses[1].ActiveUnits
$ExP2Remaining = $ExP2Total - $ExP2Consumed
$EmailBodyLic = @"
	</tbody>
</table>
<strong>License Summary:</strong>
<ul>
<li>You have consumed <strong>$E3Consumed</strong> Exchange Online (Plan 2) licenses and have <strong>$E3Remaining</strong> remaining</li>
<li>You have consumed <strong>$ExP2Consumed</strong> Office 365 Enterprise E3 licenses and have <strong>$ExP2Remaining</strong> remaining</li>
</ul>
"@
	        $EmailSubject = 'Office 365 Provisioning Report'
	        $EmailBody += $EmailBodyLic
	        $EmailBody += $EmailBodyClosure
	        Send-Report
    }

The ‘mailbox enablement phase’ connects to the local Exchange server and creates a new remote mailbox. See this post for more information on this process. This phase also attempts to generate error notification emails.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
# Mailbox Enablement Phase - Check if any new mailboxes need to be provisioned
$NeedMailbox = Get-AdGroupMember -Identity O365_Provisioning
If ($NeedMailbox) {
    Connect-Exchange
    Foreach ($User in $NeedMailbox) {
    $Username = $User.SamAccountName
    $UserInfo = Get-ADUser $Username -Properties *
    $UserLic = $UserInfo.extensionAttribute1
    $UserLoc = $UserInfo.c
    If ($UserLic -and $UserLoc){
        Try {
            Enable-RemoteMailbox $Username -RemoteRoutingAddress "$Username@$RoutingDomain"
            Add-ADGroupMember -Identity O365_License -Members $Username
            Remove-AdGroupMember -Identity O365_Provisioning -Members $Username -Confirm:$False
                }
        Catch {
        $EmailSubject = 'Office 365 Provisioning Error'
        $EmailBody = @"
<html>
<head>
</head>
<body>
Hello,
This is an automated report from the Office 365 Provisioning Service. The following errors occurred when attempting to mail-enable users:
<span style="color:#B22222;">$Error</span>
<strong>Additional Diagnostic Info:</strong>
Username: $Username
Regards,
Provisioning Service
</body>
</html>
"@
        Send-Report
        }
        }
    Else {
        $EmailSubject = 'Office 365 Provisioning Error'
        $EmailBody = @"
<html>
<head>
</head>
<body>
Hello,
This is an automated report from the Office 365 Provisioning Service. The following user could not be provisioned, please check to ensure that the required license type has been correctly entered in the "Company" field and that the "Country/region" has been set:
<span style="color:#B22222;">User: $Username</span>
Regards,
Provisioning Service
</body>
</html>
"@
        Send-Report
        }
    }
}

Putting this all together will hopefully form be a great foundation to help you build your own workflow. Once done, you can simply schedule your script to run using task scheduler.