Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/oauthlib/oauth2/rfc6749/grant_types/authorization_code.py: 13%

157 statements  

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

1""" 

2oauthlib.oauth2.rfc6749.grant_types 

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

4""" 

5import base64 

6import hashlib 

7import json 

8import logging 

9 

10from oauthlib import common 

11 

12from .. import errors 

13from .base import GrantTypeBase 

14 

15log = logging.getLogger(__name__) 

16 

17 

18def code_challenge_method_s256(verifier, challenge): 

19 """ 

20 If the "code_challenge_method" from `Section 4.3`_ was "S256", the 

21 received "code_verifier" is hashed by SHA-256, base64url-encoded, and 

22 then compared to the "code_challenge", i.e.: 

23 

24 BASE64URL-ENCODE(SHA256(ASCII(code_verifier))) == code_challenge 

25 

26 How to implement a base64url-encoding 

27 function without padding, based upon the standard base64-encoding 

28 function that uses padding. 

29 

30 To be concrete, example C# code implementing these functions is shown 

31 below. Similar code could be used in other languages. 

32 

33 static string base64urlencode(byte [] arg) 

34 { 

35 string s = Convert.ToBase64String(arg); // Regular base64 encoder 

36 s = s.Split('=')[0]; // Remove any trailing '='s 

37 s = s.Replace('+', '-'); // 62nd char of encoding 

38 s = s.Replace('/', '_'); // 63rd char of encoding 

39 return s; 

40 } 

41 

42 In python urlsafe_b64encode is already replacing '+' and '/', but preserve 

43 the trailing '='. So we have to remove it. 

44 

45 .. _`Section 4.3`: https://tools.ietf.org/html/rfc7636#section-4.3 

46 """ 

47 return base64.urlsafe_b64encode( 

48 hashlib.sha256(verifier.encode()).digest() 

49 ).decode().rstrip('=') == challenge 

50 

51 

52def code_challenge_method_plain(verifier, challenge): 

53 """ 

54 If the "code_challenge_method" from `Section 4.3`_ was "plain", they are 

55 compared directly, i.e.: 

56 

57 code_verifier == code_challenge. 

58 

59 .. _`Section 4.3`: https://tools.ietf.org/html/rfc7636#section-4.3 

60 """ 

61 return verifier == challenge 

62 

63 

64class AuthorizationCodeGrant(GrantTypeBase): 

65 

