1#------------------------------------------------------------------------------
2#
3# Copyright (c) Microsoft Corporation.
4# All rights reserved.
5#
6# This code is licensed under the MIT License.
7#
8# Permission is hereby granted, free of charge, to any person obtaining a copy
9# of this software and associated documentation files(the "Software"), to deal
10# in the Software without restriction, including without limitation the rights
11# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
12# copies of the Software, and to permit persons to whom the Software is
13# furnished to do so, subject to the following conditions :
14#
15# The above copyright notice and this permission notice shall be included in
16# all copies or substantial portions of the Software.
17#
18# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE
21# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
24# THE SOFTWARE.
25#
26#------------------------------------------------------------------------------
27import os
28import threading
29import warnings
30
31from .authority import Authority
32from . import argument
33from .code_request import CodeRequest
34from .token_request import TokenRequest
35from .token_cache import TokenCache
36from . import log
37from .constants import OAuth2DeviceCodeResponseParameters
38
39
40#warnings.simplefilter('default', DeprecationWarning) # Make them visible to end users
41
42GLOBAL_ADAL_OPTIONS = {}
43
44class AuthenticationContext(object):
45 '''Retrieves authentication tokens from Azure Active Directory.
46
47 For usages, check out the "sample" folder at:
48 https://github.com/AzureAD/azure-activedirectory-library-for-python
49 '''
50
51 def __init__(
52 self, authority, validate_authority=None, cache=None,
53 api_version=None, timeout=None, enable_pii=False, verify_ssl=None, proxies=None):
54 '''Creates a new AuthenticationContext object.
55
56 By default the authority will be checked against a list of known Azure
57 Active Directory authorities. If the authority is not recognized as
58 one of these well known authorities then token acquisition will fail.
59 This behavior can be turned off via the validate_authority parameter
60 below.
61
62 :param str authority: A URL that identifies a token authority. It should be of the
63 format https://login.microsoftonline.com/your_tenant
64 :param bool validate_authority: (optional) Turns authority validation
65 on or off. This parameter default to true.
66 :param TokenCache cache: (optional) Sets the token cache used by this
67 AuthenticationContext instance. If this parameter is not set, then
68 a default is used. Cache instances is only used by that instance of
69 the AuthenticationContext and are not shared unless it has been
70 manually passed during the construction of other
71 AuthenticationContexts.
72 :param api_version: (optional) Specifies API version using on the wire.
73 Historically it has a hardcoded default value as "1.0".
74 Developers have been encouraged to set it as None explicitly,
75 which means the underlying API version will be automatically chosen.
76 Starting from ADAL Python 1.0, this default value becomes None.
77 :param timeout: (optional) requests timeout. How long to wait for the server to send
78 data before giving up, as a float, or a `(connect timeout,
79 read timeout) <timeouts>` tuple.
80 :param enable_pii: (optional) Unless this is set to True,
81 there will be no Personally Identifiable Information (PII) written in log.
82 :param verify_ssl: (optional) requests verify. Either a boolean, in which case it
83 controls whether we verify the server's TLS certificate, or a string, in which
84 case it must be a path to a CA bundle to use. If this value is not provided, and
85 ADAL_PYTHON_SSL_NO_VERIFY env variable is set, behavior is equivalent to
86 verify_ssl=False.
87 :param proxies: (optional) requests proxies. Dictionary mapping protocol to the URL
88 of the proxy. See http://docs.python-requests.org/en/master/user/advanced/#proxies
89 for details.
90 '''
91 warnings.warn(
92 """ADAL Python library no longer receives any feature update or bugfix.
93Please use the new library, MSAL Python, which is easier to use, and more secure.
94
95MSAL Python is available here: https://pypi.org/project/msal/
96
97If you are building your new project,
98start using MSAL Python by choosing one of the samples that suit your need.
99https://msal-python.readthedocs.io/en/latest/#scenarios
100
101If you are migrating your existing ADAL-powered project into MSAL, please read
102https://learn.microsoft.com/en-us/azure/active-directory/develop/migrate-python-adal-msal
103""", DeprecationWarning)
104 self.authority = Authority(authority, validate_authority is None or validate_authority)
105 self._oauth2client = None
106 self.correlation_id = None
107 env_verify = 'ADAL_PYTHON_SSL_NO_VERIFY' not in os.environ
108 verify = verify_ssl if verify_ssl is not None else env_verify
109 if api_version is not None:
110 warnings.warn(
111 """The default behavior of including api-version=1.0 on the wire
112 is now deprecated.
113 Future version of ADAL will change the default value to None.
114
115 To ensure a smooth transition, you are recommended to explicitly
116 set it to None in your code now, and test out the new behavior.
117
118 context = AuthenticationContext(..., api_version=None)
119 """, DeprecationWarning)
120 self._call_context = {
121 'options': GLOBAL_ADAL_OPTIONS,
122 'api_version': api_version,
123 'verify_ssl': verify,
124 'proxies':proxies,
125 'timeout':timeout,
126 "enable_pii": enable_pii,
127 }
128 self._token_requests_with_user_code = {}
129 self.cache = cache or TokenCache()
130 self._lock = threading.RLock()
131
132 @property
133 def options(self):
134 return self._call_context['options']
135
136 @options.setter
137 def options(self, val):
138 self._call_context['options'] = val
139
140 def _acquire_token(self, token_func, correlation_id=None):
141 self._call_context['log_context'] = log.create_log_context(
142 correlation_id or self.correlation_id, self._call_context.get('enable_pii', False))
143 self.authority.validate(self._call_context)
144 return token_func(self)
145
146 def acquire_token(self, resource, user_id, client_id):
147 '''Gets a token for a given resource via cached tokens.
148
149 :param str resource: A URI that identifies the resource for which the
150 token is valid.
151 :param str user_id: The username of the user on behalf this application
152 is authenticating.
153 :param str client_id: The OAuth client id of the calling application.
154 :returns: dic with several keys, include "accessToken" and
155 "refreshToken".
156 '''
157 def token_func(self):
158 token_request = TokenRequest(self._call_context, self, client_id, resource)
159 return token_request.get_token_from_cache_with_refresh(user_id)
160
161 return self._acquire_token(token_func)
162
163 def acquire_token_with_username_password(self, resource, username, password, client_id):
164 '''Gets a token for a given resource via user credentails.
165
166 :param str resource: A URI that identifies the resource for which the
167 token is valid.
168 :param str username: The username of the user on behalf this
169 application is authenticating.
170 :param str password: The password of the user named in the username
171 parameter.
172 :param str client_id: The OAuth client id of the calling application.
173 :returns: dict with several keys, include "accessToken" and
174 "refreshToken".
175 '''
176 def token_func(self):
177 token_request = TokenRequest(self._call_context, self, client_id, resource)
178 return token_request.get_token_with_username_password(username, password)
179
180 return self._acquire_token(token_func)
181
182 def acquire_token_with_client_credentials(self, resource, client_id, client_secret):
183 '''Gets a token for a given resource via client credentials.
184
185 :param str resource: A URI that identifies the resource for which the
186 token is valid.
187 :param str client_id: The OAuth client id of the calling application.
188 :param str client_secret: The OAuth client secret of the calling application.
189 :returns: dict with several keys, include "accessToken".
190 '''
191 def token_func(self):
192 token_request = TokenRequest(self._call_context, self, client_id, resource)
193 return token_request.get_token_with_client_credentials(client_secret)
194
195 return self._acquire_token(token_func)
196
197 def acquire_token_with_authorization_code(self, authorization_code,
198 redirect_uri, resource,
199 client_id, client_secret=None, code_verifier=None):
200 '''Gets a token for a given resource via authorization code for a
201 server app.
202
203 :param str authorization_code: An authorization code returned from a
204 client.
205 :param str redirect_uri: the redirect uri that was used in the
206 authorize call.
207 :param str resource: A URI that identifies the resource for which the
208 token is valid.
209 :param str client_id: The OAuth client id of the calling application.
210 :param str client_secret: (only for confidential clients)The OAuth
211 client secret of the calling application. This parameter if not set,
212 defaults to None
213 :param str code_verifier: (optional)The code verifier that was used to
214 obtain authorization code if PKCE was used in the authorization
215 code grant request.(usually used by public clients) This parameter if not set,
216 defaults to None
217 :returns: dict with several keys, include "accessToken" and
218 "refreshToken".
219 '''
220 def token_func(self):
221 token_request = TokenRequest(
222 self._call_context,
223 self,
224 client_id,
225 resource,
226 redirect_uri)
227 return token_request.get_token_with_authorization_code(
228 authorization_code,
229 client_secret, code_verifier)
230
231 return self._acquire_token(token_func)
232
233 def acquire_token_with_refresh_token(self, refresh_token, client_id,
234 resource, client_secret=None):
235 '''Gets a token for a given resource via refresh tokens
236
237 :param str refresh_token: A refresh token returned in a tokne response
238 from a previous invocation of acquireToken.
239 :param str client_id: The OAuth client id of the calling application.
240 :param str resource: A URI that identifies the resource for which the
241 token is valid.
242 :param str client_secret: (optional)The OAuth client secret of the
243 calling application.
244 :returns: dict with several keys, include "accessToken" and
245 "refreshToken".
246 '''
247 def token_func(self):
248 token_request = TokenRequest(self._call_context, self, client_id, resource)
249 return token_request.get_token_with_refresh_token(refresh_token, client_secret)
250
251 return self._acquire_token(token_func)
252
253 def acquire_token_with_client_certificate(self, resource, client_id,
254 certificate, thumbprint, public_certificate=None):
255 '''Gets a token for a given resource via certificate credentials
256
257 :param str resource: A URI that identifies the resource for which the
258 token is valid.
259 :param str client_id: The OAuth client id of the calling application.
260 :param str certificate: A PEM encoded certificate private key.
261 :param str thumbprint: hex encoded thumbprint of the certificate.
262 :param str public_certificate(optional): if not None, it will be sent to the service for subject name
263 and issuer based authentication, which is to support cert auto rolls. The value must match the
264 certificate private key parameter.
265
266 Per `specs <https://tools.ietf.org/html/rfc7515#section-4.1.6>`_,
267 "the certificate containing
268 the public key corresponding to the key used to digitally sign the
269 JWS MUST be the first certificate. This MAY be followed by
270 additional certificates, with each subsequent certificate being the
271 one used to certify the previous one."
272 However, your certificate's issuer may use a different order.
273 So, if your attempt ends up with an error AADSTS700027 -
274 "The provided signature value did not match the expected signature value",
275 you may try use only the leaf cert (in PEM/str format) instead.
276
277 :returns: dict with several keys, include "accessToken".
278 '''
279 def token_func(self):
280 token_request = TokenRequest(self._call_context, self, client_id, resource)
281 return token_request.get_token_with_certificate(certificate, thumbprint, public_certificate)
282
283 return self._acquire_token(token_func)
284
285 def acquire_user_code(self, resource, client_id, language=None):
286 '''Gets the user code info which contains user_code, device_code for
287 authenticating user on device.
288
289 :param str resource: A URI that identifies the resource for which the
290 device_code and user_code is valid for.
291 :param str client_id: The OAuth client id of the calling application.
292 :param str language: The language code specifying how the message
293 should be localized to.
294 :returns: dict contains code and uri for users to login through browser.
295 '''
296 self._call_context['log_context'] = log.create_log_context(
297 self.correlation_id, self._call_context.get('enable_pii', False))
298 self.authority.validate(self._call_context)
299 code_request = CodeRequest(self._call_context, self, client_id, resource)
300 return code_request.get_user_code_info(language)
301
302 def acquire_token_with_device_code(self, resource, user_code_info, client_id):
303 '''Gets a new access token using via a device code.
304
305 :param str resource: A URI that identifies the resource for which the
306 token is valid.
307 :param dict user_code_info: The code info from the invocation of
308 "acquire_user_code"
309 :param str client_id: The OAuth client id of the calling application.
310 :returns: dict with several keys, include "accessToken" and
311 "refreshToken".
312 '''
313 def token_func(self):
314 token_request = TokenRequest(self._call_context, self, client_id, resource)
315
316 key = user_code_info[OAuth2DeviceCodeResponseParameters.DEVICE_CODE]
317 with self._lock:
318 self._token_requests_with_user_code[key] = token_request
319
320 token = token_request.get_token_with_device_code(user_code_info)
321
322 with self._lock:
323 self._token_requests_with_user_code.pop(key, None)
324
325 return token
326
327 return self._acquire_token(token_func, user_code_info.get('correlation_id', None))
328
329 def cancel_request_to_get_token_with_device_code(self, user_code_info):
330 '''Cancels the polling request to get token with device code.
331
332 :param dict user_code_info: The code info from the invocation of
333 "acquire_user_code"
334 :returns: None
335 '''
336 argument.validate_user_code_info(user_code_info)
337
338 key = user_code_info[OAuth2DeviceCodeResponseParameters.DEVICE_CODE]
339 with self._lock:
340 request = self._token_requests_with_user_code.get(key)
341
342 if not request:
343 raise ValueError('No acquire_token_with_device_code existed to be cancelled')
344
345 request.cancel_token_request_with_device_code()
346 self._token_requests_with_user_code.pop(key, None)