Haruka Shitou
Haruka Shitou

Reputation: 157

Powershell, EWS, OAuth2, and automation

I am trying to find documentation on how to implement non-interactive Oauth2 authentication to EWS using PowerShell, but I'm probably not using the correct search terms, because I can't find anything useful. The Microsoft documentation I can find on OAuth2 only has C# documentation.

So, does anyone know how to implement this?

Upvotes: 5

Views: 8351

Answers (2)

DarkLite1
DarkLite1

Reputation: 14735

This is an extension on the information provided by @stukey, which is already graat. Instead of creating your own function to retrieve an access token, one can use the MSAL.PS library. This module can simply be installed from the PowerShell Gallery:

Install-Module -Name MSAL.PS

Configure Azure app

When you configure your "App Registration" in Azure you can use the following settings. This will allow you to use Integrated Windows Authentication and avoids storing passwords in your code (useful when running Windows Scheduled Tasks as a specific user to run your scripts):

  • Authentication > Advanced Settings > Treat application as a public client: Yes

Public client

Add the scope "EWS.AccessAsUser.All" in the section "API Permissions" (it can be found within the last option "Supported legacy API's: Exchange"):

API Permissions

Request token

When all this is configured you can request a new token when logged on with the correct Windows account that has Full control exchange permissions on the desired mailbox:

$msalParams = @{
    ClientId              = $azureClientId
    TenantId              = $azureTenantId 
    Scopes                = "https://outlook.office.com/EWS.AccessAsUser.All"
    IntegratedWindowsAuth = $true
}
Get-MsalToken @msalParams 

It might be required to add the switch -Interactive, so you can consent to the proposed scopes. This will only need to be done once.

Now that a valid token is acquired a refresh of the token can simply be done with the -Silent switch. This will get a valid token form the cache or request a new token when it's no longer valid:

$msalParams = @{
    ClientId = $azureClientId
    TenantId = $azureTenantId 
    Scopes   = "https://outlook.office.com/EWS.AccessAsUser.All"
    Silent   = $true
}
Get-MsalToken @msalParams 

It would be great if both steps above can be combined into one call. For this I opened an issue.

Use the token with Exchange Web Services

$EWS = 'C:\Program Files\Microsoft\Exchange\Web Services\2.2\Microsoft.Exchange.WebServices.dll'
Import-Module -Name $EWS -EA Stop


$Service = New-Object Microsoft.Exchange.WebServices.Data.ExchangeService -ArgumentList 'Exchange2013_SP1'
$Service.Url = 'https://outlook.office365.com/EWS/Exchange.asmx'
$Service.UseDefaultCredentials = $false

$msalParams = @{
    ClientId = $azureClientId
    TenantId = $azureTenantId 
    Scopes   = "https://outlook.office.com/EWS.AccessAsUser.All"
}
$token = Get-MsalToken @msalParams
$Service.Credentials = [Microsoft.Exchange.WebServices.Data.OAuthCredentials]$token.AccessToken

Hopefully this will help others struggling with the same issues we did.

Upvotes: 3

stukey
stukey

Reputation: 126

There’s a really good overview of this on the following blog: https://ingogegenwarth.wordpress.com/2018/08/02/ews-and-oauth/#more-5139

I used the above blog to get it working in our PowerShell scripts - with a lot of trial and error. The following example script uses an applications's ClientID as registered in Azure AD. If you don't already have an application registered in Azure AD, you must do this first. There are various guides available on the web to register a new application in Azure AD. In order to utilise EWS with OAuth your registered application must have the correct permissions in Azure AD. You have two options for EWS:

  1. Use Delegated Permissions and request the 'EWS.AccessAsUser.All' API permission in Azure AD - Legacy APIs | Exchange | Delegated Permissions | EWS.AccessAsUser.All (Access mailboxes as the signed-in user via Exchange Web Services). This permission gives your registered application the same access to Exchange mailboxes as the signed-in user. If you use this permission, the first time your application's ClientID is used by any Service or User Account to access Exchange Online, the account in question must approve the ClientID through an interactive pop-up notification. Therefore before using this script in an automated fashion, you must interactively access the Exchange Online Service using the ClientID of your registered application and approve the authorisation pop-up. The easiest way to do this is to log-in to a mailbox using Microsoft's free 'EWS Editor' application and specify your app's ClientID. Once your app's ClientID has been approved, your script can run fully automated without any interaction.
  2. Use Application Permissions and request the 'full_access_as_app' API permission in Azure AD - Legacy APIs | Exchange | Delegated Permissions | EWS.AccessAsUser.All (Access mailboxes as the signed-in user via Exchange Web Services). This permission gives your registered application full access via Exchange Web Services to all mailboxes without a signed-in user. This type of permission gives the application full access to any mailbox in Exchange Online service and must be approved by an Azure AD global admin providing "admin consent". Your script will then authenticate to Exchange Online using the registered Azure AD application Client ID (effectively the username) and Client Secret (effectively the password).