66 """`Authorization Code Grant`_ 

67 

68 The authorization code grant type is used to obtain both access 

69 tokens and refresh tokens and is optimized for confidential clients. 

70 Since this is a redirection-based flow, the client must be capable of 

71 interacting with the resource owner's user-agent (typically a web 

72 browser) and capable of receiving incoming requests (via redirection) 

73 from the authorization server:: 

74 

75 +----------+ 

76 | Resource | 

77 | Owner | 

78 | | 

79 +----------+ 

80 ^ 

81 | 

82 (B) 

83 +----|-----+ Client Identifier +---------------+ 

84 | -+----(A)-- & Redirection URI ---->| | 

85 | User- | | Authorization | 

86 | Agent -+----(B)-- User authenticates --->| Server | 

87 | | | | 

88 | -+----(C)-- Authorization Code ---<| | 

89 +-|----|---+ +---------------+ 

90 | | ^ v 

91 (A) (C) | | 

92 | | | | 

93 ^ v | | 

94 +---------+ | | 

95 | |>---(D)-- Authorization Code ---------' | 

96 | Client | & Redirection URI | 

97 | | | 

98 | |<---(E)----- Access Token -------------------' 

99 +---------+ (w/ Optional Refresh Token) 

100 

101 Note: The lines illustrating steps (A), (B), and (C) are broken into 

102 two parts as they pass through the user-agent. 

103 

104 Figure 3: Authorization Code Flow 

105 

106 The flow illustrated in Figure 3 includes the following steps: 

107 

108 (A) The client initiates the flow by directing the resource owner's 

109 user-agent to the authorization endpoint. The client includes 

110 its client identifier, requested scope, local state, and a 

111 redirection URI to which the authorization server will send the 

112 user-agent back once access is granted (or denied). 

113 

114 (B) The authorization server authenticates the resource owner (via 

115 the user-agent) and establishes whether the resource owner 

116 grants or denies the client's access request. 

117 

118 (C) Assuming the resource owner grants access, the authorization 

119 server redirects the user-agent back to the client using the 

120 redirection URI provided earlier (in the request or during 

121 client registration). The redirection URI includes an 

122 authorization code and any local state provided by the client 

123 earlier. 

124 

125 (D) The client requests an access token from the authorization 

126 server's token endpoint by including the authorization code 

127 received in the previous step. When making the request, the 

128 client authenticates with the authorization server. The client 

129 includes the redirection URI used to obtain the authorization 

130 code for verification. 

131 

132 (E) The authorization server authenticates the client, validates the 

133 authorization code, and ensures that the redirection URI 

134 received matches the URI used to redirect the client in 

135 step (C). If valid, the authorization server responds back with 

136 an access token and, optionally, a refresh token. 

137 

138 OAuth 2.0 public clients utilizing the Authorization Code Grant are 

139 susceptible to the authorization code interception attack. 

140 

141 A technique to mitigate against the threat through the use of Proof Key for Code 

142 Exchange (PKCE, pronounced "pixy") is implemented in the current oauthlib 

143 implementation. 

144 

145 .. _`Authorization Code Grant`: https://tools.ietf.org/html/rfc6749#section-4.1 

146 .. _`PKCE`: https://tools.ietf.org/html/rfc7636 

147 """ 

148 

149 default_response_mode = 'query' 

150 response_types = ['code'] 

151 

152 # This dict below is private because as RFC mention it: 

153 # "S256" is Mandatory To Implement (MTI) on the server. 

154 # 

155 _code_challenge_methods = { 

156 'plain': code_challenge_method_plain, 

157 'S256': code_challenge_method_s256 

158 } 

159 

160 def create_authorization_code(self, request): 

161 """ 

162 Generates an authorization grant represented as a dictionary. 

163 

164 :param request: OAuthlib request. 

165 :type request: oauthlib.common.Request 

166 """ 

167 grant = {'code': common.generate_token()} 

168 if hasattr(request, 'state') and request.state: 

169 grant['state'] = request.state 

170 log.debug('Created authorization code grant %r for request %r.', 

171 grant, request) 

172 return grant 

173 

174 def create_authorization_response(self, request, token_handler): 

