Search code examples
c#oauth-2.0azure-active-directoryoffice365exchangewebservices

EWS GetUserPhoto Delegated to App-Only Authentication


I'm currently using Delegated Auth to get user photo as below:

var pcaOptions = new PublicClientApplicationOptions
{
    ClientId = "ClientID",
    TenantId = "TenantId"
};

var pca = PublicClientApplicationBuilder.CreateWithApplicationOptions(pcaOptions).Build();
var ewsScopes = new string[] { "https://outlook.office365.com/EWS.AccessAsUser.All" };
var authResult = await pca.AcquireTokenInteractive(ewsScopes).ExecuteAsync();

string email = "SomeEmail@email.com";
HttpWebRequest request = WebRequest.Create(string.Format("https://outlook.office365.com/EWS/Exchange.asmx/s/GetUserPhoto?email={0}&size=HR648x648", email)) as HttpWebRequest;
request.Headers.Add("Authorization", "Bearer " + authResult.AccessToken);

using (HttpWebResponse response = request.GetResponse() as HttpWebResponse){
    Stream stream = response.GetResponseStream();
    using (MemoryStream ms = new MemoryStream())
    {
        string encodedPhoto = Convert.ToBase64String((ms.ToArray()));
    }
}

I'm trying to change to use App-Only Authentication. I managed to get the access token, but seems like it can't be done the same way as Delegated Auth.

Below is what I did to switch to use App-Only Authentication.

var cca = ConfidentialClientApplicationBuilder
                .Create("ClientID")
                .WithClientSecret("ClientSecret")
                .WithTenantId("TenantID")
                .Build();

var ewsScopes = new string[] { "https://outlook.office365.com/.default" };
var authResult = await cca.AcquireTokenForClient(ewsScopes).ExecuteAsync();

string email = "SomeEmail@email.com";
HttpWebRequest request = WebRequest.Create(string.Format("https://outlook.office365.com/EWS/Exchange.asmx/s/GetUserPhoto?email={0}&size=HR648x648", email)) as HttpWebRequest;
request.Headers.Add("Authorization", "Bearer " + authResult.AccessToken);

using (HttpWebResponse response = request.GetResponse() as HttpWebResponse){
//Error: The remote server returned an error: (400) Bad Request.
    Stream stream = response.GetResponseStream();
    using (MemoryStream ms = new MemoryStream())
    {
        string encodedPhoto = Convert.ToBase64String((ms.ToArray()));
    }
}

Error: The remote server returned an error: (400) Bad Request.

Edit: Updated to use SOAP operation as below, but I got this error: "The remote server returned an error: (500) Internal Server Error."

Anything I might have missed?

var cca = ConfidentialClientApplicationBuilder
                .Create("ClientID")
                .WithClientSecret("ClientSecret")
                .WithTenantId("TenantID")
                .Build();

var ewsScopes = new string[] { "https://outlook.office365.com/.default" };
var authResult = await cca.AcquireTokenForClient(ewsScopes).ExecuteAsync();

HttpWebRequest request = WebRequest.Create("https://outlook.office365.com/EWS/Exchange.asmx") as HttpWebRequest;
request.Headers.Add("Authorization", "Bearer " + authResult.AccessToken);
request.Method = "POST";

XmlDocument SOAPReqBody = new XmlDocument();

SOAPReqBody.LoadXml(@"<?xml version=""1.0"" encoding=""utf-8"" ?>
                    <soap:Envelope xmlns:xsi=""http://www.w3.org/2001/XMLSchema-instance"" xmlns:xsd=""http://www.w3.org/2001/XMLSchema"" xmlns:soap=""http://schemas.xmlsoap.org/soap/envelope/"" xmlns:t=""https://schemas.microsoft.com/exchange/services/2006/types"" xmlns:m=""https://schemas.microsoft.com/exchange/services/2006/messages"">
                        <soap:Header>
                        <t:RequestServerVersion Version=""Exchange2013""/>
                        <t:ExchangeImpersonation>
                            <t:ConnectingSID>
                            <t:PrimarySmtpAddress>impersonatedUser@mail.com</t:PrimarySmtpAddress>
                            </t:ConnectingSID>
                        </t:ExchangeImpersonation>
                        </soap:Header>
                        <soap:Body>
                        <m:GetUserPhoto>
                            <m:Email>UserEmail@mail.com</m:Email>
                            <m:SizeRequested>HR648x648</m:SizeRequested>
                        </m:GetUserPhoto>
                        </soap:Body>
                    </soap:Envelope>");

using (Stream stream = request.GetRequestStream())
{
      SOAPReqBody.Save(stream);
}

using (HttpWebResponse response = request.GetResponse() as HttpWebResponse){
//The remote server returned an error: (500) Internal Server Error.
    Stream stream = response.GetResponseStream();
    using (MemoryStream ms = new MemoryStream())
    {
        string encodedPhoto = Convert.ToBase64String((ms.ToArray()));
    }
}

Solution

  • The app token in EWS requires that you impersonate a known user (as you requests then take on that identity) so you would need to use the SOAP operation https://learn.microsoft.com/en-us/exchange/client-developer/web-service-reference/getuserphoto-operation and then impersonate a real-user (if you having Application policies you would need to be careful which one otherwise any user in the tenant should work) eg

           var cca = ConfidentialClientApplicationBuilder
                            .Create("d4")
                            .WithClientSecret("s")
                            .WithTenantId("x")
                            .Build();
    
            var ewsScopes = new string[] { "https://outlook.office365.com/.default" };
            var authResult = cca.AcquireTokenForClient(ewsScopes).ExecuteAsync().Result;
    
    
            HttpWebRequest request = WebRequest.Create("https://outlook.office365.com/EWS/Exchange.asmx") as HttpWebRequest;
            request.Headers.Add("Authorization", "Bearer " + authResult.AccessToken);
            request.Method = "POST";
    
            XmlDocument SOAPReqBody = new XmlDocument();
    
            SOAPReqBody.LoadXml(@"<?xml version=""1.0"" encoding=""utf-8""?>
            <soap:Envelope xmlns:xsi=""http://www.w3.org/2001/XMLSchema-instance"" xmlns:m=""http://schemas.microsoft.com/exchange/services/2006/messages"" xmlns:t=""http://schemas.microsoft.com/exchange/services/2006/types"" xmlns:soap=""http://schemas.xmlsoap.org/soap/envelope/"">
              <soap:Header>
                <t:RequestServerVersion Version=""Exchange2016"" />
                <t:ExchangeImpersonation>
                  <t:ConnectingSID>
                    <t:SmtpAddress>user@domain.com</t:SmtpAddress>
                  </t:ConnectingSID>
                </t:ExchangeImpersonation>
              </soap:Header>
              <soap:Body>
                <m:GetUserPhoto>
                  <m:Email>user@domain.com</m:Email>
                  <m:SizeRequested>HR48x48</m:SizeRequested>
                </m:GetUserPhoto>
              </soap:Body>
            </soap:Envelope>");
    
            using (Stream stream = request.GetRequestStream())
            {
                SOAPReqBody.Save(stream);
            }
    
            using (HttpWebResponse response = request.GetResponse() as HttpWebResponse)
            {
                //The remote server returned an error: (500) Internal Server Error.
                Stream stream = response.GetResponseStream();
                XmlDocument Response = new XmlDocument();
                Response.Load(stream);
                var PictureDataNodes = Response.GetElementsByTagName("PictureData");
                Byte[] PictureData = Convert.FromBase64String(PictureDataNodes[0].InnerText);
                File.WriteAllBytes("c:\\temp\\pictest.jpg", PictureData);
            }
        }