The below example uses option 1. I haven't tested option 2. Whichever option you chose, you will need to handle requesting an OAuth token (example in the below code) from Azure AD and checking and refreshing the token at regularly intervals (no example). I haven't done that as all of our EWS scripts are simple, quick to run scripts that complete before the token needs to be refreshed (usually within 60 minutes). If this is something you're going to need, you will need to ask others for help. Hope this at least helps get you on the right track...

Here's the example script (the main body of the script calls the 'Get-EWSOAuthToken' function):

#Variables
$UserPrincipalName = "Enter the UPN of your Service Account ID"
$Password = "Password of your Service Account ID - store this securely"
$ClientIDfromAzureAD = "Client ID of your registered application in Azure AD"
$errRecip = "Email address of recipients to notify via email if errors occur"
$script = "Name of script"
$sender = "Email address of sender - normally the server name where your script runs"
$logfile = "Path and filename to log file"
$smtpServer = "Your SMTP server"

Function Get-EWSOAuthToken
{
    <#
        .SYNOPSIS
            Request an OAuth EWS token from Azure AD using supplied Username and Password

        .DESCRIPTION
            Request an OAuth EWS token from Azure AD using supplied Username and Password

        .PARAMETER UserPrincipalName
            The UPN of the user that will authenticate to Azure AD to request the OAuth Token

        .PARAMETER Password
            The Password (SecureString) of the user that will authenticate to Azure AD to request the OAuth Token

        .PARAMETER ADALPath
            The full path and filename on the local file system to the ADAL (Active Directory Authentication Library) DLL. This library is installed as part of various modules such as Azure AD, Exchange Online, etc.

        .PARAMETER ClientId
            Identifier of the client application that is requesting the token. You must register your calling application in Azure AD. This will provide you with a ClientID and RedirectURI

        .PARAMETER ConnectionUri
            The URI of the Exchange Online EWS endpoint. Default URI of 'https://outlook.office365.com/EWS/Exchange.asmx' is used

        .PARAMETER RedirectUri
            Address to return to upon receiving a response from the authority. You must register your calling application in Azure AD. This will provide you with a ClientID and RedirectURI

        .EXAMPLE
            $token = Get-EWSOAuthtokenFromCredential -UserPrincipalName "[email protected]" -Password $mySecurePassword -ClientId "123444454545454767687878787" -RedirectUri "https://dummyredirectdomain.com"
            $ews = New-Object Microsoft.Exchange.WebServices.Data.ExchangeService -ArgumentList Exchange2013_SP1 -ErrorAction Stop
            $ews.UseDefaultCredentials = $False
            $ews.Credentials = [Microsoft.Exchange.WebServices.Data.OAuthCredentials]$token
    #>

    [CmdletBinding()]
    Param
    (
        [System.String]$UserPrincipalName,
        [System.Security.SecureString]$Password,
        [System.String]$ADALPath,
        [System.String]$ClientId = "123444454545454767687878787",
        [System.Uri]$ConnectionUri = "https://outlook.office365.com/EWS/Exchange.asmx",
        [System.Uri]$RedirectUri = "https://dummyredirectdomain.com"
    )

    Begin
    {
        Write-Host "Starting Get-EWSOAuthTokenFromCredential function..." -ForegroundColor Yellow
        #Determine ADAL location based on Azure AD module installation path
        If([System.String]::IsNullOrEmpty($ADALPath)) 
        {
            Write-Host "Attempting to locate ADAL library..." -ForegroundColor Yellow

            $ADALPath = (Get-InstalledModule -Name "AzureAD" -ErrorAction SilentlyContinue | Select-Object InstalledLocation).InstalledLocation
            $ADALPath = Join-Path -Path $ADALPath -ChildPath "Microsoft.IdentityModel.Clients.ActiveDirectory.dll"
            Write-Host "Located library @ '$ADALPath'" -ForegroundColor Yellow
            If([System.String]::IsNullOrEmpty($ADALPath))
            {
                #Get List of installed modules and check Azure AD DLL is available
                $tmpMods = Get-Module -ListAvailable | Where-Object {$_.Name -eq "AzureAD"}

                If($tmpMods)
                {
                    $ADALPath = Split-Path $tmpMods.Path
                    $ADALPath = Join-Path -Path $ADALPath -ChildPath "Microsoft.IdentityModel.Clients.ActiveDirectory.dll"
                    Write-Host "Located library @ '$ADALPath'" -ForegroundColor Yellow
                }
                Else
                {
                    $err = "$($myinvocation.mycommand.name) requires the ADAL Library DLL files ('Microsoft.IdentityModel.Clients.ActiveDirectory.dll') that are installed as part of the 'AzureAD' module! Please install the AzureAD module from the Powershell Gallery. See: 'https://www.powershellgallery.com/packages/AzureAD' for more information"
                    Throw "$err"
                }
            }
        }

        #Load 'Microsoft.IdentityModel.Clients.ActiveDirectory' DLL
        Try
        {
            Import-Module $ADALPath -DisableNameChecking -Force -ErrorAction Stop
            Write-Host "Successfully imported ADAL Library" -ForegroundColor Yellow
        }
        Catch
        {
            $err = "$($myinvocation.mycommand.name): Could not load ADAL Library DLL '$ADALPath'. Error: $_"
            Throw "$err"
        }
    }
    Process
    {
        try
            {
            $resource = $connectionUri.Scheme + [System.Uri]::SchemeDelimiter + $connectionUri.Host
            $azureADAuthorizationEndpointUri = "https://login.windows.net/common/oauth2/authorize/"
            $AuthContext = New-Object Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext($azureADAuthorizationEndpointUri) -ErrorAction Stop
            $AuthCredential = New-Object Microsoft.IdentityModel.Clients.ActiveDirectory.UserPasswordCredential($UserPrincipalName, $Password) -ErrorAction Stop
            Write-Host "$($myinvocation.mycommand.name): Requesting a new OAuth Token..." -ForegroundColor Yellow
            $authenticationResult = ([Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContextIntegratedAuthExtensions]::AcquireTokenAsync($AuthContext, $resource, $clientId, $AuthCredential))

            If ($authenticationResult.Status.ToString() -ne "Faulted") {
                Write-Host "$($myinvocation.mycommand.name): Successfully retrieved OAuth Token" -ForegroundColor Yellow
            }
            else {
                $err = "$($myinvocation.mycommand.name): Error occurred calling ADAL 'AcquireTokenAysnc' : $authenticationResult.Exception.ToString()"
                Throw "$err"
            }
        }
        catch
        {
            #create object
            $returnValue = New-Object -TypeName PSObject

            #get all properties from last error
            $ErrorProperties =$Error[0] | Get-Member -MemberType Property

            #add existing properties to object
            foreach ($Property in $ErrorProperties)
            {
                if ($Property.Name -eq 'InvocationInfo')
                {
                    $returnValue | Add-Member -Type NoteProperty -Name 'InvocationInfo' -Value $($Error[0].InvocationInfo.PositionMessage)
                }
                else
                {
                    $returnValue | Add-Member -Type NoteProperty -Name $($Property.Name) -Value $($Error[0].$($Property.Name))
                }
            }
            #return object
            $returnValue
            break
        }
    }
    End
    {
        return $authenticationResult
    }
}