175 """ 

176 The client constructs the request URI by adding the following 

177 parameters to the query component of the authorization endpoint URI 

178 using the "application/x-www-form-urlencoded" format, per `Appendix B`_: 

179 

180 response_type 

181 REQUIRED. Value MUST be set to "code" for standard OAuth2 

182 authorization flow. For OpenID Connect it must be one of 

183 "code token", "code id_token", or "code token id_token" - we 

184 essentially test that "code" appears in the response_type. 

185 client_id 

186 REQUIRED. The client identifier as described in `Section 2.2`_. 

187 redirect_uri 

188 OPTIONAL. As described in `Section 3.1.2`_. 

189 scope 

190 OPTIONAL. The scope of the access request as described by 

191 `Section 3.3`_. 

192 state 

193 RECOMMENDED. An opaque value used by the client to maintain 

194 state between the request and callback. The authorization 

195 server includes this value when redirecting the user-agent back 

196 to the client. The parameter SHOULD be used for preventing 

197 cross-site request forgery as described in `Section 10.12`_. 

198 

199 The client directs the resource owner to the constructed URI using an 

200 HTTP redirection response, or by other means available to it via the 

201 user-agent. 

202 

203 :param request: OAuthlib request. 

204 :type request: oauthlib.common.Request 

205 :param token_handler: A token handler instance, for example of type 

206 oauthlib.oauth2.BearerToken. 

207 :returns: headers, body, status 

208 :raises: FatalClientError on invalid redirect URI or client id. 

209 

210 A few examples:: 

211 

212 >>> from your_validator import your_validator 

213 >>> request = Request('https://example.com/authorize?client_id=valid' 

214 ... '&redirect_uri=http%3A%2F%2Fclient.com%2F') 

215 >>> from oauthlib.common import Request 

216 >>> from oauthlib.oauth2 import AuthorizationCodeGrant, BearerToken 

217 >>> token = BearerToken(your_validator) 

218 >>> grant = AuthorizationCodeGrant(your_validator) 

219 >>> request.scopes = ['authorized', 'in', 'some', 'form'] 

220 >>> grant.create_authorization_response(request, token) 

221 (u'http://client.com/?error=invalid_request&error_description=Missing+response_type+parameter.', None, None, 400) 

222 >>> request = Request('https://example.com/authorize?client_id=valid' 

223 ... '&redirect_uri=http%3A%2F%2Fclient.com%2F' 

224 ... '&response_type=code') 

225 >>> request.scopes = ['authorized', 'in', 'some', 'form'] 

226 >>> grant.create_authorization_response(request, token) 

227 (u'http://client.com/?code=u3F05aEObJuP2k7DordviIgW5wl52N', None, None, 200) 

228 >>> # If the client id or redirect uri fails validation 

229 >>> grant.create_authorization_response(request, token) 

230 Traceback (most recent call last): 

231 File "<stdin>", line 1, in <module> 

232 File "oauthlib/oauth2/rfc6749/grant_types.py", line 515, in create_authorization_response 

233 >>> grant.create_authorization_response(request, token) 

234 File "oauthlib/oauth2/rfc6749/grant_types.py", line 591, in validate_authorization_request 

235 oauthlib.oauth2.rfc6749.errors.InvalidClientIdError 

236 

237 .. _`Appendix B`: https://tools.ietf.org/html/rfc6749#appendix-B 

238 .. _`Section 2.2`: https://tools.ietf.org/html/rfc6749#section-2.2 

239 .. _`Section 3.1.2`: https://tools.ietf.org/html/rfc6749#section-3.1.2 

240 .. _`Section 3.3`: https://tools.ietf.org/html/rfc6749#section-3.3 

241 .. _`Section 10.12`: https://tools.ietf.org/html/rfc6749#section-10.12 

242 """ 

243 try: 

244 self.validate_authorization_request(request) 

245 log.debug('Pre resource owner authorization validation ok for %r.', 

246 request) 

247 

248 # If the request fails due to a missing, invalid, or mismatching 

249 # redirection URI, or if the client identifier is missing or invalid, 

250 # the authorization server SHOULD inform the resource owner of the 

251 # error and MUST NOT automatically redirect the user-agent to the 

252 # invalid redirection URI. 

253 except errors.FatalClientError as e: 

254 log.debug('Fatal client error during validation of %r. %r.', 

255 request, e) 

256 raise 

257 

258 # If the resource owner denies the access request or if the request 

259 # fails for reasons other than a missing or invalid redirection URI, 

260 # the authorization server informs the client by adding the following 

261 # parameters to the query component of the redirection URI using the 

262 # "application/x-www-form-urlencoded" format, per Appendix B: 

263 # https://tools.ietf.org/html/rfc6749#appendix-B 

264 except errors.OAuth2Error as e: 

265 log.debug('Client error during validation of %r. %r.', request, e) 

266 request.redirect_uri = request.redirect_uri or self.error_uri 

267 redirect_uri = common.add_params_to_uri( 

268 request.redirect_uri, e.twotuples, 

269 fragment=request.response_mode == "fragment") 

270 return {'Location': redirect_uri}, None, 302 

271 

272 grant = self.create_authorization_code(request) 

273 for modifier in self._code_modifiers: 

274 grant = modifier(grant, token_handler, request) 

275 if 'access_token' in grant: 

