Search code examples
rubyoauth-2.0outlookimap

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?


Solution

  • 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