Microsoft/Office 365 PowerShell in Azure Functions – Exchange Management

By | September 8, 2020

And on the seventh year of Exchange Online did the Version 2 PowerShell module be released and there was great rejoicing. Except for people trying to work in Azure Function Apps.

Pretty much the most common reporting or regular maintenance activities in 365 is managing Exchange. We all have a tonne of powershell scripts for this and running locally the Version 2 powershell module has been a godsend.

Im going to go through here how to get some exchange scripts running in Azure Functions.

Create Function

First off we need to create our Function so jump into portal.azure.com, open function apps, If you already have an app open it or create a new one.

Hit Functions and add a new function with a timer. I called mine “Template” and left the timer to default.

If you go to Code and test you’ll see the standard test script generated

You will need a way to sign in to exchange (and other modules as you add them) – the best option is an App and cert-based service principal as per https://www.matts-stuff.co.uk/2020/09/07/setting-up-an-aad-application-for-power-platform-function-apps/

If you follow that guide then once you have created your AAD App and assigned the certificate for authentication go to Azure Portal | Function App | <Your Function App> | TLS/SSL Settings. Go to the “Private Key Certificates (.pfx) tab and hit Upload certificate, upload the certificate pfx you created for your AAD App.

Next you need to have the function app load the certificate. Still in the Function App go to Settings | Configuration

In here we need to make a new application setting so click “New application setting”. Go figure. The name should be “WEBSITE_LOAD_CERTIFICATES” and the value should be the thumbprint of your certificate

Okay out of that and save the Configuration settings. You will now be able to use the certificate in your functions.

Upload Module Files

This will allow you to import any version of the V2 Powershell modules for exchange BUT in order to use certificate based authentication you NEED version 2.0.3 which is in preview at time of writing.

To grab it on your desktop just open powershell7 and run:

Update-Module ExchangeOnlineManagement -RequiredVersion 2.0.3-Preview -allowprerelease

Then

 PS C:\> get-installedmodule | select name, version, installedlocation | where {$_.name -like "*exchange*"}

Name                     Version       InstalledLocation
----                     -------       -----------------
ExchangeOnlineManagement 2.0.3-Preview C:\Users\LordVader\Documents\PowerShell\Modules\ExchangeOnlineManageme…         

If you Browse to that folder you will find all the module files

Now we just need to go back to the Azure Portal in our function app and scroll down to Development Tools | Advanced Tools

Hit Go in here and its will open the Kudu environment, Drop Down Debug console and hit Powershell. This will drop you into a powershell prompt runnign in your Function App environment. Its all kinds of fun but right now we just want to browse the structure so hit Site | wwwroot and you will see a folder for each of your functions along with some system files and folders. Click the + next to wwwroot and cerate a new folder called “ExchangeOnline”

Now you can click into your new folder and drag and drop the files from the exchange module into it

Launch Powershell from Function and Import the Module

Okay now the good bit.

We cannot import our 64-bit powershell module into our 32 bit powershell core instance running our function app. What it turns out we can do is launch the 64 bit powershell.exe on our function app environment and run the import then a script from there. I love it.

This idea and half of the execution is stolen from https://github.com/eamonoreilly/ManageAzureActiveDirectoryWithPowerShellFunction so I highly recommend going there and understanding all of that solution, it’s excellent.

Back in our function app open the template function then open Code + test. Enter the entire code as:

# Input bindings are passed in via param block.
param($Timer)

$64bitPowerShellPath = Get-ChildItem -Path $Env:Windir\WinSxS -Filter PowerShell.exe -Recurse -ErrorAction SilentlyContinue | Where-Object {$_.FullName -match "amd64"}
$env:64bitPowerShellPath=$64bitPowerShellPath.VersionInfo.FileName
$script = {
    $AppID="appid"
    $CertThumbprint="certificate_thumbprint"
    $tenantdomain="starfleet.onmicrosoft.com"

    Import-Module "D:\home\site\wwwroot\ExchangeOnline\ExchangeOnlineManagement.psd1"
    Connect-ExchangeOnline -CertificateThumbprint $CertThumbprint -AppId $AppID -Organization $tenantdomain -Showbanner:$false

    get-mailbox -Identity kirkjt@enterprise.starfleet.com
}



$ScriptResult = (&$env:64bitPowerShellPath -WindowStyle Hidden -NonInteractive -Command $Script)
$ScriptResult

Lets have a look at whats going on. First we import the $timer variable. We arent going to use it in this script, dont worry about it. This next bit

$64bitPowerShellPath = Get-ChildItem -Path $Env:Windir\WinSxS -Filter PowerShell.exe -Recurse -ErrorAction SilentlyContinue | Where-Object {$_.FullName -match "amd64"}
$env:64bitPowerShellPath=$64bitPowerShellPath.VersionInfo.FileName

