OAuth2 support for O365 with Application permissions (C# .NET)

@Luk_De_Reu,

Thank you for sharing the information privately. As requested in private message, can you please also share the screen shots of application permissions as well.

@Luk_De_Reu,

I have received application permissions in a private message via your colleague @pdhert. I have associated information in our issue tracking system and will get back to you with feedback as soon as the issue will be fixed.

@Luk_De_Reu, have you been able to resolve this issue? I am having the exact same SOAP exception…

@marieke.saeij,

I regret to share that at present there are no updates available. We will share updates with you as soon as the issue will be addressed.

How this can be fixed by Aspose:

class Microsoft.Exchange.WebServices.Data.ExchangeService has a Property with name “ImpersonatedUserId”.
Setting this Property adds the following SOAP-Header to Server-Requests

soap:Header
<t:ExchangeImpersonation>
<t:ConnectingSID>
<t:PrimarySmtpAddress>alisa@contoso.com</t: PrimarySmtpAddress>
</t:ConnectingSID>
</t:ExchangeImpersonation>
</soap:Header>

Please add an overload to
EWSClient.GetEWSClient(
where the ImpersonatedUserId can also be provided.

@Markus1980Wien

Please consider following implementation on your end.

Required permissions in Azure portal:

EWS.AccessAsUser.All            // for EWS
POP.AccessAsUser.All            // for POP
IMAP.AccessAsUser.All           // for IMAP
SMTP.Send                       // for SMTP

Please use the following sample code to initialize clients.

EWS client initialization

                        string[] scopeAr = new string[]
                        {
                            "https://outlook.office.com/EWS.AccessAsUser.All",
                        };
                        ITokenProvider tokenProvider = new AzureROPCTokenProvider(
                            "Tenant", 
                            "ClientId", 
                            "ClientSecret", 
                            "EMail", 
                            "Password", 
                            scopeAr);
                        OAuthNetworkCredential credentials = new OAuthNetworkCredential(tokenProvider);
                        EWSClient client = EWSClient.GetEWSClient("EWSUrl", credentials);
                    }

Pop3 client initialization

                                    string[] scopeAr = new string[]
                                    {
                                        "https://outlook.office.com/POP.AccessAsUser.All",
                                    };
                                    ITokenProvider tokenProvider = new AzureROPCTokenProvider(
                                        "Tenant",
                                        "ClientId",
                                        "ClientSecret",
                                        "EMail",
                                        "Password",
                                        scopeAr);
                                    client = new Pop3Client(
                                        "Pop3Url",
                                        "Pop3Port",
                                        "EMail",
                                        tokenProvider,
                                        SecurityOptions.Auto);

SMTP client initialization

                                    string[] scopeAr = new string[]
                                    {
                                        "https://outlook.office.com/SMTP.Send",
                                    };
                                    ITokenProvider tokenProvider = new AzureROPCTokenProvider(
                                        "Tenant",
                                        "ClientId",
                                        "ClientSecret",
                                        "EMail",
                                        "Password",
                                        scopeAr);
                                    SmtpClient client = new SmtpClient(
                                        "SmtpUrl",
                                        "SmtpPort",
                                        "EMail",
                                        tokenProvider,
                                        SecurityOptions.Auto);

Imap client initialization

                                    string[] scopeAr = new string[]
                                    {
                                        "https://outlook.office.com/IMAP.AccessAsUser.All",
                                    };
                                    ITokenProvider tokenProvider = new AzureROPCTokenProvider(
                                        "Tenant",
                                        "ClientId",
                                        "ClientSecret",
                                        "EMail",
                                        "Password",
                                        scopeAr);
                                    ImapClient client = new ImapClient(
                                        "ImapUrl",
                                        "ImapPort,
                                        "EMail",
                                        tokenProvider,
                                        SecurityOptions.Auto);

AzureROPCTokenProvider

extern alias GlobalNewtonsoftJson;
using JsonConvert = GlobalNewtonsoftJson::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 Tests
{
    /// <summary>
    /// Azure resource owner password credential (ROPC) token provider
    /// https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth-ropc
    /// https://docs.microsoft.com/en-us/exchange/client-developer/exchange-web-services/how-to-authenticate-an-ews-application-by-using-oauth
    /// 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()
        {
        }
    }
}

your provided mehtod
public virtual OAuthToken GetAccessToken(bool ignoreExistingToken)
fails on the following line
HttpWebResponse response = (HttpWebResponse)request.GetResponse();

HTTP 401 Unauthorized
{“error”:“invalid_client”,“error_description”:"AADSTS7000218: The request body must contain the following parameter: ‘client_assertion’ or ‘client_secret

@Markus1980Wien

I have included the information in concerned ticket and will share the feedback with you as soon as it will be resolved.

Please also forward my last post to your developers, because this would solve the problem quickly.

@Markus1980Wien

I have included the information in our issue tracking system and will share the feedback with you as soon as it will be fixed.

In order to perform impersonation you have to invoke method IEWSClient.ImpersonateUser.

Your answer does not help.

In order to perform IEWSClient.ImpersonateUser I need an instance of EWSClient.
To create a new Instance of EWSClient I need to call EWSClient.GetEWSClient(…)
BUT when using OAuthNetworkCredential I am not able to call GetEWSClient
because I receive Exception “ExchangeImpersonation SOAP header must be present”.
So please add an overload to EWSClient.GetEWSClient() where the missing ExchangeImpersonation SOAP header can be provided.

1 Like

@Markus1980Wien

I have associated the information in our issue tracking system for our team’s review and will share the feedback with you as soon as it will be shared.

+1
I have finally made it to the point where I am getting an access token and thought I was past the worst of it, but now I find myself in the same predicament as you. With the retirement of basic auth for Office 365 eminent this week, I am thinking I might have to switch over to the Exchange Service. I’ll let you know if I have any luck.

Aspose.Email version 20.9.1.0 seems to have resolved this issue.
Set the oAuthNetworkCredential.UserName to the e-mail account you wish to impersonate before calling getEWSClient and it will sucessfully return a client. It is worth noting that this had no effect in prior versions of Aspose.EMail.

@dlankford

I hope the latest version will work as expected on your end.

Yes, thank you. The new version works very well.

@dlankford

Thank you for sharing the positive feedback.

I have the exact use case as the topic starter but within Java instead of .NET. The topic says that it only works from version 20.9.1.0 from the Aspose.Email library. I’m using 20.9 in Java and I still have the same problems as in .NET prior to version 20.9.1.0. There isn’t a Java version for this.

Can the solution used in 20.9.1.0 for .NET also be implemented in Java?

@bascouwenberg

We will get back to you with feedback regarding Java based implementation for the same after discussion with team.