Skip to content

Commit 6b904af

Browse files
authored
Merge pull request #110 from AzureAD/persistence-factory
Persistence factory
2 parents fa1f45b + d0696ae commit 6b904af

File tree

8 files changed

+53
-101
lines changed

8 files changed

+53
-101
lines changed

msal_extensions/__init__.py

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,13 @@
11
"""Provides auxiliary functionality to the `msal` package."""
22
__version__ = "0.3.1"
33

4-
import sys
5-
64
from .persistence import (
75
FilePersistence,
6+
build_encrypted_persistence,
87
FilePersistenceWithDataProtection,
98
KeychainPersistence,
109
LibsecretPersistence,
1110
)
1211
from .cache_lock import CrossPlatLock
1312
from .token_cache import PersistedTokenCache
1413

15-
if sys.platform.startswith('win'):
16-
from .token_cache import WindowsTokenCache as TokenCache
17-
elif sys.platform.startswith('darwin'):
18-
from .token_cache import OSXTokenCache as TokenCache
19-
else:
20-
from .token_cache import FileTokenCache as TokenCache

msal_extensions/persistence.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,22 @@ class PersistenceDecryptionError(PersistenceError):
8383
"""This could be raised by persistence.load()"""
8484

8585

86+
def build_encrypted_persistence(location):
87+
"""Build a suitable encrypted persistence instance based your current OS.
88+
89+
If you do not need encryption, then simply use ``FilePersistence`` constructor.
90+
"""
91+
# Does not (yet?) support fallback_to_plaintext flag,
92+
# because the persistence on Windows and macOS do not support built-in trial_run().
93+
if sys.platform.startswith('win'):
94+
return FilePersistenceWithDataProtection(location)
95+
if sys.platform.startswith('darwin'):
96+
return KeychainPersistence(location)
97+
if sys.platform.startswith('linux'):
98+
return LibsecretPersistence(location)
99+
raise RuntimeError("Unsupported platform: {}".format(sys.platform)) # pylint: disable=consider-using-f-string
100+
101+
86102
class BasePersistence(ABC):
87103
"""An abstract persistence defining the common interface of this family"""
88104

msal_extensions/token_cache.py

Lines changed: 1 addition & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,12 @@
11
"""Generic functions and types for working with a TokenCache that is not platform specific."""
22
import os
3-
import warnings
43
import time
54
import logging
65

76
import msal
87

98
from .cache_lock import CrossPlatLock
10-
from .persistence import (
11-
_mkdir_p, PersistenceNotFound, FilePersistence,
12-
FilePersistenceWithDataProtection, KeychainPersistence)
9+
from .persistence import _mkdir_p, PersistenceNotFound
1310

1411

1512
logger = logging.getLogger(__name__)
@@ -89,35 +86,3 @@ def find(self, credential_type, **kwargs): # pylint: disable=arguments-differ
8986
return super(PersistedTokenCache, self).find(credential_type, **kwargs)
9087
return [] # Not really reachable here. Just to keep pylint happy.
9188

92-
93-
class FileTokenCache(PersistedTokenCache):
94-
"""A token cache which uses plain text file to store your tokens."""
95-
def __init__(self, cache_location, **ignored): # pylint: disable=unused-argument
96-
warnings.warn("You are using an unprotected token cache", RuntimeWarning)
97-
warnings.warn("Use PersistedTokenCache(...) instead", DeprecationWarning)
98-
super(FileTokenCache, self).__init__(FilePersistence(cache_location))
99-
100-
UnencryptedTokenCache = FileTokenCache # For backward compatibility
101-
102-
103-
class WindowsTokenCache(PersistedTokenCache):
104-
"""A token cache which uses Windows DPAPI to encrypt your tokens."""
105-
def __init__(
106-
self, cache_location, entropy='',
107-
**ignored): # pylint: disable=unused-argument
108-
warnings.warn("Use PersistedTokenCache(...) instead", DeprecationWarning)
109-
super(WindowsTokenCache, self).__init__(
110-
FilePersistenceWithDataProtection(cache_location, entropy=entropy))
111-
112-
113-
class OSXTokenCache(PersistedTokenCache):
114-
"""A token cache which uses native Keychain libraries to encrypt your tokens."""
115-
def __init__(self,
116-
cache_location,
117-
service_name='Microsoft.Developer.IdentityService',
118-
account_name='MSALCache',
119-
**ignored): # pylint: disable=unused-argument
120-
warnings.warn("Use PersistedTokenCache(...) instead", DeprecationWarning)
121-
super(OSXTokenCache, self).__init__(
122-
KeychainPersistence(cache_location, service_name, account_name))
123-

