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

122 statements  

« prev     ^ index     » next       coverage.py v7.3.2, created at 2023-12-08 06:22 +0000

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 encode = lambda x: to_unicode(x, encoding) if encoding else x 

125 

126 self.client_key = encode(client_key) 

127 self.client_secret = encode(client_secret) 

128 self.resource_owner_key = encode(resource_owner_key) 

129 self.resource_owner_secret = encode(resource_owner_secret) 

130 self.signature_method = encode(signature_method) 

131 self.signature_type = encode(signature_type) 

132 self.callback_uri = encode(callback_uri) 

133 self.rsa_key = encode(rsa_key) 

134 self.verifier = encode(verifier) 

135 self.realm = encode(realm) 

136 self.encoding = encode(encoding) 

137 self.decoding = encode(decoding) 

138 self.nonce = encode(nonce) 

139 self.timestamp = encode(timestamp) 

140 

141 def __repr__(self): 

142 attrs = vars(self).copy() 

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

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

145 attrs[ 

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

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

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

149 

150 def get_oauth_signature(self, request): 

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

152 

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

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

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

156 value. 

157 

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

159 """ 

160 if self.signature_method == SIGNATURE_PLAINTEXT: 

161 # fast-path 

162 return signature.sign_plaintext(self.client_secret, 

163 self.resource_owner_secret) 

164 

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

166 

167 collected_params = signature.collect_parameters( 

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

169 body=body, 

170 headers=headers) 

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

172 

173 normalized_params = signature.normalize_parameters(collected_params) 

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

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

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

177 

178 base_string = signature.signature_base_string(request.http_method, 

179 normalized_uri, normalized_params) 

180 

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

182 

183 if self.signature_method not in self.SIGNATURE_METHODS: 

184 raise ValueError('Invalid signature method.') 

185 

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

187 

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

189 return sig 

190 

191 def get_oauth_params(self, request): 

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

193 """ 

194 nonce = (generate_nonce() 

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

196 timestamp = (generate_timestamp() 

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

198 params = [ 

199 ('oauth_nonce', nonce), 

200 ('oauth_timestamp', timestamp), 

201 ('oauth_version', '1.0'), 

202 ('oauth_signature_method', self.signature_method), 

203 ('oauth_consumer_key', self.client_key), 

204 ] 

205 if self.resource_owner_key: 

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

207 if self.callback_uri: 

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

209 if self.verifier: 

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

211 

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

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

214 # 4.1.1. When to include the body hash 

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

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

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

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

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

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

221 if request.body is not None and content_type_eligible: 

222 params.append(('oauth_body_hash', base64.b64encode(hashlib.sha1(request.body.encode('utf-8')).digest()).decode('utf-8'))) 

223 

224 return params 

225 

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

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

228 

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

230 

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

232 is escaped and returned as a valid formencoded string. 

233 """ 

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

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

236 

237 uri, headers, body = request.uri, request.headers, request.body 

238 

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

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

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

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

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

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

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

246 if self.signature_type == SIGNATURE_TYPE_AUTH_HEADER: 

247 headers = parameters.prepare_headers( 

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

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

250 body = parameters.prepare_form_encoded_body( 

251 request.oauth_params, request.decoded_body) 

252 if formencode: 

253 body = urlencode(body) 

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

255 elif self.signature_type == SIGNATURE_TYPE_QUERY: 

256 uri = parameters.prepare_request_uri_query( 

257 request.oauth_params, request.uri) 

258 else: 

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

260 

261 return uri, headers, body 

262 

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

264 """Sign a request 

265 

266 Signs an HTTP request with the specified parts. 

267 

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

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

270 signing process. Also worth noting is that duplicate parameters 

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

272 specified (query, body). 

273 

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

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

276 if it is present. 

277 

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

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

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

281 type is invalid and will raise an exception. 

282 

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

284 formatted formencoded string. 

285 

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

287 this changes the semantic meaning of the request. 

288 

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

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

291 strings inside body dicts, for example. 

292 """ 

293 # normalize request data 

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

295 encoding=self.encoding) 

296 

297 # sanity check 

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

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

300 should_have_params = content_type == CONTENT_TYPE_FORM_URLENCODED 

301 has_params = request.decoded_body is not None 

302 # 3.4.1.3.1. Parameter Sources 

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

304 # if [...]: 

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

306 if multipart and has_params: 

307 raise ValueError( 

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

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

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

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

312 elif should_have_params and not has_params: 

313 raise ValueError( 

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

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

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

317 elif not should_have_params and has_params: 

318 raise ValueError( 

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

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

321 CONTENT_TYPE_FORM_URLENCODED)) 

322 

323 # 3.5.2. Form-Encoded Body 

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

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

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

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

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

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

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

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

332 elif self.signature_type == SIGNATURE_TYPE_BODY and not ( 

333 should_have_params and has_params and not multipart): 

334 raise ValueError( 

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

336 

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

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

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

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

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

342 # HTTP 1.1 spec. 

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

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

345 

346 # generate the basic OAuth parameters 

347 request.oauth_params = self.get_oauth_params(request) 

348 

349 # generate the signature 

350 request.oauth_params.append( 

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

352 

353 # render the signed request and return it 

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

355 realm=(realm or self.realm)) 

356 

357 if self.decoding: 

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

359 uri = uri.encode(self.decoding) 

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

361 new_headers = {} 

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

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

364 headers = new_headers 

365 return uri, headers, body