###### Main script

#Ensure TLS 1.2 protocol is enabled
try {
    If ([Net.ServicePointManager]::SecurityProtocol -notmatch 'Tls12') {
        [Net.ServicePointManager]::SecurityProtocol += [Net.SecurityProtocolType]::Tls12
        Write-Host "Enabled Tls1.2 in '[Net.ServicePointManager]::SecurityProtocol'" -ForegroundColor Yellow
    }
    else {
        Write-Host "Tls1.2 is enabled in '[Net.ServicePointManager]::SecurityProtocol'" -ForegroundColor Yellow
    }
}
Catch {
    $err = "An error occurred enabling TLS1.2. Error: $_"
    Write-Host "`n$err" -ForegroundColor Red
    Send-MailMessage -To $errRecip -Subject "$script - Error occurred during processing" -Body $err -From $sender -Attachment $logfile -SmtpServer $smtpServer
    Exit
}

#CHECK FOR EWS MANAGED API, IF PRESENT IMPORT THE HIGHEST VERSION EWS DLL, ELSE EXIT
$EWSDLL = (($(Get-ItemProperty -ErrorAction SilentlyContinue -Path Registry::$(Get-ChildItem -ErrorAction SilentlyContinue -Path 'Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Exchange\Web Services'|Sort-Object Name -Descending | Select-Object -First 1 -ExpandProperty Name)).'Install Directory') + "Microsoft.Exchange.WebServices.dll")
If (Test-Path $EWSDLL)
{
    Try
    {
        Import-Module $EWSDLL -DisableNameChecking -ErrorAction Stop
    }
    Catch 
    {
        $err = "An error occurred importing the Exchange Web Services DLL '$EWSDLL'. Error: $_"
        Write-Host "`n$err" -ForegroundColor Red
        Send-MailMessage -To $errRecip -Subject "$script - Error occurred during processing" -Body $err -From $sender -Attachment $logfile -SmtpServer $smtpServer
        Exit
    }
}
Else
{
    $err = "This script requires the EWS Managed API 1.2 or later. Please download and install the current version of the EWS Managed API from http://go.microsoft.com/fwlink/?LinkId=255472"
    Write-Host "`n$err" -ForegroundColor Red
    Send-MailMessage -To $errRecip -Subject "$script - Error occurred during processing" -Body $err -From $sender -Attachment $logfile -SmtpServer $smtpServer
    Exit
}


