Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/oauthlib/oauth1/rfc5849/__init__.py: 25%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

123 statements  

1""" 

2oauthlib.oauth1.rfc5849 

3~~~~~~~~~~~~~~ 

4 

5This module is an implementation of various logic needed 

6for signing and checking OAuth 1.0 RFC 5849 requests. 

7 

8It supports all three standard signature methods defined in RFC 5849: 

9 

10- HMAC-SHA1 

11- RSA-SHA1 

12- PLAINTEXT 

13 

14It also supports signature methods that are not defined in RFC 5849. These are 

15based on the standard ones but replace SHA-1 with the more secure SHA-256: 

16 

17- HMAC-SHA256 

18- RSA-SHA256 

19 

20""" 

21import base64 

22import hashlib 

23import logging 

24import urllib.parse as urlparse 

25 

26from oauthlib.common import ( 

27 Request, generate_nonce, generate_timestamp, to_unicode, urlencode, 

28) 

29 

30from . import parameters, signature 

31 

32log = logging.getLogger(__name__) 

33 

34# Available signature methods 

35# 

36# Note: SIGNATURE_HMAC and SIGNATURE_RSA are kept for backward compatibility 

37# with previous versions of this library, when it the only HMAC-based and 

38# RSA-based signature methods were HMAC-SHA1 and RSA-SHA1. But now that it 

39# supports other hashing algorithms besides SHA1, explicitly identifying which 

40# hashing algorithm is being used is recommended. 

41# 

42# Note: if additional values are defined here, don't forget to update the 

43# imports in "../__init__.py" so they are available outside this module. 

44 

45SIGNATURE_HMAC_SHA1 = "HMAC-SHA1" 

46SIGNATURE_HMAC_SHA256 = "HMAC-SHA256" 

47SIGNATURE_HMAC_SHA512 = "HMAC-SHA512" 

48SIGNATURE_HMAC = SIGNATURE_HMAC_SHA1 # deprecated variable for HMAC-SHA1 

49 

50SIGNATURE_RSA_SHA1 = "RSA-SHA1" 

51SIGNATURE_RSA_SHA256 = "RSA-SHA256" 

52SIGNATURE_RSA_SHA512 = "RSA-SHA512" 

53SIGNATURE_RSA = SIGNATURE_RSA_SHA1 # deprecated variable for RSA-SHA1 

54 

55SIGNATURE_PLAINTEXT = "PLAINTEXT" 

56 

57SIGNATURE_METHODS = ( 

58 SIGNATURE_HMAC_SHA1, 

59 SIGNATURE_HMAC_SHA256, 

60 SIGNATURE_HMAC_SHA512, 

61 SIGNATURE_RSA_SHA1, 

62 SIGNATURE_RSA_SHA256, 

63 SIGNATURE_RSA_SHA512, 

64 SIGNATURE_PLAINTEXT 

65) 

66 

67SIGNATURE_TYPE_AUTH_HEADER = 'AUTH_HEADER' 

68SIGNATURE_TYPE_QUERY = 'QUERY' 

69SIGNATURE_TYPE_BODY = 'BODY' 

70 

71CONTENT_TYPE_FORM_URLENCODED = 'application/x-www-form-urlencoded' 

72 

73 

74class Client: 

75 

76 """A client used to sign OAuth 1.0 RFC 5849 requests.""" 

77 SIGNATURE_METHODS = { 

78 SIGNATURE_HMAC_SHA1: signature.sign_hmac_sha1_with_client, 

79 SIGNATURE_HMAC_SHA256: signature.sign_hmac_sha256_with_client, 

80 SIGNATURE_HMAC_SHA512: signature.sign_hmac_sha512_with_client, 

81 SIGNATURE_RSA_SHA1: signature.sign_rsa_sha1_with_client, 

82 SIGNATURE_RSA_SHA256: signature.sign_rsa_sha256_with_client, 

83 SIGNATURE_RSA_SHA512: signature.sign_rsa_sha512_with_client, 

84 SIGNATURE_PLAINTEXT: signature.sign_plaintext_with_client 

85 } 

86 

87 @classmethod 

88 def register_signature_method(cls, method_name, method_callback): 

89 cls.SIGNATURE_METHODS[method_name] = method_callback 

90 

