Configuring OAuth2 Client Credentials Grant for Custom M365 SMTP Server

Continuing the discussion from Configuring OAuth2 Client Credentials Grant for Custom SMTP Server Integration in Genesys Cloud:

I am also interested in setting this up. Could you please share the details on the internal docs that are available. Unfortunately I do not have the ability to easily create a care ticket as requested in the previous thread. I would need to hop through the hoops of a VAR and convince them to enter a care ticket for me which is not really efficient.

Hi Joe,

Here's some guidance.

Please not that settings described are for testing purpose only and are not for production, where more restrictions should be applied according to your company policy(ies).

Microsoft supports since July 2023, a new OAuth flow: credentials flow.
It does not require anymore a user/password pair and allows to use an application for sending mail on "behalf of".

Read this article to get more insights:

Some extra steps are required to allow the application to access user mail boxes. All is described with the above article.

Email configuration

Check Integration configuration

Configure Custom Smtp Integration.
Select type Credential Flow

Please note that Integration still requires a user but this requirement is for connecting to SMTP server and check the connection, not to get a token.
SMTP protocol requires a user to connect to.

Assign new permissions

At the minimum, allow application to access a mail box:

Open a power shell window in admin mode:

Load modules:
Install-Module -Name ExchangeOnlineManagement -allowprerelease
Import-module ExchangeOnlineManagement
Connect-ExchangeOnline -Organization

Then type commands:

New-ServicePrincipal -AppId -ObjectId
Get-ServicePrincipal | fl
Add-MailboxPermission -Identity "<user name>" -User -AccessRights FullAccess

$Mailbox = "user name"
Get-MailboxPermission -Identity $Mailbox | Where-Object { ($.AccessRights -eq "FullAccess") -and ($.IsInherited -eq $false) -and -not ($_.User -like "NT AUTHORITY\SELF") } | ft -AutoSize

Example

Assign application access

New-ServicePrincipal -AppId c9fd0428-a094-434d-9dcb-cd86b6838793 -ObjectId 2c85ed7e-ca0f-451a- 9021 -4b49f91be2f1

Get-ServicePrincipal | fl

Add-MailboxPermission -Identity "AdeleV@614ryb.onmicrosoft.com" -User 2c85ed7e-ca0f-451a- 9021 -4b49f91be2f1 -AccessRights FullAccess

$Mailbox = "AdeleV@614ryb.onmicrosoft.com"

Get-MailboxPermission -Identity $Mailbox | Where-Object { ($_.AccessRights -eq "FullAccess" ) -and ($_.IsInherited -eq $ false ) -and -not ($_.User -like "NT AUTHORITY\SELF" ) } | ft -AutoSize

Get a Token

Http request

POST https://login.microsoftonline.com/<tenant>/oauth2/v2.0/token
Content-Type: application/x-www-form-urlencoded
grant_type=client_credentials&client_id={clientId}&scope=https://outlook.office.com/.default&client_secret=
{secretId}

Pay attention to the scope that needs to be https://outlook.office.com/.default

Example

POST https://login.microsoftonline.com/1cf1f047-bc19-45db-9954-adf81926341b/oauth2/v2.0/token
Content-Type: application/x-www-form-urlencoded
grant_type=client_credentials&client_id=28515eac-a167-4485-85ac-f032492b4e92&scope=https://outlook.office.com/.default&client_secret=ZMocbI

Response:

credentials flow response

{
"token_type" : "Bearer" ,
"expires_in" : 3599 ,
"ext_expires_in" : 3599 ,
"access_token": "<JWT>"
}

Test script in Python

Change values accordingly to your env.

Credentials flow

import base64
import pprint
import smtplib
import msal

conf = {
 "authority": "https://login.microsoftonline.com/d44bf45a-e2fa-45bc-8c65-cc1aa10fd573",
 "client_id": "28515eac-a167-4485-85ac-f032492b4e92",
 "client_secret": "ZMocbI",
 "scope": [ "https://outlook.office.com/.default" ],
 "tenant_id": "1cf1f047-bc19-45db-9954-adf81926341b",
 "user_name": "AdeleV@614ryb.onmicrosoft.com"
}

MAIL_TO = "<your email address>"
SMTP_PORT = 587
SMTP_HOST = "smtp.office365.com"

def encode_token_as_string(username, token) -> str:
   xoauth2_token = encode_auth_token_as_bytes(username, token)
   return str(xoauth2_token, "utf-8")

def encode_auth_token_as_bytes(username, token) -> bytes:
    just_a_str = f"user={username}\x01auth=Bearer {token}\x01\x01"
    xoauth2_token = base64.b64encode(just_a_str.encode('utf-8'))
    return xoauth2_token

username = conf['user_name']

if \_\_name__ == "\_\_main__":

   app = msal.ConfidentialClientApplication(
        conf['client_id'],
       authority=conf['authority'],
        client_credential=conf['client_secret']
    )

    result = app.acquire_token_silent(conf['scope'], account=None)
    if not result:
        print("No suitable token in cache. Getting a new one.")
        result = app.acquire_token_for_client(scopes=conf['scope'])

    if "access_token" in result:
        print(result['token_type'])
        pprint.pprint(result)
    else:
        print(result.get("error"))
        print(result.get("error_description"))
        print(result.get("correlation_id"))
    
    server = smtplib.SMTP(SMTP_HOST, SMTP_PORT)
    server.set_debuglevel(True)
    print("Connect")
    server.connect(SMTP_HOST, SMTP_PORT)
    
    print("Ehlo")
    server.ehlo()
    print("StartTLS")
    server.starttls()
    server.ehlo()
    access_token = result['access_token']
    print("start XOATUH2")
    code, msg = server.docmd("auth", "XOAUTH2 " + encode_token_as_string(username, access_token))

    if code != 235:
        raise Exception(base64.b64decode(msg.decode()))
    print ("Send mail")
    message = 'Subject: {}\n\n{}'.format('hello', 'This is a test')
    server.sendmail(username, MAIL_TO, message)
    
    print("Done")
    server.quit()

Thanks so much, @vpirat I was missing the assignment of the credentials to the mailbox using the exchange online commands. I will give this a shot today. Appreciate the detailed information and the quick reply!