44
55from oauthlib .common import generate_token , urldecode
66from oauthlib .oauth2 import WebApplicationClient , InsecureTransportError
7+ from oauthlib .oauth2 import LegacyApplicationClient
78from oauthlib .oauth2 import TokenExpiredError , is_secure_transport
89import requests
910
@@ -158,11 +159,14 @@ def authorization_url(self, url, state=None, **kwargs):
158159
159160 def fetch_token (self , token_url , code = None , authorization_response = None ,
160161 body = '' , auth = None , username = None , password = None , method = 'POST' ,
161- timeout = None , headers = None , verify = True , proxies = None , ** kwargs ):
162+ timeout = None , headers = None , verify = True , proxies = None ,
163+ include_client_id = None , client_secret = None , ** kwargs ):
162164 """Generic method for fetching an access token from the token endpoint.
163165
164166 If you are using the MobileApplicationClient you will want to use
165- token_from_fragment instead of fetch_token.
167+ `token_from_fragment` instead of `fetch_token`.
168+
169+ The current implementation enforces the RFC guidelines.
166170
167171 :param token_url: Token endpoint URL, must use HTTPS.
168172 :param code: Authorization code (used by WebApplicationClients).
@@ -171,15 +175,28 @@ def fetch_token(self, token_url, code=None, authorization_response=None,
171175 WebApplicationClients instead of code.
172176 :param body: Optional application/x-www-form-urlencoded body to add the
173177 include in the token request. Prefer kwargs over body.
174- :param auth: An auth tuple or method as accepted by requests.
175- :param username: Username used by LegacyApplicationClients.
176- :param password: Password used by LegacyApplicationClients.
178+ :param auth: An auth tuple or method as accepted by `requests`.
179+ :param username: Username required by LegacyApplicationClients to appear
180+ in the request body.
181+ :param password: Password required by LegacyApplicationClients to appear
182+ in the request body.
177183 :param method: The HTTP method used to make the request. Defaults
178184 to POST, but may also be GET. Other methods should
179185 be added as needed.
180- :param headers: Dict to default request headers with.
181186 :param timeout: Timeout of the request in seconds.
187+ :param headers: Dict to default request headers with.
182188 :param verify: Verify SSL certificate.
189+ :param proxies: The `proxies` argument is passed onto `requests`.
190+ :param include_client_id: Should the request body include the
191+ `client_id` parameter. Default is `None`,
192+ which will attempt to autodetect. This can be
193+ forced to always include (True) or never
194+ include (False).
195+ :param client_secret: The `client_secret` paired to the `client_id`.
196+ This is generally required unless provided in the
197+ `auth` tuple. If the value is `None`, it will be
198+ omitted from the request, however if the value is
199+ an empty string, an empty string will be sent.
183200 :param kwargs: Extra parameters to include in the token request.
184201 :return: A token dict
185202 """
@@ -196,23 +213,65 @@ def fetch_token(self, token_url, code=None, authorization_response=None,
196213 raise ValueError ('Please supply either code or '
197214 'authorization_response parameters.' )
198215
216+ # Earlier versions of this library build an HTTPBasicAuth header out of
217+ # `username` and `password`. The RFC states, however these attributes
218+ # must be in the request body and not the header.
219+ # If an upstream server is not spec compliant and requires them to
220+ # appear as an Authorization header, supply an explicit `auth` header
221+ # to this function.
222+ # This check will allow for empty strings, but not `None`.
223+ #
224+ # Refernences
225+ # 4.3.2 - Resource Owner Password Credentials Grant
226+ # https://tools.ietf.org/html/rfc6749#section-4.3.2
227+
228+ if isinstance (self ._client , LegacyApplicationClient ):
229+ if username is None :
230+ raise ValueError ('`LegacyApplicationClient` requires both the '
231+ '`username` and `password` parameters.' )
232+ if password is None :
233+ raise ValueError ('The required paramter `username` was supplied, '
234+ 'but `password` was not.' )
235+
236+ # merge username and password into kwargs for `prepare_request_body`
237+ if username is not None :
238+ kwargs ['username' ] = username
239+ if password is not None :
240+ kwargs ['password' ] = password
241+
242+ # is an auth explicitly supplied?
243+ if auth is not None :
244+ # if we're dealing with the default of `include_client_id` (None):
245+ # we will assume the `auth` argument is for an RFC compliant server
246+ # and we should not send the `client_id` in the body.
247+ # This approach allows us to still force the client_id by submitting
248+ # `include_client_id=True` along with an `auth` object.
249+ if include_client_id is None :
250+ include_client_id = False
251+
252+ # otherwise we may need to create an auth header
253+ else :
254+ # since we don't have an auth header, we MAY need to create one
255+ # it is possible that we want to send the `client_id` in the body
256+ # if so, `include_client_id` should be set to True
257+ # otherwise, we will generate an auth header
258+ if include_client_id is not True :
259+ client_id = self .client_id
260+ if client_id :
261+ log .debug ('Encoding `client_id` "%s" with `client_secret` '
262+ 'as Basic auth credentials.' , client_id )
263+ client_secret = client_secret if client_secret is not None else ''
264+ auth = requests .auth .HTTPBasicAuth (client_id , client_secret )
265+
266+ if include_client_id :
267+ # this was pulled out of the params
268+ # it needs to be passed into prepare_request_body
269+ if client_secret is not None :
270+ kwargs ['client_secret' ] = client_secret
199271
200272 body = self ._client .prepare_request_body (code = code , body = body ,
201- redirect_uri = self .redirect_uri , username = username ,
202- password = password , ** kwargs )
203-
204- client_id = kwargs .get ('client_id' , '' )
205- if auth is None :
206- if client_id :
207- log .debug ('Encoding client_id "%s" with client_secret as Basic auth credentials.' , client_id )
208- client_secret = kwargs .get ('client_secret' , '' )
209- client_secret = client_secret if client_secret is not None else ''
210- auth = requests .auth .HTTPBasicAuth (client_id , client_secret )
211- elif username :
212- if password is None :
213- raise ValueError ('Username was supplied, but not password.' )
214- log .debug ('Encoding username, password as Basic auth credentials.' )
215- auth = requests .auth .HTTPBasicAuth (username , password )
273+ redirect_uri = self .redirect_uri ,
274+ include_client_id = include_client_id , ** kwargs )
216275
217276 headers = headers or {
218277 'Accept' : 'application/json' ,
@@ -269,9 +328,11 @@ def refresh_token(self, token_url, refresh_token=None, body='', auth=None,
269328 :param refresh_token: The refresh_token to use.
270329 :param body: Optional application/x-www-form-urlencoded body to add the
271330 include in the token request. Prefer kwargs over body.
272- :param auth: An auth tuple or method as accepted by requests.
331+ :param auth: An auth tuple or method as accepted by ` requests` .
273332 :param timeout: Timeout of the request in seconds.
333+ :param headers: A dict of headers to be used by `requests`.
274334 :param verify: Verify SSL certificate.
335+ :param proxies: The `proxies` argument will be passed to `requests`.
275336 :param kwargs: Extra parameters to include in the token request.
276337 :return: A token dict
277338 """
0 commit comments