91 def __init__(self, client_key, 

92 client_secret=None, 

93 resource_owner_key=None, 

94 resource_owner_secret=None, 

95 callback_uri=None, 

96 signature_method=SIGNATURE_HMAC_SHA1, 

97 signature_type=SIGNATURE_TYPE_AUTH_HEADER, 

98 rsa_key=None, verifier=None, realm=None, 

99 encoding='utf-8', decoding=None, 

100 nonce=None, timestamp=None): 

101 """Create an OAuth 1 client. 

102 

103 :param client_key: Client key (consumer key), mandatory. 

104 :param resource_owner_key: Resource owner key (oauth token). 

105 :param resource_owner_secret: Resource owner secret (oauth token secret). 

106 :param callback_uri: Callback used when obtaining request token. 

107 :param signature_method: SIGNATURE_HMAC, SIGNATURE_RSA or SIGNATURE_PLAINTEXT. 

108 :param signature_type: SIGNATURE_TYPE_AUTH_HEADER (default), 

109 SIGNATURE_TYPE_QUERY or SIGNATURE_TYPE_BODY 

110 depending on where you want to embed the oauth 

111 credentials. 

112 :param rsa_key: RSA key used with SIGNATURE_RSA. 

113 :param verifier: Verifier used when obtaining an access token. 

114 :param realm: Realm (scope) to which access is being requested. 

115 :param encoding: If you provide non-unicode input you may use this 

116 to have oauthlib automatically convert. 

117 :param decoding: If you wish that the returned uri, headers and body 

118 from sign be encoded back from unicode, then set 

119 decoding to your preferred encoding, i.e. utf-8. 

120 :param nonce: Use this nonce instead of generating one. (Mainly for testing) 

121 :param timestamp: Use this timestamp instead of using current. (Mainly for testing) 

122 """ 

123 # Convert to unicode using encoding if given, else assume unicode 

124 def encode(x): 

125 return to_unicode(x, encoding) if encoding else x 

126 

127 self.client_key = encode(client_key) 

128 self.client_secret = encode(client_secret) 

129 self.resource_owner_key = encode(resource_owner_key) 

130 self.resource_owner_secret = encode(resource_owner_secret) 

131 self.signature_method = encode(signature_method) 

132 self.signature_type = encode(signature_type) 

133 self.callback_uri = encode(callback_uri) 

134 self.rsa_key = encode(rsa_key) 

135 self.verifier = encode(verifier) 

136 self.realm = encode(realm) 

137 self.encoding = encode(encoding) 

138 self.decoding = encode(decoding) 

139 self.nonce = encode(nonce) 

140 self.timestamp = encode(timestamp) 

141 

142 def __repr__(self): 

143 attrs = vars(self).copy() 

144 attrs['client_secret'] = '****' if attrs['client_secret'] else None 

145 attrs['rsa_key'] = '****' if attrs['rsa_key'] else None 

146 attrs[ 

147 'resource_owner_secret'] = '****' if attrs['resource_owner_secret'] else None 

148 attribute_str = ', '.join('{}={}'.format(k, v) for k, v in attrs.items()) 

149 return '<{} {}>'.format(self.__class__.__name__, attribute_str) 

150 

151 def get_oauth_signature(self, request): 

152 """Get an OAuth signature to be used in signing a request 

153 

154 To satisfy `section 3.4.1.2`_ item 2, if the request argument's 

155 headers dict attribute contains a Host item, its value will 

156 replace any netloc part of the request argument's uri attribute 

157 value. 

158 

159 .. _`section 3.4.1.2`: https://tools.ietf.org/html/rfc5849#section-3.4.1.2 

160 """ 

161 if self.signature_method == SIGNATURE_PLAINTEXT: 

162 # fast-path 

163 return signature.sign_plaintext(self.client_secret, 

164 self.resource_owner_secret) 

165 

166 uri, headers, body = self._render(request) 

167 

168 collected_params = signature.collect_parameters( 

169 uri_query=urlparse.urlparse(uri).query, 

170 body=body, 

171 headers=headers) 

172 log.debug("Collected params: {}".format(collected_params)) 

173 

174 normalized_params = signature.normalize_parameters(collected_params) 

175 normalized_uri = signature.base_string_uri(uri, headers.get('Host', None)) 

176 log.debug("Normalized params: {}".format(normalized_params)) 

177 log.debug("Normalized URI: {}".format(normalized_uri)) 

178 

179 base_string = signature.signature_base_string(request.http_method, 

180 normalized_uri, normalized_params) 

181 

182 log.debug("Signing: signature base string: {}".format(base_string)) 

