diff --git a/adal.pyproj b/adal.pyproj
index 4b5903d3..ad558b5f 100644
--- a/adal.pyproj
+++ b/adal.pyproj
@@ -83,6 +83,11 @@
Code
+
+
+
+
+
Code
@@ -105,6 +110,7 @@
+
diff --git a/adal/__init__.py b/adal/__init__.py
index a6de258e..83774909 100644
--- a/adal/__init__.py
+++ b/adal/__init__.py
@@ -25,14 +25,18 @@
#
#------------------------------------------------------------------------------
-__version__ = '0.2.0'
+__version__ = '1.0.0rc1'
from .authentication_context import AuthenticationContext
from .token_cache import TokenCache
-from .log import LOGGING_LEVEL, set_logging_options, get_logging_options
+from .log import (LOGGING_LEVEL,
+ set_logging_options,
+ get_logging_options,
+ ADAL_LOGGER_NAME)
+from .adal_error import AdalError
# to avoid "No handler found" warnings.
import logging
-logging.getLogger(log.ADAL_LOGGER_NAME).addHandler(logging.NullHandler())
+logging.getLogger(ADAL_LOGGER_NAME).addHandler(logging.NullHandler())
diff --git a/adal/adal_error.py b/adal/adal_error.py
index a5c2c7cd..180ebccd 100644
--- a/adal/adal_error.py
+++ b/adal/adal_error.py
@@ -1,4 +1,31 @@
-class AdalError(Exception):
+#------------------------------------------------------------------------------
+#
+# Copyright (c) Microsoft Corporation.
+# All rights reserved.
+#
+# This code is licensed under the MIT License.
+#
+# 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.
+#
+#------------------------------------------------------------------------------
+
+class AdalError(Exception):
def __init__(self, error_msg, error_response=None):
super(AdalError, self).__init__(error_msg)
self.error_response = error_response
diff --git a/adal/argument.py b/adal/argument.py
index 47509d73..f2df1830 100644
--- a/adal/argument.py
+++ b/adal/argument.py
@@ -24,15 +24,21 @@
# THE SOFTWARE.
#
#------------------------------------------------------------------------------
+import sys
from .constants import OAuth2DeviceCodeResponseParameters
def validate_string_param(value, name):
-
if not value:
raise ValueError("The {0} parameter is required".format(name))
- if not isinstance(value, str):
+ result = True
+ if sys.version_info.major < 3:
+ result = isinstance(value, basestring)
+ else:
+ result = isinstance(value, str)
+
+ if not result:
raise TypeError("The {0} parameter must be of type str".format(name))
def validate_user_code_info(user_code_info):
diff --git a/adal/authentication_context.py b/adal/authentication_context.py
index 2885744a..ca2a4289 100644
--- a/adal/authentication_context.py
+++ b/adal/authentication_context.py
@@ -25,8 +25,9 @@
#
#------------------------------------------------------------------------------
-from .authority import Authority
+import threading
+from .authority import Authority
from . import argument
from .code_request import CodeRequest
from .token_request import TokenRequest
@@ -34,23 +35,46 @@
from . import log
from .constants import OAuth2DeviceCodeResponseParameters
-
GLOBAL_ADAL_OPTIONS = {}
class AuthenticationContext(object):
-
+ '''
+ Retrieves authentication tokens from Azure Active Directory.
+ For usages, check out the "sample" folder under
+ https://github.com/AzureAD/azure-activedirectory-library-for-python
+ '''
def __init__(self, authority, validate_authority=None, cache=None):
-
+ '''
+ Creates a new AuthenticationContext object. By default the authority
+ will be checked against a list of known Azure Active Directory authorities.
+ If the authority is not recognized as one of these well known authorities
+ then token acquisition will fail. This behavior can be turned off via the
+ validate_authority parameter below.
+ Args:
+ authority (str):
+ A URL that identifies a token authority.
+ validate_authority (bool, optional):
+ Turns authority validation on or off. This parameter default to true.
+ cache (TokenCache, optional):
+ Sets the token cache used by this AuthenticationContext instance.
+ If this parameter is not set, then a default is used. Cache instances
+ is only used by that instance of the AuthenticationContext and are not
+ shared unless it has been manually passed during the construction of
+ other AuthenticationContexts.
+ Returns:
+ A new AuthenticationContext object
+ '''
validate = validate_authority
if not validate_authority:
validate = True
self.authority = Authority(authority, validate)
self._oauth2client = None
- self._correlation_id = None
+ self.correlation_id = None
self._call_context = {'options': GLOBAL_ADAL_OPTIONS}
self._token_requests_with_user_code = {}
self.cache = cache or TokenCache()
+ self._lock = threading.RLock()
@property
def options(self):
@@ -61,12 +85,24 @@ def options(self, val):
self._call_context['options'] = val
def _acquire_token(self, token_func):
- self._call_context['log_context'] = log.create_log_context(self._correlation_id)
+ self._call_context['log_context'] = log.create_log_context(self.correlation_id)
self.authority.validate(self._call_context)
token = token_func(self)
return token
def acquire_token(self, resource, user_id, client_id):
+ '''
+ Gets a token for a given resource via cached tokens.
+ Args:
+ resource (str):
+ A URI that identifies the resource for which the token is valid.
+ user_id (str):
+ The username of the user on behalf this application is authenticating.
+ client_id (str):
+ The OAuth client id of the calling application.
+ Returns:
+ dict: with several keys, include "accessToken" and "refreshToken"
+ '''
argument.validate_string_param(resource, 'resource')
argument.validate_string_param(client_id, 'client_id')
@@ -79,6 +115,20 @@ def token_func(self):
return token
def acquire_token_with_username_password(self, resource, username, password, client_id):
+ '''
+ Gets a token for a given resource via user credentails.
+ Args:
+ resource (str):
+ A URI that identifies the resource for which the token is valid.
+ username (str):
+ The username of the user on behalf this application is authenticating.
+ password (str):
+ The password of the user named in the username parameter.
+ client_id (str):
+ The OAuth client id of the calling application.
+ Returns:
+ dict: with several keys, include "accessToken" and "refreshToken"
+ '''
argument.validate_string_param(resource, 'resource')
argument.validate_string_param(username, 'username')
argument.validate_string_param(password, 'password')
@@ -93,6 +143,18 @@ def token_func(self):
return token
def acquire_token_with_client_credentials(self, resource, client_id, client_secret):
+ '''
+ Gets a token for a given resource via client credentials.
+ Args:
+ resource (str):
+ A URI that identifies the resource for which the token is valid.
+ client_id (str):
+ The OAuth client id of the calling application.
+ client_secret (str):
+ The OAuth client secret of the calling application.
+ Returns:
+ dict: with several keys, include "accessToken"
+ '''
argument.validate_string_param(resource, 'resource')
argument.validate_string_param(client_id, 'client_id')
argument.validate_string_param(client_secret, 'client_secret')
@@ -105,8 +167,29 @@ def token_func(self):
token = self._acquire_token(token_func)
return token
- def acquire_token_with_authorization_code(self, authorization_code, redirect_uri, resource, client_id, client_secret):
-
+ def acquire_token_with_authorization_code(
+ self,
+ authorization_code,
+ redirect_uri,
+ resource,
+ client_id,
+ client_secret):
+ '''
+ Gets a token for a given resource via auhtorization code for a server app.
+ Args:
+ authorization_code (str):
+ An authorization code returned from a client.
+ redirect_uri (str):
+ he redirect uri that was used in the authorize call.
+ resource (str):
+ A URI that identifies the resource for which the token is valid.
+ client_id (str):
+ The OAuth client id of the calling application.
+ client_secret (str):
+ The OAuth client secret of the calling application.
+ Returns:
+ dict: with several keys, include "accessToken" and "refreshToken"
+ '''
argument.validate_string_param(authorization_code, 'authorization_code')
argument.validate_string_param(redirect_uri, 'redirect_uri')
argument.validate_string_param(resource, 'resource')
@@ -114,14 +197,36 @@ def acquire_token_with_authorization_code(self, authorization_code, redirect_uri
argument.validate_string_param(client_secret, 'client_secret')
def token_func(self):
- token_request = TokenRequest(self._call_context, self, client_id, resource, redirect_uri)
- token = token_request.get_token_with_authorization_code(authorization_code, client_secret)
+ token_request = TokenRequest(
+ self._call_context,
+ self,
+ client_id,
+ resource,
+ redirect_uri)
+ token = token_request.get_token_with_authorization_code(
+ authorization_code,
+ client_secret)
return token
token = self._acquire_token(token_func)
return token
- def acquire_token_with_refresh_token(self, refresh_token, client_id, client_secret, resource):
+ def acquire_token_with_refresh_token(self, refresh_token, client_id, resource, client_secret=None):
+ '''
+ Gets a token for a given resource via refresh tokens
+ Args:
+ refresh_token (str):
+ A refresh token returned in a tokne response from a previous invocation
+ of acquireToken.
+ client_id (str):
+ The OAuth client id of the calling application.
+ resource (str):
+ A URI that identifies the resource for which the token is valid.
+ client_secret (str, optional):
+ The OAuth client secret of the calling application.
+ Returns:
+ dict: with several keys, include "accessToken" and "refreshToken"
+ '''
argument.validate_string_param(refresh_token, 'refresh_token')
argument.validate_string_param(client_id, 'client_id')
argument.validate_string_param(resource, 'resource')
@@ -134,6 +239,20 @@ def token_func(self):
return token
def acquire_token_with_client_certificate(self, resource, client_id, certificate, thumbprint):
+ '''
+ Gets a token for a given resource via certificate credentials
+ Args:
+ resource (str):
+ A URI that identifies the resource for which the token is valid.
+ client_id (str):
+ The OAuth client id of the calling application.
+ certificate (str):
+ A PEM encoded certificate private key.
+ thumbprint (str):
+ hex encoded thumbprint of the certificate.
+ Returns:
+ dict: with several keys, include "accessToken".
+ '''
argument.validate_string_param(resource, 'resource')
argument.validate_string_param(client_id, 'client_id')
argument.validate_string_param(certificate, 'certificate')
@@ -148,31 +267,75 @@ def token_func(self):
return token
def acquire_user_code(self, resource, client_id, language=None):
- self._call_context['log_context'] = log.create_log_context(self._correlation_id)
+ '''
+ Gets the user code info which contains user_code, device_code for authenticating
+ user on device.
+ Args:
+ resource (str):
+ A URI that identifies the resource for which the device_code and
+ user_code is valid for.
+ client_id (str):
+ The OAuth client id of the calling application.
+ language (str):
+ The language code specifying how the message should be localized to.
+ Returns:
+ dict: contains code and uri for users to login through browser.
+ '''
+ self._call_context['log_context'] = log.create_log_context(self.correlation_id)
self.authority.validate(self._call_context)
code_request = CodeRequest(self._call_context, self, client_id, resource)
code = code_request.get_user_code_info(language)
return code
def acquire_token_with_device_code(self, resource, user_code_info, client_id):
- self._call_context['log_context'] = log.create_log_context(self._correlation_id)
+ '''
+ Gets a new access token using via a device code.
+ Args:
+ resource (str):
+ A URI that identifies the resource for which the token is valid.
+ user_code_info (dict):
+ The code info from the invocation of "acquire_user_code"
+ client_id (str):
+ The OAuth client id of the calling application.
+ Returns:
+ dict: with several keys, include "accessToken" and "refreshToken"
+ '''
+ self._call_context['log_context'] = log.create_log_context(self.correlation_id)
def token_func(self):
token_request = TokenRequest(self._call_context, self, client_id, resource)
- self._token_requests_with_user_code[user_code_info[OAuth2DeviceCodeResponseParameters.DEVICE_CODE]] = token_request
+
+ key = user_code_info[OAuth2DeviceCodeResponseParameters.DEVICE_CODE]
+ with self._lock:
+ self._token_requests_with_user_code[key] = token_request
+
token = token_request.get_token_with_device_code(user_code_info)
+
+ with self._lock:
+ self._token_requests_with_user_code.pop(key, None)
+
return token
token = self._acquire_token(token_func)
return token
def cancel_request_to_get_token_with_device_code(self, user_code_info):
+ '''
+ Cancels the polling request to get token with device code.
+ Args:
+ user_code_info (dict):
+ The code info from the invocation of "acquire_user_code"
+ Returns:
+ None
+ '''
argument.validate_user_code_info(user_code_info)
key = user_code_info[OAuth2DeviceCodeResponseParameters.DEVICE_CODE]
- request = self._token_requests_with_user_code.get(key)
- if not request:
- raise ValueError('No acquire_token_with_device_code existed to be cancelled')
+ with self._lock:
+ request = self._token_requests_with_user_code.get(key)
+
+ if not request:
+ raise ValueError('No acquire_token_with_device_code existed to be cancelled')
- request.cancel_token_request_with_device_code()
- self._token_requests_with_user_code.pop(key, None)
+ request.cancel_token_request_with_device_code()
+ self._token_requests_with_user_code.pop(key, None)
diff --git a/adal/authority.py b/adal/authority.py
index eb6919cd..098a59a6 100644
--- a/adal/authority.py
+++ b/adal/authority.py
@@ -25,16 +25,15 @@
#
#------------------------------------------------------------------------------
-import requests
-
try:
from urllib.parse import quote
from urllib.parse import urlparse
-
except ImportError:
from urllib import quote
from urlparse import urlparse
+import requests
+
from .constants import AADConstants
from .adal_error import AdalError
from . import log
@@ -114,8 +113,8 @@ def _perform_dynamic_instance_discovery(self):
try:
resp = requests.get(discovery_endpoint.geturl(), headers=get_options['headers'])
util.log_return_correlation_id(self._log, operation, resp)
- except Exception as exp:
- self._log.error("{0} request failed".format(operation), exp)
+ except Exception:
+ self._log.info("{0} request failed".format(operation))
raise
if not util.is_http_success(resp.status_code):
@@ -125,17 +124,17 @@ def _perform_dynamic_instance_discovery(self):
return_error_string += " and server response: {0}".format(resp.text)
try:
error_response = resp.json()
- except:
+ except ValueError:
pass
- raise AdalError(self._log.create_error(return_error_string), error_response)
+ raise AdalError(return_error_string, error_response)
else:
discovery_resp = resp.json()
if discovery_resp.get('tenant_discovery_endpoint'):
return discovery_resp['tenant_discovery_endpoint']
else:
- raise AdalError(self._log.create_error('Failed to parse instance discovery response'))
+ raise AdalError('Failed to parse instance discovery response')
def _validate_via_instance_discovery(self):
valid = self._perform_static_instance_discovery()
diff --git a/adal/cache_driver.py b/adal/cache_driver.py
index feda4535..8887e91d 100644
--- a/adal/cache_driver.py
+++ b/adal/cache_driver.py
@@ -1,10 +1,38 @@
-import base64
+#------------------------------------------------------------------------------
+#
+# Copyright (c) Microsoft Corporation.
+# All rights reserved.
+#
+# This code is licensed under the MIT License.
+#
+# 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.
+#
+#------------------------------------------------------------------------------
+
+import base64
import copy
import hashlib
import json
from datetime import datetime, timedelta
from dateutil import parser
+from .adal_error import AdalError
from .constants import TokenResponseFields, Misc
from . import log
@@ -12,10 +40,10 @@
# pylint: disable=W0212
def _create_token_hash(token):
- m = hashlib.sha256()
- m.update(token.encode('utf8')) # TODO: what is the default encoding
- hash = base64.b64encode(m.digest())
- return hash
+ hash_object = hashlib.sha256()
+ hash_object.update(token.encode('utf8'))
+ token_hash = base64.b64encode(hash_object.digest())
+ return token_hash
def _create_token_id_message(entry):
access_token_hash = _create_token_hash(entry[TokenResponseFields.ACCESS_TOKEN])
@@ -65,7 +93,9 @@ def _load_single_entry_from_cache(self, query):
potential_entries = self._get_potential_entries(query)
if potential_entries:
resource_tenant_specific_entries = [
- x for x in potential_entries if x[TokenResponseFields.RESOURCE] == self._resource and x[TokenResponseFields._AUTHORITY] == self._authority]
+ x for x in potential_entries
+ if x[TokenResponseFields.RESOURCE] == self._resource and
+ x[TokenResponseFields._AUTHORITY] == self._authority]
if not resource_tenant_specific_entries:
self._log.debug('No resource specific cache entries found.')
@@ -82,7 +112,7 @@ def _load_single_entry_from_cache(self, query):
return_val = resource_tenant_specific_entries[0]
is_resource_tenant_specific = True
else:
- raise ValueError('More than one token matches the criteria. The result is ambiguous.')
+ raise AdalError('More than one token matches the criteria. The result is ambiguous.')
if return_val:
self._log.debug(
@@ -119,7 +149,7 @@ def _acquire_new_token_from_mrrt(self, entry):
return new_entry
def _refresh_entry_if_necessary(self, entry, is_resource_specific):
- expiry_date = parser.parse(entry[TokenResponseFields.EXPIRES_ON]) #get clear on local time and time saving
+ expiry_date = parser.parse(entry[TokenResponseFields.EXPIRES_ON])
now = datetime.now(expiry_date.tzinfo)
# Add some buffer in to the time comparison to account for clock skew or latency.
@@ -197,4 +227,3 @@ def add(self, entry):
self._argument_entry_with_cached_metadata(entry)
self._update_refresh_tokens(entry)
self._cache.add([entry])
-
diff --git a/adal/code_request.py b/adal/code_request.py
index 316c673b..14926797 100644
--- a/adal/code_request.py
+++ b/adal/code_request.py
@@ -1,4 +1,31 @@
-from . import constants
+#------------------------------------------------------------------------------
+#
+# Copyright (c) Microsoft Corporation.
+# All rights reserved.
+#
+# This code is licensed under the MIT License.
+#
+# 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.
+#
+#------------------------------------------------------------------------------
+
+from . import constants
from . import log
from . import oauth2_client
diff --git a/adal/log.py b/adal/log.py
index e715f3a1..fd5e77e8 100644
--- a/adal/log.py
+++ b/adal/log.py
@@ -54,7 +54,7 @@ class LOGGING_LEVEL:
def create_log_context(correlation_id=None):
return {'correlation_id' : correlation_id or str(uuid.uuid4())}
-def set_logging_options(options={}):
+def set_logging_options(options=None):
'''
To set level: {'level': adal.log.LOGGING_LEVEL.DEBUG}
To add console log: { 'handler': logging.StreamHandler()}
diff --git a/adal/mex.py b/adal/mex.py
index f2e9a393..181a0210 100644
--- a/adal/mex.py
+++ b/adal/mex.py
@@ -26,25 +26,22 @@
#------------------------------------------------------------------------------
import random
-import requests
-
-from . import log
-from . import util
-from . import xmlutil
try:
- from urllib.parse import quote, unquote
- from urllib.parse import urlparse, urlsplit
-
+ from urllib.parse import urlparse
except ImportError:
- from urllib import quote, unquote
- from urlparse import urlparse, urlsplit
+ from urlparse import urlparse
try:
from xml.etree import cElementTree as ET
except ImportError:
from xml.etree import ElementTree as ET
+import requests
+
+from . import log
+from . import util
+from . import xmlutil
from .constants import XmlNamespaces
from .constants import MexNamespaces
from .adal_error import AdalError
@@ -63,43 +60,38 @@ def __init__(self, call_context, url):
self._log.debug("Mex created with url: {0}".format(self._url))
def discover(self):
-
self._log.debug("Retrieving mex at: {0}".format(self._url))
options = util.create_request_options(self, {'headers': {'Content-Type': 'application/soap+xml'}})
+ resp = None
try:
operation = "Mex Get"
resp = requests.get(self._url, headers=options['headers'])
util.log_return_correlation_id(self._log, operation, resp)
+ except Exception:
+ self._log.info("{0} request failed".format(operation))
+ raise
- if not util.is_http_success(resp.status_code):
- return_error_string = "{0} request returned http error: {1}".format(operation, resp.status_code)
- error_response = ""
- if resp.text:
- return_error_string += " and server response: {0}".format(resp.text)
- try:
- error_response = resp.json()
- except:
- pass
-
- raise AdalError(self._log.create_error(return_error_string), error_response)
-
- else:
+ if not util.is_http_success(resp.status_code):
+ return_error_string = "{0} request returned http error: {1}".format(operation, resp.status_code)
+ error_response = ""
+ if resp.text:
+ return_error_string += " and server response: {0}".format(resp.text)
try:
- self._mex_doc = resp.text
- #options = {'errorHandler':self._log.error}
- self._dom = ET.fromstring(self._mex_doc)
- self._parents = {c:p for p in self._dom.iter() for c in p}
- self._parse()
- except AdalError as exp:
- self._log.error('Failed to parse mex response in to DOM', exp)
- raise
- return
- return
-
- except Exception as exp:
- self._log.error("{0} request failed".format(operation), exp)
- raise
+ error_response = resp.json()
+ except ValueError:
+ pass
+ raise AdalError(return_error_string, error_response)
+ else:
+ try:
+ self._mex_doc = resp.text
+ #options = {'errorHandler':self._log.error}
+ self._dom = ET.fromstring(self._mex_doc)
+ self._parents = {c:p for p in self._dom.iter() for c in p}
+ self._parse()
+ except Exception:
+ self._log.info('Failed to parse mex response in to DOM')
+ raise
def _check_policy(self, policy_node):
policy_id = policy_node.attrib["{{{}}}Id".format(XmlNamespaces.namespaces['wsu'])]
@@ -130,9 +122,9 @@ def _select_username_password_polices(self):
for node in username_token_nodes:
policy_node = self._parents[self._parents[self._parents[self._parents[self._parents[self._parents[self._parents[node]]]]]]]
- id = self._check_policy(policy_node)
- if id:
- id_ref = '#' + id
+ policy_id = self._check_policy(policy_node)
+ if policy_id:
+ id_ref = '#' + policy_id
policies[id_ref] = {id:id_ref}
return policies if policies else None
@@ -143,7 +135,7 @@ def _check_soap_action_and_transport(self, binding_node):
soap_transport = ""
name = binding_node.get('name')
- soap_transport_attributes = []
+ soap_transport_attributes = ""
soap_action_attributes = xmlutil.xpath_find(binding_node, MexNamespaces.SOAP_ACTION_XPATH)[0].attrib['soapAction']
if soap_action_attributes:
@@ -178,8 +170,8 @@ def _get_matching_bindings(self, policies):
return bindings if bindings else None
- def _url_is_secure(self, endpoint_url):
-
+ @staticmethod
+ def _url_is_secure(endpoint_url):
parsed = urlparse(endpoint_url)
return parsed.scheme == 'https'
@@ -201,7 +193,7 @@ def _get_ports_for_policy_bindings(self, bindings, policies):
raise self._log.create_error("No address nodes on port")
address = xmlutil.find_element_text(address_node)
- if self._url_is_secure(address):
+ if Mex._url_is_secure(address):
binding_policy['url'] = address
else:
self._log.warn("Skipping insecure endpoint: {0}".format(address))
@@ -220,15 +212,15 @@ def _parse(self):
policies = self._select_username_password_polices()
if not policies:
- raise AdalError(self._log.create_error("No matching policies."))
+ raise AdalError("No matching policies.")
bindings = self._get_matching_bindings(policies)
if not bindings:
- raise AdalError(self._log.create_error("No matching bindings."))
+ raise AdalError("No matching bindings.")
self._get_ports_for_policy_bindings(bindings, policies)
self._select_single_matching_policy(policies)
if not self._url:
- raise AdalError(self._log.create_error("No ws-trust endpoints match requirements."))
+ raise AdalError("No ws-trust endpoints match requirements.")
diff --git a/adal/oauth2_client.py b/adal/oauth2_client.py
index ddf438d3..87339d6f 100644
--- a/adal/oauth2_client.py
+++ b/adal/oauth2_client.py
@@ -27,11 +27,10 @@
from datetime import datetime, timedelta
import math
-import uuid
-import requests
import re
import json
import time
+import uuid
try:
from urllib.parse import urlencode
@@ -40,6 +39,8 @@
from urllib import urlencode
from urlparse import urlparse
+import requests
+
from . import log
from . import util
from .constants import OAuth2, TokenResponseFields, IdTokenFields
@@ -86,7 +87,8 @@ def _parse_optional_ints(self, obj, keys):
try:
obj[key] = int(obj[key])
except ValueError:
- raise self._log.create_error("{0} could not be parsed as an int".format(key))
+ self._log.info("{0} could not be parsed as an int".format(key))
+ raise
except KeyError:
# if the key isn't present we can just continue
pass
@@ -94,7 +96,7 @@ def _parse_optional_ints(self, obj, keys):
@classmethod
def _crack_jwt(cls, jwt_token):
- id_token_parts_reg = "^([^\.\s]*)\.([^\.\s]+)\.([^\.\s]*)$"
+ id_token_parts_reg = r"^([^\.\s]*)\.([^\.\s]+)\.([^\.\s]*)$"
matches = re.search(id_token_parts_reg, jwt_token)
if not matches or len(matches.groups()) < 3:
raise ValueError('The token was not parsable.')
@@ -107,7 +109,8 @@ def _crack_jwt(cls, jwt_token):
return cracked_token
- def _get_user_id(self, id_token):
+ @staticmethod
+ def _get_user_id(id_token):
user_id = None
is_displayable = False
@@ -132,10 +135,11 @@ def _get_user_id(self, id_token):
return user_id_vals
- def _extract_token_values(self, id_token):
+ @staticmethod
+ def _extract_token_values(id_token):
extracted_values = {}
extracted_values = map_fields(id_token, OAuth2.IdTokenMap)
- extracted_values.update(self._get_user_id(id_token))
+ extracted_values.update(OAuth2Client._get_user_id(id_token))
return extracted_values
def _parse_id_token(self, encoded_token):
@@ -147,18 +151,17 @@ def _parse_id_token(self, encoded_token):
id_token = None
try:
b64_id_token = cracked_token['JWSPayload']
- b64_decoded = util.base64_urlsafe_decode(str(b64_id_token))
+ b64_decoded = util.base64_urlsafe_decode(b64_id_token)
if not b64_decoded:
self._log.warn('The returned id_token could not be base64 url safe decoded.')
return
id_token = json.loads(b64_decoded.decode())
-
- except Exception as exp:
- self._log.warn("The returned id_token could not be decoded: {0}".format(exp))
+ except ValueError:
+ self._log.info("The returned id_token could not be decoded: {0}".format(encoded_token))
raise
- return self._extract_token_values(id_token)
+ return OAuth2Client._extract_token_values(id_token)
def _validate_token_response(self, body):
@@ -167,8 +170,10 @@ def _validate_token_response(self, body):
try:
wire_response = json.loads(body)
- except Exception:
- raise ValueError('The token response returned from the server is unparseable as JSON')
+ except ValueError:
+ self._log.info(
+ 'The token response from the server is unparseable as JSON:' + body)
+ raise
int_keys = [
OAuth2.ResponseParameters.EXPIRES_ON,
@@ -210,8 +215,9 @@ def _validate_device_code_response(self, body):
try:
wire_response = json.loads(body)
- except Exception:
- raise ValueError('The device code response returned from the server is unparseable as JSON')
+ except ValueError:
+ self._log.info('The device code response returned from the server is unparseable as JSON:')
+ raise
int_keys = [
OAuth2.DeviceCodeResponseParameters.EXPIRES_IN,
@@ -221,13 +227,13 @@ def _validate_device_code_response(self, body):
self._parse_optional_ints(wire_response, int_keys)
if not wire_response.get(OAuth2.DeviceCodeResponseParameters.EXPIRES_IN):
- raise self._log.create_error('wire_response is missing expires_in')
+ raise AdalError('wire_response is missing expires_in', wire_response)
if not wire_response.get(OAuth2.DeviceCodeResponseParameters.DEVICE_CODE):
- raise self._log.create_error('wire_response is missing device_code')
+ raise AdalError('wire_response is missing device_code', wire_response)
if not wire_response.get(OAuth2.DeviceCodeResponseParameters.USER_CODE):
- raise self._log.create_error('wire_response is missing user_code')
+ raise AdalError('wire_response is missing user_code', wire_response)
#skip field naming tweak, becasue names from wire are python style already
return wire_response
@@ -237,8 +243,8 @@ def _handle_get_token_response(self, body):
token_response = None
try:
token_response = self._validate_token_response(body)
- except Exception as exp:
- self._log.error("Error validating get token response", exp)
+ except Exception:
+ self._log.info("Error validating get token response '{}'".format(body))
raise
return token_response
@@ -248,8 +254,8 @@ def _handle_get_device_code_response(self, body):
device_code_response = None
try:
device_code_response = self._validate_device_code_response(body)
- except Exception as exp:
- self._log.error('Error validating get user vcode response', exp)
+ except Exception:
+ self._log.info("Error validating get user code response '{}'".format(body))
raise
return device_code_response
@@ -262,66 +268,67 @@ def get_token(self, oauth_parameters):
post_options = util.create_request_options(self, {'headers' : {'content-type': 'application/x-www-form-urlencoded'}})
operation = "Get Token"
+ resp = None
try:
resp = requests.post(token_url.geturl(), data=url_encoded_token_request, headers=post_options['headers'])
util.log_return_correlation_id(self._log, operation, resp)
-
- if util.is_http_success(resp.status_code):
- token = self._handle_get_token_response(resp.text)
- return token
- else:
- return_error_string = "{0} request returned http error: {1}".format(operation, resp.status_code)
- error_response = ""
- if resp.text:
- return_error_string += " and server response: {0}".format(resp.text)
- try:
- error_response = resp.json()
- except:
- pass
-
- raise AdalError(self._log.create_error(return_error_string), error_response)
-
- except Exception as exp:
- self._log.error("{0} request failed".format(operation), exp)
+ except Exception:
+ self._log.info("{0} request failed".format(operation))
raise
+ if util.is_http_success(resp.status_code):
+ token = self._handle_get_token_response(resp.text)
+ return token
+ else:
+ return_error_string = "{0} request returned http error: {1}".format(operation, resp.status_code)
+ error_response = ""
+ if resp.text:
+ return_error_string += " and server response: {0}".format(resp.text)
+ try:
+ error_response = resp.json()
+ except ValueError:
+ pass
+ raise AdalError(return_error_string, error_response)
+
def get_user_code_info(self, oauth_parameters):
device_code_url = self._create_device_code_url()
url_encoded_code_request = urlencode(oauth_parameters)
post_options = util.create_request_options(self, {'headers' : {'content-type': 'application/x-www-form-urlencoded'}})
operation = "Get Device Code"
-
+ resp = None
try:
resp = requests.post(device_code_url.geturl(), data=url_encoded_code_request, headers=post_options['headers'])
util.log_return_correlation_id(self._log, operation, resp)
-
- if util.is_http_success(resp.status_code):
- code = self._handle_get_device_code_response(resp.text)
- return code
- else:
- return_error_string = "{0} request returned http error: {1}".format(operation, resp.status_code)
- error_response = ""
- if resp.text:
- return_error_string += " and server response: {0}".format(resp.text)
- try:
- error_response = resp.json()
- except:
- pass
-
- raise AdalError(self._log.create_error(return_error_string), error_response)
-
- except Exception as exp:
- self._log.error("{0} request failed".format(operation), exp)
+ except Exception:
+ self._log.info("{} request failed".format(operation))
raise
+ if util.is_http_success(resp.status_code):
+ code = self._handle_get_device_code_response(resp.text)
+ return code
+ else:
+ return_error_string = "{} request returned http error: {}".format(operation, resp.status_code)
+ error_response = ""
+ if resp.text:
+ return_error_string += " and server response: {}".format(resp.text)
+ try:
+ error_response = resp.json()
+ except ValueError:
+ pass
+
+ raise AdalError(return_error_string, error_response)
+
def get_token_with_polling(self, oauth_parameters, refresh_internal, expires_in):
token_response = {}
token_url = self._create_token_url()
url_encoded_code_request = urlencode(oauth_parameters)
- post_options = util.create_request_options(self, {'headers' : {'content-type': 'application/x-www-form-urlencoded'}})
+ post_options = util.create_request_options(
+ self,
+ {'headers' : {'content-type': 'application/x-www-form-urlencoded'}})
+
operation = "Get token with device code"
max_times_for_retry = math.floor(expires_in/refresh_internal)
@@ -329,7 +336,10 @@ def get_token_with_polling(self, oauth_parameters, refresh_internal, expires_in)
if self._cancel_polling_request:
raise AdalError('Polling_Request_Cancelled')
- resp = requests.post(token_url.geturl(), data=url_encoded_code_request, headers=post_options['headers'])
+ resp = requests.post(
+ token_url.geturl(),
+ data=url_encoded_code_request, headers=post_options['headers'])
+
util.log_return_correlation_id(self._log, operation, resp)
wire_response = {}
@@ -342,12 +352,13 @@ def get_token_with_polling(self, oauth_parameters, refresh_internal, expires_in)
time.sleep(refresh_internal)
continue
else:
- raise ValueError(error)
+ raise AdalError('Unexpected polling state {}'.format(error),
+ wire_response)
else:
try:
token_response = self._validate_token_response(resp.text)
- except Exception as exp:
- self._log.error("Error validating get token response", exp)
+ except Exception:
+ self._log.info("Error validating get token response '{}'".format(resp.text))
raise
return token_response
diff --git a/adal/self_signed_jwt.py b/adal/self_signed_jwt.py
index bb409ab4..ee49aabe 100644
--- a/adal/self_signed_jwt.py
+++ b/adal/self_signed_jwt.py
@@ -35,13 +35,12 @@
from .constants import Jwt
from .log import Logger
-from . import util
class SelfSignedJwt(object):
NumCharIn128BitHexString = 128/8*2
numCharIn160BitHexString = 160/8*2
- ThumbprintRegEx = "^[a-f\d]*$"
+ ThumbprintRegEx = r"^[a-f\d]*$"
def __init__(self, call_context, authority, client_id):
self._log = Logger('SelfSignedJwt', call_context['log_context'])
@@ -51,19 +50,22 @@ def __init__(self, call_context, authority, client_id):
self._token_endpoint = authority.token_endpoint
self._client_id = client_id
- def _get_date_now(self):
+ @staticmethod
+ def _get_date_now():
return datetime.datetime.now()
- def _get_new_jwt_id(self):
+ @staticmethod
+ def _get_new_jwt_id():
return str(uuid.uuid4())
- def _create_x5t_value(self, thumbprint):
- hex = binascii.a2b_hex(thumbprint)
- b64_str = base64.urlsafe_b64encode(hex).decode()
+ @staticmethod
+ def _create_x5t_value(thumbprint):
+ hex_val = binascii.a2b_hex(thumbprint)
+ b64_str = base64.urlsafe_b64encode(hex_val).decode()
return b64_str
def _create_header(self, thumbprint):
- x5t = self._create_x5t_value(thumbprint)
+ x5t = SelfSignedJwt._create_x5t_value(thumbprint)
header = {'typ':'JWT', 'alg':'RS256', 'x5t':x5t}
self._log.debug("Creating self signed JWT header. x5t: {0}".format(x5t))
@@ -72,7 +74,7 @@ def _create_header(self, thumbprint):
def _create_payload(self):
- now = self._get_date_now()
+ now = SelfSignedJwt._get_date_now()
minutes = datetime.timedelta(0, 0, 0, 0, Jwt.SELF_SIGNED_JWT_LIFETIME)
expires = now + minutes
@@ -84,7 +86,7 @@ def _create_payload(self):
jwt_payload[Jwt.SUBJECT] = self._client_id
jwt_payload[Jwt.NOT_BEFORE] = int(time.mktime(now.timetuple()))
jwt_payload[Jwt.EXPIRES_ON] = int(time.mktime(expires.timetuple()))
- jwt_payload[Jwt.JWT_ID] = self._get_new_jwt_id()
+ jwt_payload[Jwt.JWT_ID] = SelfSignedJwt._get_new_jwt_id()
return jwt_payload
@@ -100,11 +102,12 @@ def _raise_on_invalid_thumbprint(self, thumbprint):
raise self._log.create_error("The thumbprint does not match a known format")
def _sign_jwt(self, header, payload, certificate):
- encoded_jwt = self._encode_jwt(payload, certificate, header)
+ encoded_jwt = SelfSignedJwt._encode_jwt(payload, certificate, header)
self._raise_on_invalid_jwt_signature(encoded_jwt)
return encoded_jwt
- def _encode_jwt(self, payload, certificate, header):
+ @staticmethod
+ def _encode_jwt(payload, certificate, header):
return jwt.encode(payload, certificate, algorithm='RS256', headers=header).decode()
def _reduce_thumbprint(self, thumbprint):
diff --git a/adal/token_cache.py b/adal/token_cache.py
index b24bd7b2..6a46f6c7 100644
--- a/adal/token_cache.py
+++ b/adal/token_cache.py
@@ -1,4 +1,32 @@
-import json
+#------------------------------------------------------------------------------
+#
+# Copyright (c) Microsoft Corporation.
+# All rights reserved.
+#
+# This code is licensed under the MIT License.
+#
+# 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.
+#
+#------------------------------------------------------------------------------
+
+import json
+import threading
from .constants import TokenResponseFields
@@ -32,48 +60,54 @@ def __eq__(self, other):
def __ne__(self, other):
return not self == other
-#TODO: ensure thread safety
class TokenCache(object):
def __init__(self, state=None):
self._cache = {}
+ self._lock = threading.RLock()
if state:
self.deserialize(state)
self.has_state_changed = False
def find(self, query):
- entries = self._query_cache(
- query.get(TokenResponseFields.IS_MRRT),
- query.get(TokenResponseFields.USER_ID),
- query.get(TokenResponseFields._CLIENT_ID))
- return entries
+ with self._lock:
+ entries = self._query_cache(
+ query.get(TokenResponseFields.IS_MRRT),
+ query.get(TokenResponseFields.USER_ID),
+ query.get(TokenResponseFields._CLIENT_ID))
+ return entries
def remove(self, entries):
- for e in entries:
- key = TokenCache._get_cache_key(e)
- self._cache.pop(key)
- self.has_state_changed = True
+ with self._lock:
+ for e in entries:
+ key = TokenCache._get_cache_key(e)
+ self._cache.pop(key)
+ self.has_state_changed = True
def add(self, entries):
- for e in entries:
- key = TokenCache._get_cache_key(e)
- self._cache[key] = e
- self.has_state_changed = True
+ with self._lock:
+ for e in entries:
+ key = TokenCache._get_cache_key(e)
+ self._cache[key] = e
+ self.has_state_changed = True
def serialize(self):
- state = json.dumps(list(self._cache.values()))
- return state
+ with self._lock:
+ state = json.dumps(list(self._cache.values()))
+ return state
def deserialize(self, state):
- self._cache.clear()
- if state:
- tokens = json.loads(state)
- for t in tokens:
- key = self._get_cache_key(t)
- self._cache[key] = t
+ with self._lock:
+ self._cache.clear()
+ if state:
+ tokens = json.loads(state)
+ for t in tokens:
+ key = self._get_cache_key(t)
+ self._cache[key] = t
def read_items(self):
'''output list of tuples in (key, authentication-result)'''
- return self._cache.items()
+ with self._lock:
+ return self._cache.items()
@staticmethod
def _get_cache_key(entry):
@@ -87,6 +121,7 @@ def _query_cache(self, is_mrrt, user_id, client_id):
matches = []
for k in self._cache:
v = self._cache[k]
+ #None value will be taken as wildcard match
if (is_mrrt is None or is_mrrt == v.get(TokenResponseFields.IS_MRRT)) and \
(user_id is None or _string_cmp(user_id, v.get(TokenResponseFields.USER_ID))) and \
(client_id is None or _string_cmp(client_id, v.get(TokenResponseFields._CLIENT_ID))):
diff --git a/adal/token_request.py b/adal/token_request.py
index 9f106eb5..58c7db41 100644
--- a/adal/token_request.py
+++ b/adal/token_request.py
@@ -25,7 +25,6 @@
#
#------------------------------------------------------------------------------
-from functools import partial
from base64 import b64encode
from . import constants
@@ -64,11 +63,16 @@ def __init__(self, call_context, authentication_context, client_id, resource, re
self._client_id = client_id
self._redirect_uri = redirect_uri
- # This should be set at the beginning of get_token
+ self._cache_driver = None
+
+ # should be set at the beginning of get_token
# functions that have a user_id
self._user_id = None
self._user_realm = None
+ # should be set when acquire token using device flow
+ self._polling_client = None
+
def _create_user_realm_request(self, username):
return user_realm.UserRealm(self._call_context, username, self._authentication_context.authority.url)
@@ -130,9 +134,9 @@ def _create_oauth_parameters(self, grant_type):
oauth_parameters[OAUTH2_PARAMETERS.GRANT_TYPE] = grant_type
if (OAUTH2_GRANT_TYPE.AUTHORIZATION_CODE != grant_type and
- OAUTH2_GRANT_TYPE.CLIENT_CREDENTIALS != grant_type and
- OAUTH2_GRANT_TYPE.REFRESH_TOKEN != grant_type and
- OAUTH2_GRANT_TYPE.DEVICE_CODE != grant_type):
+ OAUTH2_GRANT_TYPE.CLIENT_CREDENTIALS != grant_type and
+ OAUTH2_GRANT_TYPE.REFRESH_TOKEN != grant_type and
+ OAUTH2_GRANT_TYPE.DEVICE_CODE != grant_type):
oauth_parameters[OAUTH2_PARAMETERS.SCOPE] = OAUTH2_SCOPE.OPENID
@@ -183,10 +187,12 @@ def _perform_wstrust_exchange(self, wstrust_endpoint, username, password):
wstrust_response = wstrust.acquire_token(username, password)
return wstrust_response
except AdalError as exp:
- error_msg = exp.error_msg
- if not error_msg:
- error_msg = "Unsuccessful RSTR.\n\terror code: {0}\n\tfaultMessage: {1}".format(exp.error_response.error_code, exp.error_response.fault_message)
- self._log.create_error(error_msg)
+ error_msg = str(exp)
+ if exp.error_response:
+ err_template = "Unsuccessful RSTR.\n\terror code: {0}\n\tfaultMessage: {1}"
+ error_msg = (err_template.format(exp.error_response.error_code,
+ exp.error_response.fault_message))
+ self._log.info(error_msg)
raise
def _perform_username_password_for_access_token_exchange(self, wstrust_endpoint, username, password):
@@ -198,12 +204,16 @@ def _get_token_username_password_federated(self, username, password):
self._log.debug("Acquiring token with username password for federated user")
if not self._user_realm.federation_metadata_url:
- self._log.warn("Unable to retrieve federationMetadataUrl from AAD. Attempting fallback to AAD supplied endpoint.")
+ self._log.warn("Unable to retrieve federationMetadataUrl from AAD. " +
+ "Attempting fallback to AAD supplied endpoint.")
if not self._user_realm.federation_active_auth_url:
- raise AdalError('AAD did not return a WSTrust endpoint. Unable to proceed.')
+ raise AdalError('AAD did not return a WSTrust endpoint. Unable to proceed.')
- token = self._perform_username_password_for_access_token_exchange(self._user_realm.federation_active_auth_url, username, password)
+ token = self._perform_username_password_for_access_token_exchange(
+ self._user_realm.federation_active_auth_url,
+ username,
+ password)
return token
else:
mex_endpoint = self._user_realm.federation_metadata_url
@@ -214,11 +224,13 @@ def _get_token_username_password_federated(self, username, password):
try:
mex_instance.discover()
wstrust_endpoint = mex_instance.username_password_url
- except:
- self._log.warn("MEX exchange failed. Attempting fallback to AAD supplied endpoint.")
+ except Exception:
+ warn_template = ("MEX exchange failed for {}. " +
+ "Attempting fallback to AAD supplied endpoint.")
+ self._log.warn(warn_template.format(mex_endpoint))
wstrust_endpoint = self._user_realm.federation_active_auth_url
if not wstrust_endpoint:
- raise AdalError('AAD did not return a WSTrust endpoint. Unable to proceed.')
+ raise AdalError('AAD did not return a WSTrust endpoint. Unable to proceed.')
token = self._perform_username_password_for_access_token_exchange(wstrust_endpoint, username, password)
return token
@@ -230,7 +242,7 @@ def get_token_with_username_password(self, username, password):
token = self._find_token_from_cache()
if token:
return token
- except Exception as exp:
+ except AdalError as exp:
self._log.warn('Attempt to look for token in cache resulted in Error: {}'.format(exp), True)
self._user_realm = self._create_user_realm_request(username)
@@ -242,10 +254,11 @@ def get_token_with_username_password(self, username, password):
elif self._user_realm.account_type == ACCOUNT_TYPE['Federated']:
token = self._get_token_username_password_federated(username, password)
else:
- raise AdalError(self._log.create_error("Server returned an unknown AccountType: {0}".format(self._user_realm.account_type)))
+ raise AdalError(
+ "Server returned an unknown AccountType: {0}".format(self._user_realm.account_type))
self._log.debug("Successfully retrieved token from authority.")
- except Exception as exp:
- self._log.warn("get_token_func returned with err".format(exp))
+ except Exception:
+ self._log.info("get_token_func returned with error")
raise
self._cache_driver.add(token)
@@ -257,7 +270,7 @@ def get_token_with_client_credentials(self, client_secret):
token = self._find_token_from_cache()
if token:
return token
- except Exception as exp:
+ except AdalError as exp:
self._log.warn('Attempt to look for token in cache resulted in Error: {}'.format(exp), True)
oauth_parameters = self._create_oauth_parameters(OAUTH2_GRANT_TYPE.CLIENT_CREDENTIALS)
@@ -276,7 +289,6 @@ def get_token_with_authorization_code(self, authorization_code, client_secret):
oauth_parameters[OAUTH2_PARAMETERS.CLIENT_SECRET] = client_secret
token = self._oauth_get_token(oauth_parameters)
- self._cache_driver.add(token)
return token
def _get_token_with_refresh_token(self, refresh_token, resource, client_secret):
@@ -329,7 +341,7 @@ def get_token_with_certificate(self, certificate, thumbprint):
token = self._find_token_from_cache()
if token:
return token
- except Exception as exp:
+ except AdalError as exp:
self._log.warn('Attempt to look for token in cache resulted in Error: {}'.format(exp), True)
token = self._oauth_get_token(oauth_parameters)
diff --git a/adal/user_realm.py b/adal/user_realm.py
index 57624eda..2afa666e 100644
--- a/adal/user_realm.py
+++ b/adal/user_realm.py
@@ -25,7 +25,6 @@
#
#------------------------------------------------------------------------------
import json
-import requests
try:
from urllib.parse import quote, urlencode
@@ -34,6 +33,8 @@
from urllib import quote, urlencode
from urlparse import urlunparse
+import requests
+
from . import constants
from . import log
from . import util
@@ -61,17 +62,18 @@ def __init__(self, call_context, user_principle, authority_url):
def _get_user_realm_url(self):
- user_realm_url = list(util.copy_url(self._authority_url))
+ url_components = list(util.copy_url(self._authority_url))
url_encoded_user = quote(self._user_principle, safe='~()*!.\'')
- user_realm_url[2] = '/' + USER_REALM_PATH_TEMPLATE.replace('', url_encoded_user)
+ url_components[2] = '/' + USER_REALM_PATH_TEMPLATE.replace('', url_encoded_user)
user_realm_query = {'api-version':self.api_version}
- user_realm_url[4] = urlencode(user_realm_query)
- user_realm_url = util.copy_url(urlunparse(user_realm_url))
+ url_components[4] = urlencode(user_realm_query)
+ user_realm_url = util.copy_url(urlunparse(url_components))
return user_realm_url
- def _validate_constant_value(self, constants, value, case_sensitive=False):
+ @staticmethod
+ def _validate_constant_value(value_dic, value, case_sensitive=False):
if not value:
return False
@@ -79,13 +81,15 @@ def _validate_constant_value(self, constants, value, case_sensitive=False):
if not case_sensitive:
value = value.lower()
- return value if value in constants.values() else False
+ return value if value in value_dic.values() else False
- def _validate_account_type(self, type):
- return self._validate_constant_value(ACCOUNT_TYPE, type)
+ @staticmethod
+ def _validate_account_type(account_type):
+ return UserRealm._validate_constant_value(ACCOUNT_TYPE, account_type)
- def _validate_federation_protocol(self, protocol):
- return self._validate_constant_value(FEDERATION_PROTOCOL_TYPE, protocol)
+ @staticmethod
+ def _validate_federation_protocol(protocol):
+ return UserRealm._validate_constant_value(FEDERATION_PROTOCOL_TYPE, protocol)
def _log_parsed_response(self):
@@ -102,19 +106,22 @@ def _parse_discovery_response(self, body):
response = None
try:
response = json.loads(body)
- except Exception as exp:
- raise AdalError(self._log.create_error('Parsing realm discovery response JSON failed: {0}'.format(body)))
+ except ValueError:
+ error_template = ("Parsing realm discovery response JSON failed " +
+ "for body: '{}'")
+ self._log.info(error_template.format(body))
+ raise
- account_type = self._validate_account_type(response['account_type'])
+ account_type = UserRealm._validate_account_type(response['account_type'])
if not account_type:
- raise AdalError(self._log.create_error('Cannot parse account_type: {0}'.format(account_type)))
+ raise AdalError('Cannot parse account_type: {0}'.format(account_type))
self.account_type = account_type
if self.account_type == ACCOUNT_TYPE['Federated']:
- protocol = self._validate_federation_protocol(response['federation_protocol'])
+ protocol = UserRealm._validate_federation_protocol(response['federation_protocol'])
if not protocol:
- raise AdalError(self._log.create_error('Cannot parse federation protocol: {0}'.format(protocol)))
+ raise AdalError('Cannot parse federation protocol: {0}'.format(protocol))
self.federation_protocol = protocol
self.federation_metadata_url = response['federation_metadata_url']
@@ -139,10 +146,10 @@ def discover(self):
return_error_string += " and server response: {0}".format(resp.text)
try:
error_response = resp.json()
- except:
+ except ValueError:
pass
- raise AdalError(self._log.create_error(return_error_string), error_response)
+ raise AdalError(return_error_string, error_response)
else:
self._parse_discovery_response(resp.text)
diff --git a/adal/util.py b/adal/util.py
index d35bc3df..19e3177e 100644
--- a/adal/util.py
+++ b/adal/util.py
@@ -28,18 +28,15 @@
import sys
import platform
import base64
-
-from .constants import AdalIdParameters
-import adal
-
try:
-
from urllib.parse import urlparse
-
except ImportError:
-
from urlparse import urlparse
+import adal
+
+from .constants import AdalIdParameters
+
def is_http_success(status_code):
return status_code >= 200 and status_code < 300
@@ -76,7 +73,9 @@ def create_request_options(self, *options):
def log_return_correlation_id(log, operation_message, response):
if response and response.headers and response.headers.get('client-request-id'):
- log.info("{0} Server returned this correlation_id: {1}".format(operation_message, response.headers['client-request-id']))
+ log.info("{0} Server returned this correlation_id: {1}".format(
+ operation_message,
+ response.headers['client-request-id']))
def copy_url(url_source):
if hasattr(url_source, 'geturl'):
@@ -88,6 +87,5 @@ def copy_url(url_source):
# the string needs to be correctly padded before decoding.
def base64_urlsafe_decode(b64string):
b64string += '=' * (4 - ((len(b64string) % 4)))
-
- return base64.urlsafe_b64decode(b64string)
+ return base64.urlsafe_b64decode(b64string.encode('utf-8'))
diff --git a/adal/wstrust_request.py b/adal/wstrust_request.py
index b2152ff4..981c6d1d 100644
--- a/adal/wstrust_request.py
+++ b/adal/wstrust_request.py
@@ -25,14 +25,15 @@
#
#------------------------------------------------------------------------------
-import requests
import uuid
-import time
from datetime import datetime, timedelta
+import requests
+
from . import log
from . import util
from . import wstrust_response
+from .adal_error import AdalError
class WSTrustRequest(object):
@@ -41,15 +42,17 @@ def __init__(self, call_context, watrust_endpoint_url, applies_to):
self._call_context = call_context
self._wstrust_endpoint_url = watrust_endpoint_url
self._applies_to = applies_to
-
- def _build_soap_message_credentials(self, username, password):
+
+ @staticmethod
+ def _build_soap_message_credentials(username, password):
username_token_xml = "\
{0}\
{1}\
".format(username, password)
return username_token_xml
- def _build_security_header(self, username, password):
+ @staticmethod
+ def _build_security_header(username, password):
time_now = datetime.utcnow()
expire_time = time_now + timedelta(minutes=10)
@@ -62,7 +65,7 @@ def _build_security_header(self, username, password):
{0}\
{1}\
{2}".format(time_now_str, expire_time_str,
- self._build_soap_message_credentials(username, password))
+ WSTrustRequest._build_soap_message_credentials(username, password))
return security_header_xml
def _build_rst(self, username, password):
@@ -89,7 +92,7 @@ def _build_rst(self, username, password):
http://docs.oasis-open.org/ws-sx/ws-trust/200512/Issue\
\
\
- ".format(message_id, self._wstrust_endpoint_url, self._build_security_header(username, password), self._applies_to)
+ ".format(message_id, self._wstrust_endpoint_url, WSTrustRequest._build_security_header(username, password), self._applies_to)
return rst
@@ -108,25 +111,20 @@ def acquire_token(self, username, password):
self._log.debug("Sending RST to: {0}\n{1}".format(self._wstrust_endpoint_url, rst))
operation = "WS-Trust RST"
- try:
- resp = requests.post(self._wstrust_endpoint_url, headers=options['headers'], data=rst, allow_redirects=True)
-
- util.log_return_correlation_id(self._log, operation, resp)
-
- if not util.is_http_success(resp.status_code):
- return_error_string = "{0} request returned http error: {1}".format(operation, resp.status_code)
- error_response = ""
- if resp.text:
- return_error_string += " and server response: {0}".format(resp.text)
- try:
- error_response = resp.json()
- except:
- pass
-
- raise WsTokenRequestError(self._log.create_error(return_error_string), error_response)
- else:
- self._handle_rstr(resp.text)
-
- except Exception as exp:
- self._log.error("{0} request failed".format(operation), exp)
- raise
+ resp = requests.post(self._wstrust_endpoint_url, headers=options['headers'], data=rst, allow_redirects=True)
+
+ util.log_return_correlation_id(self._log, operation, resp)
+
+ if not util.is_http_success(resp.status_code):
+ return_error_string = "{0} request returned http error: {1}".format(operation, resp.status_code)
+ error_response = ""
+ if resp.text:
+ return_error_string += " and server response: {0}".format(resp.text)
+ try:
+ error_response = resp.json()
+ except ValueError:
+ pass
+
+ raise AdalError(return_error_string, error_response)
+ else:
+ self._handle_rstr(resp.text)
diff --git a/adal/wstrust_response.py b/adal/wstrust_response.py
index 01385ed0..8ff3c865 100644
--- a/adal/wstrust_response.py
+++ b/adal/wstrust_response.py
@@ -32,6 +32,7 @@
from . import xmlutil
from . import log
+from .adal_error import AdalError
class WSTrustResponse(object):
@@ -142,28 +143,21 @@ def _parse_token(self):
raise self._log.create_error("Unable to find any tokens in RSTR.")
def parse(self):
-
if not self._response:
- raise self._log.create_error("Received empty RSTR response body.")
+ raise AdalError("Received empty RSTR response body.")
try:
- try:
- self._dom = ET.fromstring(self._response)
- self._parents = {c:p for p in self._dom.iter() for c in p}
-
- except Exception as exp:
- raise self._log.create_error("Failed to parse RSTR in to DOM", exp)
-
+ self._dom = ET.fromstring(self._response)
+ self._parents = {c:p for p in self._dom.iter() for c in p}
error_found = self._parse_error()
-
if error_found:
- str_error_code = self.error_code if self.error_code else 'NONE'
- str_fault_message = self.fault_message if self.fault_message else 'NONE'
- raise self._log.create_error('Server returned error in RSTR - ErrorCode: {0} : FaultMessage: {1}'.format(str_error_code, str_fault_message))
-
+ str_error_code = self.error_code or 'NONE'
+ str_fault_message = self.fault_message or 'NONE'
+ error_template = 'Server returned error in RSTR - ErrorCode: {} : FaultMessage: {}'
+ raise AdalError(error_template.format(str_error_code, str_fault_message))
self._parse_token()
-
- except Exception as exp:
+ finally:
+ self._log.info("Failed to parse RSTR in to DOM")
self._dom = None
self._parents = None
- raise
+
diff --git a/pylintrc b/pylintrc
index 38c8f9a9..b2a3cc16 100644
--- a/pylintrc
+++ b/pylintrc
@@ -1,6 +1,5 @@
[MASTER]
-profile=no
ignore=.svn
persistent=yes
cache-size=500
@@ -32,7 +31,6 @@ disable=C0111,C0321,C0303,C0301,W0105,W0142,W0404,W0704,I0011,R0921
# Available formats are text, parseable, colorized, msvs (Visual Studio) and html
output-format=msvs
-include-ids=yes
files-output=no
reports=yes
@@ -43,16 +41,11 @@ reports=yes
# (R0004).
evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)
-# Add a comment according to your evaluation note. This is used by the global
-# evaluation report (R0004).
-comment=no
-
#enable-report=
#disable-report=
[BASIC]
-required-attributes=
no-docstring-rgx=__.*__
# Regular expression which should only match correct module names
@@ -68,7 +61,7 @@ class-rgx=[a-zA-Z0-9_]+$
function-rgx=[a-zA-Z_][a-zA-Z0-9_]{2,30}$
# Regular expression which should only match correct method names
-method-rgx=[a-z_][a-zA-Z0-9_]{2,50}$
+method-rgx=[a-z_][a-zA-Z0-9_]{2,60}$
# Regular expression which should only match correct instance attribute names
attr-rgx=[a-z_][a-z0-9_]{1,30}$
diff --git a/sample/certificate_credentials_sample.py b/sample/certificate_credentials_sample.py
new file mode 100644
index 00000000..e962fbf0
--- /dev/null
+++ b/sample/certificate_credentials_sample.py
@@ -0,0 +1,60 @@
+import json
+import logging
+import os
+import sys
+import adal
+
+def turn_on_logging():
+ handler = logging.StreamHandler()
+ adal.set_logging_options({
+ 'level': adal.LOGGING_LEVEL.DEBUG,
+ 'handler': handler
+ })
+
+def get_private_key(filename):
+ with open(filename, 'r') as pem_file:
+ private_pem = pem_file.read()
+ return private_pem
+
+#
+# You can provide account information by using a JSON file. Either
+# through a command line argument, 'python sample.js parameters.json', or
+# specifying in an environment variable of ADAL_SAMPLE_PARAMETERS_FILE.
+# privateKeyFile must contain a PEM encoded cert with private key.
+# thumbprint must be the thumbprint of the privateKeyFile.
+# {
+# "tenant" : "naturalcauses.onmicrosoft.com",
+# "authorityHostUrl" : "https://login.microsoftonline.com",
+# "clientId" : "d6835713-b745-48d1-bb62-7a8248477d35",
+# "thumbprint" : 'C15DEA8656ADDF67BE8031D85EBDDC5AD6C436E1',
+# "privateKeyFile" : 'ncwebCTKey.pem'
+# }
+parameters_file = (sys.argv[1] if len(sys.argv) == 2 else
+ os.environ.get('ADAL_SAMPLE_PARAMETERS_FILE'))
+sample_parameters = {}
+if parameters_file:
+ with open(parameters_file, 'r') as f:
+ parameters = f.read()
+ sample_parameters = json.loads(parameters)
+else:
+ raise ValueError('Please provide parameter file with account information.')
+
+
+authority_url = (sample_parameters['authorityHostUrl'] + '/' +
+ sample_parameters['tenant'])
+RESOURCE = '00000002-0000-0000-c000-000000000000'
+
+#uncomment for verbose logging
+#turn_on_logging()
+
+context = adal.AuthenticationContext(authority_url)
+key = get_private_key(sample_parameters['privateKeyFile'])
+
+token = context.acquire_token_with_client_certificate(
+ RESOURCE,
+ sample_parameters['clientId'],
+ key,
+ sample_parameters['thumbprint'])
+
+print('Here is the token:')
+print(json.dumps(token, indent=2))
diff --git a/sample/client_credentials_sample.py b/sample/client_credentials_sample.py
new file mode 100644
index 00000000..2e3f826f
--- /dev/null
+++ b/sample/client_credentials_sample.py
@@ -0,0 +1,49 @@
+import json
+import logging
+import os
+import sys
+import adal
+
+def turn_on_logging():
+ handler = logging.StreamHandler()
+ adal.set_logging_options({
+ 'level': adal.LOGGING_LEVEL.DEBUG,
+ 'handler': handler
+ })
+
+# You can provide account information by using a JSON file. Either
+# through a command line argument, 'python sample.js parameters.json', or
+# specifying in an environment variable of ADAL_SAMPLE_PARAMETERS_FILE.
+# {
+# "tenant" : "rrandallaad1.onmicrosoft.com",
+# "authorityHostUrl" : "https://login.microsoftonline.com",
+# "clientId" : "624ac9bd-4c1c-4687-aec8-b56a8991cfb3",
+# "clientSecret" : "verySecret=""
+# }
+
+parameters_file = (sys.argv[1] if len(sys.argv) == 2 else
+ os.environ.get('ADAL_SAMPLE_PARAMETERS_FILE'))
+
+if parameters_file:
+ with open(parameters_file, 'r') as f:
+ parameters = f.read()
+ sample_parameters = json.loads(parameters)
+else:
+ raise ValueError('Please provide parameter file with account information.')
+
+authority_url = (sample_parameters['authorityHostUrl'] + '/' +
+ sample_parameters['tenant'])
+RESOURCE = '00000002-0000-0000-c000-000000000000'
+
+#uncomment for verbose log
+#turn_on_logging()
+
+context = adal.AuthenticationContext(authority_url)
+
+token = context.acquire_token_with_client_credentials(
+ RESOURCE,
+ sample_parameters['clientId'],
+ sample_parameters['clientSecret'])
+
+print('Here is the token:')
+print(json.dumps(token, indent=2))
diff --git a/sample/device_code_sample.py b/sample/device_code_sample.py
new file mode 100644
index 00000000..0bcad368
--- /dev/null
+++ b/sample/device_code_sample.py
@@ -0,0 +1,62 @@
+import json
+import logging
+import os
+import sys
+import adal
+
+def turn_on_logging():
+ handler = logging.StreamHandler()
+ adal.set_logging_options({
+ 'level': adal.LOGGING_LEVEL.DEBUG,
+ 'handler': handler
+ })
+
+# You can provide account information by using a JSON file
+# with the same parameters as the sampleParameters variable below. Either
+# through a command line argument, 'python sample.js parameters.json', or
+# specifying in an environment variable of ADAL_SAMPLE_PARAMETERS_FILE.
+# {
+# "tenant" : "rrandallaad1.onmicrosoft.com",
+# "authorityHostUrl" : "https://login.microsoftonline.com",
+# "clientId" : "",
+# "anothertenant" : "bar.onmicrosoft.com"
+# }
+
+parameters_file = (sys.argv[1] if len(sys.argv) == 2 else
+ os.environ.get('ADAL_SAMPLE_PARAMETERS_FILE'))
+
+if parameters_file:
+ with open(parameters_file, 'r') as f:
+ parameters = f.read()
+ sample_parameters = json.loads(parameters)
+else:
+ raise ValueError('Please provide parameter file with account information.')
+
+
+authority_host_url = sample_parameters['authorityHostUrl']
+authority_url = authority_host_url + '/' + sample_parameters['tenant']
+clientid = sample_parameters['clientid']
+RESOURCE = '00000002-0000-0000-c000-000000000000'
+
+#uncomment for verbose logging
+#turn_on_logging()
+
+context = adal.AuthenticationContext(authority_url)
+code = context.acquire_user_code(RESOURCE, clientid)
+print(code['message'])
+token = context.acquire_token_with_device_code(RESOURCE, code, clientid)
+
+print('Here is the token from "{}":'.format(authority_url))
+print(json.dumps(token, indent=2))
+
+#try cross tenant token refreshing
+another_tenant = sample_parameters.get('anothertenant')
+if another_tenant:
+ authority_url = authority_host_url + '/' + another_tenant
+ #reuse existing cache which has the tokens acquired early on
+ existing_cache = context.cache
+ context = adal.AuthenticationContext(authority_url, cache=existing_cache)
+ token = context.acquire_token(RESOURCE, token['userId'], clientid)
+ print('Here is the token from "{}":'.format(authority_url))
+ print(json.dumps(token, indent=2))
+
diff --git a/sample/refresh_token_sample.py b/sample/refresh_token_sample.py
new file mode 100644
index 00000000..ff417789
--- /dev/null
+++ b/sample/refresh_token_sample.py
@@ -0,0 +1,60 @@
+import json
+import logging
+import os
+import sys
+import adal
+
+def turn_on_logging():
+ handler = logging.StreamHandler()
+ adal.set_logging_options({
+ 'level': adal.LOGGING_LEVEL.DEBUG,
+ 'handler': handler
+ })
+
+# You can override the account information by using a JSON file. Either
+# through a command line argument, 'python sample.js parameters.json', or
+# specifying in an environment variable of ADAL_SAMPLE_PARAMETERS_FILE.
+# {
+# "tenant" : "rrandallaad1.onmicrosoft.com",
+# "authorityHostUrl" : "https://login.microsoftonline.com",
+# "clientId" : "624ac9bd-4c1c-4687-aec8-b56a8991cfb3",
+# "username" : "user1",
+# "password" : "verySecurePassword"
+# }
+
+parameters_file = (sys.argv[1] if len(sys.argv) == 2 else
+ os.environ.get('ADAL_SAMPLE_PARAMETERS_FILE'))
+
+if parameters_file:
+ with open(parameters_file, 'r') as f:
+ parameters = f.read()
+ sample_parameters = json.loads(parameters)
+else:
+ raise ValueError('Please provide parameter file with account information.')
+
+authority_url = (sample_parameters['authorityHostUrl'] + '/' +
+ sample_parameters['tenant'])
+RESOURCE = '00000002-0000-0000-c000-000000000000'
+
+#uncomment for verbose log
+#turn_on_logging()
+
+context = adal.AuthenticationContext(authority_url)
+
+token = context.acquire_token_with_username_password(
+ RESOURCE,
+ sample_parameters['username'],
+ sample_parameters['password'],
+ sample_parameters['clientid'])
+
+print('Here is the token')
+print(json.dumps(token, indent=2))
+
+refresh_token = token['refreshToken']
+token = context.acquire_token_with_refresh_token(
+ refresh_token,
+ sample_parameters['clientid'],
+ RESOURCE)
+
+print('Here is the token acquired from the refreshing token')
+print(json.dumps(token, indent=2))
diff --git a/sample/website_sample.py b/sample/website_sample.py
new file mode 100644
index 00000000..5187c0c8
--- /dev/null
+++ b/sample/website_sample.py
@@ -0,0 +1,135 @@
+try:
+ from http import server as httpserver
+ from http import cookies as Cookie
+except ImportError:
+ import SimpleHTTPServer as httpserver
+ import Cookie as Cookie
+
+try:
+ import socketserver
+except ImportError:
+ import SocketServer as socketserver
+
+try:
+ from urllib.parse import urlparse, parse_qs
+except ImportError:
+ from urlparse import urlparse, parse_qs
+
+import json
+import os
+import random
+import string
+import sys
+
+from adal import AuthenticationContext
+
+# You can provide account information by using a JSON file. Either
+# through a command line argument, 'python sample.js parameters.json', or
+# specifying in an environment variable of ADAL_SAMPLE_PARAMETERS_FILE.
+# {
+# "tenant" : "rrandallaad1.onmicrosoft.com",
+# "authorityHostUrl" : "https://login.microsoftonline.com",
+# "clientId" : "624ac9bd-4c1c-4687-aec8-b56a8991cfb3",
+# "clientSecret" : "verySecret=""
+# }
+
+parameters_file = (sys.argv[1] if len(sys.argv) == 2 else
+ os.environ.get('ADAL_SAMPLE_PARAMETERS_FILE'))
+
+if parameters_file:
+ with open(parameters_file, 'r') as f:
+ parameters = f.read()
+ sample_parameters = json.loads(parameters)
+else:
+ raise ValueError('Please provide parameter file with account information.')
+
+PORT = 8088
+TEMPLATE_AUTHZ_URL = ('https://login.windows.net/{}/oauth2/authorize?'+
+ 'response_type=code&client_id={}&redirect_uri={}&'+
+ 'state={}&resource={}')
+RESOURCE = '00000002-0000-0000-c000-000000000000' #Graph Resource
+REDIRECT_URI = 'http://localhost:{}/getAToken'.format(PORT)
+
+authority_url = (sample_parameters['authorityHostUrl'] + '/' +
+ sample_parameters['tenant'])
+
+class OAuth2RequestHandler(httpserver.SimpleHTTPRequestHandler):
+ def do_GET(self):
+ if self.path == '/':
+ self.send_response(307)
+ login_url = 'http://localhost:{}/login'.format(PORT)
+ self.send_header('Location', login_url)
+ self.end_headers()
+ elif self.path == '/login':
+ auth_state = (''.join(random.SystemRandom()
+ .choice(string.ascii_uppercase + string.digits)
+ for _ in range(48)))
+ cookie = Cookie.SimpleCookie()
+ cookie['auth_state'] = auth_state
+ authorization_url = TEMPLATE_AUTHZ_URL.format(
+ sample_parameters['tenant'],
+ sample_parameters['clientId'],
+ REDIRECT_URI,
+ auth_state,
+ RESOURCE)
+ self.send_response(307)
+ self.send_header('Set-Cookie', cookie.output(header=''))
+ self.send_header('Location', authorization_url)
+ self.end_headers()
+ elif self.path.startswith('/getAToken'):
+ message = None
+ is_ok = True
+ try:
+ token_response = self._acquire_token()
+ message = 'response: ' + json.dumps(token_response)
+ #Later, if the access token is expired it can be refreshed.
+ auth_context = AuthenticationContext(authority_url)
+ token_response = auth_context.acquire_token_with_refresh_token(
+ token_response['refreshToken'],
+ sample_parameters['clientId'],
+ sample_parameters['clientSecret'],
+ RESOURCE)
+ message = (message + '*** And here is the refresh response:' +
+ json.dumps(token_response))
+ except ValueError as exp:
+ message = str(exp)
+ is_ok = False
+ self._send_response(message, is_ok)
+
+ def _acquire_token(self):
+ parsed = urlparse(self.path)
+ code = parse_qs(parsed.query)['code'][0]
+ state = parse_qs(parsed.query)['state'][0]
+ cookie = Cookie.SimpleCookie(self.headers["Cookie"])
+ if state != cookie['auth_state'].value:
+ raise ValueError('state does not match')
+ auth_context = AuthenticationContext(authority_url)
+ token = auth_context.acquire_token_with_authorization_code(
+ code,
+ REDIRECT_URI,
+ RESOURCE,
+ sample_parameters['clientId'],
+ sample_parameters['clientSecret'])
+ return token
+
+ def _send_response(self, message, is_ok=True):
+ self.send_response(200 if is_ok else 400)
+ self.send_header('Content-type', 'text/html')
+ self.end_headers()
+
+ if is_ok:
+ #todo, pretty format token response in json
+ message_template = ('Succeeded' +
+ '{}
')
+ else:
+ message_template = ('Failed' +
+ '{}
')
+
+ output = message_template.format(message)
+ self.wfile.write(output.encode())
+
+httpd = socketserver.TCPServer(('', PORT), OAuth2RequestHandler)
+
+print('serving at port', PORT)
+httpd.serve_forever()
+
diff --git a/setup.py b/setup.py
index 1b700720..a5a31503 100644
--- a/setup.py
+++ b/setup.py
@@ -44,8 +44,10 @@
setup(
name='adal',
- version='0.2.0',
- description='The ADAL for Python library makes it easy for python application to authenticate to Azure Active Directory (AAD) in order to access AAD protected web resources.',
+ version='1.0.0rc1', #note, same string exists in __init__.py
+ description=('The ADAL for Python library makes it easy for python ' +
+ 'application to authenticate to Azure Active Directory ' +
+ '(AAD) in order to access AAD protected web resources.'),
license='MIT',
author='Microsoft Corporation',
author_email='nugetaad@microsoft.com',