276 self.request_validator.save_token(grant, request) 

277 log.debug('Saving grant %r for %r.', grant, request) 

278 self.request_validator.save_authorization_code( 

279 request.client_id, grant, request) 

280 return self.prepare_authorization_response( 

281 request, grant, {}, None, 302) 

282 

283 def create_token_response(self, request, token_handler): 

284 """Validate the authorization code. 

285 

286 The client MUST NOT use the authorization code more than once. If an 

287 authorization code is used more than once, the authorization server 

288 MUST deny the request and SHOULD revoke (when possible) all tokens 

289 previously issued based on that authorization code. The authorization 

290 code is bound to the client identifier and redirection URI. 

291 

292 :param request: OAuthlib request. 

293 :type request: oauthlib.common.Request 

294 :param token_handler: A token handler instance, for example of type 

295 oauthlib.oauth2.BearerToken. 

296 

297 """ 

298 headers = self._get_default_headers() 

299 try: 

300 self.validate_token_request(request) 

301 log.debug('Token request validation ok for %r.', request) 

302 except errors.OAuth2Error as e: 

303 log.debug('Client error during validation of %r. %r.', request, e) 

304 headers.update(e.headers) 

305 return headers, e.json, e.status_code 

306 

307 token = token_handler.create_token(request, refresh_token=self.refresh_token) 

308 

309 for modifier in self._token_modifiers: 

310 token = modifier(token, token_handler, request) 

311 

312 self.request_validator.save_token(token, request) 

313 self.request_validator.invalidate_authorization_code( 

314 request.client_id, request.code, request) 

315 headers.update(self._create_cors_headers(request)) 

316 return headers, json.dumps(token), 200 

317 

318 def validate_authorization_request(self, request): 

319 """Check the authorization request for normal and fatal errors. 

320 

321 A normal error could be a missing response_type parameter or the client 

322 attempting to access scope it is not allowed to ask authorization for. 

323 Normal errors can safely be included in the redirection URI and 

324 sent back to the client. 

325 

326 Fatal errors occur when the client_id or redirect_uri is invalid or 

327 missing. These must be caught by the provider and handled, how this 

328 is done is outside of the scope of OAuthLib but showing an error 

329 page describing the issue is a good idea. 

330 

331 :param request: OAuthlib request. 

332 :type request: oauthlib.common.Request 

333 """ 

334 

335 # First check for fatal errors 

336 

337 # If the request fails due to a missing, invalid, or mismatching 

338 # redirection URI, or if the client identifier is missing or invalid, 

339 # the authorization server SHOULD inform the resource owner of the 

340 # error and MUST NOT automatically redirect the user-agent to the 

341 # invalid redirection URI. 

342 

343 # First check duplicate parameters 

344 for param in ('client_id', 'response_type', 'redirect_uri', 'scope', 'state'): 

345 try: 

346 duplicate_params = request.duplicate_params 

347 except ValueError: 

348 raise errors.InvalidRequestFatalError(description='Unable to parse query string', request=request) 

349 if param in duplicate_params: 

350 raise errors.InvalidRequestFatalError(description='Duplicate %s parameter.' % param, request=request) 

351 

352 # REQUIRED. The client identifier as described in Section 2.2. 

353 # https://tools.ietf.org/html/rfc6749#section-2.2 

354 if not request.client_id: 

355 raise errors.MissingClientIdError(request=request) 

356 

357 if not self.request_validator.validate_client_id(request.client_id, request): 

358 raise errors.InvalidClientIdError(request=request) 

359 

360 # OPTIONAL. As described in Section 3.1.2. 

361 # https://tools.ietf.org/html/rfc6749#section-3.1.2 

362 log.debug('Validating redirection uri %s for client %s.', 

363 request.redirect_uri, request.client_id) 

364 

365 # OPTIONAL. As described in Section 3.1.2. 

366 # https://tools.ietf.org/html/rfc6749#section-3.1.2 

367 self._handle_redirects(request) 

368 

369 # Then check for normal errors. 

