Skip to content
This repository was archived by the owner on Nov 16, 2023. It is now read-only.

Commit 0275b6e

Browse files
authored
Merge pull request #66 from Azure-Samples/jmprieur/Msal3.0.3-preview
Update to MSAL.NET 3.0.3-preview
2 parents 564cb63 + 8415b74 commit 0275b6e

File tree

7 files changed

+138
-26
lines changed

7 files changed

+138
-26
lines changed

README.md

Lines changed: 32 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -166,37 +166,56 @@ This sample shows how to use MSAL to redeem the authorization code into an acces
166166
The redemption takes place in the `AuthorizationCodeReceived` notification of the authorization middleware. Here there's the relevant code:
167167

168168
```csharp
169+
// Use MSAL to swap the code for an access token
170+
// Extract the code from the response notification
169171
var code = context.ProtocolMessage.Code;
170-
string signedInUserID = context.Ticket.Principal.FindFirst(ClaimTypes.NameIdentifier).Value;
171-
TokenCache userTokenCache = new MSALSessionCache(signedInUserID, context.HttpContext).GetMsalCacheInstance();
172172

173-
ConfidentialClientApplication cca = new ConfidentialClientApplication(AzureAdB2COptions.ClientId, AzureAdB2COptions.Authority, AzureAdB2COptions.RedirectUri, new ClientCredential(AzureAdB2COptions.ClientSecret), userTokenCache, null);
173+
string signedInUserID = context.Principal.FindFirst(ClaimTypes.NameIdentifier).Value;
174+
IConfidentialClientApplication cca = ConfidentialClientApplicationBuilder.Create(AzureAdB2COptions.ClientId)
175+
.WithB2CAuthority(AzureAdB2COptions.Authority)
176+
.WithRedirectUri(AzureAdB2COptions.RedirectUri)
177+
.WithClientSecret(AzureAdB2COptions.ClientSecret)
178+
.Build();
179+
new MSALStaticCache(signedInUserID, context.HttpContext).EnablePersistence(cca.UserTokenCache);
174180

175181
try
176182
{
177-
AuthenticationResult result = await cca.AcquireTokenByAuthorizationCodeAsync(code, AzureAdB2COptions.ApiScopes.Split(' '));
178-
context.HandleCodeRedemption(result.AccessToken, result.IdToken);
183+
AuthenticationResult result = await cca.AcquireTokenByAuthorizationCode(AzureAdB2COptions.ApiScopes.Split(' '), code)
184+
.ExecuteAsync();
185+
186+
187+
context.HandleCodeRedemption(result.AccessToken, result.IdToken);
188+
}
179189
```
180190

181191
Important things to notice:
182-
- The `ConfidentialClientApplication` is the primitive that MSAL uses to model the application. As such, it is initialized with the main application's coordinates.
183-
- `MSALSessionCache` is a sample implementation of a custom MSAL token cache, which saves tokens in the current HTTP session. In a real-life application, you would likely want to save tokens in a long lived store instead, so that you don't need to retrieve new ones more often than necessary.
184-
- The scope requested by `AcquireTokenByAuthorizationCodeAsync` is just the one required for invoking the API targeted by the application as part of its essential features. We'll see later that the app allows for extra scopes, but you can ignore those at this point.
192+
- The `IConfidentialClientApplication` is the interface that MSAL uses to model the application. As such, it is initialized with the main application's coordinates.
193+
- `MSALStaticCache` is a sample implementation of a custom MSAL token cache, which saves tokens in memory. In a real-life application, you would likely want to save tokens in a long lived store instead, so that you don't need to retrieve new ones more often than necessary. For examples of such caches see [ASP.NET Core Web app tutorial | Token caches](https://github.com/Azure-Samples/active-directory-aspnetcore-webapp-openidconnect-v2/tree/master/2-WebApp-graph-user/2-2-TokenCache)
194+
- The scope requested by `AcquireTokenByAuthorizationCode` is just the one required for invoking the API targeted by the application as part of its essential features. We'll see later that the app allows for extra scopes, but you can ignore those at this point.
185195

186196
### Using access tokens in the app, handling token expiration
187197

188198
The `Api` action in the `HomeController` class demonstrates how to take advantage of MSAL for getting access to protected API easily and securely. Here there's the relevant code:
189199

