From e160201af60714d150ac44d3a06bc9e737af7253 Mon Sep 17 00:00:00 2001 From: Geert-Jan van den Bosch Date: Thu, 9 Oct 2025 10:02:38 +0200 Subject: [PATCH 1/4] Add revoke_oauth_token method to Client, so we can easily revoke tokens --- mollie/api/client.py | 23 +++++++++-- tests/responses/error_bad_request.json | 4 ++ tests/responses/revoke_token.json | 0 tests/test_api_client.py | 54 ++++++++++++++++++++++++++ 4 files changed, 78 insertions(+), 3 deletions(-) create mode 100644 tests/responses/error_bad_request.json create mode 100644 tests/responses/revoke_token.json diff --git a/mollie/api/client.py b/mollie/api/client.py index ebe9dc9d..a96ff00d 100644 --- a/mollie/api/client.py +++ b/mollie/api/client.py @@ -355,9 +355,26 @@ def setup_oauth_authorization_response(self, authorization_response: str) -> Non ) self.set_token(token) - # TODO Implement https://docs.mollie.com/reference/oauth2/revoke-token - # def revoke_oauth_token(self, token, type_hint): - # ... + def revoke_oauth_token(self, client_id: str, token: str, type_hint: str) -> requests.Response: + """ + :param client_id: The client ID (string) + :param token: The access token to revoke (string) + :param type_hint: The type of the token, either 'access_token' or 'refresh_token' (string) + Revoking a refresh token revokes all access tokens that were created using the same authorization. + """ + if not hasattr(self, "_oauth_client"): + raise RequestSetupError("You need to setup OAuth before you can revoke a token.") + + return self._oauth_client.request( + method="DELETE", + url=self.OAUTH_TOKEN_URL, + data={ + "token": token, + "token_type_hint": type_hint, + "client_id": client_id, + "client_secret": self.client_secret, + }, + ) def _setup_retry(self) -> None: """Configure a retry behaviour on the HTTP client.""" diff --git a/tests/responses/error_bad_request.json b/tests/responses/error_bad_request.json new file mode 100644 index 00000000..c85fc139 --- /dev/null +++ b/tests/responses/error_bad_request.json @@ -0,0 +1,4 @@ +{ + "error": "invalid_grant", + "error_description": "Authorization code doesn't exist or is invalid for the client" +} \ No newline at end of file diff --git a/tests/responses/revoke_token.json b/tests/responses/revoke_token.json new file mode 100644 index 00000000..e69de29b diff --git a/tests/test_api_client.py b/tests/test_api_client.py index 0a466cbf..c8d5fef0 100644 --- a/tests/test_api_client.py +++ b/tests/test_api_client.py @@ -1,6 +1,8 @@ +import json import re import time from datetime import datetime +from http import HTTPStatus import pytest import requests.adapters @@ -433,6 +435,58 @@ def test_unauthorized_oauth_client_should_return_authorization_url(mocker, respo ), "A client without initial token should return a correct authorization url" +def test_revoke_oauth_token_returns_request_setup_error_if_not_oauth_client(): + client = Client() + with pytest.raises(RequestSetupError) as excinfo: + client.revoke_oauth_token("client_id", "access_123", "access_token") + assert str(excinfo.value) == "You need to setup OAuth before you can revoke a token." + + +def test_revoke_oauth_token_succeeds(mocker, oauth_token, response): + client = Client() + client.setup_oauth( + client_id="client_id", + client_secret="client_secret", + redirect_uri="https://example.com/callback", + scope=("organizations.read",), + token=oauth_token, + set_token=mocker.Mock(), + ) + mocked_request = response.delete( + "https://api.mollie.com/oauth2/tokens", "revoke_token", status=HTTPStatus.NO_CONTENT + ) + result = client.revoke_oauth_token("client_id", oauth_token, "access_token") + + assert mocked_request.call_count == 1 + assert result.status_code == HTTPStatus.NO_CONTENT + + +def test_revoke_oauth_token_returns_error_response(mocker, oauth_token, response): + client = Client() + client.setup_oauth( + client_id="client_id", + client_secret="client_secret", + redirect_uri="https://example.com/callback", + scope=("organizations.read",), + token=oauth_token, + set_token=mocker.Mock(), + ) + + mocked_request = response.delete( + "https://api.mollie.com/oauth2/tokens", "error_bad_request", status=HTTPStatus.BAD_REQUEST + ) + result = client.revoke_oauth_token("client_id", oauth_token, "access_token") + + content = json.loads(result.content) + + assert mocked_request.call_count == 1 + assert result.status_code == HTTPStatus.BAD_REQUEST + assert content == { + "error": "invalid_grant", + "error_description": "Authorization code doesn't exist or is invalid for the client", + } + + def test_enable_testmode_globally_access_token(response): mocked_request = response.get( "https://api.mollie.com/v2/methods", "methods_list", match=[matchers.query_string_matcher("testmode=true")] From 21f974fdb3a6e9da30dccd59602d284aba9550b5 Mon Sep 17 00:00:00 2001 From: Geert-Jan van den Bosch Date: Thu, 9 Oct 2025 10:30:15 +0200 Subject: [PATCH 2/4] Ignore vulnerabilities used in dependencies when using python3.8 We're considering dropping python 3.8 support soon, which would allow us to stop ignoring the vulnerabilities again. --- .github/workflows/tests.yaml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index ee49fd12..5f4d1627 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -34,7 +34,10 @@ jobs: - name: Verify dependencies # Jinja, https://data.safetycli.com/v/70612/97c - run: python -m safety check --ignore 70612 + # urllib3, https://data.safetycli.com/v/77744/97c, only when using python 3.8. Consider upgrading. + # setuptools, https://data.safetycli.com/v/76752/97c, only when using python 3.8. Consider upgrading. + # regex, https://data.safetycli.com/v/78558/97c, only when using python 3.8. Consider upgrading. + run: python -m safety check --ignore 70612 --ignore 77744 --ignore 77745 --ignore 76752 --ignore 78558 - name: Verify code style run: python -m flake8 -v From 09d80984d4092173939adb3646d2eeaf5467906f Mon Sep 17 00:00:00 2001 From: Geert-Jan van den Bosch Date: Thu, 9 Oct 2025 10:42:09 +0200 Subject: [PATCH 3/4] Dont use global for client, it's not needed --- examples/oauth/oauth_app.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/examples/oauth/oauth_app.py b/examples/oauth/oauth_app.py index c42614a2..be890f66 100644 --- a/examples/oauth/oauth_app.py +++ b/examples/oauth/oauth_app.py @@ -78,8 +78,6 @@ def index(): "onboarding.write", ] - global client - authorized, authorization_url = client.setup_oauth( client_id, client_secret, @@ -99,8 +97,6 @@ def index(): @app.route("/callback") def callback(*args, **kwargs): - global client - url = request.url.replace("http", "https") # Fake https for the examples app only. DON'T DO THIS IN YOUR CODE! client.setup_oauth_authorization_response(url) body = "

Oauth client is setup

" From eeee16db222b54549de8a3013b9a8bf7882f4cc9 Mon Sep 17 00:00:00 2001 From: Geert-Jan van den Bosch Date: Thu, 9 Oct 2025 11:51:02 +0200 Subject: [PATCH 4/4] Dont use session scope for oauth_token fixture - it breaks tests due to urllib3 changes --- tests/conftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/conftest.py b/tests/conftest.py index 144f130d..1925d3a9 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -21,7 +21,7 @@ def client(): return client -@pytest.fixture(scope="session") +@pytest.fixture() def oauth_token(): """Return a valid oauth token for resuming an existing OAuth client.""" token = {