4

I'm using Microsoft Graph C#.NET SDK to access user's mail inbox. The problem is that when I do authentication the token that Microsoft sends me back is valid just for 1 hour or so and it expires so early. But it's so annoying for user to login every 1 hours just to see the outlook mail inbox. I need to make this login PERSISTENT.

Here is the code that I use:

 public static async Task Run()
            {
                string secret = "MyDamnPrivateSecret";
                PublicClientApplication clientApp = new PublicClientApplication(secret);
                GraphServiceClient graphClient = new GraphServiceClient("https://graph.microsoft.com/v1.0", new DelegateAuthenticationProvider(async (requestMessage) =>
                {
                    requestMessage.Headers.Authorization = new AuthenticationHeaderValue("bearer", await GetTokenAsync(clientApp));
                }));
//Processing mailbox ...
            }

    private static async Task<string> GetTokenAsync(PublicClientApplication clientApp)
        {
            if (string.IsNullOrEmpty(Properties.Settings.Default.token) || string.IsNullOrWhiteSpace(Properties.Settings.Default.token))
            {
                //need to pass scope of activity to get token  
                string[] Scopes = { "User.Read", "Mail.ReadWrite" };
            string token = null;
            AuthenticationResult authResult = await clientApp.AcquireTokenAsync(Scopes);
            token = authResult.AccessToken;
            Properties.Settings.Default.token = token;
            Properties.Settings.Default.Save();
            return token;
            }
            else
            {
                return Properties.Settings.Default.token;
            }

        }

Is there any way to make expiration time last longer? Or make a refresh token or something to persist login?

Michael Mainer
  • 3,387
  • 1
  • 13
  • 32
Mohsen
  • 427
  • 4
  • 20
  • Not sure if it's related, but you may want to see my answer for a similar question: https://stackoverflow.com/a/76001358/1653521 – Hieu Apr 13 '23 at 03:50

3 Answers3

3

You'll need to request the offline_access scope to get a refresh token. If you're using an older version of MSAL, you'll need to implement and pass a token cache in the PublicClientApplication constructor which I think that MSAL will use to automatically refresh the access token. I think the newer version handles the tokenCache for you.

From the docs, this is the recommended call pattern: first try to call AcquireTokenSilentAsync, and if it fails with a MsalUiRequiredException, call AcquireTokenAsync.

private static async Task<string> GetTokenAsync(PublicClientApplication clientApp)
    {
        AuthenticationResult result = null;

        try
        {
            string[] scopes = { "User.Read", "Mail.ReadWrite", "offline_access" };
            // Get the token from the cache.
            result = await app.AcquireTokenSilentAsync(scopes, clientApp.Users.FirstOrDefault());
            return result.AccessToken;
        }
        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
            {
                // Dialog opens for user.
                result = await app.AcquireTokenAsync(scopes);
                return result.AccessToken;
            }
            catch (MsalException msalex)
            {
                ResultText.Text = $"Error Acquiring Token:{System.Environment.NewLine}{msalex}";
            }
        }
        catch (Exception ex)
        {
            ResultText.Text = $"Error Acquiring Token Silently:{System.Environment.NewLine}{ex}";
            return;
        }
    }

Here's a sample for reference. https://github.com/Azure-Samples/active-directory-dotnet-desktop-msgraph-v2

Michael Mainer
  • 3,387
  • 1
  • 13
  • 32
  • I'm using MSAL V2 – Mohsen Sep 19 '18 at 11:02
  • I tried this code `(await app.GetAccountsAsync()).FirstOrDefault()` instead of `clientApp.Users.FirstOrDefault()` which was depricated. Also, I removed "offline_access" because MSAL seems to pas it by default as well as "open_id" But there's still a problem and this exception throws when I open app again or call my `GetTokenAsync()` again: Exception thrown: 'Microsoft.Identity.Client.MsalUiRequiredException' in mscorlib.dll MsalUiRequiredException: Null account was passed in AcquiretokenSilent API. Pass in an account object or call acquireToken to authenticate. – Mohsen Sep 19 '18 at 11:08
  • Thank you for providing the clarification on what is deprecated and what is included in the new MSAL 2 library. I'm not sure, but it seems like we need to figure out how to persist the tokenCache after the app is closed. I don't know the answer so either we need someone from MSAL to take a look here or we need to take a look at the source code to see how that is done. – Michael Mainer Sep 19 '18 at 15:23
  • Yes that's true. I did pass a `new TokenCache()` to the client's constructor, it remembers the login through application lifetime. However, it's not persistent; once you close the app everything is gone. There must be a way to save the cache into file system. I found a `.Serialize()` and a `.DeSerialize()` method for that cache object, but I'm not sure how it works. – Mohsen Sep 19 '18 at 15:37
  • Also the UWP and other samples of Microsoft Graph is out dated and they don't work properly. I might do a PR to fix all issues of the Microsoft samples later this week. Hope they merge :| Usually they don't – Mohsen Sep 19 '18 at 15:47
  • Taking a look at the [TokenCacheExtensions](https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/blob/16dbde8f09634e3c3f00a2f7889cbf8e4e6ac1aa/src/Microsoft.Identity.Client/Features/TokenCache/TokenCacheExtensions.cs#L76), it looks like you'd use byte array returned from serialize and write that to persistent storage, and use the same byte array on a new tokenCache with deserialize to get the tokens loaded from persistent storage. We really need a topic on this. – Michael Mainer Sep 19 '18 at 22:48
  • I saved that thing on the storage once and I read it another time when I opened app again, the deserialized token cache wasn't valid. What can we do bruh? – Mohsen Sep 20 '18 at 20:13
  • @Mohsen Down vote my answer to zero since it doesn't answer your question, and since we don't have any MSAL experts looking at this yet, open an issue in the MSAL repo, give some background in the issue, and link to this StackOverflow post. Hopefully that gets you the answer. – Michael Mainer Sep 20 '18 at 23:11
1

In your code, AcquireTokenAsync does always trigger login.

Instead, you need to implement a token cache and use AcquireTokenSilentAsync.

For more information, please review the following link:

Microsoft Graph SDK - Login

Yuki
  • 212
  • 1
  • 4
1

I will try to clarify the issues here:

  1. MSAL .net is build for different platforms - .net desktop, .net core, UWP, xamarin android and xamarin iOS. On some of these platforms (UWP and xamarin) we persist the token cache for you. On the others, we expect you to persist the cache. The reason is that we cannot provide token serialization logic that works well for all scenarios (e.g. ASP.NET server farms), so we expect you to do it. We provide samples and guidance around it this. Details and some reference implementations on the MSAL wiki:

  2. The sample code provided by @Michael is ok for MSAL v1. In MSAL v2 the things are a bit different and you can find the pattern of calling is also on the MSAL wiki:

  3. We request and store the refresh token (RT). If the auth token (AT) is expired, we will request a new one based on the RT - this will happen without user interaction. This should all be transparent to you, i.e. it should just work :). Make sure that your token cache serialization works, i.e. you get an account when performing

// perform an interactive login first 
// otherwise there will be no AT / RT in the store
var accounts = await app.GetAccountsAsync();
// there should be an account that you can use
  1. Most of our samples show how to call the Graph. See all the samples by scenario here. For your use case I recommend you check out Calling the Graph from a WPF app

Also check out @Daniel Dobalian's answer for default expiration of AT and RT: MSAL token expires after 1 hour

Bogdan Gavril MSFT
  • 20,615
  • 10
  • 53
  • 74
  • Thanks in advance for your help. I edited your post and added a working code to make this answer more useful. – Mohsen Sep 27 '18 at 23:47