Search code examples
c#microsoft-graph-apioutlook-restapi

Using Microsoft Graph to fetch emails without any user interaction


All I am trying to do is to fetch emails for a userID which is accessible to other users without having them logging into their Microsoft accounts. I have looked at numerous SO posts (this), code samples (this, this) and looked into the specs of OpenID and other docs (this), but still not able to figure it out.

I have registered app in azure portal and granted required permissions. Using the sample app I am able to fetch user list, but not the email list. I compared the request headers for both user query and email query. Both look the same. Can someone please tell me what I am doing wrong?

Code is given below:

Startup.Auth.cs

public static string clientId = "<CLIENT ID>";
public static string clientSecret = <CLIENT SECRET>;
private static string authority = "https://login.microsoftonline.com/common/v2.0";
public static string redirectUri = "https://localhost:44316/";

private void ConfigureAuth(IAppBuilder app)
{
   app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);

   app.UseCookieAuthentication(new CookieAuthenticationOptions());

   app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
   {
       ClientId = clientId,
       Authority = authority,
       RedirectUri = redirectUri,
       PostLogoutRedirectUri = redirectUri,
       //Scope = "openid profile offline_access Mail.Read",
       Scope = "email profile openid offline_access User.Read Mail.Read",
       //ResponseType = "id_token",
       ResponseType = "code id_token",
       TokenValidationParameters = new TokenValidationParameters { ValidateIssuer = false, NameClaimType = "name" },
       Notifications = new OpenIdConnectAuthenticationNotifications
       {
          AuthenticationFailed = this.OnAuthenticationFailedAsync,
          SecurityTokenValidated = this.OnSecurityTokenValidatedAsync
        }
    });
}

SyncController.cs

private const string AuthorityFormat = "https://login.microsoftonline.com/{0}/v2.0";
private const string MSGraphScope = "https://graph.microsoft.com/.default";
//private const string MSGraphQuery = "https://graph.microsoft.com/v1.0/users";
//private const string MSGraphQuery = "https://graph.microsoft.com/v1.0/me";
private const string MSGraphQuery = "https://graph.microsoft.com/v1.0/me/messages";

public async Task GetAsync(string tenantId)
{
    MSALCache appTokenCache = new MSALCache(Startup.clientId);

    // Get a token for the Microsoft Graph. If this line throws an exception for
    // any reason, we'll just let the exception be returned as a 500 response
    // to the caller, and show a generic error message to the user.
    ConfidentialClientApplication daemonClient = new ConfidentialClientApplication(Startup.clientId, string.Format(AuthorityFormat, tenantId), Startup.redirectUri,
                                                                                   new ClientCredential(Startup.clientSecret), null, appTokenCache.GetMsalCacheInstance());
    AuthenticationResult authResult = await daemonClient.AcquireTokenForClientAsync(new[] { MSGraphScope });

    HttpClient client = new HttpClient();

    // Uses SendAsync
    /*HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, MSGraphQuery);
    request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
    request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", authResult.AccessToken);
    //request.Headers.Add("client-request-id", System.Guid.NewGuid().ToString());
    //request.Headers.Add("return-client-request-id", "true");
    HttpResponseMessage response = await client.SendAsync(request);*/

    // Uses GetAsync
    client.BaseAddress = new System.Uri(MSGraphQuery);
    client.DefaultRequestHeaders.Accept.Clear();
    client.DefaultRequestHeaders.Add("Authorization", "Bearer " + authResult.AccessToken);
    client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
    HttpResponseMessage response = await client.GetAsync(MSGraphQuery);
}

Edit 1: Here are the request and response values for user list (working) and email list (not working):

Request body to fetch user list (working):

{Method: GET, RequestUri: 'https://graph.microsoft.com/v1.0/users', Version: 1.1, Content: <null>, Headers:
{
  Accept: application/json
  Authorization: Bearer eyJ0eXAiOiJKV...
}}


Response when fetching user list (working):

{StatusCode: 200, ReasonPhrase: 'OK', Version: 1.1, Content: System.Net.Http.StreamContent, Headers:
{
  Transfer-Encoding: chunked
  request-id: 02034f96-f519-4f5f-b47d-efb98dff0072
  client-request-id: 02034f96-f519-4f5f-b47d-efb98dff0072
  x-ms-ags-diagnostic: {"ServerInfo":{"DataCenter":"South India","Slice":"SliceC","Ring":"5","ScaleUnit":"002","Host":"AGSFE_IN_8","ADSiteName":"INS"}}
  OData-Version: 4.0
  Duration: 76.0712
  Strict-Transport-Security: max-age=31536000
  Cache-Control: private
  Date: Fri, 02 Nov 2018 12:36:51 GMT
  Content-Type: application/json; odata.metadata=minimal; odata.streaming=true; IEEE754Compatible=false; charset=utf-8
}}


Request body to fetch emails (not working):

{Method: GET, RequestUri: 'https://graph.microsoft.com/v1.0/me/mailFolders('Inbox')/messages', Version: 1.1, Content: <null>, Headers:
{
  Accept: application/json
  Authorization: Bearer eyJ0eXAiOiJKV...
}}    

Response when fetching list of emails (not working):

{StatusCode: 400, ReasonPhrase: 'Bad Request', Version: 1.1, Content: System.Net.Http.StreamContent, Headers:
{
  Transfer-Encoding: chunked
  request-id: 5b728866-b132-404f-9986-70fc56e57c3c
  client-request-id: 5b728866-b132-404f-9986-70fc56e57c3c
  x-ms-ags-diagnostic: {"ServerInfo":{"DataCenter":"South India","Slice":"SliceC","Ring":"5","ScaleUnit":"002","Host":"AGSFE_IN_6","ADSiteName":"INS"}}
  Duration: 4205.8126
  Strict-Transport-Security: max-age=31536000
  Cache-Control: private
  Date: Fri, 02 Nov 2018 12:43:03 GMT
  Content-Type: application/json
}}

Solution

  • You're using Client_Credentials to authenticate the app and using the /me path in your REST call. These two do not work together.

    Behind the scenes /me is translated into the currently authenticated user (i.e. /users/user@domain. Since you don't have a user authenticated, it simply isn't possible for the Graph to translate your request into an actionable call.

    You need to explicitly reference the user using either their id or their userPrincipalName:

     /v1.0/users/123e4567-e89b-12d3-a456-426655440000/mailFolders('Inbox')/messages
     /v1.0/users/[email protected]/mailFolders('Inbox')/messages