Skip to content

Commit ca58e90

Browse files
author
Tiago Brenck
committed
- Added Utils classes
- Updated Microsoft.Identity.Client to 3.0.8
1 parent b8e60e7 commit ca58e90

File tree

8 files changed

+604
-3
lines changed

8 files changed

+604
-3
lines changed

TaskWebApp/TaskWebApp.csproj

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,8 @@
5252
<HintPath>..\packages\Antlr.3.5.0.2\lib\Antlr3.Runtime.dll</HintPath>
5353
</Reference>
5454
<Reference Include="Microsoft.CSharp" />
55-
<Reference Include="Microsoft.Identity.Client, Version=3.0.5.0, Culture=neutral, PublicKeyToken=0a613f4dd989e8ae, processorArchitecture=MSIL">
56-
<HintPath>..\packages\Microsoft.Identity.Client.3.0.5-preview\lib\net45\Microsoft.Identity.Client.dll</HintPath>
55+
<Reference Include="Microsoft.Identity.Client, Version=3.0.8.0, Culture=neutral, PublicKeyToken=0a613f4dd989e8ae, processorArchitecture=MSIL">
56+
<HintPath>..\packages\Microsoft.Identity.Client.3.0.8\lib\net45\Microsoft.Identity.Client.dll</HintPath>
5757
</Reference>
5858
<Reference Include="Microsoft.IdentityModel.JsonWebTokens, Version=5.4.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
5959
<HintPath>..\packages\Microsoft.IdentityModel.JsonWebTokens.5.4.0\lib\net451\Microsoft.IdentityModel.JsonWebTokens.dll</HintPath>
@@ -182,6 +182,12 @@
182182
<Compile Include="Models\TokenCacheHelper.cs" />
183183
<Compile Include="Properties\AssemblyInfo.cs" />
184184
<Compile Include="Startup.cs" />
185+
<Compile Include="Utils\ClaimsPrincipalExtension.cs" />
186+
<Compile Include="Utils\Constants.cs" />
187+
<Compile Include="Utils\Globals.cs" />
188+
<Compile Include="Utils\MsalAppBuilder.cs" />
189+
<Compile Include="Utils\MSALPerUserMemoryTokenCache.cs" />
190+
<Compile Include="Utils\MSALPerUserSessionTokenCache.cs" />
185191
</ItemGroup>
186192
<ItemGroup>
187193
<Content Include="Content\bootstrap.css" />
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
using Microsoft.Identity.Client;
2+
using System.Security.Claims;
3+
4+
namespace TaskWebApp.Utils
5+
{
6+
public static class ClaimsPrincipalExtension
7+
{
8+
/// <summary>
9+
/// Get the Account identifier for an MSAL.NET account from a ClaimsPrincipal
10+
/// </summary>
11+
/// <param name="claimsPrincipal">Claims principal</param>
12+
/// <returns>A string corresponding to an account identifier as defined in <see cref="Microsoft.Identity.Client.AccountId.Identifier"/></returns>
13+
public static string GetMsalAccountId(this ClaimsPrincipal claimsPrincipal)
14+
{
15+
string userObjectId = GetObjectId(claimsPrincipal);
16+
string tenantId = "775527ff-9a37-4307-8b3d-cc311f58d925"; //TODO: FIX THIS
17+
18+
if (!string.IsNullOrWhiteSpace(userObjectId) && !string.IsNullOrWhiteSpace(tenantId))
19+
{
20+
return $"{userObjectId}.{tenantId}";
21+
}
22+
23+
return null;
24+
}
25+
26+
/// <summary>
27+
/// Get the unique object ID associated with the claimsPrincipal
28+
/// </summary>
29+
/// <param name="claimsPrincipal">Claims principal from which to retrieve the unique object id</param>
30+
/// <returns>Unique object ID of the identity, or <c>null</c> if it cannot be found</returns>
31+
public static string GetObjectId(this ClaimsPrincipal claimsPrincipal)
32+
{
33+
var objIdclaim = claimsPrincipal.FindFirst(ClaimConstants.ObjectId);
34+
35+
if (objIdclaim == null)
36+
{
37+
objIdclaim = claimsPrincipal.FindFirst("oid");
38+
}
39+
40+
return objIdclaim != null ? objIdclaim.Value : string.Empty;
41+
}
42+
43+
/// <summary>
44+
/// Builds a ClaimsPrincipal from an IAccount
45+
/// </summary>
46+
/// <param name="account">The IAccount instance.</param>
47+
/// <returns>A ClaimsPrincipal built from IAccount</returns>
48+
public static ClaimsPrincipal ToClaimsPrincipal(this IAccount account)
49+
{
50+
if (account != null)
51+
{
52+
var identity = new ClaimsIdentity();
53+
identity.AddClaim(new Claim(ClaimConstants.ObjectId, account.HomeAccountId.ObjectId));
54+
identity.AddClaim(new Claim(ClaimConstants.TenantId, account.HomeAccountId.TenantId));
55+
identity.AddClaim(new Claim(ClaimTypes.Upn, account.Username));
56+
return new ClaimsPrincipal(identity);
57+
}
58+
59+
return null;
60+
}
61+
}
62+
}

