Imap Auth in Office 365 Using Oauth2

Microsoft exchange - IMAP OAuth2 With client credentials

I wrote same answer here, so I am coping it here.

I think I made some progress.

I read documentation few times, tried few times from the start with same error. I even have tried using client and object ids instead of email as username, in lack of better ideas.

So this is where I think I have made mistake previous times.

On the part where it is needed to register service principal, I needed to execute

New-ServicePrincipal -AppId <APPLICATION_ID> -ServiceId <OBJECT_ID> [-Organization <ORGANIZATION_ID>]

Here I have put enterprise application object id as ServiceId argument. And that is ok.

But on

Add-MailboxPermission -Identity "email address removed for privacy reasons" -User 
<SERVICE_PRINCIPAL_ID> -AccessRights FullAccess

I have put my registered application object id as User argument. I also tried setting object id of enterprise application, but it did not have success.

When I executed

Get-ServicePrincipal -Organization <ORGANIZATION_ID> | fl

I did not pay attention to ServiceId property, even with documentation specifying it and saying it will be different.

Now I cleared everything and started fresh.

I have executed all the steps again, but on the step for creating new service principal I used data from enterprise application view. When I need to add mail permission, I list service principals, and then use ServiceId value from the output, as argument for user.

With that, I was able to authorise.

How to read my outlook mail using java and oauth2.0 with application regsitration in Azure AD

For client credentials flow, you need to add application permissions under Office 365 Exchange Online

enter image description here

Make sure to grant admin consent for all the application permissions.

Once consent has been provided, the admin must register your AAD application's service principal in Exchange using powerShell by following commands:

Install ExchangeOnlineManagement

Install-Module -Name ExchangeOnlineManagement -allowprerelease
Import-module ExchangeOnlineManagement
Connect-ExchangeOnline -Organization

Register Service Principal in Exchange:

1.New-ServicePrincipal -AppId <APPLICATION_ID> -ServiceId <OBJECT_ID> [-Organization <ORGANIZATION_ID>]

Make sure to use ObjectId from enterprise applications rather than object id of application registration.
For the same application you registered in Application Registration. A corresponding application has been created in Enterprise Application as well. You need to pass object id from there while registering service principal in Exchange:
enter image description here

2.Get-ServicePrincipal | fl

3.Add-MailboxPermission -Identity "john.smith@contoso.com" -User
<SERVICE_PRINCIPAL_ID> -AccessRights FullAccess

In the application, you need to use scope = 'https://outlook.office365.com/.default'

Once you will get the access token, you can create and open a Java Mail connection to read mails.

    Properties props = new Properties();

props.put("mail.store.protocol", "imap");
props.put("mail.imap.host", "outlook.office365.com");
props.put("mail.imap.port", "993");
props.put("mail.imap.ssl.enable", "true");
props.put("mail.imap.starttls.enable", "true");
props.put("mail.imap.auth", "true");
props.put("mail.imap.auth.mechanisms", "XOAUTH2");
props.put("mail.imap.user", mailAddress);
props.put("mail.debug", "true");
props.put("mail.debug.auth", "true");

// open mailbox....
String token = getAuthToken(tanantId,clientId,client_secret);
Session session = Session.getInstance(props);
session.setDebug(true);
Store store = session.getStore("imap");
store.connect("outlook.office365.com", mailAddress, token);

Office 365 IMAP authentication via OAuth2 and python MSAL library

The imaplib.IMAP4.error: AUTHENTICATE failed Error occured because one point in the documentation is not that clear.

When setting up the the Service Principal via Powershell you need to enter the App-ID and an Object-ID. Many people will think, it is the Object-ID you see on the overview page of the registered App, but its not!
At this point you need the Object-ID from "Azure Active Directory -> Enterprise Applications --> Your-App --> Object-ID"

New-ServicePrincipal -AppId <APPLICATION_ID> -ServiceId <OBJECT_ID> [-Organization <ORGANIZATION_ID>]

Microsoft says:

The OBJECT_ID is the Object ID from the Overview page of the
Enterprise Application node (Azure Portal) for the application
registration. It is not the Object ID from the Overview of the App
Registrations node. Using the incorrect Object ID will cause an
authentication failure.

Ofcourse you need to take care for the API-permissions and the other stuff, but this was for me the point.
So lets go trough it again, like it is explained on the documentation page.
Authenticate an IMAP, POP or SMTP connection using OAuth

  1. Register the Application in your Tenant
  2. Setup a Client-Key for the application
  3. Setup the API permissions, select the APIs my organization uses tab and search for "Office 365 Exchange Online" -> Application permissions -> Choose IMAP and IMAP.AccessAsApp
  4. Setup the Service Principal and full access for your Application on the mailbox
  5. Check if IMAP is activated for the mailbox

Thats the code I use to test it:

import imaplib
import msal
import pprint

conf = {
"authority": "https://login.microsoftonline.com/XXXXyourtenantIDXXXXX",
"client_id": "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXXX", #AppID
"scope": ['https://outlook.office365.com/.default'],
"secret": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", #Key-Value
"secret-id": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", #Key-ID
}