190200
```csharp
201+
// Retrieve the token with the specified scopes
191202
var scope = AzureAdB2COptions.ApiScopes.Split(' ');
192203
string signedInUserID = HttpContext.User.FindFirst(ClaimTypes.NameIdentifier).Value;
193-
TokenCache userTokenCache = new MSALSessionCache(signedInUserID, this.HttpContext).GetMsalCacheInstance();
194204

195-
ConfidentialClientApplication cca = new ConfidentialClientApplication(AzureAdB2COptions.ClientId, AzureAdB2COptions.Authority, AzureAdB2COptions.RedirectUri, new ClientCredential(AzureAdB2COptions.ClientSecret), userTokenCache, null);
196-
AuthenticationResult result = await cca.AcquireTokenSilentAsync(scope, cca.Users.FirstOrDefault(), AzureAdB2COptions.Authority, false);
205+
IConfidentialClientApplication cca =
206+
ConfidentialClientApplicationBuilder.Create(AzureAdB2COptions.ClientId)
207+
.WithRedirectUri(AzureAdB2COptions.RedirectUri)
208+
.WithClientSecret(AzureAdB2COptions.ClientSecret)
209+
.WithB2CAuthority(AzureAdB2COptions.Authority)
210+
.Build();
211+
new MSALStaticCache(signedInUserID, this.HttpContext).EnablePersistence(cca.UserTokenCache);
212+
213+
var accounts = await cca.GetAccountsAsync();
214+
AuthenticationResult result = await cca.AcquireTokenSilent(scope, accounts.FirstOrDefault())
215+
.ExecuteAsync();
197216
```
198217

199-
The idea is very simple. The code creates a new instance of `ConfidentialClientApplication` with the exact same coordinates as the ones used when redeeming the authorization code at authentication time. In particular, note that the exact same cache is used.
200-
That done, all you need to do is to invoke `AcquireTokenSilentAsync`, asking for the scopes you need. MSAL will look up the cache and return any cached token which match with the requirement. If such access tokens are expired or no suitable access tokens are present, but there is an associated refresh token, MSAL will automatically use that to get a new access token and return it transparently.
218+
The idea is very simple. The code creates a new instance of `IConfidentialClientApplication` with the exact same coordinates as the ones used when redeeming the authorization code at authentication time. In particular, note that the exact same cache is used.
219+
That done, all you need to do is to invoke `AcquireTokenSilent`, asking for the scopes you need. MSAL will look up the cache and return any cached token which match with the requirement. If such access tokens are expired or no suitable access tokens are present, but there is an associated refresh token, MSAL will automatically use that to get a new access token and return it transparently.
201220

202221
In the case in which refresh tokens are not present or they fail to obtain a new access token, MSAL will throw `MsalUiRequiredException`. That means that in order to obtain the requested token, the user must go through an interactive experience.

WebApp-OpenIDConnect-DotNet/Controllers/HomeController.cs

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,11 +44,18 @@ public async Task<IActionResult> Api()
4444
// Retrieve the token with the specified scopes
4545
var scope = AzureAdB2COptions.ApiScopes.Split(' ');
4646
string signedInUserID = HttpContext.User.FindFirst(ClaimTypes.NameIdentifier).Value;
47-
TokenCache userTokenCache = new MSALSessionCache(signedInUserID, this.HttpContext).GetMsalCacheInstance();
48-
ConfidentialClientApplication cca = new ConfidentialClientApplication(AzureAdB2COptions.ClientId, AzureAdB2COptions.Authority, AzureAdB2COptions.RedirectUri, new ClientCredential(AzureAdB2COptions.ClientSecret), userTokenCache, null);
47+
48+
IConfidentialClientApplication cca =
49+
ConfidentialClientApplicationBuilder.Create(AzureAdB2COptions.ClientId)
50+
.WithRedirectUri(AzureAdB2COptions.RedirectUri)
51+
.WithClientSecret(AzureAdB2COptions.ClientSecret)
52+
.WithB2CAuthority(AzureAdB2COptions.Authority)
53+
.Build();
54+
new MSALStaticCache(signedInUserID, this.HttpContext).EnablePersistence(cca.UserTokenCache);
4955

