| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
1 parent cf6fc3c commit d563898
4 files changed
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -321,6 +321,30 @@ def _build_trust_boundary_lookup_url(self): | |||
| 321 | 321 | universe_domain=self._universe_domain, pool_id=pool_id | |
| 322 | 322 | ) | |
| 323 | 323 | ||
| 324 | + def revoke(self, request): | ||
| 325 | + """Revokes the refresh token. | ||
| 326 | + | ||
| 327 | + Args: | ||
| 328 | + request (google.auth.transport.Request): The object used to make | ||
| 329 | + HTTP requests. | ||
| 330 | + | ||
| 331 | + Raises: | ||
| 332 | + google.auth.exceptions.OAuthError: If the token could not be | ||
| 333 | + revoked. | ||
| 334 | + """ | ||
| 335 | + if not self._revoke_url or not self._refresh_token_val: | ||
| 336 | + raise exceptions.OAuthError( | ||
| 337 | + "The credentials do not contain the necessary fields to " | ||
| 338 | + "revoke the refresh token. You must specify revoke_url and " | ||
| 339 | + "refresh_token." | ||
| 340 | + ) | ||
| 341 | + | ||
| 342 | + self._sts_client.revoke_token( | ||
| 343 | + request, self._refresh_token_val, "refresh_token", self._revoke_url | ||
| 344 | + ) | ||
| 345 | + self.token = None | ||
| 346 | + self._refresh_token = None | ||
| 347 | + | ||
| 324 | 348 | @_helpers.copy_docstring(credentials.Credentials) | |
| 325 | 349 | def get_cred_info(self): | |
| 326 | 350 | if self._cred_file_path: | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -57,7 +57,7 @@ def __init__(self, token_exchange_endpoint, client_authentication=None): | |||
| 57 | 57 | super(Client, self).__init__(client_authentication) | |
| 58 | 58 | self._token_exchange_endpoint = token_exchange_endpoint | |
| 59 | 59 | ||
| 60 | - def _make_request(self, request, headers, request_body): | ||
| 60 | + def _make_request(self, request, headers, request_body, url=None): | ||
| 61 | 61 | # Initialize request headers. | |
| 62 | 62 | request_headers = _URLENCODED_HEADERS.copy() | |
| 63 | 63 | ||
@@ -69,9 +69,12 @@ def _make_request(self, request, headers, request_body): | |||
| 69 | 69 | # Apply OAuth client authentication. | |
| 70 | 70 | self.apply_client_authentication_options(request_headers, request_body) | |
| 71 | 71 | ||
| 72 | + # Use default token exchange endpoint if no url is provided. | ||
| 73 | + url = url or self._token_exchange_endpoint | ||
| 74 | + | ||
| 72 | 75 | # Execute request. | |
| 73 | 76 | response = request( | |
| 74 | - url=self._token_exchange_endpoint, | ||
| 77 | + url=url, | ||
| 75 | 78 | method="POST", | |
| 76 | 79 | headers=request_headers, | |
| 77 | 80 | body=urllib.parse.urlencode(request_body).encode("utf-8"), | |
@@ -87,10 +90,12 @@ def _make_request(self, request, headers, request_body): | |||
| 87 | 90 | if response.status != http_client.OK: | |
| 88 | 91 | utils.handle_error_response(response_body) | |
| 89 | 92 | ||
| 90 | - response_data = json.loads(response_body) | ||
| 93 | + # A successful token revocation returns an empty response body. | ||
| 94 | + if not response_body: | ||
| 95 | + return {} | ||
| 91 | 96 | ||
| 92 | - # Return successful response. | ||
| 93 | - return response_data | ||
| 97 | + # Other successful responses should be valid JSON. | ||
| 98 | + return json.loads(response_body) | ||
| 94 | 99 | ||
| 95 | 100 | def exchange_token( | |
| 96 | 101 | self, | |
@@ -174,3 +179,23 @@ def refresh_token(self, request, refresh_token): | |||
| 174 | 179 | None, | |
| 175 | 180 | {"grant_type": "refresh_token", "refresh_token": refresh_token}, | |
| 176 | 181 | ) | |
| 182 | + | ||
| 183 | + def revoke_token(self, request, token, token_type_hint, revoke_url): | ||
| 184 | + """Revokes the provided token based on the RFC7009 spec. | ||
| 185 | + | ||
| 186 | + Args: | ||
| 187 | + request (google.auth.transport.Request): A callable used to make | ||
| 188 | + HTTP requests. | ||
| 189 | + token (str): The OAuth 2.0 token to revoke. | ||
| 190 | + token_type_hint (str): Hint for the type of token being revoked. | ||
| 191 | + revoke_url (str): The STS endpoint URL for revoking tokens. | ||
| 192 | + | ||
| 193 | + Raises: | ||
| 194 | + google.auth.exceptions.OAuthError: If the token revocation endpoint | ||
| 195 | + returned an error. | ||
| 196 | + """ | ||
| 197 | + request_body = {"token": token} | ||
| 198 | + if token_type_hint: | ||
| 199 | + request_body["token_type_hint"] = token_type_hint | ||
| 200 | + | ||
| 201 | + return self._make_request(request, None, request_body, revoke_url) | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -41,6 +41,9 @@ class TestStsClient(object): | |||
| 41 | 41 | ACTOR_TOKEN = "HEADER.ACTOR_TOKEN_PAYLOAD.SIGNATURE" | |
| 42 | 42 | ACTOR_TOKEN_TYPE = "urn:ietf:params:oauth:token-type:jwt" | |
| 43 | 43 | TOKEN_EXCHANGE_ENDPOINT = "https://example.com/token.oauth2" | |
| 44 | + REVOKE_URL = "https://example.com/revoke.oauth2" | ||
| 45 | + TOKEN_TO_REVOKE = "TOKEN_TO_REVOKE" | ||
| 46 | + TOKEN_TYPE_HINT = "refresh_token" | ||
| 44 | 47 | ADDON_HEADERS = {"x-client-version": "0.1.2"} | |
| 45 | 48 | ADDON_OPTIONS = {"additional": {"non-standard": ["options"], "other": "some-value"}} | |
| 46 | 49 | SUCCESS_RESPONSE = { | |
@@ -72,21 +75,24 @@ def make_client(cls, client_auth=None): | |||
| 72 | 75 | return sts.Client(cls.TOKEN_EXCHANGE_ENDPOINT, client_auth) | |
| 73 | 76 | ||
| 74 | 77 | @classmethod | |
| 75 | - def make_mock_request(cls, data, status=http_client.OK): | ||
| 78 | + def make_mock_request(cls, data, status=http_client.OK, use_json=True): | ||
| 76 | 79 | response = mock.create_autospec(transport.Response, instance=True) | |
| 77 | 80 | response.status = status | |
| 78 | - response.data = json.dumps(data).encode("utf-8") | ||
| 81 | + if use_json: | ||
| 82 | + response.data = json.dumps(data).encode("utf-8") | ||
| 83 | + else: | ||
| 84 | + response.data = data.encode("utf-8") | ||
| 79 | 85 | ||
| 80 | 86 | request = mock.create_autospec(transport.Request) | |
| 81 | 87 | request.return_value = response | |
| 82 | 88 | ||
| 83 | 89 | return request | |
| 84 | 90 | ||
| 85 | 91 | @classmethod | |
| 86 | - def assert_request_kwargs(cls, request_kwargs, headers, request_data): | ||
| 87 | - """Asserts the request was called with the expected parameters. | ||
| 88 | - """ | ||
| 89 | - assert request_kwargs["url"] == cls.TOKEN_EXCHANGE_ENDPOINT | ||
| 92 | + def assert_request_kwargs(cls, request_kwargs, headers, request_data, url=None): | ||
| 93 | + """Asserts the request was called with the expected parameters.""" | ||
| 94 | + url = url or cls.TOKEN_EXCHANGE_ENDPOINT | ||
| 95 | + assert request_kwargs["url"] == url | ||
| 90 | 96 | assert request_kwargs["method"] == "POST" | |
| 91 | 97 | assert request_kwargs["headers"] == headers | |
| 92 | 98 | assert request_kwargs["body"] is not None | |
@@ -447,6 +453,63 @@ def test_refresh_token_failure(self): | |||
| 447 | 453 | r"Error code invalid_request: Invalid subject token - https://tools.ietf.org/html/rfc6749" | |
| 448 | 454 | ) | |
| 449 | 455 | ||
| 456 | + def test_revoke_token_success(self): | ||
| 457 | + """Test revoke token with successful response.""" | ||
| 458 | + client = self.make_client(self.CLIENT_AUTH_BASIC) | ||
| 459 | + request = self.make_mock_request(data="", status=http_client.OK, use_json=False) | ||
| 460 | + | ||
| 461 | + response = client.revoke_token( | ||
| 462 | + request, self.TOKEN_TO_REVOKE, self.TOKEN_TYPE_HINT, self.REVOKE_URL | ||
| 463 | + ) | ||
| 464 | + | ||
| 465 | + headers = { | ||
| 466 | + "Authorization": "Basic {}".format(BASIC_AUTH_ENCODING), | ||
| 467 | + "Content-Type": "application/x-www-form-urlencoded", | ||
| 468 | + } | ||
| 469 | + request_data = { | ||
| 470 | + "token": self.TOKEN_TO_REVOKE, | ||
| 471 | + "token_type_hint": self.TOKEN_TYPE_HINT, | ||
| 472 | + } | ||
| 473 | + self.assert_request_kwargs( | ||
| 474 | + request.call_args[1], headers, request_data, url=self.REVOKE_URL | ||
| 475 | + ) | ||
| 476 | + assert response == {} | ||
| 477 | + | ||
| 478 | + def test_revoke_token_success_no_hint(self): | ||
| 479 | + """Test revoke token with successful response.""" | ||
| 480 | + client = self.make_client(self.CLIENT_AUTH_BASIC) | ||
| 481 | + request = self.make_mock_request(data="", status=http_client.OK, use_json=False) | ||
| 482 | + | ||
| 483 | + response = client.revoke_token( | ||
| 484 | + request, self.TOKEN_TO_REVOKE, None, self.REVOKE_URL | ||
| 485 | + ) | ||
| 486 | + | ||
| 487 | + headers = { | ||
| 488 | + "Authorization": "Basic {}".format(BASIC_AUTH_ENCODING), | ||
| 489 | + "Content-Type": "application/x-www-form-urlencoded", | ||
| 490 | + } | ||
| 491 | + request_data = {"token": self.TOKEN_TO_REVOKE} | ||
| 492 | + self.assert_request_kwargs( | ||
| 493 | + request.call_args[1], headers, request_data, url=self.REVOKE_URL | ||
| 494 | + ) | ||
| 495 | + assert response == {} | ||
| 496 | + | ||
| 497 | + def test_revoke_token_failure(self): | ||
| 498 | + """Test revoke token with failure response.""" | ||
| 499 | + client = self.make_client(self.CLIENT_AUTH_BASIC) | ||
| 500 | + request = self.make_mock_request( | ||
| 501 | + status=http_client.BAD_REQUEST, data=self.ERROR_RESPONSE | ||
| 502 | + ) | ||
| 503 | + | ||
| 504 | + with pytest.raises(exceptions.OAuthError) as excinfo: | ||
| 505 | + client.revoke_token( | ||
| 506 | + request, self.TOKEN_TO_REVOKE, self.TOKEN_TYPE_HINT, self.REVOKE_URL | ||
| 507 | + ) | ||
| 508 | + | ||
| 509 | + assert excinfo.match( | ||
| 510 | + r"Error code invalid_request: Invalid subject token - https://tools.ietf.org/html/rfc6749" | ||
| 511 | + ) | ||
| 512 | + | ||
| 450 | 513 | def test__make_request_success(self): | |
| 451 | 514 | """Test base method with successful response.""" | |
| 452 | 515 | client = self.make_client(self.CLIENT_AUTH_BASIC) | |
@@ -478,3 +541,12 @@ def test_make_request_failure(self): | |||
| 478 | 541 | assert excinfo.match( | |
| 479 | 542 | r"Error code invalid_request: Invalid subject token - https://tools.ietf.org/html/rfc6749" | |
| 480 | 543 | ) | |
| 544 | + | ||
| 545 | + def test__make_request_empty_response(self): | ||
| 546 | + """Test _make_request with a successful but empty response body.""" | ||
| 547 | + client = self.make_client() | ||
| 548 | + request = self.make_mock_request(data="", status=http_client.OK, use_json=False) | ||
| 549 | + | ||
| 550 | + response = client._make_request(request, {}, {}) | ||
| 551 | + | ||
| 552 | + assert response == {} | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -349,6 +349,50 @@ def test_refresh_without_client_secret(self): | |||
| 349 | 349 | ||
| 350 | 350 | request.assert_not_called() | |
| 351 | 351 | ||
| 352 | + def test_revoke_auth_success(self): | ||
| 353 | + request = self.make_mock_request(status=http_client.OK, data={}) | ||
| 354 | + creds = self.make_credentials(revoke_url=REVOKE_URL) | ||
| 355 | + | ||
| 356 | + creds.revoke(request) | ||
| 357 | + | ||
| 358 | + request.assert_called_once_with( | ||
| 359 | + url=REVOKE_URL, | ||
| 360 | + method="POST", | ||
| 361 | + headers={ | ||
| 362 | + "Content-Type": "application/x-www-form-urlencoded", | ||
| 363 | + "Authorization": "Basic " + BASIC_AUTH_ENCODING, | ||
| 364 | + }, | ||
| 365 | + body=("token=" + REFRESH_TOKEN + "&token_type_hint=refresh_token").encode( | ||
| 366 | + "utf-8" | ||
| 367 | + ), | ||
| 368 | + ) | ||
| 369 | + assert creds.token is None | ||
| 370 | + assert creds._refresh_token is None | ||
| 371 | + | ||
| 372 | + def test_revoke_without_revoke_url(self): | ||
| 373 | + request = self.make_mock_request() | ||
| 374 | + creds = self.make_credentials(token=ACCESS_TOKEN) | ||
| 375 | + | ||
| 376 | + with pytest.raises(exceptions.OAuthError) as excinfo: | ||
| 377 | + creds.revoke(request) | ||
| 378 | + | ||
| 379 | + assert excinfo.match( | ||
| 380 | + r"The credentials do not contain the necessary fields to revoke the refresh token. You must specify revoke_url and refresh_token." | ||
| 381 | + ) | ||
| 382 | + | ||
| 383 | + def test_revoke_without_refresh_token(self): | ||
| 384 | + request = self.make_mock_request() | ||
| 385 | + creds = self.make_credentials( | ||
| 386 | + refresh_token=None, token=ACCESS_TOKEN, revoke_url=REVOKE_URL | ||
| 387 | + ) | ||
| 388 | + | ||
| 389 | + with pytest.raises(exceptions.OAuthError) as excinfo: | ||
| 390 | + creds.revoke(request) | ||
| 391 | + | ||
| 392 | + assert excinfo.match( | ||
| 393 | + r"The credentials do not contain the necessary fields to revoke the refresh token. You must specify revoke_url and refresh_token." | ||
| 394 | + ) | ||
| 395 | + | ||
| 352 | 396 | def test_info(self): | |
| 353 | 397 | creds = self.make_credentials() | |
| 354 | 398 | info = creds.info | |
| Back | FazBrowse Home | New Git URL |
0 commit comments