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