Search code examples
c#azuremicrosoft-graph-apiazure-ad-msaloutlook-restapi

Microsoft Graph Read Mail from @outlook.com account using app-only authentication


I am using this tutorial as a base to be able to read mail from my @outlook.com account using Microsoft Graph: https://learn.microsoft.com/en-us/graph/tutorials/dotnet-app-only?tabs=aad

I've done the following:

  1. Create an App Registration in my Azure account. Set supported types to "Accounts in any organizational directory (Any Azure AD directory - Multitenant) and personal Microsoft accounts (e.g. Skype, Xbox)"
  2. Added the following API permissions:enter image description here
  3. Create a secret to be used with ClientSecretCredential
  4. I invited my @outlook.com account to be added as a user in my Azure Active Directory. Not sure if this is needed or not.

Here is the code that initializes the GraphServiceClient:

if (_clientSecretCredential == null)
{
    _clientSecretCredential = new ClientSecretCredential(
        _settings.TenantId, _settings.ClientId, _settings.ClientSecret, new TokenCredentialOptions { AuthorityHost = AzureAuthorityHosts.AzurePublicCloud });
}

if (_appClient == null)
{
    _appClient = new GraphServiceClient(_clientSecretCredential, 
        // Use the default scope, which will request the scopes
        // configured on the app registration
        new[] {"https://graph.microsoft.com/.default"});
}

When I execute the messages endpoint, I am getting two different errors, based on what userId I pass in.

userId = "someone_outlook.com#EXT#@MyAzureAdminEmail.onmicrosoft.com"; // Gives error #1 below
userId = "[email protected]"; // Gives error #2 below
userId = "<object_id_guid>"; // Gives error #2 below
var messages = await _appClient.Users[userId].MailFolders["inbox"].Messages
                               .Request()
                               .GetAsync();

Error #1

Error getting users: Status Code: NotFound Microsoft.Graph.ServiceException: Code: Request_ResourceNotFound Message: Resource 'someone_outlook.com' does not exist or one of its queried reference-property objects are not present.

Error #2

Error getting users: Status Code: Unauthorized Microsoft.Graph.ServiceException: Code: OrganizationFromTenantGuidNotFound Message: The tenant for tenant guid '[guid]' does not exist.

I cannot find any examples where someone is reading mail from a Microsoft account (@outlook.com, @hotmail.com, @live.come) using Microsoft Graph and app-only authentication. Is this possible with Microsoft Graph?

Thanks, Garry


Solution

  • Based on what @TinyWang stated in the comments above, it does not look like this will be possible.

    Instead, I will have to use delegate permissions. The following is a POC application that can use Microsoft Graph API with a personal @Outlook.com account.

    Create an App Registration

    1. Add a redirect URI that will receive the authentication code from Microsoft.
    2. Set the Supported account types to the "... Microsoft accounts" type enter image description here

    Add the following API permissions: enter image description here

    I created the following settings class to get the settings from the .config file:

    public class GraphSettings
    {
        public string? ClientId { get; set; }
        public string? TenantId { get; set; }
        public string? ClientSecret { get; set; }
        public string? RedirectUri { get; set; }
        public string[]? Scopes { get; set; }
    }
    

    appsettings.json file:

    {
      "graphSettings": {
        "tenantId": "common",
        "clientId": "[set-client-id]",
        "clientSecret": "[set-client-secret]",
        "RedirectUri": "https://localhost:44308/MailAuthorize",
        "scopes": [
          "offline_access",
          "https://graph.microsoft.com/.default"
        ]
      }
    }
    

    Note: for the scopes, offline_access will ensure that the token endpoint will be returned with a refresh token.

    Note: for the tenant ID, it needs to be "common" and not your actual tenant ID.

    And a Graph helper class:

    using Azure.Identity;
    using Microsoft.Graph;
    using System.Web;
    
    namespace POC.Graph
    {
        public class GraphHelper
        {
            private static GraphServiceClient? _graphClient;
    
            public static bool IsInitialized()
            {
                return (_graphClient != null);
            }
    
            public static void Initialize(GraphSettings settings, string authorizationCode)
            {
                var options = new AuthorizationCodeCredentialOptions
                {
                    AuthorityHost = AzureAuthorityHosts.AzurePublicCloud,
                    RedirectUri = new Uri(settings.RedirectUri)
                };
    
                var authCodeCredential = new AuthorizationCodeCredential(settings.TenantId,
                                                                         settings.ClientId,
                                                                         settings.ClientSecret,
                                                                         authorizationCode,
                                                                         options);
    
                _graphClient = new GraphServiceClient(authCodeCredential, settings.Scopes);
            }
    
            public static string GetAuthorizeUri(GraphSettings settings)
            {
                var clientId = HttpUtility.UrlEncode(settings.ClientId);
                var scopes = HttpUtility.UrlEncode(string.Join(" ", settings.Scopes));
                var redirectUri = HttpUtility.UrlEncode(settings.RedirectUri);
                return $"https://login.microsoftonline.com/common/oauth2/v2.0/authorize?response_type=code&client_id={clientId}&state=12345&scope={scopes}&redirect_uri={redirectUri}";
            }
    
            public static Task<IMailFolderMessagesCollectionPage> GetMessages()
            {
                return _graphClient.Me.MailFolders["Inbox"].Messages.Request().GetAsync();
            }
        }
    }
    

    And here is a test Controller:

    public class MailAuthorizeController : Controller
    {
        #region Actions
    
        public IActionResult Index([FromUri] string? code = null)
        {
            var model = new MailAuthorizeModel();
    
            try
            {
                var settings = LoadGraphSettings();
    
                model.AuthorizeUri = GraphHelper.GetAuthorizeUri(settings);
    
                if (code != null)
                {
                    GraphHelper.Initialize(settings, code);
                }
    
                if (GraphHelper.IsInitialized())
                {
                    var messages = GraphHelper.GetMessages().Result;
    
                    if (messages == null)
                    {
                        model.Message = "messages is null";
                    }
                    else
                    {
                        model.Message = "messages count: " + messages.Count().ToString();
                    }
                }
            }
            catch (Exception ex)
            {
                model.Message = ex.ToString();
            }
    
            return View(model);
        }
    
        #endregion
    
        #region Private Methods
    
        private GraphSettings LoadGraphSettings()
        {
            IConfiguration config = new ConfigurationBuilder()
                .AddJsonFile("appsettings.json", optional: false)
                .AddJsonFile($"appsettings.Development.json", optional: true)
                .Build();
    
            return config.GetRequiredSection("GraphSettings").Get<GraphSettings>() ??
                throw new Exception("Could not load app settings.");
        }
    
        #endregion
    }
    

    And the controller's view:

    @{
        ViewData["Title"] = "Mail Authorize";
        var model = (POC.MailAuthorizeModel)Model;
    }
    
    <a href="@(model.AuthorizeUri)">Click to Authorize</a>
    
    <span>Message: @(model.Message)</span>
    

    Now to explain what the above does:

    1. When the user hits the controller, the view is rendered with the authorization hyper link.
    2. The user clicks the link to authenticated and grant consent to my App Registration. This will return an authentication code back to the controller. This is configured by the redirect URI in the config file.
    3. The GraphHelper will then be initialized with the config settings and it will use the authentication code returned from the authorize endpoint (it is passed as a query parameter). Behind the scenes the GraphServiceClient will call the token endpoint to get a token that will be used when calling the other Graph endpoints.
    4. A call to the messages endpoint is made to get the top 10 messages from the user's inbox. This is to test that the GraphServiceClient successfully calls the token endpoint and uses the token to call the messages endpoint.

    Thanks, Garry