or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

common-auth-flows.mdconfidential-client.mdindex.mdmanaged-identity.mdpublic-client.mdsecurity-advanced.mdtoken-cache.md

security-advanced.mddocs/

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

```