[EWS]Using Oauth authentication with EWS and impersonation in Office 365

Earlier I shared the sample script for consuming EWS using Oauth (via Azure Active Directory (AAD)) in a delegation mode. Here I’m sharing a sample PowerShell script that illustrates using Oauth authentication with EWS and impersonation to access mailboxes with an app token. The script accesses the mailboxes with email addresses presented in a text file. It then outputs the total item count in Inbox folder and prints the subject of the last five items in Inbox for each of these mailboxes. The authorization grant flow used is Client Credential grant flow and it requires the global admin to gives assent to the app.

Additionally, the app must use an X.509 certificate with a public/private key pair. In this example, I’ll be using a self-issued certificate. The detailed steps for creating a self-issued certificate and uploading the details to manifest in Azure Management Portal are discussed here.

Coming to permissions, since this app will be using EWS with impersonation, the app has to be provided Exchange scope permissions (Application Permissions under Office 365 Exchange Online): full_access_as_app – "Use Exchange Web Services with full access to all mailboxes”. For detailed steps related to manual app registration in AAD, please see this article.

Correct permission

The UserAccounts.txt present in the same directory as the PowerShell script will contain the email addresses of the impersonated users as shown below:

 EmailAddress
 User1@domain.onmicrosoft.com
 User2@domain.onmicrosoft.com

The script will need the following:

1. ADAL .NET libraries

2. EWS Managed API dll (version 2.2)

3. UserAccounts.txt file containing the email addresses as described above

4. The password required to access the X.509 certificate data

5. The certificate pfx file

6. Write permission to the directory where the EWS request and response logs will be written

7. Information related to the app you’ve registered in the Azure AD (see script for more details)

There are couple of important things to note here from EWS point of view:

a. Since this script is using EWS impersonation, the request must contain the ExchangeImpersonation header

b. The “X-AnchorMailbox” header needs to be present with the value as the email address of the impersonated account. If the header is not present, you may get an error – “Exchange Web Services are not currently available for this request because none of the Client Access Servers in the destination site could process the request."

