AdhocAdam / smletsexchangeconnector

SMLets PowerShell based Exchange Connector for controlling Microsoft System Center Service Manager 2016+
https://adhocadam.github.io/smletsexchangeconnector/
GNU General Public License v3.0
29 stars 19 forks source link

OAuth2 and Office 365/Hosted Exchange #171

Closed AdhocAdam closed 4 years ago

AdhocAdam commented 4 years ago

UPDATE: Basic Authentication to be disabled in second half of 2021 for those actively using it.

With the upcoming deprecation of Basic Authentication to Office 365 through Exchange Web Services API on October 13th, 2020. It's a priority for those running their Service Manager inbox in 365 that the connector continues to work.

This request sees to enable the connector to handle on-premise and hosted Exchange environments by introducing support for OAuth2 tokens as a means to authenticate to Exchange.

High level steps include:

AdhocAdam commented 4 years ago

To be refined -

  1. Register an App in Azure Active Directory 01 Set the Redirect URI to "Public client/native (mobile & desktop). Set the value to "urn:ietf:wg:oauth:2.0.oob" newApp02

  2. Take note of the Application (client) ID and Directory (Tenant) ID 03

  3. (optional) Set the logo to distinguish your application within your tenant. This makes it easy to distinguish the app if you registered other applications within your Azure tenant. You can use the following 215x215 png. Capture 215x215

  4. Choose the APIs the app can leverage. Untitled-1

    • Delegated Permissions: Consent required on a per user basis
      • most targeted and best scoped access
    • Application Permissions: Consent is given for the entire organization
      • way too much access as the application would be able to perform whatever API Permissions were assigned for everyone. can't think of a reason why this would be preferred.

Scroll to the bottom of available APIs. Choose Exchange, then choose "Delegated Permissions", finally search for "EWS.AccessAsUser.All" and finally "Add permissions"

  1. The SCSM workflow account to one time consent to the application's permissions. Taking a page out of Windows Terminal + Azure Cloud Shell integration where you're given a code. The following PowerShell makes a request to the Azure Application created above in order to complete the one time consent process. When the script runs, it will return a message that looks like "To sign in, use a web browser to open the page https://microsoft.com/devicelogin and enter the code ######### to authenticate." Open a browser, navigate the Microsoft sign in page, and enter the code.
#this is the application/client id for the registered Azure Application
$clientId = ""
#this is your full Azure Tenant Name. For example, mydomain.onmicrosoft.com.
#Unsure? Get it from the Overview blade in the Azure portal https://portal.azure.com/#blade/Microsoft_AAD_IAM/ActiveDirectoryMenuBlade/Overview
$tenant = ""
$resource = "https://graph.microsoft.com/";

#build and make the request. Show the auth Code in the shell to enter on Microsoft's site.
$requestBody = @{
    client_id = $clientId
    resource = $resource
}
$response = Invoke-RestMethod -Method "POST" -Uri "https://login.microsoftonline.com/$tenant/oauth2/devicecode" -Body $requestBody
Write-Output $response.message

At this point, the workflow account has consented to the application. How do you know this worked? As the workflow account, head over to https://account.activedirectory.windowsazure.com/r#/applications. Here you should see the configured application in the list of applications. It's possible that it may take a few minutes before it appears.

  1. Configure the connector to use workflow account + app secret to process the inbox.
    • see below
AdhocAdam commented 4 years ago

Assuming EWS is used. Per the Microsoft documentation Authenticate an EWS application by using OAuth and then translating the C# into PowerShell that is mocked up into the connector:

$tenantName = "" #this is your Azure Tenant Name e.g. mydomain.onmicrosoft.com
$clientId = "" #this is the application/client id of the Azure Active Directory app from above
$clientSecret = "" #this is the application secret generated above
$username = "" #the workflow account's username
$password = "" #the workflow account's password

$tokenReqBody = @{
    Grant_Type    = "Password"
    Client_Id     = $clientID
    Client_Secret = $clientSecret
    Username      = $username
    Password      = $password
    Scope         = "https://outlook.office.com/EWS.AccessAsUser.All"
}
$tokenReqResponse = Invoke-RestMethod -Uri "https://login.microsoftonline.com/$TenantName/oauth2/v2.0/token" -Method "POST" -Body $tokenReqBody 