Grabs the path to the AMD64 file powershell.exe on the host system then sticks the filename and path into an environmental variable $env:64bitPowerShellPath

Next we define a script:

$script = {
    $AppID="appid"
    $CertThumbprint="certificate_thumbprint"
    $tenantdomain="starfleet.onmicrosoft.com"

    Import-Module "D:\home\site\wwwroot\ExchangeOnline\ExchangeOnlineManagement.psd1"
    Connect-ExchangeOnline -CertificateThumbprint $CertThumbprint -AppId $AppID -Organization $tenantdomain -Showbanner:$false

    get-mailbox -Identity kirkjt@enterprise.starfleet.com
}

In here we set up some variables then import the Exchange powershell module we saved earlier and connect to exchange with our certificate. (Note the Showbanner:false parameter its critical or the module will try to post the colourful banner to the console and fail miserably) I also grab a mailbox for a user solely as a test.

After the script block we have

$ScriptResult = (&$env:64bitPowerShellPath -WindowStyle Hidden -NonInteractive -Command $Script)
$ScriptResult

This basically runs the file in the environmental variable (i.e. the 64 bit powershell executable) in a hidden window (obviously) and passing the script we just wrote as a command. In this case it save to a variable called $ScriptResult then dumps it back out but obviously that would be pointless other than in test.

So once you have pasted this into the Code + test window Hit Save then Test/Run

Then Run in the side window and expand the log view. It will take a while to spin up then run but you should see informational line about your test mailbox.

So there you go, Exchange Powershell V2 running as a service principal with cert based logon from a function app. If that doesn’t excite and amuse you I don’t know what would.

It’s a great start, I’ll be making a post about my full template app very shortly which can pull in multiple modules and post stuff back out via graph.

Loading

