BrandoN
BrandoN

Reputation: 313

Ruby & IMAP - Accessing Office 365 with Oauth 2.0

So MS disabled IMAP for basic auth as we all know.

I am trying to figure out how to get the OAUTH 2.0 working using ruby (not ruby on rails). I have Azure APP and everything needed (I think), but I can not find any code related to ruby and getting the access token.

First step is completed, but next step is to get the access token. https://learn.microsoft.com/en-us/exchange/client-developer/legacy-protocols/how-to-authenticate-an-imap-pop-smtp-application-by-using-oauth

I need to read different Outlook mailboxes.

Could someone please explain how to do this?

Upvotes: 3

Views: 2396

Answers (2)

Lars Kanis
Lars Kanis

Reputation: 896

The online service of Microsoft Office 365 can be accessed by the IMAP protocol in Ruby with OAuth2.

At first the access should be checked with Thunderbird. If the permission is granted by the organization to use Thunderbird, then the following example should login too. It uses OAuth2 authentication through the browser similar to Thunderbird.

The full example is here in this gist, but the essence is this:

logon_site = 'https://login.microsoftonline.com'
# client_id and empty client_secret of Thunderbird
client_id = '9e5f94bc-e8a4-4e73-b8be-63364c29d753'
client_secret = ''
# Connect to the OAuth2 logon server
client = OAuth2::Client.new(client_id, client_secret, site: logon_site, authorize_url: "/common/oauth2/authorize", token_url: "/common/oauth2/token")

# Start a local web server with self-signed SSL certs
context = OpenSSL::SSL::SSLContext.new
context.cert, context.key = create_cert("localhost")
server = TCPServer.new(0)
sserver = OpenSSL::SSL::SSLServer.new(server, context)
port = server.addr[1].to_s
redirect_uri = "https://localhost:#{port}"

# Build and show the URI with the authentication request
auth_url = client.auth_code.authorize_url(redirect_uri: redirect_uri)
puts "Please visit the following URI for authentication.\nYou need to accept the invalid certificate warning.\n  #{auth_url}"

# Wait until the auth_code was received from the browser on the local web server
auth_code = Gem::WebauthnListener.wait_for_otp_code(logon_site, sserver)

# Request an access token for IMAP
access = client.auth_code.get_token(auth_code, redirect_uri: redirect_uri, resource: 'https://outlook.office.com', client_id: client_id)

# Connect to the IMAP port
imap = Net::IMAP.new('outlook.office365.com', 993, true)
imap.authenticate('XOAUTH2', email_address, access.token)
# List all imap folders
pp imap.list("", "*").map(&:name) # => ["INBOX", "Sent", "Trash", ...]

Upvotes: 0

BrandoN
BrandoN

Reputation: 313

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 protected]"
    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

Upvotes: 5

Related Questions