370 

371 # If the resource owner denies the access request or if the request 

372 # fails for reasons other than a missing or invalid redirection URI, 

373 # the authorization server informs the client by adding the following 

374 # parameters to the query component of the redirection URI using the 

375 # "application/x-www-form-urlencoded" format, per Appendix B. 

376 # https://tools.ietf.org/html/rfc6749#appendix-B 

377 

378 # Note that the correct parameters to be added are automatically 

379 # populated through the use of specific exceptions. 

380 

381 request_info = {} 

382 for validator in self.custom_validators.pre_auth: 

383 request_info.update(validator(request)) 

384 

385 # REQUIRED. 

386 if request.response_type is None: 

387 raise errors.MissingResponseTypeError(request=request) 

388 # Value MUST be set to "code" or one of the OpenID authorization code including 

389 # response_types "code token", "code id_token", "code token id_token" 

390 elif not 'code' in request.response_type and request.response_type != 'none': 

391 raise errors.UnsupportedResponseTypeError(request=request) 

392 

393 if not self.request_validator.validate_response_type(request.client_id, 

394 request.response_type, 

395 request.client, request): 

396 

397 log.debug('Client %s is not authorized to use response_type %s.', 

398 request.client_id, request.response_type) 

399 raise errors.UnauthorizedClientError(request=request) 

400 

401 # OPTIONAL. Validate PKCE request or reply with "error"/"invalid_request" 

402 # https://tools.ietf.org/html/rfc6749#section-4.4.1 

403 if self.request_validator.is_pkce_required(request.client_id, request) is True: 

404 if request.code_challenge is None: 

405 raise errors.MissingCodeChallengeError(request=request) 

406 

407 if request.code_challenge is not None: 

408 request_info["code_challenge"] = request.code_challenge 

409 

410 # OPTIONAL, defaults to "plain" if not present in the request. 

411 if request.code_challenge_method is None: 

412 request.code_challenge_method = "plain" 

413 

414 if request.code_challenge_method not in self._code_challenge_methods: 

415 raise errors.UnsupportedCodeChallengeMethodError(request=request) 

416 request_info["code_challenge_method"] = request.code_challenge_method 

417 

418 # OPTIONAL. The scope of the access request as described by Section 3.3 

419 # https://tools.ietf.org/html/rfc6749#section-3.3 

420 self.validate_scopes(request) 

421 

422 request_info.update({ 

423 'client_id': request.client_id, 

424 'redirect_uri': request.redirect_uri, 

425 'response_type': request.response_type, 

426 'state': request.state, 

427 'request': request 

428 }) 

429 

430 for validator in self.custom_validators.post_auth: 

431 request_info.update(validator(request)) 

432 

433 return request.scopes, request_info 

434 

435 def validate_token_request(self, request): 

436 """ 

437 :param request: OAuthlib request. 

438 :type request: oauthlib.common.Request 

439 """ 

440 # REQUIRED. Value MUST be set to "authorization_code". 

441 if request.grant_type not in ('authorization_code', 'openid'): 

442 raise errors.UnsupportedGrantTypeError(request=request) 

443 

444 for validator in self.custom_validators.pre_token: 

445 validator(request) 

446 

447 if request.code is None: 

448 raise errors.InvalidRequestError( 

449 description='Missing code parameter.', request=request) 

450 

451 for param in ('client_id', 'grant_type', 'redirect_uri'): 

452 if param in request.duplicate_params: 

453 raise errors.InvalidRequestError(description='Duplicate %s parameter.' % param, 

454 request=request) 

455 

456 if self.request_validator.client_authentication_required(request): 

457 # If the client type is confidential or the client was issued client 

458 # credentials (or assigned other authentication requirements), the 

459 # client MUST authenticate with the authorization server as described 

460 # in Section 3.2.1. 

461 # https://tools.ietf.org/html/rfc6749#section-3.2.1 

462 if not self.request_validator.authenticate_client(request): 