7 thoughts on “Microsoft/Office 365 PowerShell in Azure Functions – Exchange Management

  1. Michal

    Hello,
    I’m trying to reproduce the path You’ve described but I faced some issues.
    On this page You use .pfx certificate. Here: https://www.matts-stuff.co.uk/2020/09/07/setting-up-an-aad-application-for-power-platform-function-apps/ to .cer certifiacte. My certificate .cer and .pfx have different Thumbprints. Pfx export is blocked on my Win10 so I had to follow this manual: http://woshub.com/how-to-create-self-signed-certificate-with-powershell/
    To what AppId are You refeting in run.ps1 and in WEBSITE_LOAD_CERTIFICATES? How is connected App Registration with Function App?
    I managed to load module but it fails on certificate verification. I’ve tried both Thumbprints – .cer and .pfx

    Reply
    1. jmattmacd Post author

      Hi,

      If you run
      New-SelfSignedCertificate -certstorelocation cert:\localmachine\my -Subject M365Reporting_Michal -KeySpec KeyExchange
      it will generate your cert: (you MUST specify KeySpec which the article you linked does not)
      PSParentPath: Microsoft.PowerShell.Security\Certificate::LocalMachine\my

      Thumbprint Subject
      ---------- -------
      9E4CTHUMBPRINTF66D99AC7935F54 CN=M365Reporting_Michal

      You can then run
      $PW= ConvertTo-SecureString -String “Password123!” -Force –AsPlainText
      Export-PfxCertificate -Cert cert:\LocalMachine\My\9E4CTHUMBPRINTF66D99AC7935F54 -FilePath C:\temp\M365Reporting_Michal.pfx -Password $PW

      to get your pfx and
      Export-Certificate -Cert Cert:\LocalMachine\My\9E4CTHUMBPRINTF66D99AC7935F54 -FilePath C:\temp\M365Reporting_Michal.cer
      to get your cer.

      The next step is to create your app registration as per the article uploading your cer, you can then find the App ID on the Overview blade of the App registration as “Application (client) ID”

      You should then be able to run
      Connect-AzureAD -CertificateThumbprint '9E4CTHUMBPRINTF66D99AC7935F54' -ApplicationId (Your ApplicationID) -TenantId (Your TenantId)
      From the machine you used to generate the certificate (or any other that you install the pfx on using the password) – this step confirms your certificate generation and app registration works.

      When you come to create your Function App you upload the pfx you created above and the application setting in this example would be WEBSITE_LOAD_CERTIFICATES = 9E4CTHUMBPRINTF66D99AC7935F54

      Reply
  2. Grant

    Hi,
    Thanks for this article – it took me a long way to getting what I need.

    I am totaly stuck trying to pass in the AppId and CertThumbprint as parameters to the Script – I can pass the parameters into the function no problem but cannot fugure out how to get them into the script – Have you managed this?

    Reply
    1. jmattmacd Post author

      In the script block you just set them as string variables –
      $AppID=”appid”
      $CertThumbprint=”certificate_thumbprint”
      $tenantid=”tenant_id”
      $tenantdomain=”starfleet.onmicrosoft.com”

      e.g.
      $script = {
      #Set variables
      $AppID=”2875fb84-3562-12fd-a348-1e234eea9876″
      $CertThumbprint=”3dadd21c543dadbf3de87e34d09b78da37587765″
      $tenantid = “364758a1-867a-33e2-5a2f-d27446539087″
      $tenantdomain=”starfleet.onmicrosoft.com”

      #connection functions
      Function ConnectExchange {
      Import-Module “D:\home\site\wwwroot\ExchangeOnline\ExchangeOnlineManagement.psd1”
      Connect-ExchangeOnline -CertificateThumbprint $CertThumbprint -AppId $AppID -Organization $tenantdomain -Showbanner:$false
      }

      ConnectExchange
      get-mailbox -Identity macdonaldm@arriva.co.uk

      }

      Reply
  3. Jose Rivera

    Hi Matts,

    Thanks for sharing this information. After running the function I got the following error
    2021-04-16T13:28:20.307 [Warning] The Function app may be missing a module containing the ‘Disable-AzContextAutosave’ command definition. If this command belongs to a module available on the PowerShell Gallery, add a reference to this module to requirements.psd1. Make sure this module is compatible with PowerShell 7. For more details, see https://aka.ms/functions-powershell-managed-dependency. If the module is installed but you are still getting this error, try to import the module explicitly by invoking Import-Module just before the command that produces the error: this will not fix the issue but will expose the root cause.
    2021-04-16T13:28:21.693 [Error] ERROR: The term ‘Disable-AzContextAutosave’ is not recognized as the name of a cmdlet, function, script file, or operable program.Check the spelling of the name, or if a path was included, verify that the path is correct and try again.Exception :Type : System.Management.Automation.CommandNotFoundExceptionErrorRecord :Exception :Type : System.Management.Automation.ParentContainsErrorRecordExceptionMessage : The term ‘Disable-AzContextAutosave’ is not recognized as the name of a cmdlet, function, script file, or operable program.Check the spelling of the name, or if a path was included, verify that the path is correct and try again.HResult : -2146233087TargetObject : Disable-AzContextAutosaveCategoryInfo : ObjectNotFound: (Disable-AzContextAutosave:String) [], ParentContainsErrorRecordExceptionFullyQualifiedErrorId : CommandNotFoundExceptionInvocationInfo :ScriptLineNumber : 15OffsetInLine : 5HistoryId : 1ScriptName : D:\home\site\wwwroot\profile.ps1Line : Disable-AzContextAutosave -Scope Process | Out-NullPositionMessage : At D:\home\site\wwwroot\profile.ps1:15 char:5+ Disable-AzContextAutosave -Scope Process | Out-Null+ ~~~~~~~~~~~~~~~~~~~~~~~~~PSScriptRoot : D:\home\site\wwwrootPSCommandPath : D:\home\site\wwwroot\profile.ps1InvocationName : Disable-AzContextAutosaveCommandOrigin : InternalScriptStackTrace : at , D:\home\site\wwwroot\profile.ps1: line 15CommandName : Disable-AzContextAutosaveTargetSite :Name : LookupCommandInfoDeclaringType : System.Management.Automation.CommandDiscovery, System.Management.Automation, Version=7.0.3.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35MemberType : MethodModule : System.Management.Automation.dllStackTrace :at System.Management.Automation.CommandDiscovery.LookupCommandInfo(String commandName, CommandTypes commandTypes, SearchResolutionOptions searchResolutionOptions, CommandOrigin commandOrigin, ExecutionContext context)at System.Management.Automation.CommandDiscovery.LookupCommandInfo(String commandName, CommandOrigin commandOrigin, ExecutionContext context)at System.Management.Automation.CommandDiscovery.LookupCommandIn

    And bellow that other error. Connect-AzAccount

    Should I import/installed this module on Kudu site?

    Do you have any contact address/number to contact you?
    regards,

    Jose Rivera

    Reply
    1. jmattmacd Post author

      Hi,

      Can you share the script you are trying to run? The error seems to indicate you are trying to run use cmdlets Disable-AzContextAutosave and Connect-AzAccount which I dont use in this example?

      If you need to use these then you can actually use the AZ module natively in Function apps without having to manually upload it. Just go the “App Files” on you function blade

      and then open the file host.json and make sure managedDependency is set to “true”:

      then open the file requirements.psd1 and set the AZ module as a requirement (In this case Im setting the Az version to 5.x and am also loading the msal.ps module as I use it iften for token generation):

      Reply
  4. Andreas

    Hey Matt,
    This was a bit of a lifesaver so thanks! Bit of an FYI for you..
    It looks like MS tried to fix this with https://github.com/Azure/azure-functions-host/pull/5011 . I ran [Environment]::Is64BitProcess to see if it was indeed the right 64 bit process and it reported ‘True’. I still got the same error though so I still have to use your workaround.

    Reply

Leave a Reply

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