def generate_auth_string(user, token):
return f"user={user}\x01auth=Bearer {token}\x01\x01"

if __name__ == "__main__":
app = msal.ConfidentialClientApplication(conf['client_id'], authority=conf['authority'],
client_credential=conf['secret'])

result = app.acquire_token_silent(conf['scope'], account=None)

if not result:
print("No suitable token in cache. Get 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"))

imap = imaplib.IMAP4('outlook.office365.com')
imap.starttls()
imap.authenticate("XOAUTH2", lambda x: generate_auth_string("target_mailbox@example.com", result['access_token']).encode("utf-8"))

After setting up the Service Principal and giving the App full access on the mailbox, wait 15 - 30 minutes for the changes to take effect and test it.

Ruby & IMAP - Accessing Office 365 with Oauth 2.0

SOLUTION for me!

Steps I took.

  1. Made an Azure app ('Device Flow' was the easiest way to go for me) Check the Steps in the link. You also need to change some settings in your APP if you want to use IMAP. See the youtube link here between 2:50 - 4:30
  2. Get the postman requests from this link (scroll down a little) (click here)
  3. From postman you can use "Device Flow" requests.
  4. Start with Device Authorization Request (you need a scope and client_id for this) I used https://outlook.office.com/IMAP.AccessAsUser.All scope.
  5. go to the link that you got back from the request and enter the required code.
  6. now go to Device Access Token Request and use the "device_code" from the last request and put that under code, under body.
  7. You should get an access_token

Connect using ruby

require 'gmail_xoauth' # MUST HAVE! otherwise XOAUTH2 auth wont work
require 'net/imap'
imap = Net::IMAP.new(HOST, PORT, true)
access_token = "XXXXX"
user_name = "email@outlook.com"
p imap.authenticate('XOAUTH2',"#{user_name}", "#{access_token}")

# example
imap.list('','*').each do |folders|
p folders
end

XOAUTH2 Returns

#<struct Net::IMAP::TaggedResponse tag="RUBY0001", name="OK", data=#<struct Net::IMAP::ResponseText code=nil, text="AUTHENTICATE completed.">, raw_data="RUBY0001 OK AUTHENTICATE completed.\r\n

Just to specify

HOST = 'outlook.office365.com'
PORT = 993

UPDATE 25.01.2023

class Oauth2
require 'selenium-webdriver'
require 'webdrivers'
require 'net/http'

# Use: Oauth2.new.get_access_code
# Grants access to Office 365 emails.

def get_access_code
p "### Access Request Started #{Time.now} ###"
begin
codes = device_auth_request
authorize_device_code(codes[:user_code])
access_code = device_access_token(codes[:device_code])
access_code
rescue => e
p e
p "Something went wrong with authorizing"
end
end

def device_auth_request # Returns user_code and device_code
url = URI('https://login.microsoftonline.com/organizations/oauth2/v2.0/devicecode')

https = Net::HTTP.new(url.host, url.port)
https.use_ssl = true

request = Net::HTTP::Post.new(url)
request.body = "client_id=YOUR_CLIENT_ID&scope=%09https%3A%2F%2Foutlook.office.com%2FIMAP.AccessAsUser.All"

response = https.request(request)
{
user_code: JSON.parse(response.read_body)["user_code"],
device_code: JSON.parse(response.read_body)["device_code"]
}
end

def device_access_token(device_code)
url = URI('https://login.microsoftonline.com/organizations/oauth2/v2.0/token')

https = Net::HTTP.new(url.host, url.port)
https.use_ssl = true

request = Net::HTTP::Post.new(url)
request.body = "grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Adevice_code&code=#{device_code}&client_id=YOUR_CLIENT_ID"

response = https.request(request)
JSON.parse(response.read_body)["access_token"]
end

def authorize_device_code(device_code)
# SELENIUM SETUP
driver = setup_selenium
driver.get "https://microsoft.com/devicelogin"
sleep(4)
# ------------------------------------------

# Give Access
element = driver.find_element(:class, "form-control")
element.send_keys(device_code)
sleep(2)
element = driver.find_element(:id, "idSIButton9")
element.submit
sleep(2)
element = driver.find_element(:id, "i0116")
element.send_keys("YOUR OUTLOOK ACCOUNT EMAIL")
sleep(2)
element = driver.find_element(:class, "button_primary")
element.click
sleep(2)
element = driver.find_element(:id, "i0118")
element.send_keys("YOUR OUTLOOK PASSWORD")
element = driver.find_element(:class, "button_primary")
element.click
sleep(2)
element = driver.find_element(:class, "button_primary")
element.click
sleep(2)
# ------------------------------------------
driver.quit
end

def setup_selenium
require 'selenium-webdriver'

# set up Selenium
options = Selenium::WebDriver::Chrome::Options.new(
prefs: {
download: {
prompt_for_download: false
},
plugins: {
'always_open_pdf_externally' => true
}
}
)
options.add_argument('--headless')
options.add_argument('--no-sandbox')
# options.add_argument('-incognito')
options.add_argument('disable-popup-blocking')
Selenium::WebDriver.for :chrome, options: options
end
end


Related Topics



Leave a reply



Submit