#define Exchange assembly and connect to Office 365 EWS
$exchangeEWSAPIPath = "C:\Program Files\Microsoft\Exchange\Web Services\2.0\Microsoft.Exchange.WebServices.dll"
$exchangeEndpoint = "https://outlook.office365.com/EWS/Exchange.asmx"
[void] [Reflection.Assembly]::LoadFile("$exchangeEWSAPIPath")
$exchangeService = New-Object Microsoft.Exchange.WebServices.Data.ExchangeService
$exchangeService.Url = [System.Uri]$ExchangeEndpoint
$exchangeService.Credentials = [Microsoft.Exchange.WebServices.Data.OAuthCredentials]($tokenReqResponse.access_token)

#define search parameters, search on the defined classes and get messages that are older than the current time
$inboxFolderName = [Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::Inbox
$inboxFolder = [Microsoft.Exchange.WebServices.Data.Folder]::Bind($exchangeService,$inboxFolderName)
$itemView = New-Object -TypeName Microsoft.Exchange.WebServices.Data.ItemView -ArgumentList 1000
$propertySet = New-Object Microsoft.Exchange.WebServices.Data.PropertySet([Microsoft.Exchange.WebServices.Data.BasePropertySet]::FirstClassProperties)
$propertySet.RequestedBodyType = [Microsoft.Exchange.WebServices.Data.BodyType]::Text
$mimeContentSchema = New-Object Microsoft.Exchange.WebServices.Data.PropertySet([Microsoft.Exchange.WebServices.Data.ItemSchema]::MimeContent)
$dateTimeItem = [Microsoft.Exchange.WebServices.Data.ItemSchema]::DateTimeReceived
$now = get-date
$searchFilter = New-Object -TypeName Microsoft.Exchange.WebServices.Data.SearchFilter+IsLessThanOrEqualTo -ArgumentList $dateTimeItem,$now

#load the items in the Inbox using the the $searchFilter from above, in this case - load ANY kind of message (calendar, encrypted, regular email, OOO, etc.)
$inbox = $exchangeService.FindItems($inboxFolder.Id,$searchFilter,$itemView)

At this point the user is authenticated and is able to process the inbox through Exchange Web Services in Office 365 using OAuth tokens.

Note: If this were to be inserted into one's environment right now with EWS v2.x it should work as expected pending the user consent step.

AdhocAdam commented 4 years ago

Assuming the Microsoft Graph API is used. The mail is retrieved through an Invoke-RestMethod call to the Graph.

$tenantName = "" #this is your Azure Tenant Name e.g. mydomain.onmicrosoft.com
$clientId = "" #this is the application/client id of the Azure Active Directory app from above
$clientSecret = "" #this is the application secret generated above
$username = "" #the workflow account's username
$password = "" #the workflow account's password

$tokenReqBody = @{
    Grant_Type    = "Password"
    Client_Id     = $clientID
    Client_Secret = $clientSecret
    Username      = $username
    Password      = $password
    Scope         = "https://graph.microsoft.com/Mail.Read"
} 

#get the token to use with the Graph
$tokenReqResponse = Invoke-RestMethod -Uri "https://login.microsoftonline.com/$TenantName/oauth2/v2.0/token" -Method "POST" -Body $tokenReqBody 

### make a call and ask for the Message Class (e.g. IPM.Note) property since it isn't included in default call: ($filter = Id eq String 0x001a)
$apiUrl = "https://graph.microsoft.com/v1.0/me/mailFolders/Inbox/messages/?`$expand=SingleValueExtendedProperties(`$filter=(Id%20eq%20'String%200x001a'))"
$data = Invoke-RestMethod -Headers @{Authorization = "Bearer $($tokenReqResponse.access_token)"} -Uri $apiUrl -Method "GET"
$inbox = $data.value | select from, toRecipients, ccRecipients, subject, sentDateTime, receivedDateTime, id, conversationid, hasattachments, @{Name = 'ItemClass'; Expression = {$_.singleValueExtendedProperties | select value -ExpandProperty value }}

At this point the user is authenticated and is able to process the inbox through the Microsoft Graph in Office 365 using OAuth tokens.

Note: If this were to be inserted into one's environment right now, it will not work as expected as things like signed/encrypted messaging are not accounted for. Other properties core to the basic IPM.Note (email) class require light refactoring as some property names have changed.

AdhocAdam commented 4 years ago

The recent release of Exchange Connector 4.0 has now introduced OAuth support for Exchange Online. In an effort to continue to maintain a low barrier to entry to this connector as well as continued support for those currently using it in 365. A similar authentication method will be followed wherein:

Wiki being updated with more streamlined documentation on configuration over here.

AdhocAdam commented 4 years ago

OAuth functionality to be available with #206

AdhocAdam commented 3 years ago

The location of EWS has moved within the Azure Portal.

graphAPI