Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Bug] In .Net framework, when recreating the CCA each time one wants to acquire a token, the cache is not hit #1390

Closed
jmprieur opened this issue Aug 17, 2021 · 1 comment · Fixed by #1391
Labels
bug Something isn't working P1

Comments

@jmprieur
Copy link
Collaborator

jmprieur commented Aug 17, 2021

Which version of Microsoft Identity Web are you using?
1.15.1

Where is the issue?

  • Token cache serialization in .Net framework
    • [x ] In-memory caches
    • [x ] Distributed caches

The issue only affects the TokenCache confidential client extensions used to add token cache support in ASP.net MVC, or .NET Core, but without fully using Id.Web. It only happens when you re create the confidential client application each time you want to get a token

Repro

With the in memory and distributed in memory cache, the cache is not hit, because a new InMemory cache is instantiated each time.

The following code

// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Identity.Client;
using Microsoft.Identity.Web;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace ConfidentialClientTokenCache
{
    /// <summary>
    /// The goal of this little sample is to show how you can use MSAL token cache adapters
    /// in confidential client applications both in .NET Core, or .NET Framework 4.7.2
    /// Note that if you write a .NET Core web app or web API, Microsoft recommends that you use the
    /// IDownstreamApi or ITokenAcquisition interfaces directly (instead of MSAL)
    /// </summary>
    static class Program
    {
        static async Task<AuthenticationResult> CreateAppAndGetToken(CertificateDescription certDescription, int cacheType)
        {
            string clientId = "6af093f3-b445-4b7a-beae-046864468ad6";
            string tenant = "msidentitysamplestesting.onmicrosoft.com";
            string[] scopes = new[] { "api://8206b76f-586e-4098-b1e5-598c1aa3e2a1/.default" };

            // Create the confidential client application
            IConfidentialClientApplication app;
            app = ConfidentialClientApplicationBuilder.Create(clientId)
                // Alternatively to the certificate you can use .WithClientSecret(clientSecret)
                .WithCertificate(certDescription.Certificate)
                .WithTenantId(tenant)
                .Build();

            if (cacheType == 0)
            {
                // In memory token caches (App and User caches)
                app.AddInMemoryTokenCache();
            }

            // Or

            // Distributed token caches (App and User caches)
            // Add one of the below: SQL, Redis, CosmosDb
            else
            {
                app.AddDistributedTokenCache(services =>
                {
                    services.AddDistributedMemoryCache();
                    services.AddLogging(configure => configure.AddConsole())
                    .Configure<LoggerFilterOptions>(options => options.MinLevel = Microsoft.Extensions.Logging.LogLevel.Warning);

                /* Remove comments to use SQL cache implementation
                services.AddDistributedSqlServerCache(options =>
                {
                    // SQL Server token cache
                    // Requires to reference Microsoft.Extensions.Caching.SqlServer
                    options.ConnectionString = @"Data Source=(localdb)\MSSQLLocalDB;Initial Catalog=TestCache;Integrated Security=True;Connect Timeout=30;Encrypt=False;TrustServerCertificate=False;ApplicationIntent=ReadWrite;MultiSubnetFailover=False";
                    options.SchemaName = "dbo";
                    options.TableName = "TestCache";

                    // You don't want the SQL token cache to be purged before the access token has expired. Usually
                    // access tokens expire after 1 hour (but this can be changed by token lifetime policies), whereas
                    // the default sliding expiration for the distributed SQL database is 20 mins. 
                    // Use a value which is above 60 mins (or the lifetime of a token in case of longer lived tokens)
                    options.DefaultSlidingExpiration = TimeSpan.FromMinutes(90);
                });
                */

                /* Remove comments to use Redis cache implementation
                // Add Redis
                services.AddStackExchangeRedisCache(options =>
                {
                    options.Configuration = "localhost";
                    options.InstanceName = "Redis";
                });
                */

                /* Remove comments to use CosmosDB cache implementation
                // Add CosmosDB
                services.AddCosmosCache((CosmosCacheOptions cacheOptions) =>
                {
                    cacheOptions.ContainerName = Configuration["CosmosCacheContainer"];
                    cacheOptions.DatabaseName = Configuration["CosmosCacheDatabase"];
                    cacheOptions.ClientBuilder = new CosmosClientBuilder(Configuration["CosmosConnectionString"]);
                    cacheOptions.CreateIfNotExists = true;
                });
                */
                });
            }

            // Acquire a token (twice)
            var result = await app.AcquireTokenForClient(scopes)
                .ExecuteAsync();
            return result;
        }


        static async Task Main(string[] args)
        {

            // Simulates the configuration, could be a IConfiguration or anything
            Dictionary<string, string> Configuration = new Dictionary<string, string>();

            // Certificate Loading
            string keyVaultContainer = "https://WebAppsApisTests.vault.azure.net";
            string keyVaultReference = "Self-Signed-5-5-22";
            CertificateDescription certDescription = CertificateDescription.FromKeyVault(keyVaultContainer, keyVaultReference);
            ICertificateLoader certificateLoader = new DefaultCertificateLoader();
            certificateLoader.LoadIfNeeded(certDescription);

            var result = await CreateAppAndGetToken(certDescription, 0);
            Console.WriteLine(result.AuthenticationResultMetadata.TokenSource);
            result = await CreateAppAndGetToken(certDescription, 0);
            Console.WriteLine(result.AuthenticationResultMetadata.TokenSource);
            result = await CreateAppAndGetToken(certDescription, 1);
            Console.WriteLine(result.AuthenticationResultMetadata.TokenSource);
            result = await CreateAppAndGetToken(certDescription, 1);
            Console.WriteLine(result.AuthenticationResultMetadata.TokenSource);
        }
    }
}

Expected behavior
The console app should display:

IdentityProvider
Cache
IdentityProvider
Cache

Actual behavior
The console app actually displays:

IdentityProvider
IdentityProvider
IdentityProvider
IdentityProvider

Additional context / logs / screenshots
We instantiate a ServiceProvider each time

@jmprieur jmprieur added bug Something isn't working P1 labels Aug 17, 2021
@jmprieur jmprieur changed the title [Bug] [Bug] In .Net framework, when recreating the CCA each time one wants to acquire a token, the cache is not hit Aug 17, 2021
jmprieur added a commit that referenced this issue Aug 17, 2021
@jmprieur jmprieur mentioned this issue Aug 17, 2021
jennyf19 added a commit that referenced this issue Aug 18, 2021
* Fixes #1390

* Addressing Bogdan's PR feedback

* address PR comment (#1393)

Co-authored-by: jennyf19 <jeferrie@microsoft.com>
@jennyf19
Copy link
Collaborator

Included in 1.16 release

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working P1
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants