Aspose.Email OAuth2 Office 365

Hello,

We are using Aspose .NET Email to connect to a Microsoft Office 365 mail box using IMAP and a user id/password combination (Basic Authentication scheme) at the moment. But support for this approach of using a clear text password to connect to a Microsoft Office 365 mail box is going away by October 2020 and Microsoft recommends switching over to using OAuth2 authentication to access the mail box instead.

And, looking at the documentation for the latest version (20.x) of Aspose .NET Email, it looks like creating an IMAP client using OAuth2 is supported. We also managed to verify that we can indeed connect to a GSuite mail box using latest Aspose .NET Email using IMAP/OAuth2.

However, we have not been able to connect to an Office 365 mail box by creating an IMAP client using OAuth2. We used the Aspose email code samples to create the IMAP client and the code snippet is as given below. This code fails with an “Authentication failed” error at the line “client.SelectFolder(“Inbox”);”.

    static void AccessExchangeIMAPServer(string accessToken)
    {
        Console.WriteLine("Connecting to Exchange over Imap...");
        ImapMessageInfoCollection messageInfoCol;
        try
        {
            using (ImapClient client = new ImapClient("outlook.office365.com", 993, "testemail@testdomain.com", accessToken, true))
            {
                client.SecurityOptions = SecurityOptions.Auto;
                client.SelectFolder("Inbox");
                messageInfoCol = client.ListMessages();
            }

            if (messageInfoCol != null && messageInfoCol.Count > 0)
            {
                foreach (var message in messageInfoCol)
                {
                    Console.WriteLine(message.Subject.ToString());
                }
            }
        }
        catch(Exception e)
        {
            Console.WriteLine(e.Message.ToString() + "\r\n" + e.StackTrace);
        }
    }

Could you please tell us if Aspose .NET Email supports connecting to Office 365 mailbox using IMAP and OAuth2? If yes, could you please provide code snippets on how to achieve this?

The full error message with the stack trace is

Connecting to Exchange over Imap…
Authentication failed.

at #=ziT9KCqL0wjS2K5GK7UZEz_Atghgp.#=zSMnmBko=(IAsyncResult #=zVppe2xI=)

at #=ziT9KCqL0wjS2K5GK7UZEz_Atghgp.#=zMf8af9s=()

at #=zXL5SBveNLEHZztCytigYz9iW6Sr2$urwPBagw3A=.#=zcSL7t0Q=(#=zYT2p9TMhBQ7WSkaw8q5zjwsPVxKpUioC6Q== #=z7lfY7BI=)

at #=zfIjR1pi_K03Xao6g7KdrrzHqqWw3.#=z08aO9tWAztlJ(Int32 #=zGg5AMk4=, #=zYT2p9TMhBQ7WSkaw8q5zjwsPVxKpUioC6Q== #=z11kv_BM=)

at #=ziT9KCqL0wjS2K5GK7UZEz_Atghgp.#=zxaqcaIDbg_dh()

at #=z43Zg4xUstGvRjPVjujZpRuRaJdHwoqSegFIEVQ$L3auO.#=zxaqcaIDbg_dh()

at #=z9l1ogERylXofMpgT7EPk4krF0bu3eK_em517v0UQ8NNK…ctor(EmailClient #=zDvxBa8U=, String #=zSAFCSM8=, Nullable`1 #=zSYzqlMZuUGrt)