Here is the script -

 #NOTE - Disclaimer
  #Following programming examples is for illustration only, without warranty either expressed or implied,
  #including, but not limited to, the implied warranties of merchantability and/or fitness for a particular purpose.
  #This sample code assumes that you are familiar with the programming language being demonstrated and the tools
  #used to create and debug procedures. This sample code is provided for the purpose of illustration only and is
  #not intended to be used in a production environment.
 
 
 function GetEmailFromInbox($smtpemailaddress)
 {
 ""
 "Mailbox being accessed: " + $smtpemailaddress
 ""
 $service.httpheaders.Add("X-AnchorMailbox", $smtpemailaddress)
 
 $service.ImpersonatedUserId = new-object Microsoft.Exchange.WebServices.Data.ImpersonatedUserId([Microsoft.Exchange.WebServices.Data.ConnectingIdType]::SmtpAddress,$smtpemailaddress)
 
 $inbox = [Microsoft.Exchange.WebServices.Data.Folder]::Bind($service, [Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::Inbox)
 ""
 "Total Messages : " + $inbox.TotalCount
 $view = New-Object Microsoft.Exchange.WebServices.Data.ItemView(5)
 $findItemResults = $service.FindItems([Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::Inbox,$view)
 ""
 
 "Printing subject of last five items from Inbox:"
 
 ""
 $item = [Microsoft.Exchange.WebServices.Data.Item]
 
 $i = 1
 
 foreach ($item in $findItemResults)
 {
 "Subject " + $i + ": " + $item.Subject
 $i = $i + 1
 ""
 }
 
 $bool = $service.httpheaders.Remove("X-AnchorMailbox")
 
 $service.ImpersonatedUserId = $null
 
 }
 
 
 $Assem = (
     "Microsoft.Exchange.WebServices, Version=15.0.0.0, Culture=neutral,  PublicKeyToken =31bf3856ad364e35" #EWS Managed API 2.2
    )
 
 $Source = @"
 using System;
 using System.Text;
 using Microsoft.Exchange.WebServices.Data;
 public class TraceListener : Microsoft.Exchange.WebServices.Data.ITraceListener
     {
         public void Trace(string traceType, string traceMessage)
         {
             CreateXMLTextFile(traceType, traceMessage.ToString());
         }
         private void CreateXMLTextFile(string fileName, string traceContent)
         {
             //The path to the EWS request and response log. Please make sure you've write permission to the path/folder specified
             string strPath = "C:\\EWSLog.txt";
             System.IO.FileStream fs;
             if (System.IO.File.Exists(strPath) == false)
             {
                 fs = System.IO.File.Create(strPath);
             }
             else
             {
                 fs = System.IO.File.OpenWrite(strPath);
             }
             fs.Close();
             // Create an instance of StreamWriter to write text to a file.
             System.IO.StreamWriter sw = System.IO.File.AppendText(strPath);
             sw.WriteLine(System.DateTime.Now.ToString() + ": " + traceContent);
             sw.Close();
         }
     }
 "@
 
 Add-Type -ReferencedAssemblies $Assem -TypeDefinition $Source -Language CSharp
 
 # Load ADAL Assemblies
 
 $adal = "C:\Nugets\Microsoft.IdentityModel.Clients.ActiveDirectory.2.19.208020213\lib\net45\Microsoft.IdentityModel.Clients.ActiveDirectory.dll"
 
 $adalforms = "C:\Nugets\Microsoft.IdentityModel.Clients.ActiveDirectory.2.19.208020213\lib\net45\Microsoft.IdentityModel.Clients.ActiveDirectory.WindowsForms.dll"
 
 [System.Reflection.Assembly]::LoadFrom($adal)
 
 [System.Reflection.Assembly]::LoadFrom($adalforms)
 
 # Set Azure AD Tenant name (not needed here)
 
 $adTenant = "domain.onmicrosoft.com"
 
 # Set well-known client ID from Azure AD for this application
 
 $clientId = "client id"
 
 # Set redirect URI from Azure AD for this application
 
 $redirectUri = "reply url"
 
 # Set Resource URI to Office 365 in this case
 
 $resourceAppIdURI = "https://outlook.office365.com/"
 
 # Set Authority to Azure AD Tenant
 #https://stackoverflow.com/questions/26384034/how-to-get-the-azure-account-tenant-id
 
 $authority = "https://login.windows.net/<tenant-id>/oauth2/authorize"
 
 
 # Create Authentication Context tied to Azure AD Tenant
 
 $authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $authority
 
 # Acquire token
 
 #Provide the name of the certificate file
 $certfile = "C:\CertPrivatekey.pfx"
 
 $flag = [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::MachineKeySet
 
 #Provide the password required to access the X.509 certificate data
 $cert  = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2($certfile, "password", $flag )
 
 $cac = New-Object  Microsoft.IdentityModel.Clients.ActiveDirectory.ClientAssertionCertificate($clientID, $cert)
 
 $authResult = $authContext.AcquireToken($resourceAppIdURI, $cac)
 
 ""
 write-host "Access token:   " $authResult.AccessToken
 
 # EWS
 
 
 ## Code From https://poshcode.org/624
 ## Create a compilation environment
 $Provider=New-Object Microsoft.CSharp.CSharpCodeProvider
 $Compiler=$Provider.CreateCompiler()
 $Params=New-Object System.CodeDom.Compiler.CompilerParameters
 $Params.GenerateExecutable=$False
 $Params.GenerateInMemory=$True
 $Params.IncludeDebugInformation=$False
 $Params.ReferencedAssemblies.Add("System.DLL") | Out-Null
 
 $TASource=@'
   namespace Local.ToolkitExtensions.Net.CertificatePolicy{
     public class TrustAll : System.Net.ICertificatePolicy {
       public TrustAll() {
       }
       public bool CheckValidationResult(System.Net.ServicePoint sp,
         System.Security.Cryptography.X509Certificates.X509Certificate cert,
         System.Net.WebRequest req, int problem) {
         return true;
       }
     }
   }
 '@
 $TAResults=$Provider.CompileAssemblyFromSource($Params,$TASource)
 $TAAssembly=$TAResults.CompiledAssembly
 
 ## We now create an instance of the TrustAll and attach it to the ServicePointManager
 $TrustAll=$TAAssembly.CreateInstance("Local.ToolkitExtensions.Net.CertificatePolicy.TrustAll")
 [System.Net.ServicePointManager]::CertificatePolicy=$TrustAll
 
 Import-Module -Name "C:\Program Files\Microsoft\Exchange\Web Services\2.2\Microsoft.Exchange.WebServices.dll"
 
 
 $service = new-object Microsoft.Exchange.WebServices.Data.ExchangeService([Microsoft.Exchange.WebServices.Data.ExchangeVersion]::Exchange2013_SP1)
 $service.TraceListener = new-object TraceListener
 #Set it to $false, if you want to disable this tracing
 $service.TraceEnabled = $true
 $service.Url = new-object System.Uri("https://outlook.office365.com/ews/exchange.asmx")
 
 $service.Credentials  = new-object  Microsoft.Exchange.WebServices.Data.OAuthCredentials($authResult.AccessToken)
 
 import-csv UserAccounts.txt | foreach-object {
 
     $EmailAddress = $_.EmailAddress.ToString()
 
     GetEmailFromInbox($EmailAddress)
 
     }
 

Happy scripting!