463 log.debug('Client authentication failed, %r.', request) 

464 raise errors.InvalidClientError(request=request) 

465 elif not self.request_validator.authenticate_client_id(request.client_id, request): 

466 # REQUIRED, if the client is not authenticating with the 

467 # authorization server as described in Section 3.2.1. 

468 # https://tools.ietf.org/html/rfc6749#section-3.2.1 

469 log.debug('Client authentication failed, %r.', request) 

470 raise errors.InvalidClientError(request=request) 

471 

472 if not hasattr(request.client, 'client_id'): 

473 raise NotImplementedError('Authenticate client must set the ' 

474 'request.client.client_id attribute ' 

475 'in authenticate_client.') 

476 

477 request.client_id = request.client_id or request.client.client_id 

478 

479 # Ensure client is authorized use of this grant type 

480 self.validate_grant_type(request) 

481 

482 # REQUIRED. The authorization code received from the 

483 # authorization server. 

484 if not self.request_validator.validate_code(request.client_id, 

485 request.code, request.client, request): 

486 log.debug('Client, %r (%r), is not allowed access to scopes %r.', 

487 request.client_id, request.client, request.scopes) 

488 raise errors.InvalidGrantError(request=request) 

489 

490 # OPTIONAL. Validate PKCE code_verifier 

491 challenge = self.request_validator.get_code_challenge(request.code, request) 

492 

493 if challenge is not None: 

494 if request.code_verifier is None: 

495 raise errors.MissingCodeVerifierError(request=request) 

496 

497 challenge_method = self.request_validator.get_code_challenge_method(request.code, request) 

498 if challenge_method is None: 

499 raise errors.InvalidGrantError(request=request, description="Challenge method not found") 

500 

501 if challenge_method not in self._code_challenge_methods: 

502 raise errors.ServerError( 

503 description="code_challenge_method {} is not supported.".format(challenge_method), 

504 request=request 

505 ) 

506 

507 if not self.validate_code_challenge(challenge, 

508 challenge_method, 

509 request.code_verifier): 

510 log.debug('request provided a invalid code_verifier.') 

511 raise errors.InvalidGrantError(request=request) 

512 elif self.request_validator.is_pkce_required(request.client_id, request) is True: 

513 if request.code_verifier is None: 

514 raise errors.MissingCodeVerifierError(request=request) 

515 raise errors.InvalidGrantError(request=request, description="Challenge not found") 

516 

517 for attr in ('user', 'scopes'): 

518 if getattr(request, attr, None) is None: 

519 log.debug('request.%s was not set on code validation.', attr) 

520 

521 # REQUIRED, if the "redirect_uri" parameter was included in the 

522 # authorization request as described in Section 4.1.1, and their 

523 # values MUST be identical. 

524 if request.redirect_uri is None: 

525 request.using_default_redirect_uri = True 

526 request.redirect_uri = self.request_validator.get_default_redirect_uri( 

527 request.client_id, request) 

528 log.debug('Using default redirect_uri %s.', request.redirect_uri) 

529 if not request.redirect_uri: 

530 raise errors.MissingRedirectURIError(request=request) 

531 else: 

532 request.using_default_redirect_uri = False 

533 log.debug('Using provided redirect_uri %s', request.redirect_uri) 

534 

535 if not self.request_validator.confirm_redirect_uri(request.client_id, request.code, 

536 request.redirect_uri, request.client, 

537 request): 

538 log.debug('Redirect_uri (%r) invalid for client %r (%r).', 

539 request.redirect_uri, request.client_id, request.client) 

540 raise errors.MismatchingRedirectURIError(request=request) 

541 

542 for validator in self.custom_validators.post_token: 

543 validator(request) 

544 

545 def validate_code_challenge(self, challenge, challenge_method, verifier): 

546 if challenge_method in self._code_challenge_methods: 

547 return self._code_challenge_methods[challenge_method](verifier, challenge) 

548 raise NotImplementedError('Unknown challenge_method %s' % challenge_method)