0
# Security and Advanced Features
1
2
MSAL Python provides advanced security features including Proof-of-Possession (PoP) tokens for enhanced security, certificate-based authentication, custom authentication schemes, comprehensive error handling, and various utility functions for certificate management and JWT token processing.
3
4
## Capabilities
5
6
### Proof-of-Possession (PoP) Authentication
7
8
PoP tokens provide enhanced security by cryptographically binding access tokens to specific HTTP requests, preventing token replay attacks and improving overall security posture.
9
10
```python { .api }
11
class PopAuthScheme:
12
# HTTP method constants
13
HTTP_GET = "GET"
14
HTTP_POST = "POST"
15
HTTP_PUT = "PUT"
16
HTTP_DELETE = "DELETE"
17
HTTP_PATCH = "PATCH"
18
19
def __init__(
20
self,
21
http_method: str = None,
22
url: str = None,
23
nonce: str = None
24
):
25
"""
26
Create Proof-of-Possession authentication scheme.
27
28
Parameters:
29
- http_method: HTTP method (GET, POST, PUT, DELETE, PATCH)
30
- url: Full URL to be signed
31
- nonce: Nonce from resource server challenge
32
33
All parameters are required for PoP token creation.
34
"""
35
```
36
37
Usage example:
38
39
```python
40
import msal
41
42
# Create PoP auth scheme for specific HTTP request
43
pop_scheme = msal.PopAuthScheme(
44
http_method=msal.PopAuthScheme.HTTP_GET,
45
url="https://graph.microsoft.com/v1.0/me",
46
nonce="nonce-from-resource-server"
47
)
48
49
# Note: PoP tokens are currently only available through broker
50
app = msal.PublicClientApplication(
51
client_id="your-client-id",
52
authority="https://login.microsoftonline.com/common",
53
enable_broker_on_windows=True # PoP requires broker
54
)
55
56
# Acquire PoP token (broker-enabled apps only)
57
result = app.acquire_token_interactive(
58
scopes=["User.Read"],
59
auth_scheme=pop_scheme # Include PoP scheme
60
)
61
62
if "access_token" in result:
63
print("PoP token acquired successfully!")
64
pop_token = result["access_token"]
65
66
# Use PoP token in HTTP request
67
import requests
68
headers = {
69
"Authorization": f"PoP {pop_token}",
70
"Content-Type": "application/json"
71
}
72
response = requests.get("https://graph.microsoft.com/v1.0/me", headers=headers)
73
else:
74
print(f"PoP token acquisition failed: {result.get('error_description')}")
75
```
76
77
### Certificate Management
78
79
Utilities for handling X.509 certificates in various formats for certificate-based authentication.
80
81
```python { .api }
82
def extract_certs(public_cert_content: str) -> list:
83
"""
84
Parse public certificate content and extract certificate strings.
85
86
Parameters:
87
- public_cert_content: Raw certificate content (PEM format or base64)
88
89
Returns:
90
List of certificate strings suitable for x5c JWT header
91
92
Raises:
93
ValueError: If private key content is detected instead of public certificate
94
"""
95
```
96
97
Usage example:
98
99
```python
100
import msal
101
102
# Load certificate from file
103
with open("certificate.pem", "r") as f:
104
cert_content = f.read()
105
106
# Extract certificates for JWT header
107
try:
108
certificates = msal.extract_certs(cert_content)
109
print(f"Extracted {len(certificates)} certificates")
110
111
# Use in confidential client application
112
client_credential = {
113
"private_key": "-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----",
114
"thumbprint": "A1B2C3D4E5F6...",
115
"public_certificate": cert_content # For Subject Name/Issuer auth
116
}
117
118
app = msal.ConfidentialClientApplication(
119
client_id="your-client-id",
120
client_credential=client_credential
121
)
122
123
except ValueError as e:
124
print(f"Certificate parsing error: {e}")
125
```
126
127
### Certificate-based Authentication Examples
128
129
#### Basic Certificate Authentication
130
```python
131
import msal
132
133
# Certificate with private key and thumbprint
134
client_credential = {
135
"private_key": """-----BEGIN PRIVATE KEY-----
136
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC...
137
-----END PRIVATE KEY-----""",
138
"thumbprint": "A1B2C3D4E5F6789012345678901234567890ABCD"
139
}
140
141
app = msal.ConfidentialClientApplication(
142
client_id="your-client-id",
143
client_credential=client_credential,
144
authority="https://login.microsoftonline.com/your-tenant-id"
145
)
146
147
result = app.acquire_token_for_client(
148
scopes=["https://graph.microsoft.com/.default"]
149
)
150
```
151
152
#### Certificate with Passphrase
153
```python
154
import msal
155
156
# Encrypted private key with passphrase
157
client_credential = {
158
"private_key": """-----BEGIN ENCRYPTED PRIVATE KEY-----
159
MIIFHDBOBgkqhkiG9w0BBQ0wQTApBgkqhkiG9w0BBQwwHAQI...
160
-----END ENCRYPTED PRIVATE KEY-----""",
161
"thumbprint": "A1B2C3D4E5F6789012345678901234567890ABCD",
162
"passphrase": "your-certificate-passphrase"
163
}
164
165
app = msal.ConfidentialClientApplication(
166
client_id="your-client-id",
167
client_credential=client_credential
168
)
169
```
170
171
#### Subject Name/Issuer Authentication (SNI)
172
```python
173
import msal
174
175
# SNI authentication for certificate auto-rotation
176
client_credential = {
177
"private_key": """-----BEGIN PRIVATE KEY-----
178
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC...
179
-----END PRIVATE KEY-----""",
180
"thumbprint": "A1B2C3D4E5F6789012345678901234567890ABCD",
181
"public_certificate": """-----BEGIN CERTIFICATE-----
182
MIIDXTCCAkWgAwIBAgIJAKhwU2Y4bkFoMA0GCSqGSIb3DQEBCwUA...
183
-----END CERTIFICATE-----"""
184
}
185
186
app = msal.ConfidentialClientApplication(
187
client_id="your-client-id",
188
client_credential=client_credential
189
)
190
191
# SNI allows certificate rotation without updating application registration
192
result = app.acquire_token_for_client(
193
scopes=["https://graph.microsoft.com/.default"]
194
)
195
```
196
197
### Prompt Constants
198
199
OIDC prompt parameter constants for controlling authentication UI behavior.
200
201
```python { .api }
202
class Prompt:
203
NONE = "none" # No UI, fail if interaction required
204
LOGIN = "login" # Force re-authentication
205
CONSENT = "consent" # Force consent screen
206
SELECT_ACCOUNT = "select_account" # Show account picker
207
CREATE = "create" # Show account creation option
208
```
209
210
Usage example:
211
212
```python
213
import msal
214
215
app = msal.PublicClientApplication(
216
client_id="your-client-id",
217
authority="https://login.microsoftonline.com/common"
218
)
219
220
# Force account selection
221
result = app.acquire_token_interactive(
222
scopes=["User.Read"],
223
prompt=msal.Prompt.SELECT_ACCOUNT
224
)
225
226
# Force re-authentication (ignore SSO)
227
result = app.acquire_token_interactive(
228
scopes=["User.Read"],
229
prompt=msal.Prompt.LOGIN
230
)
231
232
# Force consent (even if previously granted)
233
result = app.acquire_token_interactive(
234
scopes=["User.Read", "Mail.Read"],
235
prompt=msal.Prompt.CONSENT
236
)
237
238
# Fail if any interaction required
239
result = app.acquire_token_interactive(
240
scopes=["User.Read"],
241
prompt=msal.Prompt.NONE
242
)
243
```
244
245
### JWT Token Processing
246
247
Utilities for decoding and validating JWT tokens, particularly ID tokens.
248
249
```python { .api }
250
def decode_part(raw, encoding="utf-8"):
251
"""
252
Decode a part of JWT token.
253
254
Parameters:
255
- raw: Base64-encoded JWT part
256
- encoding: Character encoding (default: utf-8), use None for binary output
257
258
Returns:
259
Decoded string or binary data based on encoding parameter
260
"""
261
262
def decode_id_token(
263
id_token,
264
client_id=None,
265
issuer=None,
266
nonce=None,
267
now=None
268
):
269
"""
270
Decode and validate ID token.
271
272
Parameters:
273
- id_token: JWT ID token string
274
- client_id: Expected audience (client ID)
275
- issuer: Expected issuer
276
- nonce: Expected nonce value
277
- now: Current time for expiration checking
278
279
Returns:
280
Dictionary of ID token claims
281
282
Raises:
283
IdTokenError: If token is invalid or validation fails
284
"""
285
```
286
287
Usage example:
288
289
```python
290
import msal
291
import time
292
293
app = msal.PublicClientApplication(
294
client_id="your-client-id",
295
authority="https://login.microsoftonline.com/common"
296
)
297
298
# Acquire token with ID token
299
result = app.acquire_token_interactive(
300
scopes=["openid", "profile", "User.Read"]
301
)
302
303
if "id_token" in result:
304
id_token = result["id_token"]
305
306
try:
307
# Decode and validate ID token
308
id_token_claims = msal.decode_id_token(
309
id_token=id_token,
310
client_id="your-client-id",
311
now=int(time.time())
312
)
313
314
print("ID Token Claims:")
315
print(f" Subject: {id_token_claims.get('sub')}")
316
print(f" Name: {id_token_claims.get('name')}")
317
print(f" Email: {id_token_claims.get('preferred_username')}")
318
print(f" Issued at: {id_token_claims.get('iat')}")
319
print(f" Expires at: {id_token_claims.get('exp')}")
320
321
except msal.IdTokenError as e:
322
print(f"ID token validation failed: {e}")
323
324
# Decode individual JWT parts
325
try:
326
# Split JWT into parts
327
header, payload, signature = id_token.split('.')
328
329
# Decode header
330
header_claims = msal.decode_part(header)
331
print(f"JWT Algorithm: {header_claims.get('alg')}")
332
print(f"Token Type: {header_claims.get('typ')}")
333
334
# Decode payload
335
payload_claims = msal.decode_part(payload)
336
print(f"Issuer: {payload_claims.get('iss')}")
337
print(f"Audience: {payload_claims.get('aud')}")
338
339
except Exception as e:
340
print(f"JWT decoding error: {e}")
341
```
342
343
### Advanced Authentication Scenarios
344
345
#### Custom HTTP Client
346
```python
347
import msal
348
import requests
349
from requests.adapters import HTTPAdapter
350
from urllib3.util.retry import Retry
351
352
# Custom HTTP client with retry logic
353
class RetryHTTPClient:
354
def __init__(self):
355
self.session = requests.Session()
356
357
# Configure retry strategy
358
retry_strategy = Retry(
359
total=3,
360
backoff_factor=1,
361
status_forcelist=[429, 500, 502, 503, 504]
362
)
363
364
adapter = HTTPAdapter(max_retries=retry_strategy)
365
self.session.mount("http://", adapter)
366
self.session.mount("https://", adapter)
367
368
def post(self, url, **kwargs):
369
return self.session.post(url, **kwargs)
370
371
def get(self, url, **kwargs):
372
return self.session.get(url, **kwargs)
373
374
# Use custom HTTP client
375
custom_http_client = RetryHTTPClient()
376
377
app = msal.ConfidentialClientApplication(
378
client_id="your-client-id",
379
client_credential="your-client-secret",
380
http_client=custom_http_client
381
)
382
```
383
384
#### Regional Endpoints and Performance
385
```python
386
import msal
387
388
# Use Azure regional endpoints for better performance
389
app = msal.ConfidentialClientApplication(
390
client_id="your-client-id",
391
client_credential="your-client-secret",
392
authority="https://login.microsoftonline.com/your-tenant-id",
393
azure_region="eastus" # Specify region
394
)
395
396
# Auto-detect region in Azure environment
397
app = msal.ConfidentialClientApplication(
398
client_id="your-client-id",
399
client_credential="your-client-secret",
400
azure_region=msal.ClientApplication.ATTEMPT_REGION_DISCOVERY
401
)
402
403
# Check if PoP is supported
404
if app.is_pop_supported():
405
print("Proof-of-Possession tokens are supported")
406
# Use PoP authentication scheme
407
else:
408
print("PoP not supported without broker")
409
```
410
411
#### Claims Challenges and Conditional Access
412
```python
413
import msal
414
import json
415
416
app = msal.PublicClientApplication(
417
client_id="your-client-id",
418
authority="https://login.microsoftonline.com/your-tenant-id"
419
)
420
421
# Handle claims challenge from Conditional Access
422
def handle_claims_challenge(claims_challenge_header):
423
"""Parse and handle claims challenge from API response."""
424
425
# Extract claims challenge from WWW-Authenticate header
426
# Format: Bearer authorization_uri="...", error="insufficient_claims", claims="..."
427
if "claims=" in claims_challenge_header:
428
claims_start = claims_challenge_header.find('claims="') + 8
429
claims_end = claims_challenge_header.find('"', claims_start)
430
claims_challenge = claims_challenge_header[claims_start:claims_end]
431
432
# Parse claims challenge JSON
433
try:
434
claims_dict = json.loads(claims_challenge)
435
return claims_challenge
436
except json.JSONDecodeError:
437
return None
438
439
return None
440
441
# Initial token acquisition
442
result = app.acquire_token_interactive(scopes=["User.Read"])
443
444
if "access_token" in result:
445
access_token = result["access_token"]
446
447
# Call API that may require additional claims
448
import requests
449
headers = {"Authorization": f"Bearer {access_token}"}
450
response = requests.get("https://graph.microsoft.com/v1.0/me", headers=headers)
451
452
if response.status_code == 401:
453
# Check for claims challenge
454
www_auth_header = response.headers.get("WWW-Authenticate", "")
455
claims_challenge = handle_claims_challenge(www_auth_header)
456
457
if claims_challenge:
458
print("Claims challenge detected, re-authenticating...")
459
460
# Re-authenticate with claims challenge
461
result = app.acquire_token_interactive(
462
scopes=["User.Read"],
463
claims_challenge=claims_challenge
464
)
465
466
if "access_token" in result:
467
# Retry API call with new token
468
new_token = result["access_token"]
469
headers = {"Authorization": f"Bearer {new_token}"}
470
response = requests.get("https://graph.microsoft.com/v1.0/me", headers=headers)
471
```
472
473
### Comprehensive Error Handling
474
475
Advanced error handling patterns for robust applications:
476
477
```python
478
import msal
479
import logging
480
import time
481
482
# Configure logging
483
logging.basicConfig(level=logging.INFO)
484
logger = logging.getLogger(__name__)
485
486
class MSALAuthHandler:
487
def __init__(self, client_id, authority):
488
self.app = msal.PublicClientApplication(
489
client_id=client_id,
490
authority=authority
491
)
492
self.max_retries = 3
493
self.retry_delay = 1 # seconds
494
495
def acquire_token_with_retry(self, scopes, **kwargs):
496
"""Acquire token with automatic retry logic."""
497
498
for attempt in range(self.max_retries):
499
try:
500
# Try silent first if we have accounts
501
accounts = self.app.get_accounts()
502
if accounts:
503
result = self.app.acquire_token_silent(
504
scopes=scopes,
505
account=accounts[0]
506
)
507
508
if "access_token" in result:
509
logger.info("Silent authentication successful")
510
return result
511
512
error = result.get("error")
513
if error == "interaction_required":
514
logger.info("Interaction required, falling back to interactive")
515
elif error in ["invalid_grant", "token_expired"]:
516
logger.warning(f"Token issue: {error}, removing account")
517
self.app.remove_account(accounts[0])
518
519
# Fall back to interactive
520
result = self.app.acquire_token_interactive(scopes=scopes, **kwargs)
521
522
if "access_token" in result:
523
logger.info("Interactive authentication successful")
524
return result
525
526
# Handle specific errors
527
error = result.get("error")
528
error_description = result.get("error_description", "")
529
530
if error == "access_denied":
531
logger.error("User denied consent")
532
return result
533
elif error == "invalid_scope":
534
logger.error(f"Invalid scope: {error_description}")
535
return result
536
elif error in ["server_error", "temporarily_unavailable"]:
537
if attempt < self.max_retries - 1:
538
logger.warning(f"Server error, retrying in {self.retry_delay}s...")
539
time.sleep(self.retry_delay)
540
self.retry_delay *= 2 # Exponential backoff
541
continue
542
543
logger.error(f"Authentication failed: {error} - {error_description}")
544
return result
545
546
except msal.BrowserInteractionTimeoutError:
547
logger.error("Browser interaction timed out")
548
if attempt < self.max_retries - 1:
549
logger.info("Retrying authentication...")
550
continue
551
return {"error": "timeout", "error_description": "Browser interaction timed out"}
552
553
except Exception as e:
554
logger.error(f"Unexpected error: {e}")
555
if attempt < self.max_retries - 1:
556
logger.info("Retrying after unexpected error...")
557
time.sleep(self.retry_delay)
558
continue
559
return {"error": "unexpected_error", "error_description": str(e)}
560
561
return {"error": "max_retries_exceeded", "error_description": "Failed after maximum retry attempts"}
562
563
# Usage
564
auth_handler = MSALAuthHandler(
565
client_id="your-client-id",
566
authority="https://login.microsoftonline.com/common"
567
)
568
569
result = auth_handler.acquire_token_with_retry(
570
scopes=["User.Read", "Mail.Read"],
571
timeout=120
572
)
573
574
if "access_token" in result:
575
print("Authentication successful with retry logic!")
576
else:
577
print(f"Authentication ultimately failed: {result.get('error_description')}")
578
```
579
580
### Security Best Practices
581
582
#### Secure Token Storage
583
```python
584
import msal
585
import keyring
586
import json
587
588
class SecureTokenCache(msal.SerializableTokenCache):
589
def __init__(self, service_name, username):
590
super().__init__()
591
self.service_name = service_name
592
self.username = username
593
594
# Load from secure storage
595
try:
596
cache_data = keyring.get_password(service_name, username)
597
if cache_data:
598
self.deserialize(cache_data)
599
except Exception as e:
600
print(f"Warning: Could not load secure cache: {e}")
601
602
# Register cleanup
603
import atexit
604
atexit.register(self._save_cache)
605
606
def _save_cache(self):
607
if self.has_state_changed:
608
try:
609
keyring.set_password(
610
self.service_name,
611
self.username,
612
self.serialize()
613
)
614
except Exception as e:
615
print(f"Warning: Could not save secure cache: {e}")
616
617
# Usage with secure storage
618
secure_cache = SecureTokenCache("MyApp", "TokenCache")
619
620
app = msal.PublicClientApplication(
621
client_id="your-client-id",
622
token_cache=secure_cache
623
)
624
```
625
626
#### Environment-based Configuration
627
```python
628
import msal
629
import os
630
from typing import Optional
631
632
def create_msal_app(
633
client_type: str = "public",
634
enable_logging: bool = False
635
) -> Optional[msal.ClientApplication]:
636
"""Create MSAL application with environment-based configuration."""
637
638
# Required environment variables
639
client_id = os.environ.get("AZURE_CLIENT_ID")
640
if not client_id:
641
raise ValueError("AZURE_CLIENT_ID environment variable required")
642
643
# Optional configuration
644
authority = os.environ.get("AZURE_AUTHORITY", "https://login.microsoftonline.com/common")
645
646
# Enable PII logging if requested
647
if enable_logging:
648
logging.basicConfig(level=logging.DEBUG)
649
enable_pii_log = True
650
else:
651
enable_pii_log = False
652
653
if client_type == "public":
654
return msal.PublicClientApplication(
655
client_id=client_id,
656
authority=authority,
657
enable_pii_log=enable_pii_log
658
)
659
elif client_type == "confidential":
660
# Confidential client requires credential
661
client_secret = os.environ.get("AZURE_CLIENT_SECRET")
662
cert_path = os.environ.get("AZURE_CERTIFICATE_PATH")
663
cert_thumbprint = os.environ.get("AZURE_CERTIFICATE_THUMBPRINT")
664
665
if client_secret:
666
client_credential = client_secret
667
elif cert_path and cert_thumbprint:
668
with open(cert_path, 'r') as f:
669
private_key = f.read()
670
client_credential = {
671
"private_key": private_key,
672
"thumbprint": cert_thumbprint
673
}
674
else:
675
raise ValueError("Either AZURE_CLIENT_SECRET or certificate configuration required")
676
677
return msal.ConfidentialClientApplication(
678
client_id=client_id,
679
client_credential=client_credential,
680
authority=authority,
681
enable_pii_log=enable_pii_log
682
)
683
else:
684
raise ValueError("client_type must be 'public' or 'confidential'")
685
686
# Usage
687
try:
688
app = create_msal_app(client_type="public", enable_logging=True)
689
result = app.acquire_token_interactive(scopes=["User.Read"])
690
except ValueError as e:
691
print(f"Configuration error: {e}")
692
```