0
# Token Caching
1
2
MSAL Python provides comprehensive token caching capabilities to minimize authentication requests and improve application performance. The cache system supports both in-memory caching for single-session scenarios and persistent caching for applications that need to maintain tokens across sessions.
3
4
## Capabilities
5
6
### TokenCache (In-Memory)
7
8
Base token cache class that maintains tokens in memory using a unified schema compatible across all MSAL libraries. Automatically handles token refresh and account management.
9
10
```python { .api }
11
class TokenCache:
12
def __init__(self):
13
"""Create an in-memory token cache."""
14
15
def find(
16
self,
17
credential_type: str,
18
target=None,
19
query=None,
20
*,
21
now=None
22
) -> list:
23
"""
24
Find matching cache entries.
25
26
.. deprecated::
27
Use list(search(...)) instead to explicitly get a list.
28
29
Parameters:
30
- credential_type: Type of credential (ACCESS_TOKEN, REFRESH_TOKEN, ACCOUNT, ID_TOKEN)
31
- target: Scope string for filtering
32
- query: Additional query parameters dict
33
- now: Current time for expiration checking
34
35
Returns:
36
List of matching cache entries
37
"""
38
39
def search(
40
self,
41
credential_type: str,
42
target=None,
43
query=None,
44
now=None
45
):
46
"""
47
Generator version of find() for memory efficiency.
48
49
Returns:
50
Generator yielding matching cache entries
51
"""
52
53
def add(self, event: dict, now=None):
54
"""
55
Add token to cache from authentication event.
56
57
Parameters:
58
- event: Authentication result dictionary
59
- now: Current time override
60
"""
61
62
def modify(
63
self,
64
credential_type: str,
65
old_entry: dict,
66
new_key_value_pairs=None
67
):
68
"""
69
Modify existing cache entry.
70
71
Parameters:
72
- credential_type: Type of credential to modify
73
- old_entry: Existing cache entry
74
- new_key_value_pairs: Dict of new values, or None to remove entry
75
"""
76
```
77
78
### Credential Types
79
80
```python { .api }
81
class TokenCache:
82
class CredentialType:
83
ACCESS_TOKEN = "AccessToken"
84
REFRESH_TOKEN = "RefreshToken"
85
ACCOUNT = "Account"
86
ID_TOKEN = "IdToken"
87
APP_METADATA = "AppMetadata"
88
89
class AuthorityType:
90
ADFS = "ADFS"
91
MSSTS = "MSSTS" # AAD v2 for both AAD & MSA
92
```
93
94
### Token Management Methods
95
96
Specific methods for managing different token types:
97
98
```python { .api }
99
def remove_rt(self, rt_item: dict):
100
"""Remove refresh token from cache."""
101
102
def update_rt(self, rt_item: dict, new_rt: str):
103
"""Update refresh token in cache."""
104
105
def remove_at(self, at_item: dict):
106
"""Remove access token from cache."""
107
108
def remove_idt(self, idt_item: dict):
109
"""Remove ID token from cache."""
110
111
def remove_account(self, account_item: dict):
112
"""Remove account from cache."""
113
```
114
115
Usage example:
116
117
```python
118
import msal
119
120
# Create custom cache
121
cache = msal.TokenCache()
122
123
# Use cache with application
124
app = msal.PublicClientApplication(
125
client_id="your-client-id",
126
token_cache=cache
127
)
128
129
# After authentication, tokens are automatically cached
130
result = app.acquire_token_interactive(scopes=["User.Read"])
131
132
# Find cached access tokens
133
access_tokens = cache.find(
134
credential_type=cache.CredentialType.ACCESS_TOKEN,
135
target="User.Read"
136
)
137
138
print(f"Found {len(access_tokens)} cached access tokens")
139
140
# Find accounts
141
accounts = cache.find(credential_type=cache.CredentialType.ACCOUNT)
142
for account in accounts:
143
print(f"Account: {account.get('username')}")
144
```
145
146
### SerializableTokenCache (Persistent)
147
148
Extended token cache that supports serialization to/from persistent storage. Tracks state changes to optimize serialization operations.
149
150
```python { .api }
151
class SerializableTokenCache(TokenCache):
152
def __init__(self):
153
"""Create a serializable token cache."""
154
155
def serialize(self) -> str:
156
"""
157
Serialize current cache state to JSON string.
158
159
Returns:
160
JSON string representation of cache state
161
"""
162
163
def deserialize(self, state: str):
164
"""
165
Deserialize cache from JSON string.
166
167
Parameters:
168
- state: JSON string from previous serialize() call
169
"""
170
171
def add(self, event: dict, **kwargs):
172
"""
173
Add token and mark cache as changed.
174
175
Parameters:
176
- event: Authentication result dictionary
177
"""
178
179
def modify(
180
self,
181
credential_type: str,
182
old_entry: dict,
183
new_key_value_pairs=None
184
):
185
"""
186
Modify cache entry and mark cache as changed.
187
188
Parameters:
189
- credential_type: Type of credential to modify
190
- old_entry: Existing cache entry
191
- new_key_value_pairs: Dict of new values, or None to remove entry
192
"""
193
194
@property
195
def has_state_changed(self) -> bool:
196
"""Check if cache state has changed since last serialization."""
197
198
def _mark_as_changed(self):
199
"""Mark cache as changed (internal use)."""
200
```
201
202
### File-Based Cache Implementation
203
204
Example implementation of persistent file-based cache:
205
206
```python
207
import msal
208
import os
209
import json
210
import atexit
211
212
class FilePersistenceTokenCache(msal.SerializableTokenCache):
213
def __init__(self, file_path):
214
super().__init__()
215
self.file_path = file_path
216
217
# Load existing cache
218
if os.path.exists(file_path):
219
try:
220
with open(file_path, 'r') as f:
221
cache_data = f.read()
222
if cache_data:
223
self.deserialize(cache_data)
224
except Exception as e:
225
print(f"Warning: Failed to load cache: {e}")
226
227
# Save cache on exit
228
atexit.register(self._save_cache)
229
230
def _save_cache(self):
231
if self.has_state_changed:
232
try:
233
with open(self.file_path, 'w') as f:
234
f.write(self.serialize())
235
except Exception as e:
236
print(f"Warning: Failed to save cache: {e}")
237
238
# Usage
239
cache = FilePersistenceTokenCache("token_cache.json")
240
241
app = msal.PublicClientApplication(
242
client_id="your-client-id",
243
token_cache=cache
244
)
245
246
# First run - tokens will be cached to file
247
result = app.acquire_token_interactive(scopes=["User.Read"])
248
249
# Subsequent runs - tokens loaded from file
250
accounts = app.get_accounts()
251
if accounts:
252
result = app.acquire_token_silent(scopes=["User.Read"], account=accounts[0])
253
```
254
255
### Cross-Application Token Sharing
256
257
Multiple applications can share the same cache for single sign-on experiences:
258
259
```python
260
import msal
261
262
# Shared cache file
263
shared_cache = FilePersistenceTokenCache("shared_cache.json")
264
265
# Application 1
266
app1 = msal.PublicClientApplication(
267
client_id="app1-client-id",
268
token_cache=shared_cache
269
)
270
271
# Application 2
272
app2 = msal.PublicClientApplication(
273
client_id="app2-client-id",
274
token_cache=shared_cache
275
)
276
277
# User authenticates in app1
278
result1 = app1.acquire_token_interactive(scopes=["User.Read"])
279
280
# App2 can use cached tokens for same user
281
accounts = app2.get_accounts()
282
if accounts:
283
result2 = app2.acquire_token_silent(
284
scopes=["User.Read"],
285
account=accounts[0]
286
)
287
```
288
289
### Cache Querying and Management
290
291
Advanced cache operations for monitoring and managing cached tokens:
292
293
```python
294
import msal
295
from datetime import datetime
296
297
app = msal.PublicClientApplication(
298
client_id="your-client-id",
299
token_cache=cache
300
)
301
302
# Get all accounts
303
accounts = app.get_accounts()
304
print(f"Cached accounts: {len(accounts)}")
305
306
for account in accounts:
307
print(f" User: {account.get('username')}")
308
print(f" Environment: {account.get('environment')}")
309
print(f" Home Account ID: {account.get('home_account_id')}")
310
311
# Find all access tokens
312
access_tokens = cache.find(
313
credential_type=cache.CredentialType.ACCESS_TOKEN
314
)
315
316
print(f"\nCached access tokens: {len(access_tokens)}")
317
for token in access_tokens:
318
scopes = token.get('target', 'Unknown scopes')
319
expires_on = token.get('expires_on')
320
if expires_on:
321
expires_dt = datetime.fromtimestamp(int(expires_on))
322
print(f" Scopes: {scopes}")
323
print(f" Expires: {expires_dt}")
324
print(f" Expired: {expires_dt < datetime.now()}")
325
326
# Find refresh tokens
327
refresh_tokens = cache.find(
328
credential_type=cache.CredentialType.REFRESH_TOKEN
329
)
330
print(f"\nCached refresh tokens: {len(refresh_tokens)}")
331
332
# Remove specific account
333
if accounts:
334
app.remove_account(accounts[0])
335
print("Removed first account from cache")
336
```
337
338
### Cache Security Considerations
339
340
Important security practices for token caching:
341
342
```python
343
import msal
344
import os
345
import stat
346
347
class SecureFileCache(msal.SerializableTokenCache):
348
def __init__(self, file_path):
349
super().__init__()
350
self.file_path = file_path
351
352
# Load with secure permissions
353
if os.path.exists(file_path):
354
# Verify file permissions (owner read/write only)
355
file_stat = os.stat(file_path)
356
if file_stat.st_mode & 0o077: # Check if group/other have permissions
357
print("Warning: Cache file has insecure permissions")
358
359
with open(file_path, 'r') as f:
360
self.deserialize(f.read())
361
362
atexit.register(self._save_cache)
363
364
def _save_cache(self):
365
if self.has_state_changed:
366
# Create file with secure permissions
367
with open(self.file_path, 'w') as f:
368
f.write(self.serialize())
369
370
# Set restrictive permissions (owner only)
371
os.chmod(self.file_path, stat.S_IRUSR | stat.S_IWUSR)
372
373
# Additional security measures:
374
# 1. Store cache files in user-specific directories
375
# 2. Encrypt cache contents for sensitive environments
376
# 3. Set appropriate file system permissions
377
# 4. Consider in-memory cache for highly sensitive applications
378
# 5. Implement cache expiration and cleanup policies
379
```
380
381
## Error Handling
382
383
Cache-related error handling patterns:
384
385
```python
386
try:
387
cache = FilePersistenceTokenCache("cache.json")
388
389
app = msal.PublicClientApplication(
390
client_id="your-client-id",
391
token_cache=cache
392
)
393
394
# Attempt silent authentication
395
accounts = app.get_accounts()
396
if accounts:
397
result = app.acquire_token_silent(
398
scopes=["User.Read"],
399
account=accounts[0]
400
)
401
402
if "access_token" not in result:
403
# Silent auth failed, try interactive
404
print("Silent authentication failed, trying interactive...")
405
result = app.acquire_token_interactive(scopes=["User.Read"])
406
else:
407
# No cached accounts
408
print("No cached accounts found, using interactive authentication...")
409
result = app.acquire_token_interactive(scopes=["User.Read"])
410
411
except Exception as e:
412
print(f"Cache operation failed: {e}")
413
# Fall back to default in-memory cache
414
app = msal.PublicClientApplication(client_id="your-client-id")
415
```