[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!

Comments

  • Anonymous
    January 30, 2016
    Hi Manish, Is it also possible to use OAuth and impersonation with the REST API, or only EWS?

    • Anonymous
      November 02, 2018
      Yes. You can use OAUTH with REST. REST only supports OAUTH. It will no longer allow Basic from what I understand.
  • Anonymous
    February 01, 2016
    Hello Jim, Yes you can use impersonation with EXO REST APIs though technically it may not be called impersonation. It's more like your app will have permission (through Application Permission dropdown) to read all the mails/calendar/contacts and not actually a service account impersonating all other accounts. Also, with REST APIs there aren't any headers involved when making the request where you've to specify the impersonated account as in EWS request. The Office 365 API permissions are discussed here - msdn.microsoft.com/.../application-manifest Manish

  • Anonymous
    March 19, 2017
    Hi Manish,Is it possible to PSCredential using an access token (Oauth credentials) for creating a New-PSSession. I would like to be able to add new Transport Rules.Thanks!

  • Anonymous
    April 13, 2017
    I've created an application in portal.azure.com but I'm unable to find this scope - Use Exchange Web Services with full access to all mailboxes anywhere. Can you please help with this?

  • Anonymous
    November 02, 2018
    Fixed up some of the links because the content moved.