1 from __future__ import unicode_literals
5 from oauthlib.common import generate_token, urldecode
6 from oauthlib.oauth2 import WebApplicationClient, InsecureTransportError
7 from oauthlib.oauth2 import TokenExpiredError, is_secure_transport
10 log = logging.getLogger(__name__)
13 class TokenUpdated(Warning):
14 def __init__(self, token):
15 super(TokenUpdated, self).__init__()
19 class OAuth2Session(requests.Session):
20 """Versatile OAuth 2 extension to :class:`requests.Session`.
22 Supports any grant type adhering to :class:`oauthlib.oauth2.Client` spec
23 including the four core OAuth 2 grants.
25 Can be used to create authorization urls, fetch tokens and access protected
26 resources using the :class:`requests.Session` interface you are used to.
28 - :class:`oauthlib.oauth2.WebApplicationClient` (default): Authorization Code Grant
29 - :class:`oauthlib.oauth2.MobileApplicationClient`: Implicit Grant
30 - :class:`oauthlib.oauth2.LegacyApplicationClient`: Password Credentials Grant
31 - :class:`oauthlib.oauth2.BackendApplicationClient`: Client Credentials Grant
33 Note that the only time you will be using Implicit Grant from python is if
34 you are driving a user agent able to obtain URL fragments.
37 def __init__(self, client_id=None, client=None, auto_refresh_url=None,
38 auto_refresh_kwargs=None, scope=None, redirect_uri=None, token=None,
39 state=None, token_updater=None, **kwargs):
40 """Construct a new OAuth 2 client session.
42 :param client_id: Client id obtained during registration
43 :param client: :class:`oauthlib.oauth2.Client` to be used. Default is
44 WebApplicationClient which is useful for any
45 hosted application but not mobile or desktop.
46 :param scope: List of scopes you wish to request access to
47 :param redirect_uri: Redirect URI you registered as callback
48 :param token: Token dictionary, must include access_token
50 :param state: State string used to prevent CSRF. This will be given
51 when creating the authorization url and must be supplied
52 when parsing the authorization response.
53 Can be either a string or a no argument callable.
54 :auto_refresh_url: Refresh token endpoint URL, must be HTTPS. Supply
55 this if you wish the client to automatically refresh
57 :auto_refresh_kwargs: Extra arguments to pass to the refresh token
59 :token_updater: Method with one argument, token, to be used to update
60 your token databse on automatic token refresh. If not
61 set a TokenUpdated warning will be raised when a token
62 has been refreshed. This warning will carry the token
63 in its token argument.
64 :param kwargs: Arguments to pass to the Session constructor.
66 super(OAuth2Session, self).__init__(**kwargs)
67 self.client_id = client_id or client.client_id
69 self.redirect_uri = redirect_uri
70 self.token = token or {}
71 self.state = state or generate_token
73 self.auto_refresh_url = auto_refresh_url
74 self.auto_refresh_kwargs = auto_refresh_kwargs or {}
75 self.token_updater = token_updater
76 self._client = client or WebApplicationClient(client_id, token=token)
77 self._client._populate_attributes(token or {})
79 # Allow customizations for non compliant providers through various
80 # hooks to adjust requests and responses.
81 self.compliance_hook = {
82 'access_token_response': set([]),
83 'refresh_token_response': set([]),
84 'protected_request': set([]),
88 """Generates a state string to be used in authorizations."""
90 self._state = self.state()
91 log.debug('Generated new state %s.', self._state)
93 self._state = self.state
94 log.debug('Re-using previously supplied state %s.', self._state)
99 """Boolean that indicates whether this session has an OAuth token
100 or not. If `self.authorized` is True, you can reasonably expect
101 OAuth-protected requests to the resource to succeed. If
102 `self.authorized` is False, you need the user to go through the OAuth
103 authentication dance before OAuth-protected requests to the resource
106 return bool(self._client.access_token)
108 def authorization_url(self, url, state=None, **kwargs):
109 """Form an authorization URL.
111 :param url: Authorization endpoint url, must be HTTPS.
112 :param state: An optional state string for CSRF protection. If not
113 given it will be generated for you.
114 :param kwargs: Extra parameters to include.
115 :return: authorization_url, state
117 state = state or self.new_state()
118 return self._client.prepare_request_uri(url,
119 redirect_uri=self.redirect_uri,
124 def fetch_token(self, token_url, code=None, authorization_response=None,
125 body='', auth=None, username=None, password=None, method='POST',
126 timeout=None, headers=None, verify=True, **kwargs):
127 """Generic method for fetching an access token from the token endpoint.
129 If you are using the MobileApplicationClient you will want to use
130 token_from_fragment instead of fetch_token.
132 :param token_url: Token endpoint URL, must use HTTPS.
133 :param code: Authorization code (used by WebApplicationClients).
134 :param authorization_response: Authorization response URL, the callback
135 URL of the request back to you. Used by
136 WebApplicationClients instead of code.
137 :param body: Optional application/x-www-form-urlencoded body to add the
138 include in the token request. Prefer kwargs over body.
139 :param auth: An auth tuple or method as accepted by requests.
140 :param username: Username used by LegacyApplicationClients.
141 :param password: Password used by LegacyApplicationClients.
142 :param method: The HTTP method used to make the request. Defaults
143 to POST, but may also be GET. Other methods should
145 :param headers: Dict to default request headers with.
146 :param timeout: Timeout of the request in seconds.
147 :param verify: Verify SSL certificate.
148 :param kwargs: Extra parameters to include in the token request.
149 :return: A token dict
151 if not is_secure_transport(token_url):
152 raise InsecureTransportError()
154 if not code and authorization_response:
155 self._client.parse_request_uri_response(authorization_response,
157 code = self._client.code
158 elif not code and isinstance(self._client, WebApplicationClient):
159 code = self._client.code
161 raise ValueError('Please supply either code or '
162 'authorization_code parameters.')
165 body = self._client.prepare_request_body(code=code, body=body,
166 redirect_uri=self.redirect_uri, username=username,
167 password=password, **kwargs)
169 headers = headers or {
170 'Accept': 'application/json',
171 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8',
173 if method.upper() == 'POST':
174 r = self.post(token_url, data=dict(urldecode(body)),
175 timeout=timeout, headers=headers, auth=auth,
177 log.debug('Prepared fetch token request body %s', body)
178 elif method.upper() == 'GET':
179 # if method is not 'POST', switch body to querystring and GET
180 r = self.get(token_url, params=dict(urldecode(body)),
181 timeout=timeout, headers=headers, auth=auth,
183 log.debug('Prepared fetch token request querystring %s', body)
185 raise ValueError('The method kwarg must be POST or GET.')
187 log.debug('Request to fetch token completed with status %s.',
189 log.debug('Request headers were %s', r.request.headers)
190 log.debug('Request body was %s', r.request.body)
191 log.debug('Response headers were %s and content %s.',
193 log.debug('Invoking %d token response hooks.',
194 len(self.compliance_hook['access_token_response']))
195 for hook in self.compliance_hook['access_token_response']:
196 log.debug('Invoking hook %s.', hook)
201 self._client.parse_request_body_response(r.text, scope=self.scope)
202 self.token = self._client.token
203 log.debug('Obtained token %s.', self.token)
206 def token_from_fragment(self, authorization_response):
207 """Parse token from the URI fragment, used by MobileApplicationClients.
209 :param authorization_response: The full URL of the redirect back to you
210 :return: A token dict
212 self._client.parse_request_uri_response(authorization_response,
214 self.token = self._client.token
217 def refresh_token(self, token_url, refresh_token=None, body='', auth=None,
218 timeout=None, verify=True, **kwargs):
219 """Fetch a new access token using a refresh token.
221 :param token_url: The token endpoint, must be HTTPS.
222 :param refresh_token: The refresh_token to use.
223 :param body: Optional application/x-www-form-urlencoded body to add the
224 include in the token request. Prefer kwargs over body.
225 :param auth: An auth tuple or method as accepted by requests.
226 :param timeout: Timeout of the request in seconds.
227 :param verify: Verify SSL certificate.
228 :param kwargs: Extra parameters to include in the token request.
229 :return: A token dict
232 raise ValueError('No token endpoint set for auto_refresh.')
234 if not is_secure_transport(token_url):
235 raise InsecureTransportError()
237 # Need to nullify token to prevent it from being added to the request
238 refresh_token = refresh_token or self.token.get('refresh_token')
241 log.debug('Adding auto refresh key word arguments %s.',
242 self.auto_refresh_kwargs)
243 kwargs.update(self.auto_refresh_kwargs)
244 body = self._client.prepare_refresh_body(body=body,
245 refresh_token=refresh_token, scope=self.scope, **kwargs)
246 log.debug('Prepared refresh token request body %s', body)
247 r = self.post(token_url, data=dict(urldecode(body)), auth=auth,
248 timeout=timeout, verify=verify)
249 log.debug('Request to refresh token completed with status %s.',
251 log.debug('Response headers were %s and content %s.',
253 log.debug('Invoking %d token response hooks.',
254 len(self.compliance_hook['refresh_token_response']))
255 for hook in self.compliance_hook['refresh_token_response']:
256 log.debug('Invoking hook %s.', hook)
259 self.token = self._client.parse_request_body_response(r.text, scope=self.scope)
260 if not 'refresh_token' in self.token:
261 log.debug('No new refresh token given. Re-using old.')
262 self.token['refresh_token'] = refresh_token
265 def request(self, method, url, data=None, headers=None, **kwargs):
266 """Intercept all requests and add the OAuth 2 token if present."""
267 if not is_secure_transport(url):
268 raise InsecureTransportError()
270 log.debug('Invoking %d protected resource request hooks.',
271 len(self.compliance_hook['protected_request']))
272 for hook in self.compliance_hook['protected_request']:
273 log.debug('Invoking hook %s.', hook)
274 url, headers, data = hook(url, headers, data)
276 log.debug('Adding token %s to request.', self.token)
278 url, headers, data = self._client.add_token(url,
279 http_method=method, body=data, headers=headers)
280 # Attempt to retrieve and save new access token if expired
281 except TokenExpiredError:
282 if self.auto_refresh_url:
283 log.debug('Auto refresh is set, attempting to refresh at %s.',
284 self.auto_refresh_url)
285 token = self.refresh_token(self.auto_refresh_url)
286 if self.token_updater:
287 log.debug('Updating token to %s using %s.',
288 token, self.token_updater)
289 self.token_updater(token)
290 url, headers, data = self._client.add_token(url,
291 http_method=method, body=data, headers=headers)
293 raise TokenUpdated(token)
297 log.debug('Requesting url %s using method %s.', url, method)
298 log.debug('Supplying headers %s and data %s', headers, data)
299 log.debug('Passing through key word arguments %s.', kwargs)
300 return super(OAuth2Session, self).request(method, url,
301 headers=headers, data=data, **kwargs)
303 def register_compliance_hook(self, hook_type, hook):
304 """Register a hook for request/response tweaking.
307 access_token_response invoked before token parsing.
308 refresh_token_response invoked before refresh token parsing.
309 protected_request invoked before making a request.
311 If you find a new hook is needed please send a GitHub PR request
314 if hook_type not in self.compliance_hook:
315 raise ValueError('Hook type %s is not in %s.',
316 hook_type, self.compliance_hook)
317 self.compliance_hook[hook_type].add(hook)