5056
var accounts = await cca.GetAccountsAsync();
51-
AuthenticationResult result = await cca.AcquireTokenSilentAsync(scope, accounts.FirstOrDefault(), AzureAdB2COptions.Authority, false);
57+
AuthenticationResult result = await cca.AcquireTokenSilent(scope, accounts.FirstOrDefault())
58+
.ExecuteAsync();
5259

5360
HttpClient client = new HttpClient();
5461
HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, AzureAdB2COptions.ApiUrl);

WebApp-OpenIDConnect-DotNet/Models/MSALSessionCache.cs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
using System.Threading;
44
using Microsoft.AspNetCore.Http;
5+
using Microsoft.Extensions.Caching.Memory;
56

67
namespace WebApp_OpenIDConnect_DotNet.Models
78
{
@@ -12,7 +13,7 @@ public class MSALSessionCache
1213
string CacheId = string.Empty;
1314
HttpContext httpContext = null;
1415

15-
TokenCache cache = new TokenCache();
16+
ITokenCache cache;
1617

1718
public MSALSessionCache(string userId, HttpContext httpcontext)
1819
{
@@ -23,8 +24,9 @@ public MSALSessionCache(string userId, HttpContext httpcontext)
2324
Load();
2425
}
2526

26-
public TokenCache GetMsalCacheInstance()
27+
public ITokenCache EnablePersistence(ITokenCache cache)
2728
{
29+
this.cache = cache;
2830
cache.SetBeforeAccess(BeforeAccessNotification);
2931
cache.SetAfterAccess(AfterAccessNotification);
3032
Load();
@@ -51,7 +53,7 @@ public void Load()
5153
byte[] blob = httpContext.Session.Get(CacheId);
5254
if(blob != null)
5355
{
54-
cache.Deserialize(blob);
56+
cache.DeserializeMsalV3(blob);
5557
}
5658
SessionLock.ExitReadLock();
5759
}
@@ -61,7 +63,7 @@ public void Persist()
6163
SessionLock.EnterWriteLock();
6264

6365
// Reflect changes in the persistent store
64-
httpContext.Session.Set(CacheId, cache.Serialize());
66+
httpContext.Session.Set(CacheId, cache.SerializeMsalV3());
6567
SessionLock.ExitWriteLock();
6668
}
6769

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
using Microsoft.Identity.Client;
2+
3+
using System.Threading;
4+
using Microsoft.AspNetCore.Http;
5+
using Microsoft.Extensions.Caching.Memory;
6+
using System.Collections.Generic;
7+
8+
namespace WebApp_OpenIDConnect_DotNet.Models
9+
{
10+
public class MSALStaticCache
11+
{
12+
private static Dictionary<string, byte[]> staticCache = new Dictionary<string, byte[]>();
13+
14+
private static ReaderWriterLockSlim SessionLock = new ReaderWriterLockSlim(LockRecursionPolicy.NoRecursion);
15+
string UserId = string.Empty;
16+
string CacheId = string.Empty;
17+
HttpContext httpContext = null;
18+
19+
ITokenCache cache;
20+
21+
public MSALStaticCache(string userId, HttpContext httpcontext)
22+
{
23+
// not object, we want the SUB
24+
UserId = userId;
25+
CacheId = UserId + "_TokenCache";
26+
httpContext = httpcontext;
27+
}
28+
29+
public ITokenCache EnablePersistence(ITokenCache cache)
30+
{
31+
this.cache = cache;
32+
cache.SetBeforeAccess(BeforeAccessNotification);
33+
cache.SetAfterAccess(AfterAccessNotification);
34+
Load();
35+
return cache;
36+
}
37+
38+
public void Load()
39+
{
40+
SessionLock.EnterReadLock();
41+
byte[] blob = staticCache.ContainsKey(CacheId) ? staticCache[CacheId] : null ;
42+
if(blob != null)
43+
{
44+
cache.DeserializeMsalV3(blob);
45+
}
46+
SessionLock.ExitReadLock();
47+
}
48+
49+
public void Persist()
50+
{
51+
SessionLock.EnterWriteLock();
52+
53+
// Reflect changes in the persistent store
54+
staticCache[CacheId] = cache.SerializeMsalV3();
55+
SessionLock.ExitWriteLock();
56+
}
57+
58+
// Triggered right before MSAL needs to access the cache.
59+
// Reload the cache from the persistent store in case it changed since the last access.
60+
void BeforeAccessNotification(TokenCacheNotificationArgs args)
61+
{
62+
Load();
63+
}
64+
65+
// Triggered right after MSAL accessed the cache.
66+
void AfterAccessNotification(TokenCacheNotificationArgs args)
67+
{
68+
// if the access operation resulted in a cache update
69+
if (args.HasStateChanged)
70+
{
71+
Persist();
72+
}
73+
}
74+
}
75+
}