at Aspose.Email.Clients.Imap.ImapClient.BeginSelectFolder(IConnection connection, String folderName, Nullable`1 readOnly, AsyncCallback callback, Object state)

Please let me know if you need any more information.

Thank you,
Murali

@muraliHuron,

I have observed your requirements and suggest you to please refer to following thread that is exactly as per your requirements.

I reviewed the thread you indicated and the main difference I notice is that my question is about using IMAP/OAuth2 to connect to Office 365 mail box where as the suggested thread is about creating an “EWSExchangeClient” which is based off Exchange Web Services. We want to know if IMAP will be supported for OAuth2 when dealing with Office 365.

@muraliHuron,

I have observed your requirements. An issue with ID EMAILNET-39758 has been created in our issue tracking system to further investigate and resolve the issue. This thread has been linked with the issue so that you may be notified once feedback will be shared.

Thanks for opening an issue on my question around support for IMAP/OAuth2 for Office/Outlook 365 mail boxes.

As a follow-up, I would also like to know if Aspose Email supports connecting to Outlook.com mail boxes using IMAP/OAuth2. May I know if this mode is supported at all?

Also, Microsoft said they are releasing support for IMAP/OAuth2 for Office 365 mail boxes in Feb 2020. See here (Basic Auth and Exchange Online – February 2020 Update - Microsoft Community Hub under POP, IMAP and SMTP section)

Could you please let me know by when Aspose would be supporting this in their Aspose Email feature? It is very time critical (need to add to the codebase in next 2 - 3 weeks) for us to have support for Office 365 mail boxes using IMAP and OAuth2.

@muraliHuron,

I have observed the information shared by you. I like to mention here that at present the shared issue is still in waiting queue for investigation. I have included the new information shared by you in our issue tracking system and will get back to you with feedback as soon as possible.

Hi Aspose team,

Microsoft has released OAuth2.0 support over IMAP for accessing Office 365 mailboxes as of 04/30/2020. See this link https://developer.microsoft.com/en-us/office/blogs/announcing-oauth-2-0-support-for-imap-smtp-client-protocols-in-exchange-online/

Could you please let us know if Aspose is supporting this configuration (OAuth2/IMAP/Office 365) for accessing mail boxes? And if yes, which version of Aspose.Email contains support for this setup? Please treat our query as urgent.

Thank you,
Murali

1 Like

@muraliHuron,

We are investigating the requirements on our end and will share the feedback with you as soon as possible.

@muraliHuron,

Aspose.Email still can’t be tested with O365, but earlier it has been tested with google services.
Authentication implementation is common for all services, so highly likely that it has to work with O365 also, as soon as Azure portal allow to add required permissions.

The Code snippet that presumably could be used with office 365. The similar initialization is applied for POP3 and SMTP clients. Constructors are identical.

    string[] scopeAr = new string[]
    {
        "IMAP.AccessAsUser.All", 
        //"POP.AccessAsUser.All",
        //"SMTP.Send",
    };
    ITokenProvider tokenProvider = new AzureROPCTokenProvider(
        "Tenant",
        "ClientId",
        "ClientSecret",
        "EMail",
        "Password",
        scopeAr);
    using (ImapClient client = new ImapClient(
        server.ImapUrl,
        server.ImapPort,
        user.EMail,
        tokenProvider,
        server.ImapSecurityOptions))
    {
        ImapMessageInfoCollection messageInfoCol = client.ListMessages();
    }

using JsonConvert = Newtonsoft.Json.JsonConvert;
using Aspose.Email.Clients;
using Aspose.Email.Common.Utils;
using Aspose.Email.Tests.TestUtils;
using Newtonsoft.Json;
using System;
using System.IO;
using System.Net;
using System.Text;

namespace Aspose.Email.Tests
{
/// <summary>
/// Azure resource owner password credential (ROPC) token provider
/// https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth-ropc
/// https://portal.azure.com
/// https://developer.microsoft.com/en-us/graph/graph-explorer/#
/// token parser https://jwt.io
/// </summary>
internal class AzureROPCTokenProvider : ITokenProvider
{
    private const string uriFormat = "https://login.microsoftonline.com/{0}/oauth2/v2.0/token";
    private const string bodyFormat =
        "client_id={0}" +
        "&scope={1}" +
        "&username={2}" +
        "&password={3}" +
        "&grant_type={4}";

    private readonly string scope;
    private const string grant_type = "password";
    private readonly object tokenSyncObj = new object();
    private OAuthToken token;
    private readonly string tenant;
    private readonly string clientId;
    private readonly string clientSecret;
    private readonly string userName;
    private readonly string password;

    /// <summary>
    /// Initializes a new instance of the <see cref="AzureROPCTokenProvider"/> class
    /// </summary>
    /// <param name="tenant"></param>
    /// <param name="clientId"></param>
    /// <param name="clientSecret"></param>
    /// <param name="scope"></param>
    /// <param name="userName"></param>
    /// <param name="password"></param>
    /// <param name="scopeAr"></param>
    public AzureROPCTokenProvider(
        string tenant, 
        string clientId, 
        string clientSecret, 
        string userName, 
        string password,
        string[] scopeAr)
    {
        this.tenant = tenant;
        this.clientId = clientId;
        this.clientSecret = clientSecret;
        this.userName = userName;
        this.password = password;
        this.scope = string.Join(" ", scopeAr);
    }

    /// <summary>
    /// Gets oAuth access token. 
    /// </summary>
    /// <param name="ignoreExistingToken">
    /// If ignoreExistingToken is true, requests new token from a server. Otherwise behaviour is depended on whether token exists or not.
    /// If token exists and its expiration date is not expired returns current token, otherwise requests new token from a server.
    /// </param>
    /// <returns>Returns oAuth access token</returns>
    public virtual OAuthToken GetAccessToken(bool ignoreExistingToken)
    {
        lock (tokenSyncObj)
        {
            if (this.token != null && !this.token.Expired && !ignoreExistingToken)
                return this.token;
            token = null;
            string uri = string.Format(uriFormat, string.IsNullOrWhiteSpace(tenant) ? "common" : tenant);
            HttpWebRequest request = (HttpWebRequest)HttpWebRequest.Create(uri);
            string body = string.Format(bodyFormat,
                HttpUtility.UrlEncode(clientId),
                HttpUtility.UrlEncode(scope),
                HttpUtility.UrlEncode(userName),
                HttpUtility.UrlEncode(password),
                HttpUtility.UrlEncode(grant_type));
            byte[] bytes = Encoding.ASCII.GetBytes(body);
            request.Method = "POST";
            request.ContentType = "application/x-www-form-urlencoded";
            request.ContentLength = bytes.Length;
            MemoryStream ms = new MemoryStream(bytes);
            using (Stream requestStream = request.GetRequestStream())
                requestStream.Write(bytes, 0, bytes.Length);
            HttpWebResponse response = (HttpWebResponse)request.GetResponse();
            StringBuilder responseText = new StringBuilder();
            bytes = new byte[1024];
            int read = 0;
            using (Stream stream = response.GetResponseStream())
            {
                while ((read = stream.Read(bytes, 0, bytes.Length)) > 0)
                    responseText.Append(Encoding.ASCII.GetString(bytes, 0, read));
            }
            string jsonString = responseText.ToString();
            AzureTokenResponse t = JsonConvert.DeserializeObject<AzureTokenResponse>(jsonString);
            token = new OAuthToken(
                t.access_token,
                TokenType.AccessToken,
                DateTime.Now.AddSeconds(t.expires_in));
            return token;
        }
    }

    /// <summary>
    /// Gets oAuth access token.
    /// If token exists and its expiration date is not expired returns current token, otherwise requests new token from a server.
    /// </summary>
    /// <returns>Returns oAuth access token</returns>
    public OAuthToken GetAccessToken()
    {
        return GetAccessToken(false);
    }

    /// <summary>
    /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
    /// </summary>
    public virtual void Dispose()
    {
    }
}
}

Hi Mudassir Fayyaz,

Many thanks for the reply. We have tested the Aspose.Email OAuth functionality for below scenarios.

  1. IMAP/OAuth2.0/GMail - this works
  2. IMAP/OAuth2/Office 365 using below IMAPClient constructor - this does not work
    public ImapClient(string host, int port, string username, string authInfo, bool useOAuth);
  3. IMAP/OAuth2/Office 365 using IMAPClient constructor recommended above - this does not work
 using (ImapClient client = new ImapClient(server.ImapUrl, server.ImapPort, user.EMail, tokenProvider, server.ImapSecurityOptions))

Some differences between your code snippet and what we have in our POC (Scenario 3 above) -

  • You recommended using a scope of “IMAP.AccessAsUser.All”. But we used the scope of “offline_access [https://graph.microsoft.com/IMAP.AccessAsUser.All ](https://graph.microsoft.com/IMAP.AccessAsUser.All) IMAP.AccessAsUser.All” in the application. Although in Azure AD, I cannot locate a permission that is just “IMAP.AccessAsUser.All” now. Can you tell me under which API Category this permission is located?
  • You seem to have referenced ROPC flow because the AzureROPCTokenProvider has a “password” parameter in the constructor. But we have used a 3-legged authorization flow (which is a better approach compared to ROPC flow and Microsoft does not recommend using ROPC flow). Could you confirm if using this alternate approach is what is causing the failure?

By the way, the error/stack trace we are seeing when trying to connect over IMAP using OAuth access token is below -

Connecting to Exchange over Imap...
The operation 'Connect' terminated. Timeout '100000' has been reached.
   at #=ziT9KCqL0wjS2K5GK7UZEz_Atghgp.#=zSMnmBko=(IAsyncResult #=zVppe2xI=)
   at #=ziT9KCqL0wjS2K5GK7UZEz_Atghgp.#=zMf8af9s=()
   at #=zXL5SBveNLEHZztCytigYz9iW6Sr2$urwPBagw3A=.#=zcSL7t0Q=(#=zYT2p9TMhBQ7WSkaw8q5zjwsPVxKpUioC6Q== #=z7lfY7BI=)
   at #=zfIjR1pi_K03Xao6g7KdrrzHqqWw3.#=z08aO9tWAztlJ(Int32 #=zGg5AMk4=, #=zYT2p9TMhBQ7WSkaw8q5zjwsPVxKpUioC6Q== #=z11kv_BM=)
   at #=ziT9KCqL0wjS2K5GK7UZEz_Atghgp.#=zxaqcaIDbg_dh()
   at #=ziT9KCqL0wjS2K5GK7UZEz_Atghgp.#=zde8yHpE=(AsyncCallback #=zqEdC7xs=, Object #=zXVi_qHc=)
   at Aspose.Email.Clients.Imap.ImapClient.BeginListMessages(IConnection connection, String folderName, Int64 modificationSequence, Boolean retrieveRecursively, IEnumerable`1 messageExtraFields, AsyncCallback callback, Object state)
   at Aspose.Email.Clients.Imap.ImapClient.ListMessages()
   at OAuthConsoleApp.Program.AccessExchangeIMAPServer(String accessToken)