sample/persistence_sample.py

Lines changed: 11 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,21 @@
1-
import sys
21
import logging
32
import json
43

5-
from msal_extensions import *
4+
from msal_extensions import build_encrypted_persistence, FilePersistence, CrossPlatLock
65

76

87
def build_persistence(location, fallback_to_plaintext=False):
98
"""Build a suitable persistence instance based your current OS"""
10-
if sys.platform.startswith('win'):
11-
return FilePersistenceWithDataProtection(location)
12-
if sys.platform.startswith('darwin'):
13-
return KeychainPersistence(location)
14-
if sys.platform.startswith('linux'):
15-
try:
16-
return LibsecretPersistence(
17-
# By using same location as the fall back option below,
18-
# this would override the unencrypted data stored by the
19-
# fall back option. It is probably OK, or even desirable
20-
# (in order to aggressively wipe out plain-text persisted data),
21-
# unless there would frequently be a desktop session and
22-
# a remote ssh session being active simultaneously.
23-
location,
24-
)
25-
except: # pylint: disable=bare-except
26-
if not fallback_to_plaintext:
27-
raise
28-
logging.warning("Encryption unavailable. Opting in to plain text.")
29-
return FilePersistence(location)
9+
# Note: This sample stores both encrypted persistence and plaintext persistence
10+
# into same location, therefore their data would likely override with each other.
11+
try:
12+
return build_encrypted_persistence(location)
13+
except: # pylint: disable=bare-except
14+
# Known issue: Currently, only Linux
15+
if not fallback_to_plaintext:
16+
raise
17+
logging.warning("Encryption unavailable. Opting in to plain text.")
18+
return FilePersistence(location)
3019

3120
persistence = build_persistence("storage.bin", fallback_to_plaintext=False)
3221
print("Type of persistence: {}".format(persistence.__class__.__name__))

sample/token_cache_sample.py

Lines changed: 11 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,31 +2,21 @@
22
import logging
33
import json
44

5-
from msal_extensions import *
5+
from msal_extensions import build_encrypted_persistence, FilePersistence
66

77

88
def build_persistence(location, fallback_to_plaintext=False):
99
"""Build a suitable persistence instance based your current OS"""
10-
if sys.platform.startswith('win'):
11-
return FilePersistenceWithDataProtection(location)
12-
if sys.platform.startswith('darwin'):
13-
return KeychainPersistence(location)
14-
if sys.platform.startswith('linux'):
15-
try:
16-
return LibsecretPersistence(
17-
# By using same location as the fall back option below,
18-
# this would override the unencrypted data stored by the
19-
# fall back option. It is probably OK, or even desirable
20-
# (in order to aggressively wipe out plain-text persisted data),
21-
# unless there would frequently be a desktop session and
22-
# a remote ssh session being active simultaneously.
23-
location,
24-
)
25-
except: # pylint: disable=bare-except
26-
if not fallback_to_plaintext:
27-
raise
28-
logging.exception("Encryption unavailable. Opting in to plain text.")
29-
return FilePersistence(location)
10+
# Note: This sample stores both encrypted persistence and plaintext persistence
11+
# into same location, therefore their data would likely override with each other.
12+
try:
13+
return build_encrypted_persistence(location)
14+
except: # pylint: disable=bare-except
15+
# Known issue: Currently, only Linux
16+
if not fallback_to_plaintext:
17+
raise
18+
logging.warning("Encryption unavailable. Opting in to plain text.")
19+
return FilePersistence(location)
3020

3121
persistence = build_persistence("token_cache.bin")
3222
print("Type of persistence: {}".format(persistence.__class__.__name__))

tests/test_agnostic_backend.py

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -34,18 +34,16 @@ def _test_token_cache_roundtrip(cache):
3434
assert token1['access_token'] == token2['access_token']
3535

3636
def test_file_token_cache_roundtrip(temp_location):
37-
from msal_extensions.token_cache import FileTokenCache
38-
_test_token_cache_roundtrip(FileTokenCache(temp_location))
37+
_test_token_cache_roundtrip(PersistedTokenCache(FilePersistence(temp_location)))
3938

