Skip to content

Commit

Permalink
Adding a new DistributedTokenCacheProvider (#173)
Browse files Browse the repository at this point in the history
* Adding a new DistributedTokenCacheProvider
This enables to decouple the serialization itself (done by a.NET Core IDistrributedCache implementation), from the token cache logic (done by the DistributedTokenCacheProvider)

See https://docs.microsoft.com/en-us/aspnet/core/performance/caching/distributed?view=aspnetcore-2.2#distributed-memory-cache.

```CSharp
               // or use a distributed Token Cache by adding
                           .AddDistributedTokenCaches();

               // and then choose your implementation.
               // See https://docs.microsoft.com/en-us/aspnet/core/performance/caching/distributed?view=aspnetcore-2.2#distributed-memory-cache

               // For instance the distributed in memory cache (not cleaned when you stop the app)
                services.AddDistributedMemoryCache()

               // Or a Redis cache
               services.AddStackExchangeRedisCache(options =>
                    {
                        options.Configuration = "localhost";
                        options.InstanceName = "SampleInstance";
                    });

               // Or even a SQL Server token cache
               services.AddDistributedSqlServerCache(options =>
                {
                    options.ConnectionString =
                        _config["DistCache_ConnectionString"];
                    options.SchemaName = "dbo";
                    options.TableName = "TestCache";
                });
```

* processing PR feedback

* Add more comments

* Improving the identation

* Updating the README.md with new pictures, and details
about the Distributed token caches

* updating the diagrams

* Renaming DistributedTokenCacheProvider to DistributedTokenCacheAdapter as this is an adapter in this particular case
cc: @bgavrilMS
  • Loading branch information
jmprieur committed Sep 4, 2019
1 parent 12e5980 commit e6c7c61
Show file tree
Hide file tree
Showing 8 changed files with 289 additions and 11 deletions.
31 changes: 31 additions & 0 deletions 2-WebApp-graph-user/2-1-Call-MSGraph/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,37 @@ The two new lines of code:
> - replace `using Microsoft.Identity.Web.TokenCacheProviders.InMemory` by `using Microsoft.Identity.Web.TokenCacheProviders.Session`
> - Replace `.AddInMemoryTokenCaches()` by `.AddSessionTokenCaches()`
> add `app.UseSession();` in the `Configure(IApplicationBuilder app, IHostingEnvironment env)` method, for instance after `app.UseCookiePolicy();`
>
>
> You can also use a distributed token cache, and choose the serialization implementation. For this, in **Startup.cs**:
> - replace `using Microsoft.Identity.Web.TokenCacheProviders.InMemory` by `using Microsoft.Identity.Web.TokenCacheProviders.Distributed`
> - Replace `.AddInMemoryTokenCaches()` by `.AddDistributedTokenCaches()`
> - Then choose the distributed cache implementation. For details, see https://docs.microsoft.com/en-us/aspnet/core/performance/caching/distributed?view=aspnetcore-2.2#distributed-memory-cache
>
> ```CSharp
> // use a distributed Token Cache by adding
> .AddDistributedTokenCaches();
>
> // and then choose your implementation.
>
> // For instance the distributed in memory cache (not cleaned when you stop the app)
> services.AddDistributedMemoryCache()
>
> // Or a Redis cache
> services.AddStackExchangeRedisCache(options =>
> {
> options.Configuration = "localhost";
> options.InstanceName = "SampleInstance";
> });
>
> // Or even a SQL Server token cache
> services.AddDistributedSqlServerCache(options =>
> {
> options.ConnectionString =_config["DistCache_ConnectionString"];
> options.SchemaName = "dbo";
> options.TableName = "TestCache";
> });
> ```
### Add additional files to call Microsoft Graph
Expand Down
27 changes: 27 additions & 0 deletions 2-WebApp-graph-user/2-1-Call-MSGraph/Startup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Identity.Web;
using Microsoft.Identity.Web.TokenCacheProviders.Distributed;
using Microsoft.Identity.Web.TokenCacheProviders.InMemory;
using WebApp_OpenIDConnect_DotNet.Infrastructure;
using WebApp_OpenIDConnect_DotNet.Services;
Expand Down Expand Up @@ -40,6 +41,32 @@ public void ConfigureServices(IServiceCollection services)
.AddMsal(Configuration, new string[] { Constants.ScopeUserRead })
.AddInMemoryTokenCaches();

/*
// or use a distributed Token Cache by adding
.AddDistributedTokenCaches();
// and then choose your implementation.
// See https://docs.microsoft.com/en-us/aspnet/core/performance/caching/distributed?view=aspnetcore-2.2#distributed-memory-cache
// For instance the distributed in memory cache (not cleared when you stop the app)
services.AddDistributedMemoryCache()
// Or a Redis cache
services.AddStackExchangeRedisCache(options =>
{
options.Configuration = "localhost";
options.InstanceName = "SampleInstance";
});
// Or even a SQL Server token cache
services.AddDistributedSqlServerCache(options =>
{
options.ConnectionString =
_config["DistCache_ConnectionString"];
options.SchemaName = "dbo";
options.TableName = "TestCache";
});
*/
// Add Graph
services.AddGraphService(Configuration);

Expand Down
14 changes: 7 additions & 7 deletions Microsoft.Identity.Web/Diagrams.cd
Original file line number Diff line number Diff line change
Expand Up @@ -46,28 +46,28 @@
</TypeIdentifier>
</Class>
<Class Name="Microsoft.Identity.Web.WebAppServiceCollectionExtensions">
<Position X="0.5" Y="0.5" Width="11" />
<Position X="0.5" Y="0.5" Width="13.25" />
<TypeIdentifier>
<HashCode>AAAAAAAAAAAAAgAAAAAAAAAAAAAAAACAAAAAAAAAAAA=</HashCode>
<HashCode>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAI=</HashCode>
<FileName>WebAppServiceCollectionExtensions.cs</FileName>
</TypeIdentifier>
</Class>
<Class Name="Microsoft.Identity.Web.WebApiServiceCollectionExtensions">
<Position X="0.5" Y="2" Width="15" />
<Position X="0.5" Y="2" Width="14.5" />
<TypeIdentifier>
<HashCode>AAAAAACAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAA=</HashCode>
<HashCode>AAAAAACAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAA=</HashCode>
<FileName>WebApiServiceCollectionExtensions.cs</FileName>
</TypeIdentifier>
</Class>
<Class Name="Microsoft.Identity.Web.MsalUiRequiredExceptionFilterAttribute">
<Class Name="Microsoft.Identity.Web.AuthorizeForScopesAttribute">
<Position X="0.5" Y="5.75" Width="3.5" />
<Members>
<Method Name="BuildAuthenticationPropertiesForIncrementalConsent" Hidden="true" />
<Method Name="CanBeSolvedByReSignInUser" Hidden="true" />
</Members>
<TypeIdentifier>
<HashCode>AAAAAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAAAgAIAIA=</HashCode>
<FileName>MsalUiRequiredExceptionFilterAttribute.cs</FileName>
<HashCode>AAAAAAAAAAAAACAAAAAAIAAAAAAAAAAAAAAAAgAIAIA=</HashCode>
<FileName>AuthorizeForScopesAttribute.cs</FileName>
</TypeIdentifier>
</Class>
<Class Name="Microsoft.Identity.Web.AccountExtensions">
Expand Down
45 changes: 41 additions & 4 deletions Microsoft.Identity.Web/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ to enable them to work with the Microsoft identity platform (formerly named Azur

As of today, ASP.NET Core web apps templates (`dot net new mvc -auth`) create web apps that sign in users with the Azure AD v1.0 endpoint (allowing to sign in users with their organizational accounts, also named *Work or school accounts*). This library brings `ServiceCollection` extension methods to be used in the ASP.NET Core web app **Startup.cs** file to enable the web app to sign in users with the Microsoft identity platform (formerly Azure AD v2.0 endpoint), and, optionally enable the web app to call APIs on behalf of the signed-in user.

![WebAppServiceCollectionExtensions](https://user-images.githubusercontent.com/13203188/62526924-0a563780-b7ef-11e9-8ce0-db284db3f02c.png)
![WebAppServiceCollectionExtensions](https://user-images.githubusercontent.com/13203188/64252959-82ae3680-cf1c-11e9-8a01-0a0be728a78e.png)

### Web apps that sign in users - Startup.cs

Expand Down Expand Up @@ -84,6 +84,11 @@ public class Startup
}
```

Note that by default, `AddMicrosoftIdentityPlatformAuthentication` gets the configuration from the "AzureAD" section of the configuration files. It has
several parameters that you can change.

Also the proposed token cache serialization is in memory. you can also use the session cache, or various distributed caches

### Web app controller

For your web app to call web APIs on behalf of the signed-in user, you'll need to add a parameter of type `ITokenAcquisition` to the constructor of your controller (the `ITokenAcquisition` service will be injected by dependency injection by ASP.NET Core)
Expand Down Expand Up @@ -125,7 +130,7 @@ public class HomeController : Controller

The controller action is decorated by an attribute `AuthorizeForScopesAttribute` which enables to process the `MsalUiRequiredException` that could be thrown by the service implementing `ITokenAcquisition.GetAccessTokenOnBehalfOfUserAsync` so that the web app interacts with the user, and ask them to consent to the scopes, or re-sign-in if needed.

<img alt="AuthorizeForScopesAttribute" src="https://user-images.githubusercontent.com/13203188/62526956-18a45380-b7ef-11e9-99f3-c75085d61ce5.png" width="50%"/>
<img alt="AuthorizeForScopesAttribute" src="https://user-images.githubusercontent.com/13203188/64253212-0bc56d80-cf1d-11e9-9666-2e72b78886ed.png" width="50%"/>

### Samples and documentation

Expand All @@ -139,7 +144,7 @@ You can see in details how the library is used in the following samples:

The library also enables web APIs to work with the Microsoft identity platform, enabling them to process access tokens for both work and school and Microsoft personal accounts.

![image](https://user-images.githubusercontent.com/13203188/62526937-10e4af00-b7ef-11e9-9fee-c205c97653c5.png)
![image](https://user-images.githubusercontent.com/13203188/64253058-ba1ce300-cf1c-11e9-8f01-88180fc0faed.png)
### Protected web APIS - Startup.cs

Expand Down Expand Up @@ -209,6 +214,8 @@ public class Startup
}
```

Like for Web Apps, you can choose various token cache implementations.

If you're certain that your web API will need some specific scopes, you can optionally pass them as arguments to `AddProtectedApiCallsWebApis`.

### Web API controller
Expand All @@ -218,7 +225,7 @@ For your web API to call downstream APIs, you'll need to:
- add (like in web apps), a parameter of type `ITokenAcquisition` to the constructor of your controller (the `ITokenAcquisition` service will be injected by dependency injection by ASP.NET Core)
- verify, in your controller actions, that the token contains the scopes expected by the action. For this, you'll call the `VerifyUserHasAnyAcceptedScope` extension method on the `HttpContext`

<img alt="ScopesRequiredHttpContextExtensions" src="https://user-images.githubusercontent.com/13203188/62527104-60c37600-b7ef-11e9-8dcb-66bb982fe147.png" width="80%"/>
<img alt="ScopesRequiredHttpContextExtensions" src="https://user-images.githubusercontent.com/13203188/64253176-f9e3ca80-cf1c-11e9-8fe9-df06cee11c25.png" width="80%"/>

- in your controller actions, to call: `ITokenAcquisition.GetAccessTokenOnBehalfOfUserAsync` passing the scopes for which to request a token.

Expand Down Expand Up @@ -262,6 +269,36 @@ For web apps that calls web apis, and web APIs that call downstream APIs, the co
| `AddInMemoryTokenCaches` | `TokenCacheProviders.InMemory` | In memory token cache serialization. This implementation is great in samples. It's also good in production applications provided you don't mind if the token cache is lost when the web app is restarted. `AddInMemoryTokenCaches` takes an optional parameter of type `MsalMemoryTokenCacheOptions` that enables you to specify the duration after which the cache entry will expire unless it's used.
| `AddSqlTokenCaches` | `TokenCacheProviders.Sql` | The token cache maintained in a SQL database. This implementation is ideal for production applications that need to keep their token caches. AddSqlTokenCaches takes a parameter of type `MsalSqlTokenCacheOptions` that let you specify the SQL connection string
| `AddSessionTokenCaches` | `TokenCacheProviders.Session` | The token cache is bound to the user session. This option isn't ideal if the ID token is too large because it contains too many claims as the cookie would be too large.
| `AddDistributedTokenCaches` | `TokenCacheProviders.Distributed` | The token cache is an adapter against the ASP.NET Core `IDistributedCache` implementation, therefore enabling you to choose between a distributed memory cache, a Redis cache, or a SQL Server cache. For details about the IDistributedCache` implementations, see https://docs.microsoft.com/en-us/aspnet/core/performance/caching/distributed?view=aspnetcore-2.2#distributed-memory-cache.
Examples of possible distributed cache:

```CSharp
// or use a distributed Token Cache by adding
services.AddMicrosoftIdentityPlatformAuthentication(Configuration)
.AddMsal(new string[] { scopesToRequest })
.AddDistributedTokenCaches();

// and then choose your implementation
// For instance the distributed in memory cache (not cleared when you stop the app)
services.AddDistributedMemoryCache()

// Or a Redis cache
services.AddStackExchangeRedisCache(options =>
{
options.Configuration = "localhost";
options.InstanceName = "SampleInstance";
});

// Or even a SQL Server token cache
services.AddDistributedSqlServerCache(options =>
{
options.ConnectionString = _config["DistCache_ConnectionString"];
options.SchemaName = "dbo";
options.TableName = "TestCache";
});
```

## Other utility classes

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using Microsoft.Extensions.DependencyInjection;

namespace Microsoft.Identity.Web.TokenCacheProviders.Distributed
{
/// <summary>
/// Extension class used to add an in-memory token cache serializer to MSAL
/// </summary>
public static class DistributedTokenCacheAdapterExtension
{
/// <summary>Adds both the app and per-user in-memory token caches.</summary>
/// <param name="services">The services collection to add to.</param>
/// <param name="cacheOptions">The MSALMemoryTokenCacheOptions allows the caller to set the token cache expiration</param>
/// <returns></returns>
public static IServiceCollection AddDistributedTokenCaches(
this IServiceCollection services)
{
AddDistributedAppTokenCache(services);
AddDistributedUserTokenCache(services);
return services;
}

/// <summary>Adds the in-memory based application token cache to the service collection.</summary>
/// <param name="services">The services collection to add to.</param>
/// <param name="cacheOptions">The MSALMemoryTokenCacheOptions allows the caller to set the token cache expiration</param>
public static IServiceCollection AddDistributedAppTokenCache(
this IServiceCollection services)
{
services.AddDistributedMemoryCache();
services.AddSingleton<IMsalAppTokenCacheProvider, MsalAppDistributedTokenCacheProvider>();
return services;
}

/// <summary>Adds the in-memory based per user token cache to the service collection.</summary>
/// <param name="services">The services collection to add to.</param>
/// <param name="cacheOptions">The MSALMemoryTokenCacheOptions allows the caller to set the token cache expiration</param>
/// <returns></returns>
public static IServiceCollection AddDistributedUserTokenCache(
this IServiceCollection services)
{
services.AddDistributedMemoryCache();
services.AddHttpContextAccessor();
services.AddSingleton<IMsalUserTokenCacheProvider, MsalPerUserDistributedTokenCacheProvider>();
return services;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System.Security.Principal;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication.AzureAD.UI;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options;
using Microsoft.Identity.Client;

namespace Microsoft.Identity.Web.TokenCacheProviders.Distributed
{
/// <summary>
/// An implementation of token cache for Confidential clients backed by MemoryCache.
/// MemoryCache is useful in Api scenarios where there is no HttpContext to cache data.
/// </summary>
/// <seealso cref="https://aka.ms/msal-net-token-cache-serialization"/>
public class MsalAppDistributedTokenCacheProvider : MsalDistributedTokenCacheAdapter, IMsalAppTokenCacheProvider
{
public MsalAppDistributedTokenCacheProvider(IOptions<AzureADOptions> azureAdOptions,
IHttpContextAccessor httpContextAccessor,
IDistributedCache memoryCache,
IOptions<DistributedCacheEntryOptions> cacheOptions) :
base(azureAdOptions, httpContextAccessor, memoryCache, cacheOptions)
{

}

public async Task InitializeAsync(ITokenCache tokenCache)
{
await InitializeAsync(tokenCache, true).ConfigureAwait(false);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication.AzureAD.UI;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options;

namespace Microsoft.Identity.Web.TokenCacheProviders.Distributed
{
/// <summary>
/// An implementation of token cache for both Confidential and Public clients backed by MemoryCache.
/// </summary>
/// <seealso cref="https://aka.ms/msal-net-token-cache-serialization"/>
public class MsalDistributedTokenCacheAdapter : MsalAbstractTokenCacheProvider
{
/// <summary>
/// .NET Core Memory cache
/// </summary>
private readonly IDistributedCache _distributedCache;

/// <summary>
/// Msal memory token cache options
/// </summary>
private readonly DistributedCacheEntryOptions _cacheOptions;

/// <summary>
/// Constructor
/// </summary>
/// <param name="azureAdOptions"></param>
/// <param name="httpContextAccessor"></param>
/// <param name="memoryCache"></param>
/// <param name="cacheOptions"></param>
public MsalDistributedTokenCacheAdapter(IOptions<AzureADOptions> azureAdOptions,
IHttpContextAccessor httpContextAccessor,
IDistributedCache memoryCache,
IOptions<DistributedCacheEntryOptions> cacheOptions) :
base(azureAdOptions, httpContextAccessor)
{
_distributedCache = memoryCache;
_cacheOptions = cacheOptions.Value;
}

protected override async Task RemoveKeyAsync(string cacheKey)
{
await _distributedCache.RemoveAsync(cacheKey).ConfigureAwait(false);
}

protected override async Task<byte[]> ReadCacheBytesAsync(string cacheKey)
{
return await _distributedCache.GetAsync(cacheKey).ConfigureAwait(false);
}

protected override async Task WriteCacheBytesAsync(string cacheKey, byte[] bytes)
{
await _distributedCache.SetAsync(cacheKey, bytes, _cacheOptions).ConfigureAwait(false) ;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using Microsoft.AspNetCore.Authentication.AzureAD.UI;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Options;
using Microsoft.Identity.Client;
using System.Threading.Tasks;
using Microsoft.Extensions.Caching.Distributed;


namespace Microsoft.Identity.Web.TokenCacheProviders.Distributed
{
/// <summary>
/// An implementation of token cache for both Confidential and Public clients backed by MemoryCache.
/// MemoryCache is useful in Api scenarios where there is no HttpContext.Session to cache data.
/// </summary>
/// <seealso cref="https://aka.ms/msal-net-token-cache-serialization"/>
public class MsalPerUserDistributedTokenCacheProvider : MsalDistributedTokenCacheAdapter, IMsalUserTokenCacheProvider
{
public MsalPerUserDistributedTokenCacheProvider(IOptions<AzureADOptions> azureAdOptions,
IHttpContextAccessor httpContextAccessor,
IDistributedCache memoryCache,
IOptions<DistributedCacheEntryOptions> cacheOptions) :
base(azureAdOptions, httpContextAccessor, memoryCache, cacheOptions)
{

}

public async Task InitializeAsync(ITokenCache tokenCache)
{
await InitializeAsync(tokenCache, false).ConfigureAwait(false);
}
}
}

0 comments on commit e6c7c61

Please sign in to comment.