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

Hello,

I would like to access an Office 365 mailbox using OAuth Application permissions.
If I’m not mistaken, the code sample in OAuth2 support for O365 uses OAuth Delegated permissions and not Application permissions.

When we use the following code to implement an ITokenProvider:

internal class AzureTokenProvider : ITokenProvider
{
    private readonly string _tenantId;
    private readonly string _clientId;
    private readonly string _clientSecret;
    private readonly string[] _scopes;

    private readonly object _tokenSyncObj = new object();
    private OAuthToken _token;

    /// <summary>
    /// Initializes a new instance of the <see cref="AzureTokenProvider"/> class
    /// </summary>
    public AzureTokenProvider(string tenant, string clientId, string clientSecret, string[] scopes)
    {
        _tenantId = tenant;
        _clientId = clientId;
        _clientSecret = clientSecret;
        _scopes = scopes;
    }

    /// <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 (_token != null && !this._token.Expired && !ignoreExistingToken)
                return _token;

            _token = GetNewAccessToken();
            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);
    }

    private OAuthToken GetNewAccessToken()
    {
        // https://docs.microsoft.com/en-us/exchange/client-developer/exchange-web-services/how-to-authenticate-an-ews-application-by-using-oauth
        var app = ConfidentialClientApplicationBuilder
            .Create(_clientId)
            .WithAuthority(AzureCloudInstance.AzurePublic, _tenantId)
            .WithClientSecret(_clientSecret)
            .Build();

        //Make the token request
        var authResultTask = app.AcquireTokenForClient(_scopes).ExecuteAsync();
        var authResult = authResultTask.Result;

        return new OAuthToken(authResult.AccessToken, TokenType.AccessToken, authResult.ExpiresOn.LocalDateTime);
    }

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

And use the following code to connect:

        var ewsScopes = new string[] { "https://outlook.office.com/.default" };
        var azureTokenProvider = new AzureTokenProvider(_mailboxSettings.AzureTenantId,
            _mailboxSettings.AzureClientId,
            _mailboxSettings.AzureClientSecret,
            ewsScopes);

        NetworkCredential credentials = new OAuthNetworkCredential(_mailboxSettings.Username, azureTokenProvider);
        _ewsClient = EWSClient.GetEWSClient(_mailboxSettings.Host, credentials);

we always get the following exception:
System.Web.Services.Protocols.SoapException
HResult=0x80131501
Message=ExchangeImpersonation SOAP header must be present for this type of OAuth token.
Source=System.Web.Services
StackTrace:
at System.Web.Services.Protocols.SoapHttpClientProtocol.ReadResponse(SoapClientMessage message, WebResponse response, Stream responseStream, Boolean asyncCall)
at System.Web.Services.Protocols.SoapHttpClientProtocol.Invoke(String methodName, Object[] parameters)
at #=zXtdwOglEJf$aKzfPTIvDlrbACe_cFhBgycEaMNg8xwNKGTyUxA==.GetFolder(GetFolderType GetFolder1)
at Aspose.Email.Clients.Exchange.WebService.EWSClient.GetEWSClient(String mailboxUri, ICredentials credentials, WebProxy proxy)
at Aspose.Email.Clients.Exchange.WebService.EWSClient.GetEWSClient(String mailboxUri, ICredentials credentials)
at GenericDocumentImport.Modules.Email.ExchangeProtocolApi.CheckClient()

We are using Aspose.Email version 20.02

@Luk_De_Reu,

I suggest you to please visit the following thread for your convenience as it has solution for addressing the similar OAut2 authentication issues. Also, please try using latest Aspose.Email for .NET 20.5 as there was a bug related to EWSClient in OAuth authentication which has been fixed.

We have upgraded to Aspose.Email for .NET 20.5, but the issue remains.

I have read the thread that you refer to, but this is not applicable because:

  1. We don’t have a problem obtaining an OAuth token, I have tested that AzureTokenProvider.GetToken() indeed manages to obtain a token for the requested scope. We get an exception from EWSClient.GetEWSClient() after it has called GetToken() on the AzureTokenProvider we have provided. The message has something to do with the SOAP call to EWS, not with the authentication. Cfr. the exception message is: ExchangeImpersonation SOAP header must be present for this type of OAuth token.
  2. In our scenario, we are using EWSClient and not IMAPClient.

I can share a code sample which connects to a Office 365 demo tenant if this would assist you in troubleshooting.

@Luk_De_Reu,

That would be good if you provide us a working example reproducing issue along with all necessary details.

The code sample relies on an Office 365 tenant, can I send you the Azure connection data by mail instead of posting them on this forum?
Code sample in attachment.
ConnectToEWSWithAppPermissions.zip (4.3 KB)

@Luk_De_Reu,

You can please share the connection data privately with us using Message option. For this, you will have to press on my Name Icon and use Message option to send the private message.

image.png (9.0 KB)

@Luk_De_Reu,

Thank you for sharing the information. A ticket with ID EMAILNET-39868 has been created in our issue tracking system to further investigate the issue. We will share the notification with you as soon as the issue will be investigated.

@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.