pax_global_header00006660000000000000000000000064135571552700014524gustar00rootroot0000000000000052 comment=4efc5213f14e41f4b045388953a59e724109dc9c microsoft-authentication-extensions-for-python-0.1.3/000077500000000000000000000000001355715527000230475ustar00rootroot00000000000000microsoft-authentication-extensions-for-python-0.1.3/.gitignore000066400000000000000000000127721355715527000250500ustar00rootroot00000000000000## Ignore Visual Studio temporary files, build results, and ## files generated by popular Visual Studio add-ons. ## ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore # User-specific files *.suo *.user *.userosscache *.sln.docstates # User-specific files (MonoDevelop/Xamarin Studio) *.userprefs # Build results [Dd]ebug/ [Dd]ebugPublic/ [Rr]elease/ [Rr]eleases/ x64/ x86/ bld/ [Bb]in/ [Oo]bj/ [Ll]og/ # Visual Studio 2015/2017 cache/options directory .vs/ # Uncomment if you have tasks that create the project's static files in wwwroot #wwwroot/ # Visual Studio 2017 auto generated files Generated\ Files/ # MSTest test Results [Tt]est[Rr]esult*/ [Bb]uild[Ll]og.* # NUNIT *.VisualState.xml TestResult.xml # Build Results of an ATL Project [Dd]ebugPS/ [Rr]eleasePS/ dlldata.c # Benchmark Results BenchmarkDotNet.Artifacts/ # .NET Core project.lock.json project.fragment.lock.json artifacts/ **/Properties/launchSettings.json # StyleCop StyleCopReport.xml # Files built by Visual Studio *_i.c *_p.c *_i.h *.ilk *.meta *.obj *.iobj *.pch *.pdb *.ipdb *.pgc *.pgd *.rsp *.sbr *.tlb *.tli *.tlh *.tmp *.tmp_proj *.log *.vspscc *.vssscc .builds *.pidb *.svclog *.scc # Chutzpah Test files _Chutzpah* # Visual C++ cache files ipch/ *.aps *.ncb *.opendb *.opensdf *.sdf *.cachefile *.VC.db *.VC.VC.opendb # Visual Studio profiler *.psess *.vsp *.vspx *.sap # Visual Studio Trace Files *.e2e # TFS 2012 Local Workspace $tf/ # Guidance Automation Toolkit *.gpState # ReSharper is a .NET coding add-in _ReSharper*/ *.[Rr]e[Ss]harper *.DotSettings.user # JustCode is a .NET coding add-in .JustCode # TeamCity is a build add-in _TeamCity* # DotCover is a Code Coverage Tool *.dotCover # AxoCover is a Code Coverage Tool .axoCover/* !.axoCover/settings.json # Visual Studio code coverage results *.coverage *.coveragexml # NCrunch _NCrunch_* .*crunch*.local.xml nCrunchTemp_* # MightyMoose *.mm.* AutoTest.Net/ # Web workbench (sass) .sass-cache/ # Installshield output folder [Ee]xpress/ # DocProject is a documentation generator add-in DocProject/buildhelp/ DocProject/Help/*.HxT DocProject/Help/*.HxC DocProject/Help/*.hhc DocProject/Help/*.hhk DocProject/Help/*.hhp DocProject/Help/Html2 DocProject/Help/html # Click-Once directory publish/ # Publish Web Output *.[Pp]ublish.xml *.azurePubxml # Note: Comment the next line if you want to checkin your web deploy settings, # but database connection strings (with potential passwords) will be unencrypted *.pubxml *.publishproj # Microsoft Azure Web App publish settings. Comment the next line if you want to # checkin your Azure Web App publish settings, but sensitive information contained # in these scripts will be unencrypted PublishScripts/ # NuGet Packages *.nupkg # The packages folder can be ignored because of Package Restore **/[Pp]ackages/* # except build/, which is used as an MSBuild target. !**/[Pp]ackages/build/ # Uncomment if necessary however generally it will be regenerated when needed #!**/[Pp]ackages/repositories.config # NuGet v3's project.json files produces more ignorable files *.nuget.props *.nuget.targets # Microsoft Azure Build Output csx/ *.build.csdef # Microsoft Azure Emulator ecf/ rcf/ # Windows Store app package directories and files AppPackages/ BundleArtifacts/ Package.StoreAssociation.xml _pkginfo.txt *.appx # Visual Studio cache files # files ending in .cache can be ignored *.[Cc]ache # but keep track of directories ending in .cache !*.[Cc]ache/ # Others ClientBin/ ~$* *~ *.dbmdl *.dbproj.schemaview *.jfm *.pfx *.publishsettings orleans.codegen.cs # Including strong name files can present a security risk # (https://github.com/github/gitignore/pull/2483#issue-259490424) #*.snk # Since there are multiple workflows, uncomment next line to ignore bower_components # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) #bower_components/ # RIA/Silverlight projects Generated_Code/ # Backup & report files from converting an old project file # to a newer Visual Studio version. Backup files are not needed, # because we have git ;-) _UpgradeReport_Files/ Backup*/ UpgradeLog*.XML UpgradeLog*.htm ServiceFabricBackup/ *.rptproj.bak # SQL Server files *.mdf *.ldf *.ndf # Business Intelligence projects *.rdl.data *.bim.layout *.bim_*.settings *.rptproj.rsuser # Microsoft Fakes FakesAssemblies/ # GhostDoc plugin setting file *.GhostDoc.xml # Node.js Tools for Visual Studio .ntvs_analysis.dat node_modules/ # Visual Studio 6 build log *.plg # Visual Studio 6 workspace options file *.opt # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) *.vbw # Visual Studio LightSwitch build output **/*.HTMLClient/GeneratedArtifacts **/*.DesktopClient/GeneratedArtifacts **/*.DesktopClient/ModelManifest.xml **/*.Server/GeneratedArtifacts **/*.Server/ModelManifest.xml _Pvt_Extensions # Paket dependency manager .paket/paket.exe paket-files/ # FAKE - F# Make .fake/ # JetBrains Rider .idea/ *.sln.iml # CodeRush .cr/ # Python Tools for Visual Studio (PTVS) __pycache__/ *.pyc # Python Auxiliary Tools *.egg-info/ .tox/ # Cake - Uncomment if you are using it # tools/** # !tools/packages.config # Tabs Studio *.tss # Telerik's JustMock configuration file *.jmconfig # BizTalk build output *.btp.cs *.btm.cs *.odx.cs *.xsd.cs # OpenCover UI analysis results OpenCover/ # Azure Stream Analytics local run output ASALocalRun/ # MSBuild Binary and Structured Log *.binlog # NVidia Nsight GPU debugger configuration file *.nvuser # MFractors (Xamarin productivity tool) working folder .mfractor/ microsoft-authentication-extensions-for-python-0.1.3/.pylintrc000066400000000000000000000000761355715527000247170ustar00rootroot00000000000000[MESSAGES CONTROL] disable= useless-object-inheritance microsoft-authentication-extensions-for-python-0.1.3/.travis.yml000066400000000000000000000060521355715527000251630ustar00rootroot00000000000000language: python matrix: fast_finish: true include: - python: "2.7" env: TOXENV=py27 PYPI=true os: linux - python: "3.5" env: TOXENV=py35 os: linux - python: "3.6" env: TOXENV=py36 os: linux - python: "3.7" env: TOXENV=py37 os: linux dist: xenial - python: "3.8" env: TOXENV=py38 os: linux dist: xenial - name: "Python 3.7 on macOS" env: TOXENV=py37 os: osx osx_image: xcode10.2 language: shell - name: "Python 2.7 on Windows" env: TOXENV=py27 PATH=/c/Python27:/c/Python27/Scripts:$PATH os: windows before_install: choco install python2 language: shell - name: "Python 3.5 on Windows" env: TOXENV=py35 PATH=/c/Python35:/c/Python35/Scripts:$PATH os: windows before_install: choco install python3 --version 3.5.4 language: shell - name: "Python 3.7 on Windows" env: TOXENV=py37 PATH=/c/Python37:/c/Python37/Scripts:$PATH os: windows before_install: choco install python3 --version 3.7.3 language: shell install: - pip install tox pylint - pip install . script: - pylint msal_extensions - tox deploy: - # test pypi provider: pypi distributions: "sdist bdist_wheel" server: https://test.pypi.org/legacy/ user: "nugetaad" password: secure: dpNi6BsZyiAx/gkxJ5Mz6m2yDz2dRGWsSgS5pF+ywNzgHJ6+0e234GyLbSUY5bFeeA7WtOr4is3bxSLB/6tTWDVWdw3TL4FGlDM/54MSLWg8R5bR9PRwO+VU1kvQ03yz+B9mTpzuiwL2e+OSwcwo97jForADzmSRA5OpEq5Z7zAs7WR8J2tyhl+288NwLtKJMVy39UmPl9oifu6/5RfBn7EWLmC7MrMFhHTb2Gj7fJWw4u+5vx9bsQ7ubfiwPbRAtYXLz6wDMtwtFzwme4zZPg5HwWCn0WWlX4b6x7xXirZ7yKsy9iACLgTrLMeAkferrex7f03NFeIDobasML+fLbZufATaL3M97kNGZwulEYNp2+RWyLu/NW6FoZCbS+cSL8HuFnkIDHzEoO56ItMiD9EH47q/NeKgwrrzKjfY+KzaMQOYLlVgCa4WrIeFh5CkwJ4RHrfanMIV2vbEvMxsnHc/mZ+yvgBOFoBNXYN1HEDzEv1NxDPcyt7MBlPUVinEreQaHba7w6qH9Rf0eWgfW2ypBXe+nHaZxQgaGC6J+WGUkzalYQspmHVU4CcuwJa55kuchJs/gbyZKkyK6P8uD5IP6VZiavwZcjWcfvwbZaLeOqzSDVCDMg8M2zYZHoa+6ZR4EgDVW7RvaRvjvvhPTPj5twmLf3YYVJtHIyJSLug= on: branch: master tags: false condition: $PYPI = "true" - # production pypi provider: pypi distributions: "sdist bdist_wheel" user: "nugetaad" password: secure: dpNi6BsZyiAx/gkxJ5Mz6m2yDz2dRGWsSgS5pF+ywNzgHJ6+0e234GyLbSUY5bFeeA7WtOr4is3bxSLB/6tTWDVWdw3TL4FGlDM/54MSLWg8R5bR9PRwO+VU1kvQ03yz+B9mTpzuiwL2e+OSwcwo97jForADzmSRA5OpEq5Z7zAs7WR8J2tyhl+288NwLtKJMVy39UmPl9oifu6/5RfBn7EWLmC7MrMFhHTb2Gj7fJWw4u+5vx9bsQ7ubfiwPbRAtYXLz6wDMtwtFzwme4zZPg5HwWCn0WWlX4b6x7xXirZ7yKsy9iACLgTrLMeAkferrex7f03NFeIDobasML+fLbZufATaL3M97kNGZwulEYNp2+RWyLu/NW6FoZCbS+cSL8HuFnkIDHzEoO56ItMiD9EH47q/NeKgwrrzKjfY+KzaMQOYLlVgCa4WrIeFh5CkwJ4RHrfanMIV2vbEvMxsnHc/mZ+yvgBOFoBNXYN1HEDzEv1NxDPcyt7MBlPUVinEreQaHba7w6qH9Rf0eWgfW2ypBXe+nHaZxQgaGC6J+WGUkzalYQspmHVU4CcuwJa55kuchJs/gbyZKkyK6P8uD5IP6VZiavwZcjWcfvwbZaLeOqzSDVCDMg8M2zYZHoa+6ZR4EgDVW7RvaRvjvvhPTPj5twmLf3YYVJtHIyJSLug= on: branch: master tags: true condition: $PYPI = "true" microsoft-authentication-extensions-for-python-0.1.3/LICENSE000066400000000000000000000022371355715527000240600ustar00rootroot00000000000000 MIT License Copyright (c) Microsoft Corporation. All rights reserved. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE microsoft-authentication-extensions-for-python-0.1.3/README.md000066400000000000000000000016421355715527000243310ustar00rootroot00000000000000 # Contributing This project welcomes contributions and suggestions. Most contributions require you to agree to a Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us the rights to use your contribution. For details, visit https://cla.microsoft.com. When you submit a pull request, a CLA-bot will automatically determine whether you need to provide a CLA and decorate the PR appropriately (e.g., label, comment). Simply follow the instructions provided by the bot. You will only need to do this once across all repos using our CLA. This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. microsoft-authentication-extensions-for-python-0.1.3/azure-pipelines.yml000066400000000000000000000001371355715527000267070ustar00rootroot00000000000000resources: - repo: self trigger: batch: true branches: include: - '*' microsoft-authentication-extensions-for-python-0.1.3/msal_extensions/000077500000000000000000000000001355715527000262625ustar00rootroot00000000000000microsoft-authentication-extensions-for-python-0.1.3/msal_extensions/__init__.py000066400000000000000000000005511355715527000303740ustar00rootroot00000000000000"""Provides auxiliary functionality to the `msal` package.""" __version__ = "0.1.3" import sys if sys.platform.startswith('win'): from .token_cache import WindowsTokenCache as TokenCache elif sys.platform.startswith('darwin'): from .token_cache import OSXTokenCache as TokenCache else: from .token_cache import UnencryptedTokenCache as TokenCache microsoft-authentication-extensions-for-python-0.1.3/msal_extensions/cache_lock.py000066400000000000000000000022751355715527000307150ustar00rootroot00000000000000"""Provides a mechanism for not competing with other processes interacting with an MSAL cache.""" import os import sys import errno import portalocker class CrossPlatLock(object): """Offers a mechanism for waiting until another process is finished interacting with a shared resource. This is specifically written to interact with a class of the same name in the .NET extensions library. """ def __init__(self, lockfile_path): self._lockpath = lockfile_path self._fh = None def __enter__(self): pid = os.getpid() self._fh = open(self._lockpath, 'wb+', buffering=0) portalocker.lock(self._fh, portalocker.LOCK_EX) self._fh.write('{} {}'.format(pid, sys.argv[0]).encode('utf-8')) def __exit__(self, *args): self._fh.close() try: # Attempt to delete the lockfile. In either of the failure cases enumerated below, it is # likely that another process has raced this one and ended up clearing or locking the # file for itself. os.remove(self._lockpath) except OSError as ex: if ex.errno != errno.ENOENT and ex.errno != errno.EACCES: raise microsoft-authentication-extensions-for-python-0.1.3/msal_extensions/osx.py000066400000000000000000000214661355715527000274560ustar00rootroot00000000000000# pylint: disable=duplicate-code """Implements a macOS specific TokenCache, and provides auxiliary helper types.""" import os import ctypes as _ctypes OS_RESULT = _ctypes.c_int32 class KeychainError(OSError): """The RuntimeError that will be run when a function interacting with Keychain fails.""" ACCESS_DENIED = -128 NO_SUCH_KEYCHAIN = -25294 NO_DEFAULT = -25307 ITEM_NOT_FOUND = -25300 def __init__(self, exit_status): super(KeychainError, self).__init__() self.exit_status = exit_status # TODO: pylint: disable=fixme # use SecCopyErrorMessageString to fetch the appropriate message here. self.message = \ '{} ' \ 'see https://opensource.apple.com/source/CarbonHeaders/CarbonHeaders-18.1/MacErrors.h'\ .format(self.exit_status) def _get_native_location(name): # type: (str) -> str """ Fetches the location of a native MacOS library. :param name: The name of the library to be loaded. :return: The location of the library on a MacOS filesystem. """ return '/System/Library/Frameworks/{0}.framework/{0}'.format(name) # Load native MacOS libraries _SECURITY = _ctypes.CDLL(_get_native_location('Security')) _CORE = _ctypes.CDLL(_get_native_location('CoreFoundation')) # Bind CFRelease from native MacOS libraries. _CORE_RELEASE = _CORE.CFRelease _CORE_RELEASE.argtypes = ( _ctypes.c_void_p, ) # Bind SecCopyErrorMessageString from native MacOS libraries. # https://developer.apple.com/documentation/security/1394686-seccopyerrormessagestring?language=objc _SECURITY_COPY_ERROR_MESSAGE_STRING = _SECURITY.SecCopyErrorMessageString _SECURITY_COPY_ERROR_MESSAGE_STRING.argtypes = ( OS_RESULT, _ctypes.c_void_p ) _SECURITY_COPY_ERROR_MESSAGE_STRING.restype = _ctypes.c_char_p # Bind SecKeychainOpen from native MacOS libraries. # https://developer.apple.com/documentation/security/1396431-seckeychainopen _SECURITY_KEYCHAIN_OPEN = _SECURITY.SecKeychainOpen _SECURITY_KEYCHAIN_OPEN.argtypes = ( _ctypes.c_char_p, _ctypes.POINTER(_ctypes.c_void_p) ) _SECURITY_KEYCHAIN_OPEN.restype = OS_RESULT # Bind SecKeychainCopyDefault from native MacOS libraries. # https://developer.apple.com/documentation/security/1400743-seckeychaincopydefault?language=objc _SECURITY_KEYCHAIN_COPY_DEFAULT = _SECURITY.SecKeychainCopyDefault _SECURITY_KEYCHAIN_COPY_DEFAULT.argtypes = ( _ctypes.POINTER(_ctypes.c_void_p), ) _SECURITY_KEYCHAIN_COPY_DEFAULT.restype = OS_RESULT # Bind SecKeychainItemFreeContent from native MacOS libraries. _SECURITY_KEYCHAIN_ITEM_FREE_CONTENT = _SECURITY.SecKeychainItemFreeContent _SECURITY_KEYCHAIN_ITEM_FREE_CONTENT.argtypes = ( _ctypes.c_void_p, _ctypes.c_void_p, ) _SECURITY_KEYCHAIN_ITEM_FREE_CONTENT.restype = OS_RESULT # Bind SecKeychainItemModifyAttributesAndData from native MacOS libraries. _SECURITY_KEYCHAIN_ITEM_MODIFY_ATTRIBUTES_AND_DATA = \ _SECURITY.SecKeychainItemModifyAttributesAndData _SECURITY_KEYCHAIN_ITEM_MODIFY_ATTRIBUTES_AND_DATA.argtypes = ( _ctypes.c_void_p, _ctypes.c_void_p, _ctypes.c_uint32, _ctypes.c_void_p, ) _SECURITY_KEYCHAIN_ITEM_MODIFY_ATTRIBUTES_AND_DATA.restype = OS_RESULT # Bind SecKeychainFindGenericPassword from native MacOS libraries. # https://developer.apple.com/documentation/security/1397301-seckeychainfindgenericpassword?language=objc _SECURITY_KEYCHAIN_FIND_GENERIC_PASSWORD = _SECURITY.SecKeychainFindGenericPassword _SECURITY_KEYCHAIN_FIND_GENERIC_PASSWORD.argtypes = ( _ctypes.c_void_p, _ctypes.c_uint32, _ctypes.c_char_p, _ctypes.c_uint32, _ctypes.c_char_p, _ctypes.POINTER(_ctypes.c_uint32), _ctypes.POINTER(_ctypes.c_void_p), _ctypes.POINTER(_ctypes.c_void_p), ) _SECURITY_KEYCHAIN_FIND_GENERIC_PASSWORD.restype = OS_RESULT # Bind SecKeychainAddGenericPassword from native MacOS # https://developer.apple.com/documentation/security/1398366-seckeychainaddgenericpassword?language=objc _SECURITY_KEYCHAIN_ADD_GENERIC_PASSWORD = _SECURITY.SecKeychainAddGenericPassword _SECURITY_KEYCHAIN_ADD_GENERIC_PASSWORD.argtypes = ( _ctypes.c_void_p, _ctypes.c_uint32, _ctypes.c_char_p, _ctypes.c_uint32, _ctypes.c_char_p, _ctypes.c_uint32, _ctypes.c_char_p, _ctypes.POINTER(_ctypes.c_void_p), ) _SECURITY_KEYCHAIN_ADD_GENERIC_PASSWORD.restype = OS_RESULT class Keychain(object): """Encapsulates the interactions with a particular MacOS Keychain.""" def __init__(self, filename=None): # type: (str) -> None self._ref = _ctypes.c_void_p() if filename: filename = os.path.expanduser(filename) self._filename = filename.encode('utf-8') else: self._filename = None def __enter__(self): if self._filename: status = _SECURITY_KEYCHAIN_OPEN(self._filename, self._ref) else: status = _SECURITY_KEYCHAIN_COPY_DEFAULT(self._ref) if status: raise OSError(status) return self def __exit__(self, *args): if self._ref: _CORE_RELEASE(self._ref) def get_generic_password(self, service, account_name): # type: (str, str) -> str """Fetch the password associated with a particular service and account. :param service: The service that this password is associated with. :param account_name: The account that this password is associated with. :return: The value of the password associated with the specified service and account. """ service = service.encode('utf-8') account_name = account_name.encode('utf-8') length = _ctypes.c_uint32() contents = _ctypes.c_void_p() exit_status = _SECURITY_KEYCHAIN_FIND_GENERIC_PASSWORD( self._ref, len(service), service, len(account_name), account_name, length, contents, None, ) if exit_status: raise KeychainError(exit_status=exit_status) value = _ctypes.create_string_buffer(length.value) _ctypes.memmove(value, contents.value, length.value) _SECURITY_KEYCHAIN_ITEM_FREE_CONTENT(None, contents) return value.raw.decode('utf-8') def set_generic_password(self, service, account_name, value): # type: (str, str, str) -> None """Associate a password with a given service and account. :param service: The service to associate this password with. :param account_name: The account to associate this password with. :param value: The string that should be used as the password. """ service = service.encode('utf-8') account_name = account_name.encode('utf-8') value = value.encode('utf-8') entry = _ctypes.c_void_p() find_exit_status = _SECURITY_KEYCHAIN_FIND_GENERIC_PASSWORD( self._ref, len(service), service, len(account_name), account_name, None, None, entry, ) if not find_exit_status: modify_exit_status = _SECURITY_KEYCHAIN_ITEM_MODIFY_ATTRIBUTES_AND_DATA( entry, None, len(value), value, ) if modify_exit_status: raise KeychainError(exit_status=modify_exit_status) elif find_exit_status == KeychainError.ITEM_NOT_FOUND: add_exit_status = _SECURITY_KEYCHAIN_ADD_GENERIC_PASSWORD( self._ref, len(service), service, len(account_name), account_name, len(value), value, None ) if add_exit_status: raise KeychainError(exit_status=add_exit_status) else: raise KeychainError(exit_status=find_exit_status) def get_internet_password(self, service, username): # type: (str, str) -> str """ Fetches a password associated with a domain and username. NOTE: THIS IS NOT YET IMPLEMENTED :param service: The website/service that this password is associated with. :param username: The account that this password is associated with. :return: The password that was associated with the given service and username. """ raise NotImplementedError() def set_internet_password(self, service, username, value): # type: (str, str, str) -> None """Sets a password associated with a domain and a username. NOTE: THIS IS NOT YET IMPLEMENTED :param service: The website/service that this password is associated with. :param username: The account that this password is associated with. :param value: The password that should be associated with the given service and username. """ raise NotImplementedError() microsoft-authentication-extensions-for-python-0.1.3/msal_extensions/token_cache.py000066400000000000000000000136221355715527000311030ustar00rootroot00000000000000"""Generic functions and types for working with a TokenCache that is not platform specific.""" import os import sys import warnings import time import errno import msal from .cache_lock import CrossPlatLock if sys.platform.startswith('win'): from .windows import WindowsDataProtectionAgent elif sys.platform.startswith('darwin'): from .osx import Keychain def _mkdir_p(path): """Creates a directory, and any necessary parents. This implementation based on a Stack Overflow question that can be found here: https://stackoverflow.com/questions/600268/mkdir-p-functionality-in-python If the path provided is an existing file, this function raises an exception. :param path: The directory name that should be created. """ try: os.makedirs(path) except OSError as exp: if exp.errno == errno.EEXIST and os.path.isdir(path): pass else: raise class FileTokenCache(msal.SerializableTokenCache): """Implements basic unprotected SerializableTokenCache to a plain-text file.""" def __init__(self, cache_location, lock_location=None): super(FileTokenCache, self).__init__() self._cache_location = cache_location self._lock_location = lock_location or self._cache_location + '.lockfile' self._last_sync = 0 # _last_sync is a Unixtime self._cache_location = os.path.expanduser(self._cache_location) self._lock_location = os.path.expanduser(self._lock_location) _mkdir_p(os.path.dirname(self._lock_location)) _mkdir_p(os.path.dirname(self._cache_location)) def _needs_refresh(self): # type: () -> Bool """ Inspects the file holding the encrypted TokenCache to see if a read is necessary. :return: True if there are changes not reflected in memory, False otherwise. """ try: updated = os.path.getmtime(self._cache_location) return self._last_sync < updated except IOError as exp: if exp.errno != errno.ENOENT: raise exp return False def _write(self, contents): # type: (str) -> None """Handles actually committing the serialized form of this TokenCache to persisted storage. For types derived of this, class that will be a file, which has the ability to track a last modified time. :param contents: The serialized contents of a TokenCache """ with open(self._cache_location, 'w+') as handle: handle.write(contents) def _read(self): # type: () -> str """Fetches the contents of a file and invokes deserialization.""" with open(self._cache_location, 'r') as handle: return handle.read() def modify(self, credential_type, old_entry, new_key_value_pairs=None): with CrossPlatLock(self._lock_location): if self._needs_refresh(): try: self.deserialize(self._read()) except IOError as exp: if exp.errno != errno.ENOENT: raise super(FileTokenCache, self).modify( credential_type, old_entry, new_key_value_pairs=new_key_value_pairs) self._write(self.serialize()) self._last_sync = os.path.getmtime(self._cache_location) def find(self, credential_type, **kwargs): # pylint: disable=arguments-differ with CrossPlatLock(self._lock_location): if self._needs_refresh(): try: self.deserialize(self._read()) except IOError as exp: if exp.errno != errno.ENOENT: raise self._last_sync = time.time() return super(FileTokenCache, self).find(credential_type, **kwargs) class UnencryptedTokenCache(FileTokenCache): """An unprotected token cache to default to when no-platform specific option is available.""" def __init__(self, cache_location, **kwargs): warnings.warn("You are using an unprotected token cache, " "because an encrypted option is not available for {}".format(sys.platform), RuntimeWarning) super(UnencryptedTokenCache, self).__init__(cache_location, **kwargs) class WindowsTokenCache(FileTokenCache): """A SerializableTokenCache implementation which uses Win32 encryption APIs to protect your tokens. """ def __init__(self, cache_location, entropy='', **kwargs): super(WindowsTokenCache, self).__init__(cache_location, **kwargs) self._dp_agent = WindowsDataProtectionAgent(entropy=entropy) def _write(self, contents): with open(self._cache_location, 'wb') as handle: handle.write(self._dp_agent.protect(contents)) def _read(self): with open(self._cache_location, 'rb') as handle: cipher_text = handle.read() return self._dp_agent.unprotect(cipher_text) class OSXTokenCache(FileTokenCache): """A SerializableTokenCache implementation which uses native Keychain libraries to protect your tokens. """ def __init__(self, cache_location, service_name='Microsoft.Developer.IdentityService', account_name='MSALCache', **kwargs): super(OSXTokenCache, self).__init__(cache_location, **kwargs) self._service_name = service_name self._account_name = account_name def _read(self): with Keychain() as locker: return locker.get_generic_password(self._service_name, self._account_name) def _write(self, contents): with Keychain() as locker: locker.set_generic_password(self._service_name, self._account_name, contents) with open(self._cache_location, "w+") as handle: handle.write('{} {}'.format(os.getpid(), sys.argv[0])) microsoft-authentication-extensions-for-python-0.1.3/msal_extensions/windows.py000066400000000000000000000102441355715527000303270ustar00rootroot00000000000000"""Implements a Windows Specific TokenCache, and provides auxiliary helper types.""" import ctypes from ctypes import wintypes _LOCAL_FREE = ctypes.windll.kernel32.LocalFree _GET_LAST_ERROR = ctypes.windll.kernel32.GetLastError _MEMCPY = ctypes.cdll.msvcrt.memcpy _CRYPT_PROTECT_DATA = ctypes.windll.crypt32.CryptProtectData _CRYPT_UNPROTECT_DATA = ctypes.windll.crypt32.CryptUnprotectData _CRYPTPROTECT_UI_FORBIDDEN = 0x01 class DataBlob(ctypes.Structure): # pylint: disable=too-few-public-methods """A wrapper for interacting with the _CRYPTOAPI_BLOB type and its many aliases. This type is exposed from Wincrypt.h in XP and above. The memory associated with a DataBlob itself does not need to be freed, as the Python runtime will correctly clean it up. However, depending on the data it points at, it may still need to be freed. For instance, memory created by ctypes.create_string_buffer is already managed, and needs to not be freed. However, memory allocated by CryptProtectData and CryptUnprotectData must have LocalFree called on pbData. See documentation for this type at: https://msdn.microsoft.com/en-us/7a06eae5-96d8-4ece-98cb-cf0710d2ddbd """ _fields_ = [("cbData", wintypes.DWORD), ("pbData", ctypes.POINTER(ctypes.c_char))] def raw(self): # type: () -> bytes """Copies the message from the DataBlob in natively allocated memory into Python controlled memory. :return A byte array that matches what is stored in native-memory.""" cb_data = int(self.cbData) pb_data = self.pbData blob_buffer = ctypes.create_string_buffer(cb_data) _MEMCPY(blob_buffer, pb_data, cb_data) return blob_buffer.raw # This code is modeled from a StackOverflow question, which can be found here: # https://stackoverflow.com/questions/463832/using-dpapi-with-python class WindowsDataProtectionAgent(object): """A mechanism for interacting with the Windows DP API Native library, e.g. Crypt32.dll.""" def __init__(self, entropy=None): # type: (str) -> None self._entropy_blob = None if entropy: entropy_utf8 = entropy.encode('utf-8') blob_buffer = ctypes.create_string_buffer(entropy_utf8, len(entropy_utf8)) self._entropy_blob = DataBlob(len(entropy_utf8), blob_buffer) def protect(self, message): # type: (str) -> bytes """Encrypts a message. :return cipher text holding the original message.""" message = message.encode('utf-8') message_buffer = ctypes.create_string_buffer(message, len(message)) message_blob = DataBlob(len(message), message_buffer) result = DataBlob() if self._entropy_blob: entropy = ctypes.byref(self._entropy_blob) else: entropy = None if _CRYPT_PROTECT_DATA( ctypes.byref(message_blob), u"python_data", entropy, None, None, _CRYPTPROTECT_UI_FORBIDDEN, ctypes.byref(result)): try: return result.raw() finally: _LOCAL_FREE(result.pbData) err_code = _GET_LAST_ERROR() raise OSError(256, '', '', err_code) def unprotect(self, cipher_text): # type: (bytes) -> str """Decrypts cipher text that is provided. :return The original message hidden in the cipher text.""" ct_buffer = ctypes.create_string_buffer(cipher_text, len(cipher_text)) ct_blob = DataBlob(len(cipher_text), ct_buffer) result = DataBlob() if self._entropy_blob: entropy = ctypes.byref(self._entropy_blob) else: entropy = None if _CRYPT_UNPROTECT_DATA( ctypes.byref(ct_blob), None, entropy, None, None, _CRYPTPROTECT_UI_FORBIDDEN, ctypes.byref(result) ): try: return result.raw().decode('utf-8') finally: _LOCAL_FREE(result.pbData) err_code = _GET_LAST_ERROR() raise OSError(256, '', '', err_code) microsoft-authentication-extensions-for-python-0.1.3/setup.cfg000066400000000000000000000000321355715527000246630ustar00rootroot00000000000000[bdist_wheel] universal=1 microsoft-authentication-extensions-for-python-0.1.3/setup.py000066400000000000000000000010741355715527000245630ustar00rootroot00000000000000#!/usr/bin/env python from setuptools import setup, find_packages import re, io __version__ = re.search( r'__version__\s*=\s*[rRfFuU]{0,2}[\'"]([^\'"]*)[\'"]', io.open('msal_extensions/__init__.py', encoding='utf_8_sig').read() ).group(1) setup( name='msal-extensions', version=__version__, packages=find_packages(), classifiers=[ 'Development Status :: 2 - Pre-Alpha', ], package_data={'': ['LICENSE']}, install_requires=[ 'msal>=0.4.1,<2.0.0', 'portalocker~=1.0', ], tests_require=['pytest'], ) microsoft-authentication-extensions-for-python-0.1.3/tests/000077500000000000000000000000001355715527000242115ustar00rootroot00000000000000microsoft-authentication-extensions-for-python-0.1.3/tests/lock_acquire.py000066400000000000000000000016031355715527000272240ustar00rootroot00000000000000import sys import os import time import datetime from msal_extensions import CrossPlatLock def main(hold_time): # type: (datetime.timedelta) -> None """ Grabs a lock from a well-known file in order to test the CrossPlatLock class across processes. :param hold_time: The approximate duration that this process should hold onto the lock. :return: None """ pid = os.getpid() print('{} starting'.format(pid)) with CrossPlatLock('./delete_me.lockfile'): print('{} has acquired the lock'.format(pid)) time.sleep(hold_time.total_seconds()) print('{} is releasing the lock'.format(pid)) print('{} done.'.format(pid)) if __name__ == '__main__': lock_hold_time = datetime.timedelta(seconds=5) if len(sys.argv) > 1: hold_time = datetime.timedelta(seconds=int(sys.argv[1])) main(lock_hold_time) microsoft-authentication-extensions-for-python-0.1.3/tests/test_agnostic_backend.py000066400000000000000000000044251355715527000311050ustar00rootroot00000000000000import os import shutil import tempfile import pytest import msal def test_file_token_cache_roundtrip(): from msal_extensions.token_cache import FileTokenCache client_id = os.getenv('AZURE_CLIENT_ID') client_secret = os.getenv('AZURE_CLIENT_SECRET') if not (client_id and client_secret): pytest.skip('no credentials present to test FileTokenCache round-trip with.') test_folder = tempfile.mkdtemp(prefix="msal_extension_test_file_token_cache_roundtrip") cache_file = os.path.join(test_folder, 'msal.cache') try: subject = FileTokenCache(cache_location=cache_file) app = msal.ConfidentialClientApplication( client_id=client_id, client_credential=client_secret, token_cache=subject) desired_scopes = ['https://graph.microsoft.com/.default'] token1 = app.acquire_token_for_client(scopes=desired_scopes) os.utime(cache_file, None) # Mock having another process update the cache. token2 = app.acquire_token_silent(scopes=desired_scopes, account=None) assert token1['access_token'] == token2['access_token'] finally: shutil.rmtree(test_folder, ignore_errors=True) def test_current_platform_cache_roundtrip(): from msal_extensions import TokenCache client_id = os.getenv('AZURE_CLIENT_ID') client_secret = os.getenv('AZURE_CLIENT_SECRET') if not (client_id and client_secret): pytest.skip('no credentials present to test FileTokenCache round-trip with.') test_folder = tempfile.mkdtemp(prefix="msal_extension_test_file_token_cache_roundtrip") cache_file = os.path.join(test_folder, 'msal.cache') try: subject = TokenCache(cache_location=cache_file) app = msal.ConfidentialClientApplication( client_id=client_id, client_credential=client_secret, token_cache=subject) desired_scopes = ['https://graph.microsoft.com/.default'] token1 = app.acquire_token_for_client(scopes=desired_scopes) os.utime(cache_file, None) # Mock having another process update the cache. token2 = app.acquire_token_silent(scopes=desired_scopes, account=None) assert token1['access_token'] == token2['access_token'] finally: shutil.rmtree(test_folder, ignore_errors=True) microsoft-authentication-extensions-for-python-0.1.3/tests/test_crossplatlock.py000066400000000000000000000005601355715527000305060ustar00rootroot00000000000000import pytest from msal_extensions.cache_lock import CrossPlatLock def test_ensure_file_deleted(): lockfile = './test_lock_1.txt' try: FileNotFoundError except NameError: FileNotFoundError = IOError with CrossPlatLock(lockfile): pass with pytest.raises(FileNotFoundError): with open(lockfile): pass microsoft-authentication-extensions-for-python-0.1.3/tests/test_macos_backend.py000066400000000000000000000032441355715527000303760ustar00rootroot00000000000000import sys import os import shutil import tempfile import pytest import uuid import msal if not sys.platform.startswith('darwin'): pytest.skip('skipping MacOS-only tests', allow_module_level=True) else: from msal_extensions.osx import Keychain from msal_extensions.token_cache import OSXTokenCache def test_keychain_roundtrip(): with Keychain() as subject: location, account = "msal_extension_test1", "test_account1" want = uuid.uuid4().hex subject.set_generic_password(location, account, want) got = subject.get_generic_password(location, account) assert got == want def test_osx_token_cache_roundtrip(): client_id = os.getenv('AZURE_CLIENT_ID') client_secret = os.getenv('AZURE_CLIENT_SECRET') if not (client_id and client_secret): pytest.skip('no credentials present to test OSXTokenCache round-trip with.') test_folder = tempfile.mkdtemp(prefix="msal_extension_test_osx_token_cache_roundtrip") cache_file = os.path.join(test_folder, 'msal.cache') try: subject = OSXTokenCache(cache_location=cache_file) app = msal.ConfidentialClientApplication( client_id=client_id, client_credential=client_secret, token_cache=subject) desired_scopes = ['https://graph.microsoft.com/.default'] token1 = app.acquire_token_for_client(scopes=desired_scopes) os.utime(cache_file, None) # Mock having another process update the cache. token2 = app.acquire_token_silent(scopes=desired_scopes, account=None) assert token1['access_token'] == token2['access_token'] finally: shutil.rmtree(test_folder, ignore_errors=True) microsoft-authentication-extensions-for-python-0.1.3/tests/test_windows_backend.py000066400000000000000000000074161355715527000307730ustar00rootroot00000000000000import sys import os import errno import shutil import tempfile import pytest import uuid import msal if not sys.platform.startswith('win'): pytest.skip('skipping windows-only tests', allow_module_level=True) else: from msal_extensions.windows import WindowsDataProtectionAgent from msal_extensions.token_cache import WindowsTokenCache def test_dpapi_roundtrip_with_entropy(): subject_without_entropy = WindowsDataProtectionAgent() subject_with_entropy = WindowsDataProtectionAgent(entropy=uuid.uuid4().hex) test_cases = [ '', 'lorem ipsum', 'lorem-ipsum', '', uuid.uuid4().hex, ] try: for tc in test_cases: ciphered = subject_with_entropy.protect(tc) assert ciphered != tc got = subject_with_entropy.unprotect(ciphered) assert got == tc ciphered = subject_without_entropy.protect(tc) assert ciphered != tc got = subject_without_entropy.unprotect(ciphered) assert got == tc except OSError as exp: if exp.errno == errno.EIO and os.getenv('TRAVIS_REPO_SLUG'): pytest.skip('DPAPI tests are known to fail in TravisCI. This effort tracked by ' 'https://github.com/AzureAD/microsoft-authentication-extentions-for-python' '/issues/21') def test_read_msal_cache_direct(): """ This loads and unprotects an MSAL cache directly, only using the DataProtectionAgent. It is not meant to test the wrapper `WindowsTokenCache`. """ localappdata_location = os.getenv('LOCALAPPDATA', os.path.expanduser('~')) cache_locations = [ os.path.join(localappdata_location, '.IdentityService', 'msal.cache'), # this is where it's supposed to be os.path.join(localappdata_location, '.IdentityServices', 'msal.cache'), # There was a miscommunications about whether this was plural or not. os.path.join(localappdata_location, 'msal.cache'), # The earliest most naive builds used this locations. ] found = False for loc in cache_locations: try: with open(loc, mode='rb') as fh: contents = fh.read() found = True break except IOError as exp: if exp.errno != errno.ENOENT: raise exp if not found: pytest.skip('could not find the msal.cache file (try logging in using MSAL)') subject = WindowsDataProtectionAgent() raw = subject.unprotect(contents) assert raw != "" cache = msal.SerializableTokenCache() cache.deserialize(raw) access_tokens = cache.find(msal.TokenCache.CredentialType.ACCESS_TOKEN) assert len(access_tokens) > 0 def test_windows_token_cache_roundtrip(): client_id = os.getenv('AZURE_CLIENT_ID') client_secret = os.getenv('AZURE_CLIENT_SECRET') if not (client_id and client_secret): pytest.skip('no credentials present to test WindowsTokenCache round-trip with.') test_folder = tempfile.mkdtemp(prefix="msal_extension_test_windows_token_cache_roundtrip") cache_file = os.path.join(test_folder, 'msal.cache') try: subject = WindowsTokenCache(cache_location=cache_file) app = msal.ConfidentialClientApplication( client_id=client_id, client_credential=client_secret, token_cache=subject) desired_scopes = ['https://graph.microsoft.com/.default'] token1 = app.acquire_token_for_client(scopes=desired_scopes) os.utime(cache_file, None) # Mock having another process update the cache. token2 = app.acquire_token_silent(scopes=desired_scopes, account=None) assert token1['access_token'] == token2['access_token'] finally: shutil.rmtree(test_folder, ignore_errors=True) microsoft-authentication-extensions-for-python-0.1.3/tox.ini000066400000000000000000000001371355715527000243630ustar00rootroot00000000000000[tox] envlist = py27,py35,py36,py37,py38 [testenv] deps = pytest commands = pytest