PS: If the issue has been closed by accident, could you please reopen it? The issue still isn’t resolved for us yet.

Thank you!

@muraliHuron,

Thank you for sharing the details with us. I am going to add feedback with concerned ticket in our issue tracking system.

Hi Mudassir,

Are there any updates on my 05/22/2020 findings about encountering issues trying to connect to Office 365 email boxes using Aspose Email/IMAP/OAuth2?

We are looking to add support to this configuration in the next 2 weeks and a quick solution to our issue would be hugely appreciated.

Thank you,
Murali

@muraliHuron,

Can you please try using following suggested option on your end.

   string[] scopeAr = new string[]
        {
            "https://outlook.office.com/IMAP.AccessAsUser.All",
        };
        ITokenProvider tokenProvider = new AzureROPCTokenProvider(
            "Tenant",
            "ClientId",
            "ClientSecret",
            "EMail",
            "Password",
            scopeAr);
        using (ImapClient client = new ImapClient(
            server.ImapUrl,
            server.ImapPort,
            user.EMail,
            tokenProvider,
            server.ImapSecurityOptions))
        {
            ImapMessageInfoCollection messageInfoCol = client.ListMessages();
        } 

Sign in to Outlook"

AzureROPCTokenProvider.zip (1.9 KB)

I wanted to quickly send out my findings on using the “Sign in to Outlook” scope. I get a Login Failed error and the full error stack trace is below. However, it appears that the scope should actually be Sign in to Outlook looking at this link. But that scope also errored out, but with a “timeout” error like before.

