Reputation: 157
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
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):
Add the scope "EWS.AccessAsUser.All" in the section "API Permissions" (it can be found within the last option "Supported legacy API's: Exchange"):
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
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:
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