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

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

157 statements  

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 'code' not 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 and request.code_challenge is None: 

404 raise errors.MissingCodeChallengeError(request=request) 

405 

406 if request.code_challenge is not None: 

407 request_info["code_challenge"] = request.code_challenge 

408 

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

410 if request.code_challenge_method is None: 

411 request.code_challenge_method = "plain" 

412 

413 if request.code_challenge_method not in self._code_challenge_methods: 

414 raise errors.UnsupportedCodeChallengeMethodError(request=request) 

415 request_info["code_challenge_method"] = request.code_challenge_method 

416 

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

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

419 self.validate_scopes(request) 

420 

421 request_info.update({ 

422 'client_id': request.client_id, 

423 'redirect_uri': request.redirect_uri, 

424 'response_type': request.response_type, 

425 'state': request.state, 

426 'request': request 

427 }) 

428 

429 for validator in self.custom_validators.post_auth: 

430 request_info.update(validator(request)) 

431 

432 return request.scopes, request_info 

433 

434 def validate_token_request(self, request): 

435 """ 

436 :param request: OAuthlib request. 

437 :type request: oauthlib.common.Request 

438 """ 

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

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

441 raise errors.UnsupportedGrantTypeError(request=request) 

442 

443 for validator in self.custom_validators.pre_token: 

444 validator(request) 

445 

446 if request.code is None: 

447 raise errors.InvalidRequestError( 

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

449 

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

451 if param in request.duplicate_params: 

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

453 request=request) 

454 

455 if self.request_validator.client_authentication_required(request): 

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

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

458 # client MUST authenticate with the authorization server as described 

459 # in Section 3.2.1. 

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

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

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

463 raise errors.InvalidClientError(request=request) 

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

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

466 # authorization server as described in Section 3.2.1. 

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

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

469 raise errors.InvalidClientError(request=request) 

470 

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

472 raise NotImplementedError('Authenticate client must set the ' 

473 'request.client.client_id attribute ' 

474 'in authenticate_client.') 

475 

476 request.client_id = request.client_id or request.client.client_id 

477 

478 # Ensure client is authorized use of this grant type 

479 self.validate_grant_type(request) 

480 

481 # REQUIRED. The authorization code received from the 

482 # authorization server. 

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

484 request.code, request.client, request): 

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

486 request.client_id, request.client, request.scopes) 

487 raise errors.InvalidGrantError(request=request) 

488 

489 # OPTIONAL. Validate PKCE code_verifier 

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

491 

492 if challenge is not None: 

493 if request.code_verifier is None: 

494 raise errors.MissingCodeVerifierError(request=request) 

495 

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

497 if challenge_method is None: 

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

499 

500 if challenge_method not in self._code_challenge_methods: 

501 raise errors.ServerError( 

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

503 request=request 

504 ) 

505 

506 if not self.validate_code_challenge(challenge, 

507 challenge_method, 

508 request.code_verifier): 

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

510 raise errors.InvalidGrantError(request=request) 

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

512 if request.code_verifier is None: 

513 raise errors.MissingCodeVerifierError(request=request) 

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

515 

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

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

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

519 

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

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

522 # values MUST be identical. 

523 if request.redirect_uri is None: 

524 request.using_default_redirect_uri = True 

525 request.redirect_uri = self.request_validator.get_default_redirect_uri( 

526 request.client_id, request) 

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

528 if not request.redirect_uri: 

529 raise errors.MissingRedirectURIError(request=request) 

530 else: 

531 request.using_default_redirect_uri = False 

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

533 

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

535 request.redirect_uri, request.client, 

536 request): 

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

538 request.redirect_uri, request.client_id, request.client) 

539 raise errors.MismatchingRedirectURIError(request=request) 

540 

541 for validator in self.custom_validators.post_token: 

542 validator(request) 

543 

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

545 if challenge_method in self._code_challenge_methods: 

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

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