183 

184 if self.signature_method not in self.SIGNATURE_METHODS: 

185 raise ValueError('Invalid signature method.') 

186 

187 sig = self.SIGNATURE_METHODS[self.signature_method](base_string, self) 

188 

189 log.debug("Signature: {}".format(sig)) 

190 return sig 

191 

192 def get_oauth_params(self, request): 

193 """Get the basic OAuth parameters to be used in generating a signature. 

194 """ 

195 nonce = (generate_nonce() 

196 if self.nonce is None else self.nonce) 

197 timestamp = (generate_timestamp() 

198 if self.timestamp is None else self.timestamp) 

199 params = [ 

200 ('oauth_nonce', nonce), 

201 ('oauth_timestamp', timestamp), 

202 ('oauth_version', '1.0'), 

203 ('oauth_signature_method', self.signature_method), 

204 ('oauth_consumer_key', self.client_key), 

205 ] 

206 if self.resource_owner_key: 

207 params.append(('oauth_token', self.resource_owner_key)) 

208 if self.callback_uri: 

209 params.append(('oauth_callback', self.callback_uri)) 

210 if self.verifier: 

211 params.append(('oauth_verifier', self.verifier)) 

212 

213 # providing body hash for requests other than x-www-form-urlencoded 

214 # as described in https://tools.ietf.org/html/draft-eaton-oauth-bodyhash-00#section-4.1.1 

215 # 4.1.1. When to include the body hash 

216 # * [...] MUST NOT include an oauth_body_hash parameter on requests with form-encoded request bodies 

217 # * [...] SHOULD include the oauth_body_hash parameter on all other requests. 

218 # Note that SHA-1 is vulnerable. The spec acknowledges that in https://tools.ietf.org/html/draft-eaton-oauth-bodyhash-00#section-6.2 

219 # At this time, no further effort has been made to replace SHA-1 for the OAuth Request Body Hash extension. 

220 content_type = request.headers.get('Content-Type', None) 

221 content_type_eligible = content_type and content_type.find('application/x-www-form-urlencoded') < 0 

222 if request.body is not None and content_type_eligible: 

223 params.append(('oauth_body_hash', base64.b64encode(hashlib.sha1(request.body.encode('utf-8')).digest()).decode('utf-8'))) # noqa: S324 

224 

225 return params 

226 

227 def _render(self, request, formencode=False, realm=None): 

228 """Render a signed request according to signature type 

229 

230 Returns a 3-tuple containing the request URI, headers, and body. 

231 

232 If the formencode argument is True and the body contains parameters, it 

233 is escaped and returned as a valid formencoded string. 

234 """ 

235 # TODO what if there are body params on a header-type auth? 

236 # TODO what if there are query params on a body-type auth? 

237 

238 uri, headers, body = request.uri, request.headers, request.body 

239 

240 # TODO: right now these prepare_* methods are very narrow in scope--they 

241 # only affect their little thing. In some cases (for example, with 

242 # header auth) it might be advantageous to allow these methods to touch 

243 # other parts of the request, like the headers—so the prepare_headers 

244 # method could also set the Content-Type header to x-www-form-urlencoded 

245 # like the spec requires. This would be a fundamental change though, and 

246 # I'm not sure how I feel about it. 

247 if self.signature_type == SIGNATURE_TYPE_AUTH_HEADER: 

248 headers = parameters.prepare_headers( 

249 request.oauth_params, request.headers, realm=realm) 

250 elif self.signature_type == SIGNATURE_TYPE_BODY and request.decoded_body is not None: 

251 body = parameters.prepare_form_encoded_body( 

252 request.oauth_params, request.decoded_body) 

253 if formencode: 

254 body = urlencode(body) 

255 headers['Content-Type'] = 'application/x-www-form-urlencoded' 

256 elif self.signature_type == SIGNATURE_TYPE_QUERY: 

257 uri = parameters.prepare_request_uri_query( 

258 request.oauth_params, request.uri) 

259 else: 

260 raise ValueError('Unknown signature type specified.') 

261 

262 return uri, headers, body 

263 

264 def sign(self, uri, http_method='GET', body=None, headers=None, realm=None): 