TaskWebApp/Utils/Constants.cs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/************************************************************************************************
2+
The MIT License (MIT)
3+
4+
Copyright (c) 2015 Microsoft Corporation
5+
6+
Permission is hereby granted, free of charge, to any person obtaining a copy
7+
of this software and associated documentation files (the "Software"), to deal
8+
in the Software without restriction, including without limitation the rights
9+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
copies of the Software, and to permit persons to whom the Software is
11+
furnished to do so, subject to the following conditions:
12+
13+
The above copyright notice and this permission notice shall be included in all
14+
copies or substantial portions of the Software.
15+
16+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22+
SOFTWARE.
23+
***********************************************************************************************/
24+
25+
namespace TaskWebApp.Utils
26+
{
27+
/// <summary>
28+
/// claim keys constants
29+
/// </summary>
30+
public static class ClaimConstants
31+
{
32+
public const string ObjectId = "http://schemas.microsoft.com/identity/claims/objectidentifier";
33+
public const string TenantId = "http://schemas.microsoft.com/identity/claims/tenantid";
34+
public const string tid = "tid";
35+
}
36+
}

TaskWebApp/Utils/Globals.cs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Configuration;
4+
using System.Linq;
5+
using System.Web;
6+
7+
namespace TaskWebApp.Utils
8+
{
9+
public static class Globals
10+
{
11+
// App config settings
12+
public static string ClientId = ConfigurationManager.AppSettings["ida:ClientId"];
13+
public static string ClientSecret = ConfigurationManager.AppSettings["ida:ClientSecret"];
14+
public static string AadInstance = ConfigurationManager.AppSettings["ida:AadInstance"];
15+
public static string Tenant = ConfigurationManager.AppSettings["ida:Tenant"];
16+
public static string RedirectUri = ConfigurationManager.AppSettings["ida:RedirectUri"];
17+
public static string ServiceUrl = ConfigurationManager.AppSettings["api:TaskServiceUrl"];
18+
19+
// B2C policy identifiers
20+
public static string SignUpSignInPolicyId = ConfigurationManager.AppSettings["ida:SignUpSignInPolicyId"];
21+
public static string EditProfilePolicyId = ConfigurationManager.AppSettings["ida:EditProfilePolicyId"];
22+
public static string ResetPasswordPolicyId = ConfigurationManager.AppSettings["ida:ResetPasswordPolicyId"];
23+
24+
public static string DefaultPolicy = SignUpSignInPolicyId;
25+
26+
// API Scopes
27+
public static string ApiIdentifier = ConfigurationManager.AppSettings["api:ApiIdentifier"];
28+
public static string ReadTasksScope = ApiIdentifier + ConfigurationManager.AppSettings["api:ReadScope"];
29+
public static string WriteTasksScope = ApiIdentifier + ConfigurationManager.AppSettings["api:WriteScope"];
30+
public static string[] Scopes = new string[] { ReadTasksScope, WriteTasksScope };
31+
32+
// OWIN auth middleware constants
33+
public const string ObjectIdElement = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier";
34+
35+
// Authorities
36+
public static string B2CAuthority = string.Format(AadInstance, Tenant, DefaultPolicy);
37+
public static string WellKnownMetadata = $"{AadInstance}/v2.0/.well-known/openid-configuration";
38+
39+
}
40+
}
Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
/************************************************************************************************
2+
The MIT License (MIT)
3+
4+
Copyright (c) 2015 Microsoft Corporation
5+
6+
Permission is hereby granted, free of charge, to any person obtaining a copy
7+
of this software and associated documentation files (the "Software"), to deal
8+
in the Software without restriction, including without limitation the rights
9+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
copies of the Software, and to permit persons to whom the Software is
11+
furnished to do so, subject to the following conditions:
12+
13+
The above copyright notice and this permission notice shall be included in all
14+
copies or substantial portions of the Software.
15+
16+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22+
SOFTWARE.
23+
***********************************************************************************************/
24+
25+
using Microsoft.Identity.Client;
26+
using System;
27+
using System.Runtime.Caching;
28+
using System.Security.Claims;
29+
30+
namespace TaskWebApp.Utils
31+
{
32+
public class MSALPerUserMemoryTokenCache
33+
{
34+
/// <summary>
35+
/// The backing MemoryCache instance
36+
/// </summary>
37+
internal readonly MemoryCache memoryCache = MemoryCache.Default;
38+
39+
/// <summary>
40+
/// The duration till the tokens are kept in memory cache. In production, a higher value, upto 90 days is recommended.
41+
/// </summary>
42+
private readonly DateTimeOffset cacheDuration = DateTimeOffset.Now.AddHours(48);
43+
44+
/// <summary>
45+
/// The internal handle to the client's instance of the Cache
46+
/// </summary>
47+
private ITokenCache UserTokenCache;
48+
49+
/// <summary>
50+
/// Once the user signes in, this will not be null and can be ontained via a call to Thread.CurrentPrincipal
51+
/// </summary>
52+
internal ClaimsPrincipal SignedInUser;
53+
54+
/// <summary>
55+
/// Initializes a new instance of the <see cref="MSALPerUserMemoryTokenCache"/> class.
56+
/// </summary>
57+
/// <param name="tokenCache">The client's instance of the token cache.</param>
58+
public MSALPerUserMemoryTokenCache(ITokenCache tokenCache)
59+
{
60+
this.Initialize(tokenCache, ClaimsPrincipal.Current);
61+
}
62+
63+
/// <summary>
64+
/// Initializes a new instance of the <see cref="MSALPerUserMemoryTokenCache"/> class.
65+
/// </summary>
66+
/// <param name="tokenCache">The client's instance of the token cache.</param>
67+
/// <param name="user">The signed-in user for whom the cache needs to be established.</param>
68+
public MSALPerUserMemoryTokenCache(ITokenCache tokenCache, ClaimsPrincipal user)
69+
{
70+
this.Initialize(tokenCache, user);
71+
}
72+
73+
/// <summary>
74+
/// Explores the Claims of a signed-in user (if available) to populate the unique Id of this cache's instance.
75+
/// </summary>
76+
/// <returns>The signed in user's object Id , if available in the ClaimsPrincipal.Current instance</returns>
77+
private string GetSignedInUsersUniqueId()
78+
{
79+
return ClaimsPrincipal.Current?.FindFirst("http://schemas.microsoft.com/identity/claims/objectidentifier")?.Value;
80+
}
81+
82+
/// <summary>Initializes the cache instance</summary>
83+
/// <param name="tokenCache">The ITokenCache passed through the constructor</param>
84+
/// <param name="user">The signed-in user for whom the cache needs to be established..</param>
85+
private void Initialize(ITokenCache tokenCache, ClaimsPrincipal user)
86+
{
87+
this.SignedInUser = user;
88+
89+
this.UserTokenCache = tokenCache;
90+
this.UserTokenCache.SetBeforeAccess(this.UserTokenCacheBeforeAccessNotification);
91+
this.UserTokenCache.SetAfterAccess(this.UserTokenCacheAfterAccessNotification);
92+
this.UserTokenCache.SetBeforeWrite(this.UserTokenCacheBeforeWriteNotification);
93+
94+
if (this.SignedInUser == null)
95+
{
96+
// No users signed in yet, so we return
97+
return;
98+
}
99+
100+
this.LoadUserTokenCacheFromMemory();
101+
}
102+
103+
/// <summary>
104+
/// Explores the Claims of a signed-in user (if available) to populate the unique Id of this cache's instance.
105+
/// </summary>
106+
/// <returns>The signed in user's object.tenant Id , if available in the ClaimsPrincipal.Current instance</returns>
107+
internal string GetMsalAccountId()
108+
{
109+
if (this.SignedInUser != null)
110+
{
111+
return this.SignedInUser.GetMsalAccountId();
112+
}
113+
return null;
114+
}
115+
116+
/// <summary>
117+
/// Loads the user token cache from memory.
118+
/// </summary>
119+
private void LoadUserTokenCacheFromMemory()
120+
{
121+
string cacheKey = this.GetMsalAccountId();
122+
123+
if (string.IsNullOrWhiteSpace(cacheKey))
124+
return;
125+
126+
// Ideally, methods that load and persist should be thread safe. MemoryCache.Get() is thread safe.
127+
byte[] tokenCacheBytes = (byte[])this.memoryCache.Get(this.GetMsalAccountId());
128+
this.UserTokenCache.DeserializeMsalV3(tokenCacheBytes);
129+
}
130+
131+
/// <summary>
132+
/// Persists the user token blob to the memoryCache.
133+
/// </summary>
134+
private void PersistUserTokenCache()
135+
{
136+
string cacheKey = this.GetMsalAccountId();
137+
138+
if (string.IsNullOrWhiteSpace(cacheKey))
139+
return;
140+
141+
// Ideally, methods that load and persist should be thread safe.MemoryCache.Get() is thread safe.
142+
this.memoryCache.Set(this.GetMsalAccountId(), this.UserTokenCache.SerializeMsalV3(), this.cacheDuration);
143+
}
144+
145+
/// <summary>
146+
/// Clears the TokenCache's copy of this user's cache.
147+
/// </summary>
148+
public void Clear()
149+
{
150+
this.memoryCache.Remove(this.GetMsalAccountId());
151+
152+
// Nulls the currently deserialized instance
153+
this.LoadUserTokenCacheFromMemory();
154+
}
155+
156+
/// <summary>
157+
/// Triggered right after MSAL accessed the cache.
158+
/// </summary>
159+
/// <param name="args">Contains parameters used by the MSAL call accessing the cache.</param>
160+
private void UserTokenCacheAfterAccessNotification(TokenCacheNotificationArgs args)
161+
{
162+
this.SetSignedInUserFromNotificationArgs(args);
163+
164+
// if the access operation resulted in a cache update
165+
if (args.HasStateChanged)
166+
{
167+
this.PersistUserTokenCache();
168+
}
169+
}
170+
171+
/// <summary>
172+
/// Triggered right before MSAL needs to access the cache. Reload the cache from the persistence store in case it changed since the last access.
173+
/// </summary>
174+
/// <param name="args">Contains parameters used by the MSAL call accessing the cache.</param>
175+
private void UserTokenCacheBeforeAccessNotification(TokenCacheNotificationArgs args)
176+
{
177+
this.LoadUserTokenCacheFromMemory();
178+
}
179+
180+
/// <summary>
181+
/// if you want to ensure that no concurrent write take place, use this notification to place a lock on the entry
182+
/// </summary>
183+
/// <param name="args">Contains parameters used by the MSAL call accessing the cache.</param>
184+
private void UserTokenCacheBeforeWriteNotification(TokenCacheNotificationArgs args)
185+
{
186+
// Since we are using a MemoryCache ,whose methods are threads safe, we need not to do anything in this handler.
187+
}
188+
189+
/// <summary>
190+
/// To keep the cache, ClaimsPrincipal and Sql in sync, we ensure that the user's object Id we obtained by MSAL after
191+
/// successful sign-in is set as the key for the cache.
192+
/// </summary>
193+
/// <param name="args">Contains parameters used by the MSAL call accessing the cache.</param>
194+
private void SetSignedInUserFromNotificationArgs(TokenCacheNotificationArgs args)
195+
{
196+
if (this.SignedInUser == null && args.Account != null)
197+
{
198+
this.SignedInUser = args.Account.ToClaimsPrincipal();
199+
}
200+
}
201+
}
202+
}

0 commit comments

Comments
 (0)