Connect an Azure Function to Office 365
In the past couple of weeks I’ve uploaded a few scripts to help manage Office 365 customer environments in bulk via delegated administration. These scripts work well for us, though they only work when they’re initiated by a delegated administrator here. Sure, we could set them up on a server as a scheduled task, though in the interest of keeping things in the cloud, we’re moving them to Azure Functions.
If you’re interested, the scripts I’ve posted so far regarding Delegated Administration are here:
- Enabling the Unified Audit Log on all delegated Office 365 tenants via PowerShell
- Export a list of unused Office 365 licenses in your delegated administration tenants
- Get a list of every customers’ Office 365 administrators via PowerShell and delegated administration
- Set Office 365 Password Expiration Policy for all delegated customer tenants
What are Azure Functions?
The Azure Functions service is Microsoft’s Function as a Service offering (FaaS). It’s similar to Hook.io, Google Cloud Functions or AWS Lambda if you’ve used any of those. Basically it lets you run standalone scripts or functions of a program in the cloud. One of Azure Functions’ benefits is that you don’t have to look after the underlying infrastructure, you can just add in your code and you’re pretty much done. You can start an Azure function using a HTTP or Azure Storage Queue trigger, or just set it to run on a timer. Azure Functions can run a variety of languages, though in this scenario, we’ll convert a simple Office 365 PowerShell script into a timer trigger function that runs each weekday.
Consumption Plan vs App Service Plan
For the number of functions we’ll be running, Azure functions are pretty much free with a Consumption Plan. This plan gives you a grant of 1 million executions and 400,000 GB-s of bandwidth, which we’ll be well under. However, Azure functions can also run on top of a paid Azure App Service Plan – which we’ll be taking advantage of.
Why pay for an Azure App Service Plan to run Azure Functions?
One of the limitations of the (almost) free version of Azure Functions is that it’s executions have a 5 minute limit, after which time they are terminated automatically. Apparently this is because the underlying virtual machines that run the functions are regularly recycled. Since some of our scripts have the potential to run longer than five minutes, we need to provision a small Azure App Service resource and then run our Azure functions on top of this. The VM that runs our App service runs continuously and will support long running functions
Here’s what we want to achieve:
- Set up an Azure Function App running on an App Service Plan
- Connect an Azure Function to Office 365
- Modify an existing PowerShell script to run on an Azure function
In another post we’ll look at connecting Azure Functions to Azure Storage to use in reporting via Power BI, and triggers for Microsoft Flow.
How to set up a new Azure Function App
- Log on to https://portal.azure.com using an account with an active Azure subscription.
- Click the Green + button on the left menu, search for Functions, then click Function App
- Click Create on the bottom right
- Complete the required fields for the Function App
- Choose to create a new Resource Group and Storage Account. For the Hosting Plan option, choose App Service Plan, then select an existing subscription or create a new one. In my case, I chose an S1 Plan, which is probably overkill. You’ll be able to get by with something much smaller.
- Once you’ve completed the required fields, click Create and wait for it to complete deployment
- After it’s finished deploying, open your function app and click the + button to create a new function.
- Choose Custom function at the bottom
- On the dropdown on the right, choose PowerShell
- Choose TimerTigger-PowerShell and enter a name for your Azure Function.
- For the Schedule, enter a cron expression. There used to be documentation at the bottom of the page on how to format these, though at the time of writing it hasn’t appeared. For a function that runs Monday to Friday at 9:30 AM GMT time, enter the following:
0 30 9 * * 1-5
- Click Create, you’ll be greeted with an almost blank screen where you can start to enter your PowerShell script. Before we do this, we’ll set up the Azure function to connect to Office 365, and secure your credentials within the function app.
Set up your Azure Function to connect to Office 365
In this step, we’ll be doing the following:
Define and retrieve your FTP Details
The FTP Details of the Azure Function are needed to upload resources that the Azure Function requires to connect to Office 365.
Download, then upload the MSOnline PowerShell Module via FTP
Azure Functions have a lot of PowerShell Modules installed by default, though they don’t have the MSOnline module that lets us connect to Office 365. We’ll need to download the module on our local computer, then upload it into the Azure function. This method was borrowed from this article by Alexandre Verkinderen.
Secure your Office 365 Credentials within the Function App
Right now, Azure Functions don’t integrate with the Azure Key Vault service. While we can store credentials within the function, these credentials are stored in plain text where anyone with access to the function can view them. This method was borrowed from this article by Tao Yang.
How to define and retrieve the FTP credentials for your Azure function app
- Click on the name of your function on the left menu.
- Click Platform Features at the top, then click Deployment Credentials
- Define a username and password for your FTP Credentials
- Next under General Settings, click Properties.
- Copy the FTP Host Name and make a note of it. You’ll need it to connect to the function’s storage via FTP and upload the MSOnline Module
Download, then upload the MSOnline PowerShell Module via FTP
- Open PowerShell on your computer, then run the following command. Make sure there’s a folder called ‘temp’ in your C:\ drive.
Save-Module msonline -Repository PSGallery -Path "C:\temp"
- Wait for it to download, then make sure it exists within C:\temp
- Open Windows Explorer, and connect to your function via FTP using the FTP Hostname and credentials we retrieved earlier.
- Navigate to site/wwwroot/YourFunctionName then create a new folder called bin
- Open the bin directory, and upload the MSOnline folder from your C:\Temp Directory
Secure your Office 365 Credentials within the Azure Function App
- On your computer, open PowerShell again and run the following commands. When you’re asked for your password, enter the password for the delegated admin account that you’ll use to manage your customers Office 365 environments. Make sure you press Enter again to run the final command to output the EncryptedPassword.txt file.
$AESKey = New-Object Byte[] 32 $Path = "C:\Temp\PassEncryptKey.key" $EncryptedPasswordPath = "C:\Temp\EncryptedPassword.txt" [Security.Cryptography.RNGCryptoServiceProvider]::Create().GetBytes($AESKey) Set-Content $Path $AESKey $Password = Read-Host "Please enter the password" $secPw = ConvertTo-SecureString -AsPlainText $Password -Force $AESKey = Get-content $Path $Encryptedpassword = $secPw | ConvertFrom-SecureString -Key $AESKey $Encryptedpassword | Out-File -filepath $EncryptedPasswordPath
This will create two files on in your C:\temp folder. An EncryptedPassword text file and a PassEncryptKey file. Be sure to delete the EncryptedPassword file once we’re done. - Return to the FTP connection and create a directory called keys under the bin directory
- Upload the PassEncryptKey file into the keys directory.
- Return to your Azure Function Platform Settings, then open Application Settings.
- Under Application Settings, create two new Key-Value pairs. One called user, which contains the username of your delegated admin account, and another called password, which contains the contents of your EncryptedPassword.txt file. Once you’ve added this, be sure to delete the EncryptedPassword.txt file from your computer.
- Before you leave Application settings, update the Platform from 32 bit to 64 bit.
- Wait for the settings to apply, then return to the Develop Section of your Azure Function
Modify your Office 365 PowerShell script for Azure Functions
- Update the variables at the top of the script to ensure they match the function name, Module Name and Module Version.For your existing scripts, you may need to update your Write-Host references to Write-Output.This sample script is a modified version of this one. It will set the default password expiration policy for all of your customers’ domains to never expire.You can use this one or create your own script under the # Start Script comment
Write-Output "PowerShell Timer trigger function executed at:$(get-date)"; $FunctionName = 'SetPasswordExpirationPolicy' $ModuleName = 'MSOnline' $ModuleVersion = '1.1.166.0' $username = $Env:user $pw = $Env:password #import PS module $PSModulePath = "D:\home\site\wwwroot\$FunctionName\bin\$ModuleName\$ModuleVersion\$ModuleName.psd1" $res = "D:\home\site\wwwroot\$FunctionName\bin" Import-module $PSModulePath # Build Credentials $keypath = "D:\home\site\wwwroot\$FunctionName\bin\keys\PassEncryptKey.key" $secpassword = $pw | ConvertTo-SecureString -Key (Get-Content $keypath) $credential = New-Object System.Management.Automation.PSCredential ($username, $secpassword) # Connect to MSOnline Connect-MsolService -Credential $credential # Start Script $Customers = Get-MsolPartnerContract -All $PartnerInfo = Get-MsolCompanyInformation Write-Output "Found $($Customers.Count) customers for $($PartnerInfo.DisplayName)" foreach ($Customer in $Customers) { Write-Output "-----------------------------------------------" Write-Output " " Write-Output "Checking the Password Expiration Policy on each domain for $($Customer.Name)" Write-Output " " $domains = Get-MsolDomain -TenantId $Customer.TenantId | Where-Object {$_.Status -eq "Verified"} foreach($domain in $domains){ $domainStatus = Get-MsolPasswordPolicy -TenantId $Customer.TenantId -DomainName $domain.Name if($domainStatus.ValidityPeriod -eq 2147483647){ Write-Output "Password Expiration Policy is set for $($domain.name) already" $PasswordsWillExpire = $false $MsolPasswordPolicyInfo = @{ TenantId = $Customer.TenantId CompanyName = $Customer.Name DomainName = $domain.Name ValidityPeriod = $domainStatus.ValidityPeriod NotificationDays = $domainStatus.NotificationDays PasswordsWillExpire = $PasswordsWillExpire } } if($domainStatus.ValidityPeriod -ne 2147483647){ Write-Output "Setting the Password Expiration Policy on $($domain.Name) for $($Customer.Name):" Write-Output " " Set-MsolPasswordPolicy -TenantId $Customer.TenantId -DomainName $domain.Name -ValidityPeriod 2147483647 -NotificationDays 30 $PasswordPolicyResult = Get-MsolPasswordPolicy -TenantId $Customer.TenantId -DomainName $domain.Name if($PasswordPolicyResult.ValidityPeriod -eq 2147483647){ $PasswordsWillExpire = $false Write-Output "Password policy change confirmed working" } if($PasswordPolicyResult.ValidityPeriod -ne 2147483647){ $PasswordsWillExpire = $true Write-Output "Password policy change not confirmed yet, you may need to run this again." } $MsolPasswordPolicyInfo = @{ TenantId = $Customer.TenantId CompanyName = $Customer.Name DomainName = $domain.Name ValidityPeriod = $PasswordPolicyResult.ValidityPeriod NotificationDays = $PasswordPolicyResult.NotificationDays PasswordsWillExpire = $PasswordsWillExpire } } } }
- Click Run to manually start the script. You should see following output under Logs
Leave a Reply
Want to join the discussion?Feel free to contribute!