#Create EWS Object
$ews = New-Object Microsoft.Exchange.WebServices.Data.ExchangeService -ArgumentList "Exchange2013_SP1" -ErrorAction Stop

#Authenticate EWS using OAuth
Try {
    $ews.UseDefaultCredentials = $False
    Write-Host "Requesting EWS OAuth Token using registered Client ID" -ForegroundColor Yellow

    $OAuthResult = Get-EWSOAuthToken -UserPrincipalName $UserPrincipalName -Password $Password -ClientId "$ClientIDfromAzureAD" -ErrorAction Stop
    $token = $OAuthResult.Result.AccessToken

#Check if we successfully retrieved an Oauth Token
If ([System.String]::IsNullOrEmpty($token))
        {
            $err = "Get-EWSOAuthtoken returned an empty Auth Token. Aborted. Latest error details:`n$_error $($OAuthResult.Exception)"
            Write-Host "`n$err" -ForegroundColor Red
            $OAuthResult | Format-List -Force
            $OAuthResult.Result | Format-List -Force
            Send-MailMessage -To $errRecip -Subject "$script - Error occurred during processing" -Body "$err" -From $sender -Attachment $logfile -SmtpServer $smtpServer
            Exit
        }
        else
        {
            $OAuthchk = $true
            $ews.Credentials = [Microsoft.Exchange.WebServices.Data.OAuthCredentials]$token
            Write-Host "Set EWS credentials to OAuth token" -ForegroundColor Yellow
        }
    }
Catch
{
    $err = "An error occurred creating a new EWS object. Error:`n $_"
    write-host "`n$err" -ForegroundColor Red
    Send-MailMessage -To $errRecip -Subject "$script - Error occurred during processing" -Body "$err" -From $sender -Attachment $logfile -SmtpServer $smtpServer
    Exit
}

# Do your processing using EWS
....

Upvotes: 5

Related Questions