0
votes

I need help with how to retrieve a stored MSAL token so I can reuse it across executions of my app.

Scenario:

I am following this Microsoft demo which shows how to use MSAL to obtain an Oauth2 token for authentication to EWS in a console app. It works fine, and pops up an interactive login window to obtain the token each time I run the app.

I now want to make use of the same token (or its refresh token, if the original one has expired) in a later, independent execution of the app.

Ultimately, I want to implement an app where users provide the initial interactive Oauth login via my web UI, and from that point on I store the token for a background app which interacts with their EWS mailbox, and performs the refresh when needed.

What I'm trying:

I have been trying to make sense of this "naive implementation" of local file-based token caching. I can see that it's creating a local file, but how can I make it check this file in a later execution, and use the token that it's stored there. I've then found my way to this MSAL extension library, but it has no documentation and I can't even seem to adapt from its tests.

My code:

static async System.Threading.Tasks.Task MainAsync(string[] args)
    {
        // Configure the MSAL client to get tokens
        var pcaOptions = new PublicClientApplicationOptions
        {
            ClientId = ConfigurationManager.AppSettings["appId"],
            TenantId = ConfigurationManager.AppSettings["tenantId"]
        };

        var pca = PublicClientApplicationBuilder
            .CreateWithApplicationOptions(pcaOptions).Build();
        TokenCacheHelper.EnableSerialization(pca.UserTokenCache); // added based on 'naive implementation'

        var ewsScopes = new string[] { "https://outlook.office.com/EWS.AccessAsUser.All" };

        try
        {
            // Make the interactive token request
            var authResult = await pca.AcquireTokenInteractive(ewsScopes).ExecuteAsync(); // must have to change this to something else that is aware of the cache?

            // Configure the ExchangeService with the access token
            var ewsClient = new ExchangeService();
            ewsClient.Url = new Uri("https://outlook.office365.com/EWS/Exchange.asmx");
            ewsClient.Credentials = new OAuthCredentials(authResult.AccessToken);

            // Make an EWS call
            // ... do stuff in EWS ...
        }
    }

Helper code (from 'naive implementation')

static class TokenCacheHelper
{
    public static void EnableSerialization(ITokenCache tokenCache)
    {
        tokenCache.SetBeforeAccess(BeforeAccessNotification);
        tokenCache.SetAfterAccess(AfterAccessNotification);
    }

    /// <summary>
    /// Path to the token cache
    /// </summary>
    public static readonly string CacheFilePath = System.Reflection.Assembly.GetExecutingAssembly().Location + ".msalcache.bin3";

    private static readonly object FileLock = new object();


    private static void BeforeAccessNotification(TokenCacheNotificationArgs args)
    {
        lock (FileLock)
        {
            args.TokenCache.DeserializeMsalV3(File.Exists(CacheFilePath)
                    ? ProtectedData.Unprotect(File.ReadAllBytes(CacheFilePath),
                                              null,
                                              DataProtectionScope.CurrentUser)
                    : null);
        }
    }

    private static void AfterAccessNotification(TokenCacheNotificationArgs args)
    {
        // if the access operation resulted in a cache update
        if (args.HasStateChanged)
        {
            lock (FileLock)
            {
                // reflect changesgs in the persistent store
                File.WriteAllBytes(CacheFilePath,
                                    ProtectedData.Protect(args.TokenCache.SerializeMsalV3(),
                                                            null,
                                                            DataProtectionScope.CurrentUser)
                                    );
            }
        }
    }
}
1

1 Answers

1
votes

I think the pattern you want should be to:

  • Invoke the system browser for logins
  • Return to your non browser app after login
  • Store resulting tokens in operating system secure storage

This is OAuth for desktop apps - even if the desktop process does not have a UI (as for a console app).

Do EWS apps run as a plug-in to another app? If so you will need to use the loopback variation of desktop logins.

I have two online desktop samples that may help you to understand issues - you should be able to run them both quite easily: code-samples-quickstart.

I also have some detailed write ups starting here: desktop-apps-overview.

My code is in Node.js.