WebApp-OpenIDConnect-DotNet/OpenIdConnectOptionsSetup.cs

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -107,11 +107,17 @@ public async Task OnAuthorizationCodeReceived(AuthorizationCodeReceivedContext c
107107
var code = context.ProtocolMessage.Code;
108108

109109
string signedInUserID = context.Principal.FindFirst(ClaimTypes.NameIdentifier).Value;
110-
TokenCache userTokenCache = new MSALSessionCache(signedInUserID, context.HttpContext).GetMsalCacheInstance();
111-
ConfidentialClientApplication cca = new ConfidentialClientApplication(AzureAdB2COptions.ClientId, AzureAdB2COptions.Authority, AzureAdB2COptions.RedirectUri, new ClientCredential(AzureAdB2COptions.ClientSecret), userTokenCache, null);
110+
IConfidentialClientApplication cca = ConfidentialClientApplicationBuilder.Create(AzureAdB2COptions.ClientId)
111+
.WithB2CAuthority(AzureAdB2COptions.Authority)
112+
.WithRedirectUri(AzureAdB2COptions.RedirectUri)
113+
.WithClientSecret(AzureAdB2COptions.ClientSecret)
114+
.Build();
115+
new MSALStaticCache(signedInUserID, context.HttpContext).EnablePersistence(cca.UserTokenCache);
116+
112117
try
113118
{
114-
AuthenticationResult result = await cca.AcquireTokenByAuthorizationCodeAsync(code, AzureAdB2COptions.ApiScopes.Split(' '));
119+
AuthenticationResult result = await cca.AcquireTokenByAuthorizationCode(AzureAdB2COptions.ApiScopes.Split(' '), code)
120+
.ExecuteAsync();
115121

116122

117123
context.HandleCodeRedemption(result.AccessToken, result.IdToken);

WebApp-OpenIDConnect-DotNet/Startup.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ public Startup(IHostingEnvironment env)
1919
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
2020
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true)
2121
.AddEnvironmentVariables();
22+
2223
Configuration = builder.Build();
2324
}
2425

@@ -45,7 +46,8 @@ public void ConfigureServices(IServiceCollection services)
4546
services.AddSession(options =>
4647
{
4748
options.IdleTimeout = TimeSpan.FromHours(1);
48-
options.CookieHttpOnly = true;
49+
options.Cookie.HttpOnly = true;
50+
options.Cookie.IsEssential = true;
4951
});
5052

5153

@@ -54,6 +56,7 @@ public void ConfigureServices(IServiceCollection services)
5456
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
5557
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
5658
{
59+
loggerFactory.AddConsole();
5760
loggerFactory.AddConsole(Configuration.GetSection("Logging"));
5861
loggerFactory.AddDebug();
5962

WebApp-OpenIDConnect-DotNet/WebApp-OpenIDConnect-DotNet.csproj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
<Project Sdk="Microsoft.NET.Sdk.Web">
1+
<Project Sdk="Microsoft.NET.Sdk.Web">
22
<PropertyGroup>
33
<TargetFramework>netcoreapp2.2</TargetFramework>
44
<RootNamespace>WebApp_OpenIDConnect_DotNet</RootNamespace>
55
</PropertyGroup>
66
<ItemGroup>
7-
<PackageReference Include="Microsoft.Identity.Client" Version="2.7.1" />
7+
<PackageReference Include="Microsoft.Identity.Client" Version="3.0.3-preview" />
88
<PackageReference Include="Microsoft.AspNetCore" Version="2.2.0" />
99
<PackageReference Include="Microsoft.AspNetCore.Authentication.Cookies" Version="2.2.0" />
1010
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="2.2.0" />

0 commit comments

Comments
 (0)