def create_token_response(self, uri, http_method='GET', body=None, headers=None, credentials=None, grant_type_for_scope=None, claims=None): """Extract grant_type and route to the designated handler.""" request = Request( uri, http_method=http_method, body=body, headers=headers) # 'scope' is an allowed Token Request param in both the "Resource Owner Password Credentials Grant" # and "Client Credentials Grant" flows # https://tools.ietf.org/html/rfc6749#section-4.3.2 # https://tools.ietf.org/html/rfc6749#section-4.4.2 request.scopes = utils.scope_to_list(request.scope) request.extra_credentials = credentials if grant_type_for_scope: request.grant_type = grant_type_for_scope # OpenID Connect claims, if provided. The server using oauthlib might choose # to implement the claims parameter of the Authorization Request. In this case # it should retrieve those claims and pass them via the claims argument here, # as a dict. if claims: request.claims = claims grant_type_handler = self.grant_types.get(request.grant_type, self.default_grant_type_handler) log.debug('Dispatching grant_type %s request to %r.', request.grant_type, grant_type_handler) return grant_type_handler.create_token_response( request, self.default_token_type)
def _get_authorization_header(request, client_key, client_secret): """ Get proper HTTP Authorization header for a given request Arguments: request: Request object to log Authorization header for Returns: authorization header """ sha1 = hashlib.sha1() body = request.body or '' sha1.update(body) oauth_body_hash = unicode(base64.b64encode( sha1.digest() # pylint: disable=too-many-function-args )) client = Client(client_key, client_secret) params = client.get_oauth_params(request) params.append((u'oauth_body_hash', oauth_body_hash)) blank_request = Request(urllib.unquote(request.url), http_method=request.method, body='', headers=request.headers, encoding='utf_8') blank_request.oauth_params = params blank_request.decoded_body = '' signature = client.get_oauth_signature(blank_request) blank_request.oauth_params.append((u'oauth_signature', signature)) headers = client._render( # pylint: disable=protected-access blank_request )[1] return headers['Authorization']
def create_authorization_response(self, uri, http_method='GET', body=None, headers=None): """Extract response_type and route to the designated handler.""" request = Request(uri, http_method=http_method, body=body, headers=headers) query_params = params_from_uri(request.uri) body_params = request.decoded_body # Prioritize response_type defined as query param over those in body. # Chosen because the two core grant types utilizing the response type # parameter both supply it in the uri. However it is not specified # explicitely in RFC 6748. if 'response_type' in query_params: request.response_type = query_params.get('response_type') elif 'response_type' in body_params: request.response_type = body_params.get('response_type') else: raise errors.InvalidRequestError( description='The response_type parameter is missing.') if not request.response_type in self.response_types: raise errors.UnsupportedResponseTypeError( description='Invalid response type') return self.response_types.get( request.response_type).create_authorization_response( request, self.default_token)
def create_authorization_response(self, uri, http_method='GET', body=None, headers=None, realms=None, credentials=None): """Create an authorization response, with a new request token if valid. :param uri: The full URI of the token request. :param http_method: A valid HTTP verb, i.e. GET, POST, PUT, HEAD, etc. :param body: The request body as a string. :param headers: The request headers as a dict. :param credentials: A list of credentials to include in the verifier. :returns: A tuple of 4 elements. 1. The URI to be used to redirect the user back to client. 2. A dict of headers to set on the response. 3. The response body as a string. 4. The response status code as an integer. An example of a valid request:: >>> from your_validator import your_validator >>> from oauthlib.oauth1 import RequestTokenEndpoint >>> endpoint = RequestTokenEndpoint(your_validator) >>> u, h, b, s = endpoint.create_request_token_response( ... 'https://your.provider/request_token?foo=bar', ... headers={ ... 'Authorization': 'OAuth realm=movies user, oauth_....' ... }, ... credentials={ ... 'extra': 'argument', ... }) >>> u 'https://the.client/callback?oauth_verifier=...&mextra=argument' >>> h {} >>> b '' >>> s 302 """ request = Request(uri, http_method=http_method, body=body, headers=headers) if not self.request_validator.verify_request_token( request.oauth_token, request): raise errors.InvalidClientError() if not request.oauth_token: raise NotImplementedError('request.oauth_token must be set after ' 'request token verification.') request.realms = realms if (request.realms and not self.request_validator.verify_realms( request.oauth_token, request.realms, request)): raise errors.InvalidRequestError( description=('User granted access to realms outside of ' 'what the client may request.')) redirect_uri = self.request_validator.get_redirect_uri( request.oauth_token, request) verifier = self.create_verifier(request, credentials or {}) uri = add_params_to_uri(redirect_uri, verifier.items()) return uri, {}, None, 302
def validate_authorization_request(self, uri, http_method='GET', body=None, headers=None): """Extract response_type and route to the designated handler.""" request = Request(uri, http_method=http_method, body=body, headers=headers) request.scopes = None response_type_handler = self.response_types.get( request.response_type, self.default_response_type_handler) return response_type_handler.validate_authorization_request(request)
def verify_request(self, uri, http_method='GET', body=None, headers=None): """Validate client, code etc, return body + headers""" request = Request(uri, http_method, body, headers) request.token_type = self.find_token_type(request) token_type_handler = self.tokens.get(request.token_type, self.default_token_type_handler) return token_type_handler.validate_request(request)
def verify_request(self, uri, http_method='GET', body=None, headers=None, scopes=None): """Validate client, code etc, return body + headers""" request = Request(uri, http_method, body, headers) request.token_type = self.find_token_type(request) request.scopes = scopes token_type_handler = self.tokens.get(request.token_type, self.default_token_type_handler) log.debug('Dispatching token_type %s request to %r.', request.token_type, token_type_handler) return token_type_handler.validate_request(request), request
def create_token_response(self, uri, http_method='GET', body=None, headers=None, credentials=None): """Extract grant_type and route to the designated handler.""" request = Request(uri, http_method=http_method, body=body, headers=headers) request.extra_credentials = credentials grant_type_handler = self.grant_types.get(request.grant_type, self.default_grant_type_handler) log.debug('Dispatching grant_type %s request to %r.', request.grant_type, grant_type_handler) return grant_type_handler.create_token_response( request, self.default_token_type)
def create_authorization_response(self, uri, http_method='GET', body=None, headers=None, scopes=None, credentials=None): """Extract response_type and route to the designated handler.""" request = Request(uri, http_method=http_method, body=body, headers=headers) request.authorized_scopes = scopes for k, v in (credentials or {}).items(): setattr(request, k, v) response_type_handler = self.response_types.get( request.response_type, self.default_response_type_handler) return response_type_handler.create_authorization_response( request, self.default_token)
def test_request_invalid_client_id(self): request = Request('https://a.b./path') request.scope = 'openid profile' request.response_type = 'code' with pytest.raises(errors.MissingClientIdError): self.auth.validate_authorization_request(request) request.client_id = 'invalid_client' self.validator.validate_client_id.return_value = False with pytest.raises(errors.InvalidClientIdError): self.auth.validate_authorization_request(request)
def create_authorization_response( self, uri, http_method="GET", body=None, headers=None, scopes=None, credentials=None ): """Extract response_type and route to the designated handler.""" request = Request(uri, http_method=http_method, body=body, headers=headers) request.scopes = scopes # TODO: decide whether this should be a required argument request.user = None # TODO: explain this in docs for k, v in (credentials or {}).items(): setattr(request, k, v) response_type_handler = self.response_types.get(request.response_type, self.default_response_type_handler) log.debug("Dispatching response_type %s request to %r.", request.response_type, response_type_handler) return response_type_handler.create_authorization_response(request, self.default_token_type)
def verify_request(self, uri, http_method='GET', body=None, headers=None): """Validate client, code etc, return body + headers""" request = Request(uri, http_method, body, headers) request.token_type = self.find_token_type(request) # TODO(ib-lundgren): How to return errors is not strictly defined and # should allow for customization. if not request.token_type: raise ValueError('Could not determine the token type.') if not request.token_type in self.tokens: raise ValueError('Unsupported token type.') return self.tokens.get(request.token_type).validate_request(request)
def test_header_with_multispaces_is_validated(self): request_validator = mock.MagicMock() request_validator.validate_bearer_token = self._mocked_validate_bearer_token request = Request("/", headers=self.valid_header_with_multiple_spaces) result = BearerToken( request_validator=request_validator).validate_request(request) self.assertTrue(result)
def test_extract_params_with_urlencoded_json(self): wsgi_environ = { 'QUERY_STRING': 'state=%7B%22t%22%3A%22a%22%2C%22i%22%3A%22l%22%7D' } with set_flask_request(wsgi_environ): uri, http_method, body, headers = extract_params() # Request constructor will try to urldecode the querystring, make # sure this doesn't fail. Request(uri, http_method, body, headers)
def setUp(self): self.request = Request('http://a.b/path') request_validator = mock.MagicMock() implicit_grant = ImplicitGrant(request_validator) openid_connect_implicit = OpenIDConnectImplicit(request_validator) self.dispatcher = ImplicitTokenGrantDispatcher( default_implicit_grant=implicit_grant, oidc_implicit_grant=openid_connect_implicit)
def test_create_authorization_grant(self): bearer = BearerToken(self.mock_validator) self.request.response_mode = 'query' h, b, s = self.auth.create_authorization_response(self.request, bearer) grant = dict(Request(h['Location']).uri_query_params) self.assertIn('code', grant) self.assertTrue(self.mock_validator.validate_redirect_uri.called) self.assertTrue(self.mock_validator.validate_response_type.called) self.assertTrue(self.mock_validator.validate_scopes.called)
def create_token_response(self, uri, http_method='GET', body=None, headers=None, credentials=None): """Extract grant_type and route to the designated handler.""" request = Request(uri, http_method=http_method, body=body, headers=headers) request.extra_credentials = credentials grant_type_handler = self.grant_types.get( request.grant_type, self.default_grant_type_handler) log.debug('Dispatching grant_type %s request to %r.', request.grant_type, grant_type_handler) return grant_type_handler.create_token_response( request, self.default_token_type)
def test_fake_bearer_is_not_validated(self): request_validator = mock.MagicMock() request_validator.validate_bearer_token = self._mocked_validate_bearer_token for fake_header in self.fake_bearer_headers: request = Request("/", headers=fake_header) result = BearerToken( request_validator=request_validator).validate_request(request) self.assertFalse(result)
def clean_oauth_signature(self): """ Cleans and validates the 'oauth signature'. The signature is verified by calculating the hash (using the algorithm specified in 'oauth_signature_method') of the URL, METHOD and BODY. After the calculation the signature is compared with the 'oauth_signature' to check if they match. When the signatures match we can assure that the request is from a authorized entity. """ oauth_request = Request(self.request.build_absolute_uri(), self.request.method, self.request.POST) oauth_request.signature = self.cleaned_data["oauth_signature"] oauth_request.params = [(k, v) for k, v in self.request.POST.items() if k != "oauth_signature"] if not oauth.verify_hmac_sha1(oauth_request, settings.LTI_SECRET): raise ValidationError( "Invalid signature, URL: {}, method: {}".format( self.request.build_absolute_uri(), self.request.method)) return self.cleaned_data["oauth_signature"]
def test_authenticate_client(self): request = Request('http://localhost/authenticate_client', 'GET', None, { 'client_id': '10', 'client_secret': 'secret' }) self.assertTrue(self.validator.authenticate_client(request)) request = Request('http://localhost/authenticate_client', 'GET', None, { 'client_id': '10', 'client_secret': 'secre' }) self.assertFalse(self.validator.authenticate_client(request)) request = Request('http://localhost/authenticate_client', 'GET', None, { 'client_id': '1', 'client_secret': 'secret' }) self.assertFalse(self.validator.authenticate_client(request))
def setUp(self): mock_client = mock.MagicMock() mock_client.user.return_value = 'mocked user' self.request = Request('http://a.b/path') self.request.assertion = 'mocked assertion' self.request.grant_type = JWT_BEARER self.request.client = mock_client self.request.scope = 'foo' self.mock_validator = mock.MagicMock() self.auth = JWTBearerGrant(request_validator=self.mock_validator)
def setUp(self): mock_client = mock.MagicMock() mock_client.user.return_value = 'mocked user' self.request = Request('http://a.b/path') self.request.grant_type = 'urn:ietf:params:oauth:grant-type:saml2-bearer' self.request.assertion = 'assertion' self.request.client = mock_client self.request.scopes = ('mocked', 'scopes') self.mock_validator = mock.MagicMock() self.auth = SAML2BearerGrant(request_validator=self.mock_validator)
def test_sanitizing_authorization_header(self): r = Request(URI, headers={ 'Accept': 'application/json', 'Authorization': 'Basic Zm9vOmJhcg==' }) self.assertNotIn('Zm9vOmJhcg==', repr(r)) self.assertIn('<SANITIZED>', repr(r)) # Double-check we didn't modify the underlying object: self.assertEqual(r.headers['Authorization'], 'Basic Zm9vOmJhcg==')
def setUp(self): mock_client = mock.MagicMock() mock_client.user.return_value = 'mocked user' self.request = Request('http://a.b/path') self.request.grant_type = 'client_credentials' self.request.client = mock_client self.request.scopes = ('mocked', 'scopes') self.mock_validator = mock.MagicMock() self.auth = ClientCredentialsGrant( request_validator=self.mock_validator)
def setUp(self): self.request = Request('http://a.b/path') self.request.grant_type = 'password' self.request.username = '******' self.request.password = '******' self.request.client = 'mock authenticated' self.request.scopes = ('mocked', 'scopes') self.mock_validator = mock.MagicMock() self.auth = ResourceOwnerPasswordCredentialsGrant( request_validator=self.mock_validator)
def setUp(self): mock_client = mock.MagicMock() mock_client.user.return_value = 'mocked user' self.request = Request('http://a.b/path') self.request.grant_type = 'refresh_token' self.request.refresh_token = 'lsdkfhj230' self.request.client = mock_client self.request.scope = 'foo' self.mock_validator = mock.MagicMock() self.auth = RefreshTokenGrant(request_validator=self.mock_validator)
def to_representation(self, instance): core = OAuthLibCore() uri, http_method, body, headers = core._extract_params( self.context['request']) headers = { **headers, 'client_id': self.initial_data['client_id'], 'client_secret': self.initial_data['client_secret'], } request = Request(uri=uri, http_method=http_method, body=body, headers=headers) request.scopes = ['read', 'write'] request.user = instance.user validator = OAuth2Validator() validator.authenticate_client(request) token = BearerToken(request_validator=validator).create_token( request, refresh_token=True, save_token=True) return {**token, 'profile': super().to_representation(instance)}
def setUp(self): self.request = Request('http://a.b/path') self.request.scopes = ('hello', 'world') self.request.client = 'batman' self.request.client_id = 'abcdef' self.request.response_type = 'token' self.request.state = 'xyz' self.request.redirect_uri = 'https://b.c/p' self.mock_validator = mock.MagicMock() self.auth = ImplicitGrant(request_validator=self.mock_validator)
def setUp(self): self.request = Request('http://a.b/path') self.request.decoded_body = ( ("client_id", "me"), ("code", "code"), ("redirect_url", "https://a.b/cb"), ) self.request_validator = mock.MagicMock() self.auth_grant = OAuth2AuthorizationCodeGrant(self.request_validator) self.openid_connect_auth = AuthorizationCodeGrant(self.request_validator)
def oauth_error(self, request, error, **kwargs): # UGLY HACK from oauthlib.common import Request core = self.get_oauthlib_core() uri, http_method, body, headers = core._extract_params(request) orequest = Request(uri, http_method=http_method, body=body, headers=headers) raise OAuthToolkitError( error=error(request=orequest, state=orequest.state, **kwargs))
def test_non_unicode_params(self): r = Request( bytes_type('http://a.b/path?query', 'utf-8'), http_method=bytes_type('GET', 'utf-8'), body=bytes_type('you=shall+pass', 'utf-8'), headers={bytes_type('a', 'utf-8'): bytes_type('b', 'utf-8')}) self.assertEqual(r.uri, 'http://a.b/path?query') self.assertEqual(r.http_method, 'GET') self.assertEqual(r.body, 'you=shall+pass') self.assertEqual(r.decoded_body, [('you', 'shall pass')]) self.assertEqual(r.headers, {'a': 'b'})
def test_validate_lti_old_timestamp(self): request = Request(uri='https://example.com/lti', http_method='POST', body=self.read_data_file('lti_old_timestamp.txt')) parameters = LTIAuthBackend._get_validated_lti_params_from_values( # pylint: disable=protected-access request=request, current_time=1436900000, lti_consumer_valid=True, lti_consumer_secret='secret', lti_max_timestamp_age=10) self.assertFalse(parameters)
def test_validate_token_request(self): mock_validator = mock.MagicMock() auth = AuthorizationCodeGrant(request_validator=mock_validator) request = Request('http://a.b/path') self.assertRaises(UnsupportedGrantTypeError, auth.validate_token_request, request) request.grant_type = 'authorization_code' self.assertRaises(InvalidRequestError, auth.validate_token_request, request) mock_validator.validate_client = mock.MagicMock(return_value=False) request.code = 'waffles' request.client = 'batman' self.assertRaises(UnauthorizedClientError, auth.validate_token_request, request) mock_validator.validate_client = mock.MagicMock(return_value=True) mock_validator.validate_code = mock.MagicMock(return_value=False) self.assertRaises(InvalidGrantError, auth.validate_token_request, request)
def test_non_unicode_params(self): r = Request(b'http://a.b/path?query', http_method=b'GET', body=b'you=shall+pass', headers={ b'a': b'b', }) self.assertEqual(r.uri, 'http://a.b/path?query') self.assertEqual(r.http_method, 'GET') self.assertEqual(r.body, 'you=shall+pass') self.assertEqual(r.decoded_body, [('you', 'shall pass')]) self.assertEqual(r.headers, {'a': 'b'})
def test_validate_lti_cannot_add_get_params(self): request = Request( uri='https://example.com/lti?custom_another=parameter', http_method='POST', body=self.read_data_file('lti_cannot_add_get_params.txt') ) parameters = LTIAuthBackend._get_validated_lti_params_from_values( # pylint: disable=protected-access request=request, current_time=1436823554, lti_consumer_valid=True, lti_consumer_secret='secret', lti_max_timestamp_age=10 ) assert not parameters
def test_extract_params_with_json(self): data = {'test': 'foo', 'foo': 'bar'} json_payload = json.dumps(data).encode('utf-8') wsgi_environ = { 'CONTENT_TYPE': 'application/json', 'CONTENT_LENGHT': str(len(data)) } with set_flask_request(wsgi_environ, data): uri, http_method, body, headers = extract_params() Request(uri, http_method, body, headers) self.assertEqual(body, {'test': 'foo', 'foo': 'bar'})
def validate_2legged_oauth(oauth, uri, method, auth_header): """ "Two-legged" OAuth authorization isn't standard and so not supported by current versions of oauthlib. The implementation here is sufficient for simple developer tools and testing. Real usage of OAuth will always require directing the user to the authorization page so that a resource-owner token can be generated. """ req = Request(uri, method, "", auth_header) typ, params, oauth_params = oauth._get_signature_type_and_params(req) oauth_params = dict(oauth_params) req.params = filter(lambda x: x[0] not in ("oauth_signature", "realm"), params) req.signature = oauth_params.get("oauth_signature") req.client_key = oauth_params.get("oauth_consumer_key") req.nonce = oauth_params.get("oauth_nonce") req.timestamp = oauth_params.get("oauth_timestamp") if oauth_params.get("oauth_signature_method").lower() != "hmac-sha1": raise TwoLeggedOAuthError(u"unsupported signature method " + oauth_params.get("oauth_signature_method")) secret = validator.get_client_secret(req.client_key, req) valid_signature = signature.verify_hmac_sha1(req, secret, None) if valid_signature: return req.client_key else: raise TwoLeggedOAuthError(u"Cannot find APIAccess token with that key: %s" % req.client_key)
def create_token_response(self, uri, http_method='POST', body=None, headers=None, credentials=None, grant_type_for_scope=None, claims=None): """Extract grant_type and route to the designated handler.""" request = Request( uri, http_method=http_method, body=body, headers=headers) self.validate_token_request(request) # 'scope' is an allowed Token Request param in both the "Resource Owner Password Credentials Grant" # and "Client Credentials Grant" flows # https://tools.ietf.org/html/rfc6749#section-4.3.2 # https://tools.ietf.org/html/rfc6749#section-4.4.2 request.scopes = utils.scope_to_list(request.scope) request.extra_credentials = credentials if grant_type_for_scope: request.grant_type = grant_type_for_scope # OpenID Connect claims, if provided. The server using oauthlib might choose # to implement the claims parameter of the Authorization Request. In this case # it should retrieve those claims and pass them via the claims argument here, # as a dict. if claims: request.claims = claims grant_type_handler = self.grant_types.get(request.grant_type, self.default_grant_type_handler) log.debug('Dispatching grant_type %s request to %r.', request.grant_type, grant_type_handler) return grant_type_handler.create_token_response( request, self.default_token_type)
def create_token_response(self, uri, http_method='GET', body=None, headers=None, credentials=None, grant_type_for_scope=None, claims=None): """Extract grant_type and route to the designated handler.""" request = Request(uri, http_method=http_method, body=body, headers=headers) request.scopes = None request.extra_credentials = credentials if grant_type_for_scope: request.grant_type = grant_type_for_scope # OpenID Connect claims, if provided. The server using oauthlib might choose # to implement the claims parameter of the Authorization Request. In this case # it should retrieve those claims and pass them via the claims argument here, # as a dict. if claims: request.claims = claims grant_type_handler = self.grant_types.get( request.grant_type, self.default_grant_type_handler) log.debug('Dispatching grant_type %s request to %r.', request.grant_type, grant_type_handler) return grant_type_handler.create_token_response( request, self.default_token_type)
async def create_revocation_response(self, uri, http_method='POST', body=None, headers=None): """Revoke supplied access or refresh token. The authorization server responds with HTTP status code 200 if the token has been revoked sucessfully or if the client submitted an invalid token. Note: invalid tokens do not cause an error response since the client cannot handle such an error in a reasonable way. Moreover, the purpose of the revocation request, invalidating the particular token, is already achieved. The content of the response body is ignored by the client as all necessary information is conveyed in the response code. An invalid token type hint value is ignored by the authorization server and does not influence the revocation response. """ resp_headers = { 'Content-Type': 'application/json', 'Cache-Control': 'no-store', 'Pragma': 'no-cache', } request = Request(uri, http_method=http_method, body=body, headers=headers) try: await self.validate_revocation_request(request) log.debug('Token revocation valid for %r.', request) except OAuth2Error as e: log.debug('Client error during validation of %r. %r.', request, e) response_body = e.json if self.enable_jsonp and request.callback: response_body = '{}({});'.format(request.callback, response_body) resp_headers.update(e.headers) return resp_headers, response_body, e.status_code await self.request_validator.revoke_token(request.token, request.token_type_hint, request) response_body = '' if self.enable_jsonp and request.callback: response_body = request.callback + '();' return {}, response_body, 200
def test_create_authorization_grant_state(self): self.request.state = 'abc' self.request.redirect_uri = None self.mock_validator.get_default_redirect_uri.return_value = 'https://a.b/cb' bearer = BearerToken(self.mock_validator) h, b, s = self.auth.create_authorization_response(self.request, bearer) grant = dict(Request(h['Location']).uri_query_params) self.assertIn('code', grant) self.assertIn('state', grant) self.assertFalse(self.mock_validator.validate_redirect_uri.called) self.assertTrue(self.mock_validator.get_default_redirect_uri.called) self.assertTrue(self.mock_validator.validate_response_type.called) self.assertTrue(self.mock_validator.validate_scopes.called)
def validate_authorization_request(self, uri, http_method='GET', body=None, headers=None): """Extract response_type and route to the designated handler.""" request = Request(uri, http_method=http_method, body=body, headers=headers) response_type_handler = self.response_types.get( request.response_type, self.default_response_type_handler) return response_type_handler.validate_authorization_request(request)
def sign(self, uri, http_method=u'GET', body='', headers=None): """Sign a request Signs an HTTP request with the specified parts. Returns a 3-tuple of the signed request's URI, headers, and body. Note that http_method is not returned as it is unaffected by the OAuth signing process. The body argument may be a dict, a list of 2-tuples, or a formencoded string. If the body argument is not a formencoded string and/or the Content-Type header is not 'x-www-form-urlencoded', it will be returned verbatim as it is unaffected by the OAuth signing process. Attempting to sign a request with non-formencoded data using the OAuth body signature type is invalid and will raise an exception. If the body does contain parameters, it will be returned as a properly- formatted formencoded string. All string data MUST be unicode. This includes strings inside body dicts, for example. """ # normalize request data request = Request(uri, http_method, body, headers) # sanity check content_type = request.headers.get('Content-Type', None) if content_type == 'application/x-www-form-urlencoded' and not request.body_has_params: raise ValueError("Headers indicate a formencoded body but body was not decodable.") # generate the basic OAuth parameters request.oauth_params = self.get_oauth_params() # generate the signature request.oauth_params.append((u'oauth_signature', self.get_oauth_signature(request))) # render the signed request and return it return self._render(request, formencode=True)
def create_token_response(self, uri, http_method='GET', body=None, headers=None, credentials=None, grant_type_for_scope=None, claims=None): """Extract grant_type and route to the designated handler.""" request = Request( uri, http_method=http_method, body=body, headers=headers) request.scopes = None request.extra_credentials = credentials if grant_type_for_scope: request.grant_type = grant_type_for_scope # OpenID Connect claims, if provided. The server using oauthlib might choose # to implement the claims parameter of the Authorization Request. In this case # it should retrieve those claims and pass them via the claims argument here, # as a dict. if claims: request.claims = claims grant_type_handler = self.grant_types.get(request.grant_type, self.default_grant_type_handler) log.debug('Dispatching grant_type %s request to %r.', request.grant_type, grant_type_handler) return grant_type_handler.create_token_response( request, self.default_token_type)
def create_token_response(self, uri, http_method='GET', body=None, headers=None): """Extract grant_type and route to the designated handler.""" request = Request(uri, http_method=http_method, body=body, headers=headers) query_params = params_from_uri(request.uri) body_params = self.request.decoded_body # Prioritize grant_type defined as body param over those in uri. # Chosen because all three core grant types supply this parameter # in the body. However it is not specified explicitely in RFC 6748. if 'grant_type' in body_params: request.grant_type = query_params.get('grant_type') elif 'grant_type' in query_params: request.grant_type = body_params.get('grant_type') else: raise errors.InvalidRequestError( description='The grant_type parameter is missing.') if not request.grant_type in self.grant_types: raise errors.UnsupportedGrantTypeError( description='Invalid response type') return self.grant_types.get( request.grant_type).create_token_response( request, self.default_token)
def make_request(self, response_type='code', scope='openid profile email', **kwargs): request = Request('https://a.b/path') request.scope = scope request.client = 'superman' request.client_id = 'abcdef' request.redirect_uri = 'https://a.b/' request.response_type = response_type for prop, val in kwargs.items(): setattr(request, prop, val) return request
def authorize_request_token(self, request_token, user): verifier = self.token_generator() request = Request('') request.resource_owner_key = user return self.request_validator.save_verifier(request_token, verifier, request)
def sign(self, uri, http_method='GET', body=None, headers=None, realm=None): __doc__ = Client.sign.__doc__ # normalize request data request = Request(uri, http_method, body, headers, encoding=self.encoding) # sanity check content_type = request.headers.get('Content-Type', None) multipart = content_type and content_type.startswith('multipart/') should_have_params = content_type == CONTENT_TYPE_FORM_URLENCODED has_params = request.decoded_body is not None # 3.4.1.3.1. Parameter Sources # [Parameters are collected from the HTTP request entity-body, but only # if [...]: # * The entity-body is single-part. if multipart and has_params: raise ValueError("Headers indicate a multipart body but body contains parameters.") # * The entity-body follows the encoding requirements of the # "application/x-www-form-urlencoded" content-type as defined by # [W3C.REC-html40-19980424]. elif should_have_params and not has_params: raise ValueError("Headers indicate a formencoded body but body was not decodable.") # * The HTTP request entity-header includes the "Content-Type" # header field set to "application/x-www-form-urlencoded". elif not should_have_params and has_params: raise ValueError("Body contains parameters but Content-Type header was not set.") # 3.5.2. Form-Encoded Body # Protocol parameters can be transmitted in the HTTP request entity- # body, but only if the following REQUIRED conditions are met: # o The entity-body is single-part. # o The entity-body follows the encoding requirements of the # "application/x-www-form-urlencoded" content-type as defined by # [W3C.REC-html40-19980424]. # o The HTTP request entity-header includes the "Content-Type" header # field set to "application/x-www-form-urlencoded". elif self.signature_type == SIGNATURE_TYPE_BODY and not ( should_have_params and has_params and not multipart): raise ValueError('Body signatures may only be used with form-urlencoded content') # We amend http://tools.ietf.org/html/rfc5849#section-3.4.1.3.1 # with the clause that parameters from body should only be included # in non GET or HEAD requests. Extracting the request body parameters # and including them in the signature base string would give semantic # meaning to the body, which it should not have according to the # HTTP 1.1 spec. elif http_method.upper() in ('GET', 'HEAD') and has_params: raise ValueError('GET/HEAD requests should not include body.') # generate the basic OAuth parameters request.oauth_params = self.get_oauth_params(request) # generate the signature request.oauth_params.append(('oauth_signature', self.get_oauth_signature(request))) # render the signed request and return it uri, headers, body = self._render(request, formencode=True, realm=(realm or self.realm)) if self.decoding: log.debug('Encoding URI, headers and body to %s.', self.decoding) uri = uri.encode(self.decoding) body = body.encode(self.decoding) if body else body new_headers = {} for k, v in headers.items(): new_headers[k.encode(self.decoding)] = v.encode(self.decoding) headers = new_headers return uri, headers, body
def _create_request(self, uri, http_method, body, headers): # Only include body data from x-www-form-urlencoded requests headers = headers or {} if ("Content-Type" in headers and CONTENT_TYPE_FORM_URLENCODED in headers["Content-Type"]): request = Request(uri, http_method, body, headers) else: request = Request(uri, http_method, '', headers) signature_type, params, oauth_params = ( self._get_signature_type_and_params(request)) # The server SHOULD return a 400 (Bad Request) status code when # receiving a request with duplicated protocol parameters. if len(dict(oauth_params)) != len(oauth_params): raise errors.InvalidRequestError( description='Duplicate OAuth1 entries.') oauth_params = dict(oauth_params) request.signature = oauth_params.get('oauth_signature') request.client_key = oauth_params.get('oauth_consumer_key') request.resource_owner_key = oauth_params.get('oauth_token') request.nonce = oauth_params.get('oauth_nonce') request.timestamp = oauth_params.get('oauth_timestamp') request.redirect_uri = oauth_params.get('oauth_callback') request.verifier = oauth_params.get('oauth_verifier') request.signature_method = oauth_params.get('oauth_signature_method') request.realm = dict(params).get('realm') request.oauth_params = oauth_params # Parameters to Client depend on signature method which may vary # for each request. Note that HMAC-SHA1 and PLAINTEXT share parameters request.params = [(k, v) for k, v in params if k != "oauth_signature"] if 'realm' in request.headers.get('Authorization', ''): request.params = [(k, v) for k, v in request.params if k != "realm"] return request
def verify_request(self, uri, http_method='GET', body=None, headers=None, require_resource_owner=True, require_verifier=False, require_realm=False, required_realm=None, require_callback=False): """Verifies a request ensuring that the following is true: Per `section 3.2`_ of the spec. - all mandated OAuth parameters are supplied - parameters are only supplied in one source which may be the URI query, the Authorization header or the body - all parameters are checked and validated, see comments and the methods and properties of this class for further details. - the supplied signature is verified against a recalculated one A ValueError will be raised if any parameter is missing, supplied twice or invalid. A HTTP 400 Response should be returned upon catching an exception. A HTTP 401 Response should be returned if verify_request returns False. `Timing attacks`_ are prevented through the use of dummy credentials to create near constant time verification even if an invalid credential is used. Early exit on invalid credentials would enable attackers to perform `enumeration attacks`_. Near constant time string comparison is used to prevent secret key guessing. Note that timing attacks can only be prevented through near constant time execution, not by adding a random delay which would only require more samples to be gathered. .. _`section 3.2`: http://tools.ietf.org/html/rfc5849#section-3.2 .. _`Timing attacks`: http://rdist.root.org/2010/07/19/exploiting-remote-timing-attacks/ .. _`enumeration attacks`: http://www.sans.edu/research/security-laboratory/article/attacks-browsing """ # Only include body data from x-www-form-urlencoded requests headers = headers or {} if ("Content-Type" in headers and headers["Content-Type"] == CONTENT_TYPE_FORM_URLENCODED): request = Request(uri, http_method, body, headers) else: request = Request(uri, http_method, '', headers) if self.enforce_ssl and not request.uri.lower().startswith("https://"): raise ValueError("Insecure transport, only HTTPS is allowed.") signature_type, params, oauth_params = self.get_signature_type_and_params(request) # The server SHOULD return a 400 (Bad Request) status code when # receiving a request with duplicated protocol parameters. if len(dict(oauth_params)) != len(oauth_params): raise ValueError("Duplicate OAuth entries.") oauth_params = dict(oauth_params) request.signature = oauth_params.get('oauth_signature') request.client_key = oauth_params.get('oauth_consumer_key') request.resource_owner_key = oauth_params.get('oauth_token') request.nonce = oauth_params.get('oauth_nonce') request.timestamp = oauth_params.get('oauth_timestamp') request.callback_uri = oauth_params.get('oauth_callback') request.verifier = oauth_params.get('oauth_verifier') request.signature_method = oauth_params.get('oauth_signature_method') request.realm = dict(params).get('realm') # The server SHOULD return a 400 (Bad Request) status code when # receiving a request with missing parameters. if not all((request.signature, request.client_key, request.nonce, request.timestamp, request.signature_method)): raise ValueError("Missing OAuth parameters.") # OAuth does not mandate a particular signature method, as each # implementation can have its own unique requirements. Servers are # free to implement and document their own custom methods. # Recommending any particular method is beyond the scope of this # specification. Implementers should review the Security # Considerations section (`Section 4`_) before deciding on which # method to support. # .. _`Section 4`: http://tools.ietf.org/html/rfc5849#section-4 if not request.signature_method in self.allowed_signature_methods: raise ValueError("Invalid signature method.") # Servers receiving an authenticated request MUST validate it by: # If the "oauth_version" parameter is present, ensuring its value is # "1.0". if ('oauth_version' in request.oauth_params and request.oauth_params['oauth_version'] != '1.0'): raise ValueError("Invalid OAuth version.") # The timestamp value MUST be a positive integer. Unless otherwise # specified by the server's documentation, the timestamp is expressed # in the number of seconds since January 1, 1970 00:00:00 GMT. if len(request.timestamp) != 10: raise ValueError("Invalid timestamp size") try: ts = int(request.timestamp) except ValueError: raise ValueError("Timestamp must be an integer") else: # To avoid the need to retain an infinite number of nonce values for # future checks, servers MAY choose to restrict the time period after # which a request with an old timestamp is rejected. if time.time() - ts > self.timestamp_lifetime: raise ValueError("Request too old, over 10 minutes.") # Provider specific validation of parameters, used to enforce # restrictions such as character set and length. if not self.check_client_key(request.client_key): raise ValueError("Invalid client key.") if not request.resource_owner_key and require_resource_owner: raise ValueError("Missing resource owner.") if (require_resource_owner and not require_verifier and not self.check_access_token(request.resource_owner_key)): raise ValueError("Invalid resource owner key.") if (require_resource_owner and require_verifier and not self.check_request_token(request.resource_owner_key)): raise ValueError("Invalid resource owner key.") if not self.check_nonce(request.nonce): raise ValueError("Invalid nonce.") if request.realm and not self.check_realm(request.realm): raise ValueError("Invalid realm. Allowed are %s" % self.realms) if not request.verifier and require_verifier: raise ValueError("Missing verifier.") if require_verifier and not self.check_verifier(request.verifier): raise ValueError("Invalid verifier.") # Servers receiving an authenticated request MUST validate it by: # If using the "HMAC-SHA1" or "RSA-SHA1" signature methods, ensuring # that the combination of nonce/timestamp/token (if present) # received from the client has not been used before in a previous # request (the server MAY reject requests with stale timestamps as # described in `Section 3.3`_). # .._`Section 3.3`: http://tools.ietf.org/html/rfc5849#section-3.3 # # We check this before validating client and resource owner for # increased security and performance, both gained by doing less work. if require_verifier: token = {"request_token": request.resource_owner_key} else: token = {"access_token": request.resource_owner_key} if not self.validate_timestamp_and_nonce(request.client_key, request.timestamp, request.nonce, **token): return False, request # The server SHOULD return a 401 (Unauthorized) status code when # receiving a request with invalid client credentials. # Note: This is postponed in order to avoid timing attacks, instead # a dummy client is assigned and used to maintain near constant # time request verification. # # Note that early exit would enable client enumeration valid_client = self.validate_client_key(request.client_key) if not valid_client: client_key = self.dummy_client # Callback is normally never required, except for requests for # a Temporary Credential as described in `Section 2.1`_ # .._`Section 2.1`: http://tools.ietf.org/html/rfc5849#section-2.1 if require_callback: valid_redirect = self.validate_redirect_uri(request.client_key, request.callback_uri) else: valid_redirect = True # The server SHOULD return a 401 (Unauthorized) status code when # receiving a request with invalid or expired token. # Note: This is postponed in order to avoid timing attacks, instead # a dummy token is assigned and used to maintain near constant # time request verification. # # Note that early exit would enable resource owner enumeration if request.resource_owner_key: if require_verifier: valid_resource_owner = self.validate_request_token( request.client_key, request.resource_owner_key) if not valid_resource_owner: resource_owner_key = self.dummy_request_token else: valid_resource_owner = self.validate_access_token( request.client_key, request.resource_owner_key) if not valid_resource_owner: resource_owner_key = self.dummy_access_token else: valid_resource_owner = True # Note that `realm`_ is only used in authorization headers and how # it should be interepreted is not included in the OAuth spec. # However they could be seen as a scope or realm to which the # client has access and as such every client should be checked # to ensure it is authorized access to that scope or realm. # .. _`realm`: http://tools.ietf.org/html/rfc2617#section-1.2 # # Note that early exit would enable client realm access enumeration. # # The require_realm indicates this is the first step in the OAuth # workflow where a client requests access to a specific realm. # This first step (obtaining request token) need not require a realm # and can then be identified by checking the require_resource_owner # flag and abscence of realm. # # Clients obtaining an access token will not supply a realm and it will # not be checked. Instead the previously requested realm should be # transferred from the request token to the access token. # # Access to protected resources will always validate the realm but note # that the realm is now tied to the access token and not provided by # the client. if ((require_realm and not request.resource_owner_key) or (not require_resource_owner and not request.realm)): valid_realm = self.validate_requested_realm(request.client_key, request.realm) elif require_verifier: valid_realm = True else: valid_realm = self.validate_realm(request.client_key, request.resource_owner_key, uri=request.uri, required_realm=required_realm) # The server MUST verify (Section 3.2) the validity of the request, # ensure that the resource owner has authorized the provisioning of # token credentials to the client, and ensure that the temporary # credentials have not expired or been used before. The server MUST # also verify the verification code received from the client. # .. _`Section 3.2`: http://tools.ietf.org/html/rfc5849#section-3.2 # # Note that early exit would enable resource owner authorization # verifier enumertion. if request.verifier: valid_verifier = self.validate_verifier(request.client_key, request.resource_owner_key, request.verifier) else: valid_verifier = True # Parameters to Client depend on signature method which may vary # for each request. Note that HMAC-SHA1 and PLAINTEXT share parameters request.params = filter(lambda x: x[0] not in ("oauth_signature", "realm"), params) # ---- RSA Signature verification ---- if request.signature_method == SIGNATURE_RSA: # The server verifies the signature per `[RFC3447] section 8.2.2`_ # .. _`[RFC3447] section 8.2.2`: http://tools.ietf.org/html/rfc3447#section-8.2.1 rsa_key = self.get_rsa_key(request.client_key) valid_signature = signature.verify_rsa_sha1(request, rsa_key) # ---- HMAC or Plaintext Signature verification ---- else: # Servers receiving an authenticated request MUST validate it by: # Recalculating the request signature independently as described in # `Section 3.4`_ and comparing it to the value received from the # client via the "oauth_signature" parameter. # .. _`Section 3.4`: http://tools.ietf.org/html/rfc5849#section-3.4 client_secret = self.get_client_secret(request.client_key) resource_owner_secret = None if require_resource_owner: if require_verifier: resource_owner_secret = self.get_request_token_secret( request.client_key, request.resource_owner_key) else: resource_owner_secret = self.get_access_token_secret( request.client_key, request.resource_owner_key) if request.signature_method == SIGNATURE_HMAC: valid_signature = signature.verify_hmac_sha1(request, client_secret, resource_owner_secret) else: valid_signature = signature.verify_plaintext(request, client_secret, resource_owner_secret) # We delay checking validity until the very end, using dummy values for # calculations and fetching secrets/keys to ensure the flow of every # request remains almost identical regardless of whether valid values # have been supplied. This ensures near constant time execution and # prevents malicious users from guessing sensitive information v = all((valid_client, valid_resource_owner, valid_realm, valid_redirect, valid_verifier, valid_signature)) logger = logging.getLogger("oauthlib") if not v: logger.info("[Failure] OAuthLib request verification failed.") logger.info("Valid client:\t%s" % valid_client) logger.info("Valid token:\t%s\t(Required: %s" % (valid_resource_owner, require_resource_owner)) logger.info("Valid realm:\t%s\t(Required: %s)" % (valid_realm, require_realm)) logger.info("Valid callback:\t%s" % valid_redirect) logger.info("Valid verifier:\t%s\t(Required: %s)" % (valid_verifier, require_verifier)) logger.info("Valid signature:\t%s" % valid_signature) return v, request
def sign(self, uri, http_method='GET', body=None, headers=None, realm=None): """Sign a request Signs an HTTP request with the specified parts. Returns a 3-tuple of the signed request's URI, headers, and body. Note that http_method is not returned as it is unaffected by the OAuth signing process. The body argument may be a dict, a list of 2-tuples, or a formencoded string. The Content-Type header must be 'application/x-www-form-urlencoded' if it is present. If the body argument is not one of the above, it will be returned verbatim as it is unaffected by the OAuth signing process. Attempting to sign a request with non-formencoded data using the OAuth body signature type is invalid and will raise an exception. If the body does contain parameters, it will be returned as a properly- formatted formencoded string. All string data MUST be unicode. This includes strings inside body dicts, for example. """ # normalize request data request = Request(uri, http_method, body, headers, encoding=self.encoding) # sanity check content_type = request.headers.get('Content-Type', None) multipart = content_type and content_type.startswith('multipart/') should_have_params = content_type == CONTENT_TYPE_FORM_URLENCODED has_params = request.decoded_body is not None # 3.4.1.3.1. Parameter Sources # [Parameters are collected from the HTTP request entity-body, but only # if [...]: # * The entity-body is single-part. if multipart and has_params: raise ValueError("Headers indicate a multipart body but body contains parameters.") # * The entity-body follows the encoding requirements of the # "application/x-www-form-urlencoded" content-type as defined by # [W3C.REC-html40-19980424]. elif should_have_params and not has_params: raise ValueError("Headers indicate a formencoded body but body was not decodable.") # * The HTTP request entity-header includes the "Content-Type" # header field set to "application/x-www-form-urlencoded". elif not should_have_params and has_params: raise ValueError("Body contains parameters but Content-Type header was not set.") # 3.5.2. Form-Encoded Body # Protocol parameters can be transmitted in the HTTP request entity- # body, but only if the following REQUIRED conditions are met: # o The entity-body is single-part. # o The entity-body follows the encoding requirements of the # "application/x-www-form-urlencoded" content-type as defined by # [W3C.REC-html40-19980424]. # o The HTTP request entity-header includes the "Content-Type" header # field set to "application/x-www-form-urlencoded". elif self.signature_type == SIGNATURE_TYPE_BODY and not ( should_have_params and has_params and not multipart): raise ValueError('Body signatures may only be used with form-urlencoded content') # generate the basic OAuth parameters request.oauth_params = self.get_oauth_params() # generate the signature request.oauth_params.append(('oauth_signature', self.get_oauth_signature(request))) # render the signed request and return it return self._render(request, formencode=True, realm=(realm or self.realm))
def check_request_signature(self, uri, http_method=u'GET', body='', headers=None): """Check a request's supplied signature to make sure the request is valid. Servers should return HTTP status 400 if a ValueError exception is raised and HTTP status 401 on return value False. Per `section 3.2`_ of the spec. .. _`section 3.2`: http://tools.ietf.org/html/rfc5849#section-3.2 """ headers = headers or {} signature_type = None # FIXME: urlparse does not return unicode! uri_query = urlparse.urlparse(uri).query signature_type, params = self.get_signature_type_and_params(uri_query, headers, body) # the parameters may not include duplicate oauth entries filtered_params = utils.filter_oauth_params(params) if len(filtered_params) != len(params): raise ValueError("Duplicate OAuth entries.") params = dict(params) request_signature = params.get(u'oauth_signature') client_key = params.get(u'oauth_consumer_key') resource_owner_key = params.get(u'oauth_token') nonce = params.get(u'oauth_nonce') timestamp = params.get(u'oauth_timestamp') callback_uri = params.get(u'oauth_callback') verifier = params.get(u'oauth_verifier') signature_method = params.get(u'oauth_signature_method') # ensure all mandatory parameters are present if not all((request_signature, client_key, nonce, timestamp, signature_method)): raise ValueError("Missing OAuth parameters.") # if version is supplied, it must be "1.0" if u'oauth_version' in params and params[u'oauth_version'] != u'1.0': raise ValueError("Invalid OAuth version.") # signature method must be valid if not signature_method in SIGNATURE_METHODS: raise ValueError("Invalid signature method.") # ensure client key is valid if not self.check_client_key(client_key): return False # ensure resource owner key is valid and not expired if not self.check_resource_owner_key(client_key, resource_owner_key): return False # ensure the nonce and timestamp haven't been used before if not self.check_timestamp_and_nonce(timestamp, nonce): return False # FIXME: extract realm, then self.check_realm # oauth_client parameters depend on client chosen signature method # which may vary for each request, section 3.4 # HMAC-SHA1 and PLAINTEXT share parameters if signature_method == SIGNATURE_RSA: oauth_client = Client(client_key, resource_owner_key=resource_owner_key, callback_uri=callback_uri, signature_method=signature_method, signature_type=signature_type, rsa_key=self.rsa_key, verifier=verifier) else: client_secret = self.get_client_secret(client_key) resource_owner_secret = self.get_resource_owner_secret( resource_owner_key) oauth_client = Client(client_key, client_secret=client_secret, resource_owner_key=resource_owner_key, resource_owner_secret=resource_owner_secret, callback_uri=callback_uri, signature_method=signature_method, signature_type=signature_type, verifier=verifier) request = Request(uri, http_method, body, headers) # OAuth parameters transmitted in the query or entity-body are # already present in the parameter sources (section 3.4.1.3.1) # used to construct the signature base string. # FIXME: This should probably be handled by Request. if signature_type == SIGNATURE_TYPE_AUTH_HEADER: request.oauth_params = params.items() client_signature = oauth_client.get_oauth_signature(request) # FIXME: use near constant time string compare to avoid timing attacks return client_signature == request_signature
def sign(self, uri, http_method='GET', body=None, headers=None, realm=None): """Sign a request Signs an HTTP request with the specified parts. Returns a 3-tuple of the signed request's URI, headers, and body. Note that http_method is not returned as it is unaffected by the OAuth signing process. Also worth noting is that duplicate parameters will be included in the signature, regardless of where they are specified (query, body). The body argument may be a dict, a list of 2-tuples, or a formencoded string. The Content-Type header must be 'application/x-www-form-urlencoded' if it is present. If the body argument is not one of the above, it will be returned verbatim as it is unaffected by the OAuth signing process. Attempting to sign a request with non-formencoded data using the OAuth body signature type is invalid and will raise an exception. If the body does contain parameters, it will be returned as a properly- formatted formencoded string. Body may not be included if the http_method is either GET or HEAD as this changes the semantic meaning of the request. All string data MUST be unicode or be encoded with the same encoding scheme supplied to the Client constructor, default utf-8. This includes strings inside body dicts, for example. """ # normalize request data request = Request(uri, http_method, body, headers, encoding=self.encoding) # sanity check content_type = request.headers.get('Content-Type', None) multipart = content_type and content_type.startswith('multipart/') should_have_params = content_type == CONTENT_TYPE_FORM_URLENCODED has_params = request.decoded_body is not None # 3.4.1.3.1. Parameter Sources # [Parameters are collected from the HTTP request entity-body, but only # if [...]: # * The entity-body is single-part. if multipart and has_params: raise ValueError("Headers indicate a multipart body but body contains parameters.") # * The entity-body follows the encoding requirements of the # "application/x-www-form-urlencoded" content-type as defined by # [W3C.REC-html40-19980424]. elif should_have_params and not has_params: raise ValueError("Headers indicate a formencoded body but body was not decodable.") # * The HTTP request entity-header includes the "Content-Type" # header field set to "application/x-www-form-urlencoded". elif not should_have_params and has_params: raise ValueError("Body contains parameters but Content-Type header was not set.") # 3.5.2. Form-Encoded Body # Protocol parameters can be transmitted in the HTTP request entity- # body, but only if the following REQUIRED conditions are met: # o The entity-body is single-part. # o The entity-body follows the encoding requirements of the # "application/x-www-form-urlencoded" content-type as defined by # [W3C.REC-html40-19980424]. # o The HTTP request entity-header includes the "Content-Type" header # field set to "application/x-www-form-urlencoded". elif self.signature_type == SIGNATURE_TYPE_BODY and not ( should_have_params and has_params and not multipart): raise ValueError('Body signatures may only be used with form-urlencoded content') # We amend http://tools.ietf.org/html/rfc5849#section-3.4.1.3.1 # with the clause that parameters from body should only be included # in non GET or HEAD requests. Extracting the request body parameters # and including them in the signature base string would give semantic # meaning to the body, which it should not have according to the # HTTP 1.1 spec. elif http_method.upper() in ('GET', 'HEAD') and has_params: raise ValueError('GET/HEAD requests should not include body.') # generate the basic OAuth parameters request.oauth_params = self.get_oauth_params() # generate the signature request.oauth_params.append(('oauth_signature', self.get_oauth_signature(request))) # render the signed request and return it uri, headers, body = self._render(request, formencode=True, realm=(realm or self.realm)) if self.decoding: log.debug('Encoding URI, headers and body to %s.', self.decoding) uri = uri.encode(self.decoding) body = body.encode(self.decoding) if body else body new_headers = {} for k, v in headers.items(): new_headers[k.encode(self.decoding)] = v.encode(self.decoding) headers = new_headers return uri, headers, body