AE_1_1_0002 NO LOGIN failed.
   at #=ziT9KCqL0wjS2K5GK7UZEz_Atghgp.#=zSMnmBko=(IAsyncResult #=zVppe2xI=)
   at #=ziT9KCqL0wjS2K5GK7UZEz_Atghgp.#=zMf8af9s=()
   at #=zXL5SBveNLEHZztCytigYz9iW6Sr2$urwPBagw3A=.#=zcSL7t0Q=(#=zYT2p9TMhBQ7WSkaw8q5zjwsPVxKpUioC6Q== #=z7lfY7BI=)
   at #=zfIjR1pi_K03Xao6g7KdrrzHqqWw3.#=z08aO9tWAztlJ(Int32 #=zGg5AMk4=, #=zYT2p9TMhBQ7WSkaw8q5zjwsPVxKpUioC6Q== #=z11kv_BM=)
   at #=ziT9KCqL0wjS2K5GK7UZEz_Atghgp.#=zxaqcaIDbg_dh()
   at #=z43Zg4xUstGvRjPVjujZpRuRaJdHwoqSegFIEVQ$L3auO.#=zxaqcaIDbg_dh()
   at #=z9l1ogERylXofMpgT7EPk4krF0bu3eK_em517v0UQ8NNK..ctor(EmailClient #=zDvxBa8U=, String #=zSAFCSM8=, Nullable`1 #=zSYzqlMZuUGrt)
   at Aspose.Email.Clients.Imap.ImapClient.BeginSelectFolder(IConnection connection, String folderName, Nullable`1 readOnly, AsyncCallback callback, Object state)
   at Aspose.Email.Clients.Imap.ImapClient.SelectFolder(IConnection connection, String folderName, Nullable`1 readOnly)
   at Aspose.Email.Clients.Imap.ImapClient.SelectFolder(String folderName)
   at OAuthConsoleApp.Program.AccessExchangeIMAPServer(String accessToken)