265 """Sign a request 

266 

267 Signs an HTTP request with the specified parts. 

268 

269 Returns a 3-tuple of the signed request's URI, headers, and body. 

270 Note that http_method is not returned as it is unaffected by the OAuth 

271 signing process. Also worth noting is that duplicate parameters 

272 will be included in the signature, regardless of where they are 

273 specified (query, body). 

274 

275 The body argument may be a dict, a list of 2-tuples, or a formencoded 

276 string. The Content-Type header must be 'application/x-www-form-urlencoded' 

277 if it is present. 

278 

279 If the body argument is not one of the above, it will be returned 

280 verbatim as it is unaffected by the OAuth signing process. Attempting to 

281 sign a request with non-formencoded data using the OAuth body signature 

282 type is invalid and will raise an exception. 

283 

284 If the body does contain parameters, it will be returned as a properly- 

285 formatted formencoded string. 

286 

287 Body may not be included if the http_method is either GET or HEAD as 

288 this changes the semantic meaning of the request. 

289 

290 All string data MUST be unicode or be encoded with the same encoding 

291 scheme supplied to the Client constructor, default utf-8. This includes 

292 strings inside body dicts, for example. 

293 """ 

294 # normalize request data 

295 request = Request(uri, http_method, body, headers, 

296 encoding=self.encoding) 

297 

298 # sanity check 

299 content_type = request.headers.get('Content-Type', None) 

300 multipart = content_type and content_type.startswith('multipart/') 

301 should_have_params = content_type == CONTENT_TYPE_FORM_URLENCODED 

302 has_params = request.decoded_body is not None 

303 # 3.4.1.3.1. Parameter Sources 

304 # [Parameters are collected from the HTTP request entity-body, but only 

305 # if [...]: 

306 # * The entity-body is single-part. 

307 if multipart and has_params: 

308 raise ValueError( 

309 "Headers indicate a multipart body but body contains parameters.") 

310 # * The entity-body follows the encoding requirements of the 

311 # "application/x-www-form-urlencoded" content-type as defined by 

312 # [W3C.REC-html40-19980424]. 

313 elif should_have_params and not has_params: 

314 raise ValueError( 

315 "Headers indicate a formencoded body but body was not decodable.") 

316 # * The HTTP request entity-header includes the "Content-Type" 

317 # header field set to "application/x-www-form-urlencoded". 

318 elif not should_have_params and has_params: 

319 raise ValueError( 

320 "Body contains parameters but Content-Type header was {} " 

321 "instead of {}".format(content_type or "not set", 

322 CONTENT_TYPE_FORM_URLENCODED)) 

323 

324 # 3.5.2. Form-Encoded Body 

325 # Protocol parameters can be transmitted in the HTTP request entity- 

326 # body, but only if the following REQUIRED conditions are met: 

327 # o The entity-body is single-part. 

328 # o The entity-body follows the encoding requirements of the 

329 # "application/x-www-form-urlencoded" content-type as defined by 

330 # [W3C.REC-html40-19980424]. 

331 # o The HTTP request entity-header includes the "Content-Type" header 

332 # field set to "application/x-www-form-urlencoded". 

333 elif self.signature_type == SIGNATURE_TYPE_BODY and not ( 

334 should_have_params and has_params and not multipart): 

335 raise ValueError( 

336 'Body signatures may only be used with form-urlencoded content') 

337 

338 # We amend https://tools.ietf.org/html/rfc5849#section-3.4.1.3.1 

339 # with the clause that parameters from body should only be included 

340 # in non GET or HEAD requests. Extracting the request body parameters 

341 # and including them in the signature base string would give semantic 

342 # meaning to the body, which it should not have according to the 

343 # HTTP 1.1 spec. 

344 elif http_method.upper() in ('GET', 'HEAD') and has_params: 

345 raise ValueError('GET/HEAD requests should not include body.') 

346 

347 # generate the basic OAuth parameters 

348 request.oauth_params = self.get_oauth_params(request) 

349 

350 # generate the signature 

351 request.oauth_params.append( 

352 ('oauth_signature', self.get_oauth_signature(request))) 

353 

354 # render the signed request and return it 

355 uri, headers, body = self._render(request, formencode=True, 

356 realm=(realm or self.realm)) 

357 

358 if self.decoding: 

359 log.debug('Encoding URI, headers and body to %s.', self.decoding) 

360 uri = uri.encode(self.decoding) 

361 body = body.encode(self.decoding) if body else body 

362 new_headers = {} 

363 for k, v in headers.items(): 

364 new_headers[k.encode(self.decoding)] = v.encode(self.decoding) 

365 headers = new_headers 

366 return uri, headers, body