0

This is my code to authenticate for using Microsoft Graph with Outlook:

public async Task AquireToken()
{
    try
    {
        if (_AuthResult == null)
        {
            _AuthResult = await Program.PublicClientApp.AcquireTokenSilentAsync(
                _scopes, Program.PublicClientApp.Users.FirstOrDefault());
        }
    }
    catch (MsalUiRequiredException ex)
    {
        // A MsalUiRequiredException happened on AcquireTokenSilentAsync. 
        // This indicates you need to call AcquireTokenAsync to acquire a token.
        System.Diagnostics.Debug.WriteLine($"MsalUiRequiredException: {ex.Message}");

        try
        {
            _AuthResult = await Program.PublicClientApp.AcquireTokenAsync(_scopes);
        }
        catch (MsalException msalex)
        {
            _ResultsText = $"Error Acquiring Token:{System.Environment.NewLine}{msalex}";
        }
    }
    catch (Exception ex)
    {
        _ResultsText = $"Error Acquiring Token Silently:{System.Environment.NewLine}{ex}";
    }

    if (_AuthResult != null)
    {
        _ResultsText = await GetHttpContentWithToken(_graphAPIEndpoint, _AuthResult.AccessToken);
    }
}

It is based on the samples provided by Microsoft. In the console output it says:

Token Expires: 04/09/2017 14:18:06 +01:00

That code is displayed from:

$"Token Expires: {_AuthResult.ExpiresOn.ToLocalTime()}" + Environment.NewLine;

Thus, this implies that the token is valid for one hour. So if I run my utility again I am expecting it to use the same token until it needs to ask for a new one. But it doesn't. It always shows the prompt.

What step have I missed?

The Exception

As per the request in the comments, this is the details from the exception:

MsalUiRequiredException: Null user was passed in AcquiretokenSilent API. Pass in a user object or call acquireToken authenticate.

This might help

Microsoft Graph SDK - Login

I need to review the answer provided:

you need to implement a token cache and use AcquireTokenSilentAsync. https://learn.microsoft.com/en-us/outlook/rest/dotnet-tutorial has a web app example.

Dan Kershaw - MSFT
  • 5,833
  • 1
  • 14
  • 23
Andrew Truckle
  • 17,769
  • 16
  • 66
  • 164
  • 1
    What is the output of ` System.Diagnostics.Debug.WriteLine($"MsalUiRequiredException: {ex.Message}");` ? The exception triggers the call, so you need to figure out why it runs into trouble getting the token silently. – Ewald Sep 04 '17 at 12:27
  • @Ewald Seems to be something to do with the user? – Andrew Truckle Sep 04 '17 at 12:37
  • 1
    A null user will definitely make the call fail, which, by the logic of the app, will trigger the unexpected call. So your root cause is that you are passing in a null user. If you didn't, I'd expect the call to work as you think it should. You need to investigate why `Program.PublicClientApp.Users.FirstOrDefault()` is giving you a null user. – Ewald Sep 04 '17 at 12:42
  • @Ewald The tutorial linked to in a related answer speaks about `TokenCache` and this seems to be the aspect I am missing. But the tutorial is for a **ASP.NET** application and not a **Console** application. I am struggling to understand how to migrate that tutorial to my context. In addition, the tutorial does a token cache in the app session as opposed to saving it to the hard drive between instances (from what I can tell). – Andrew Truckle Sep 04 '17 at 13:13
  • The logic should be sort of the same, the console app would need to cache user data, I guess. Console apps are a dark art though - I'm not going to be much help other than pointing you in all kinds of wrong directions! – Ewald Sep 04 '17 at 13:38

2 Answers2

1

MSAL .NET on the desktop does not offer a persistent cache, because there's no obvious storage it can rely on out of the box (while MSAL does offer persistent storage on UWP, Xamarin iOS and Android, where app isolated storage is available). Out of the box, MSAL .NET on the desktop uses an inmemory cache that will vanish as soon as the process ends. Please refer to https://azure.microsoft.com/en-us/resources/samples/active-directory-dotnet-desktop-msgraph-v2/ for an example demonstrating how to provide a simple file based cache that will persist tokens across executions.

vibronet
  • 7,364
  • 2
  • 19
  • 21
1

I made use of the registry. Save the token when you get a successful log in then call the token back each time you need to make use of the GraphServiceClient. If the token has expired or an error appears you can recall the log in process and save the new token.

 public static async Task<GraphServiceClient> GetAuthenticatedClientAsync()
    {
        GraphServiceClient graphClient = new GraphServiceClient(
            new DelegateAuthenticationProvider(
                async (requestMessage) =>
                {
                    string appID = ConfigurationManager.AppSettings["ida:AppId"];

                    PublicClientApplication PublicClientApp = new PublicClientApplication(appID);
                    string[] _scopes = new string[] { "Calendars.read", "Calendars.readwrite", "Mail.read", "User.read" };

                    AuthenticationResult authResult = null;

                    string keyName = @"Software\xxx\Security";
                    string valueName = "Status";
                    string token = "";

                    RegistryKey regKey = Registry.CurrentUser.OpenSubKey(keyName, false);
                    if (regKey != null)
                    {
                        token = (string)regKey.GetValue(valueName);
                    }

                    if (regKey == null || string.IsNullOrEmpty(token))
                    {
                        authResult = await PublicClientApp.AcquireTokenAsync(_scopes); //Opens Microsoft Login Screen
                        //code if key Not Exist
                        RegistryKey key;
                        key = Registry.CurrentUser.CreateSubKey(@"Software\xxx\Security");
                        key.OpenSubKey(@"Software\xxx\Security", true);
                        key.SetValue("Status", authResult.AccessToken);
                        key.SetValue("Expire", authResult.ExpiresOn.ToString());
                        key.Close();
                        // Append the access token to the request.
                        requestMessage.Headers.Authorization = new AuthenticationHeaderValue("bearer", token);
                    }
                    else
                    {
                        //code if key Exists
                        RegistryKey reg = Registry.CurrentUser.OpenSubKey(@"Software\xxx\Login", true);
                        // set value of "abc" to "efd"
                        token = (string)regKey.GetValue(valueName);
                        // Append the access token to the request.
                        requestMessage.Headers.Authorization = new AuthenticationHeaderValue("bearer", token);
                    }
                }));
        try
        {      
            Microsoft.Graph.User me = await graphClient.Me.Request().GetAsync();

        }
        catch(Exception e)
        {
            if (e.ToString().Contains("Access token validation failure") || e.ToString().Contains("Access token has expired"))
            {
                string keyName = @"Software\xxx\Security";
                using (RegistryKey key = Registry.CurrentUser.OpenSubKey(keyName, true))
                {
                    if (key != null)
                    {
                        key.DeleteValue("Status");
                        key.DeleteValue("Expire");
                    }
                    else
                    {
                        MessageBox.Show("Error! Something went wrong. Please contact your administrator.", "Error!", MessageBoxButtons.OK, MessageBoxIcon.Error);
                    }
                }
                await GetAuthenticatedClientAsync();
            }
        }
       
        return graphClient;
    }
Andrew Truckle
  • 17,769
  • 16
  • 66
  • 164