40-
def test_current_platform_cache_roundtrip_with_alias_class(temp_location):
41-
from msal_extensions import TokenCache
42-
_test_token_cache_roundtrip(TokenCache(temp_location))
39+
def test_current_platform_cache_roundtrip_with_persistence_builder(temp_location):
40+
_test_token_cache_roundtrip(PersistedTokenCache(build_encrypted_persistence(temp_location)))
4341

4442
def test_persisted_token_cache(temp_location):
4543
_test_token_cache_roundtrip(PersistedTokenCache(FilePersistence(temp_location)))
4644

4745
def test_file_not_found_error_is_not_raised():
4846
persistence = FilePersistence('non_existing_file')
49-
cache = PersistedTokenCache(persistence=persistence)
47+
cache = PersistedTokenCache(persistence)
5048
# An exception raised here will fail the test case as it is supposed to be a NO-OP
5149
cache.find('')

tests/test_macos_backend.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@
1010
pytest.skip('skipping MacOS-only tests', allow_module_level=True)
1111
else:
1212
from msal_extensions.osx import Keychain
13-
from msal_extensions.token_cache import OSXTokenCache
13+
from msal_extensions.token_cache import PersistedTokenCache
14+
from msal_extensions.persistence import KeychainPersistence
1415

1516

1617
def test_keychain_roundtrip():
@@ -26,12 +27,12 @@ def test_osx_token_cache_roundtrip():
2627
client_id = os.getenv('AZURE_CLIENT_ID')
2728
client_secret = os.getenv('AZURE_CLIENT_SECRET')
2829
if not (client_id and client_secret):
29-
pytest.skip('no credentials present to test OSXTokenCache round-trip with.')
30+
pytest.skip('no credentials present to test PersistedTokenCache round-trip with.')
3031

3132
test_folder = tempfile.mkdtemp(prefix="msal_extension_test_osx_token_cache_roundtrip")
3233
cache_file = os.path.join(test_folder, 'msal.cache')
3334
try:
34-
subject = OSXTokenCache(cache_location=cache_file)
35+
subject = PersistedTokenCache(KeychainPersistence(cache_file))
3536
app = msal.ConfidentialClientApplication(
3637
client_id=client_id,
3738
client_credential=client_secret,

tests/test_windows_backend.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@
1111
pytest.skip('skipping windows-only tests', allow_module_level=True)
1212
else:
1313
from msal_extensions.windows import WindowsDataProtectionAgent
14-
from msal_extensions.token_cache import WindowsTokenCache
14+
from msal_extensions.token_cache import PersistedTokenCache
15+
from msal_extensions.persistence import FilePersistenceWithDataProtection
1516

1617

1718
def test_dpapi_roundtrip_with_entropy():
@@ -48,8 +49,7 @@ def test_dpapi_roundtrip_with_entropy():
4849

4950
def test_read_msal_cache_direct():
5051
"""
51-
This loads and unprotects an MSAL cache directly, only using the DataProtectionAgent. It is not meant to test the
52-
wrapper `WindowsTokenCache`.
52+
This loads and unprotects an MSAL cache directly, only using the DataProtectionAgent.
5353
"""
5454
localappdata_location = os.getenv('LOCALAPPDATA', os.path.expanduser('~'))
5555
cache_locations = [
@@ -87,12 +87,12 @@ def test_windows_token_cache_roundtrip():
8787
client_id = os.getenv('AZURE_CLIENT_ID')
8888
client_secret = os.getenv('AZURE_CLIENT_SECRET')
8989
if not (client_id and client_secret):
90-
pytest.skip('no credentials present to test WindowsTokenCache round-trip with.')
90+
pytest.skip('no credentials present to test PersistedTokenCache round-trip with.')
9191

9292
test_folder = tempfile.mkdtemp(prefix="msal_extension_test_windows_token_cache_roundtrip")
9393
cache_file = os.path.join(test_folder, 'msal.cache')
9494
try:
95-
subject = WindowsTokenCache(cache_location=cache_file)
95+
subject = PersistedTokenCache(FilePersistenceWithDataProtection(cache_file))
9696
app = msal.ConfidentialClientApplication(
9797
client_id=client_id,
9898
client_credential=client_secret,

0 commit comments

Comments
 (0)