@muraliHuron,

Microsoft just recently enabled this feature. Earlier, when code snippet above was written this code snippet was made just presumably, and couldn’t be tested at that moment.

Now, after MS updated it’s services, we have been able test them.

Following code works correctly:

    string[] scopeAr = new string[]
    {
        "https://outlook.office.com/IMAP.AccessAsUser.All",
    };
    ITokenProvider tokenProvider = new AzureROPCTokenProvider(
        "Tenant",
        "ClientId",
        "ClientSecret",
        "EMail",
        "Password",
        scopeAr);
    using (ImapClient client = new ImapClient(
        "outlook.office365.com",
        993,
        "EMail",
        tokenProvider,
        server.ImapSecurityOptions))
    {
        ImapMessageInfoCollection messageInfoCol = client.ListMessages();
    }

Please note following points:

Has to be used following scopes

https://outlook.office.com/IMAP.AccessAsUser.All
https://outlook.office.com/SMTP.Send
https://outlook.office.com/POP.AccessAsUser.All

You have to enable these permissions for your application.

We use ROPC authentication just as simplest sample, and to show how to use ITokenProvider for authentication. You may use any OAuth authentication mechanism and may implement your own ITokenProvider with 3-legged authorization, it also will work. But we recommend first to check it with ROPC authentication like more simple way.

To summarize, it looks like customer’s Azure application configured incorrectly. You selected scope correctly. You have to check application permissions and use code snippet from this comment to check operability.

If you won’t can solve this problem then please provide access to azure account (you may do test account if you can’t provide access to corporate) and provide us your test application.

Hi Mudassir,

Appreciate the response from you on our query. I tested with the “Sign in to Outlook” scope in addition to other outlook365 scopes that are currently available in Azure AD. However, we haven’t found the IMAP/OAuth2 based connections to work for Outlook 365 accounts. We continue to experience a “connection timeout” error which is tied to a “BAD User is authenticated but not connected” IMAP message in the Aspose activity log.

We would be happy to walk through our setup with your team over a video call. If the team is open to a video call, could you provide 3 convenient times in this week that would work?

Thank you,
Murali

Hi Aspose Support,

I am attaching our POC source code and the Aspose detailed Activity logs for functional (Legacy IMAP Plain authentication for Outlook 365) and non-functional (IMAP OAuth authentication for Outlook 365) scenarios.

  1. In the Code folder, Program.cs contains the code we have used to test connect to Outlook 365 account using IMAP and OAuth2. Please navigate the “Option 2” selection for the IMAP/OAuth/Outlook 365 code path. The “AccessExchangeIMAPServer” method has 3 code blocks. One uses IMAP with OAuth2, one uses IMAP with the AzureROPCProvider and the third one uses Legacy IMAP connection to the Outlook 365 mail box. Only the last option (Legacy) works in our tests.

  2. In the “Aspose IMAP logs” folder, there are 2 log files.
    (1) Aspose.Email.IMAP_2020-6-1_0_OAuth.log: Are the IMAP calls made when OAuth is used. You can see that we are noticing a “BAD User is authenticated but not connected” error that we don’t see using Legacy/Plain authentication
    (2) Aspose.Email.IMAP_2020-6-1-Legacy.log: contains the IMAP calls made when using Legacy authentication.

  3. In the “Screen Shots” folder, you will find a screen shot of the API permissions given to the Application registered in Azure portal whose ClientId/ClientSecret is being used by the POC Console application.

Please let us know what you find in your analysis of our setup/issue.

Thank you,
Murali

ToAsposeSupport 06012020.zip (62.1 KB)

@muraliHuron,

We are verifying the information shared by you and will share the feedback with you as soon as possible.

@muraliHuron,

We have investigated the issue on our end and request you to please create account with Microsoft 365 Developer Program and provide it us. Please also create there test azure application and we will help you to configure it. We actually need credentials to completely check this problem on our end.