From 5b328d479c17e44a6dde96408d6c8680b6c11ee0 Mon Sep 17 00:00:00 2001 From: halcy Date: Sun, 13 Nov 2022 21:57:12 +0200 Subject: s/tootsuite/mastodon/ --- docs/index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/index.rst b/docs/index.rst index e555912..1e90bc2 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1407,6 +1407,6 @@ Mastodon.py contains work by a large amount of contributors, many of which have put significant work into making it a better library. You can find some information about who helped with which particular feature or fix in the changelog. -.. _Mastodon: https://github.com/tootsuite/mastodon +.. _Mastodon: https://github.com/mastodon/mastodon .. _Mastodon flagship instance: http://mastodon.social/ .. _Official Mastodon api docs: https://docs.joinmastodon.org/api/rest -- cgit v1.2.3 From 24c686f6b274e04082b13f159ea10d995c2ca281 Mon Sep 17 00:00:00 2001 From: halcy Date: Sun, 13 Nov 2022 22:32:04 +0200 Subject: Improve auth support --- mastodon/Mastodon.py | 31 ++++- tests/cassettes/test_revoke.yaml | 253 +++++++++++++++++++++++++++++++++++++++ tests/test_auth.py | 27 ++++- 3 files changed, 304 insertions(+), 7 deletions(-) create mode 100644 tests/cassettes/test_revoke.yaml diff --git a/mastodon/Mastodon.py b/mastodon/Mastodon.py index e84df6d..48d850b 100644 --- a/mastodon/Mastodon.py +++ b/mastodon/Mastodon.py @@ -486,8 +486,7 @@ class Mastodon: """ return Mastodon.__SUPPORTED_MASTODON_VERSION - def auth_request_url(self, client_id=None, redirect_uris="urn:ietf:wg:oauth:2.0:oob", - scopes=__DEFAULT_SCOPES, force_login=False): + def auth_request_url(self, client_id=None, redirect_uris="urn:ietf:wg:oauth:2.0:oob", scopes=__DEFAULT_SCOPES, force_login=False, state=None): """ Returns the url that a client needs to request an oauth grant from the server. @@ -501,6 +500,10 @@ class Mastodon: Pass force_login if you want the user to always log in even when already logged into web mastodon (i.e. when registering multiple different accounts in an app). + + State is the oauth `state`parameter to pass to the server. It is strongly suggested + to use a random, nonguessable value (i.e. nothing meaningful and no incrementing ID) + to preserve security guarantees. It can be left out for non-web login flows. """ if client_id is None: client_id = self.client_id @@ -515,12 +518,11 @@ class Mastodon: params['redirect_uri'] = redirect_uris params['scope'] = " ".join(scopes) params['force_login'] = force_login + params['state'] = state formatted_params = urlencode(params) return "".join([self.api_base_url, "/oauth/authorize?", formatted_params]) - def log_in(self, username=None, password=None, - code=None, redirect_uri="urn:ietf:wg:oauth:2.0:oob", refresh_token=None, - scopes=__DEFAULT_SCOPES, to_file=None): + def log_in(self, username=None, password=None, code=None, redirect_uri="urn:ietf:wg:oauth:2.0:oob", refresh_token=None, scopes=__DEFAULT_SCOPES, to_file=None): """ Get the access token for a user. @@ -588,6 +590,25 @@ class Mastodon: return response['access_token'] + def revoke_access_token(self): + """ + Revoke the oauth token the user is currently authenticated with, effectively removing + the apps access and requiring the user to log in again. + """ + if self.access_token is None: + raise MastodonIllegalArgumentError("Not logged in, do not have a token to revoke.") + if self.client_id is None or self.client_secret is None: + raise MastodonIllegalArgumentError("Client authentication (id + secret) is required to revoke tokens.") + params = collections.OrderedDict([]) + params['client_id'] = self.client_id + params['client_secret'] = self.client_secret + params['token'] = self.access_token + self.__api_request('POST', '/oauth/revoke', params) + + # We are now logged out, clear token and logged in id + self.access_token = None + self.__logged_in_id = None + @api_version("2.7.0", "2.7.0", "2.7.0") def create_account(self, username, password, email, agreement=False, reason=None, locale="en", scopes=__DEFAULT_SCOPES, to_file=None): """ diff --git a/tests/cassettes/test_revoke.yaml b/tests/cassettes/test_revoke.yaml new file mode 100644 index 0000000..1bbc487 --- /dev/null +++ b/tests/cassettes/test_revoke.yaml @@ -0,0 +1,253 @@ +interactions: +- request: + body: username=mastodonpy_test_2%40localhost%3A3000&password=5fc638e0e53eafd9c4145b6bb852667d&redirect_uri=urn%3Aietf%3Awg%3Aoauth%3A2.0%3Aoob&grant_type=password&client_id=__MASTODON_PY_TEST_CLIENT_ID&client_secret=__MASTODON_PY_TEST_CLIENT_SECRET&scope=read+write+follow+push + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '271' + Content-Type: + - application/x-www-form-urlencoded + User-Agent: + - tests/v311 + method: POST + uri: http://localhost:3000/oauth/token + response: + body: + string: '{"access_token":"s3ZSxpaa2Uhe9EcHankvkfaQZQGiWpdEWIhX7GuhDlk","token_type":"Bearer","scope":"read + write follow push","created_at":1668370881}' + headers: + Cache-Control: + - no-store + Content-Security-Policy: + - 'base-uri ''none''; default-src ''none''; frame-ancestors ''none''; font-src + ''self'' http://localhost:3000; img-src ''self'' https: data: blob: http://localhost:3000; + style-src ''self'' http://localhost:3000 ''nonce-pCi2AQ9aKYXwS29cp2OHAg==''; + media-src ''self'' https: data: http://localhost:3000; frame-src ''self'' + https:; manifest-src ''self'' http://localhost:3000; connect-src ''self'' + data: blob: http://localhost:3000 http://localhost:3000 ws://localhost:4000 + ws://localhost:3035 http://localhost:3035; script-src ''self'' ''unsafe-inline'' + ''unsafe-eval'' http://localhost:3000; child-src ''self'' blob: http://localhost:3000; + worker-src ''self'' blob: http://localhost:3000' + Content-Type: + - application/json; charset=utf-8 + ETag: + - W/"b4c5259bc2edbe94aab5df0f3b8ba79a" + Pragma: + - no-cache + Referrer-Policy: + - strict-origin-when-cross-origin + Transfer-Encoding: + - chunked + Vary: + - Accept, Origin + X-Content-Type-Options: + - nosniff + X-Download-Options: + - noopen + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Request-Id: + - 086350f4-00fc-4d82-a5ce-2b557df59682 + X-Runtime: + - '0.040539' + X-XSS-Protection: + - 1; mode=block + status: + code: 200 + message: OK +- request: + body: client_id=__MASTODON_PY_TEST_CLIENT_ID&client_secret=__MASTODON_PY_TEST_CLIENT_SECRET&token=s3ZSxpaa2Uhe9EcHankvkfaQZQGiWpdEWIhX7GuhDlk + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Authorization: + - Bearer s3ZSxpaa2Uhe9EcHankvkfaQZQGiWpdEWIhX7GuhDlk + Connection: + - keep-alive + Content-Length: + - '135' + Content-Type: + - application/x-www-form-urlencoded + User-Agent: + - tests/v311 + method: POST + uri: http://localhost:3000/oauth/revoke + response: + body: + string: '{}' + headers: + Cache-Control: + - max-age=0, private, must-revalidate + Content-Security-Policy: + - 'base-uri ''none''; default-src ''none''; frame-ancestors ''none''; font-src + ''self'' http://localhost:3000; img-src ''self'' https: data: blob: http://localhost:3000; + style-src ''self'' http://localhost:3000 ''nonce-PqM4ChK427oGZ5jIKprkYQ==''; + media-src ''self'' https: data: http://localhost:3000; frame-src ''self'' + https:; manifest-src ''self'' http://localhost:3000; connect-src ''self'' + data: blob: http://localhost:3000 http://localhost:3000 ws://localhost:4000 + ws://localhost:3035 http://localhost:3035; script-src ''self'' ''unsafe-inline'' + ''unsafe-eval'' http://localhost:3000; child-src ''self'' blob: http://localhost:3000; + worker-src ''self'' blob: http://localhost:3000' + Content-Type: + - application/json; charset=utf-8 + ETag: + - W/"44136fa355b3678a1146ad16f7e8649e" + Referrer-Policy: + - strict-origin-when-cross-origin + Transfer-Encoding: + - chunked + Vary: + - Accept + X-Content-Type-Options: + - nosniff + X-Download-Options: + - noopen + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Request-Id: + - 36ec7e63-b15b-487a-aa4a-c396db945794 + X-Runtime: + - '0.010431' + X-XSS-Protection: + - 1; mode=block + status: + code: 200 + message: OK +- request: + body: status=illegal+access+detected + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '30' + Content-Type: + - application/x-www-form-urlencoded + User-Agent: + - tests/v311 + method: POST + uri: http://localhost:3000/api/v1/statuses + response: + body: + string: '{"error":"The access token is invalid"}' + headers: + Cache-Control: + - no-store + Content-Security-Policy: + - 'base-uri ''none''; default-src ''none''; frame-ancestors ''none''; font-src + ''self'' http://localhost:3000; img-src ''self'' https: data: blob: http://localhost:3000; + style-src ''self'' http://localhost:3000 ''nonce-UClOh6+Y0zf3a4O/ysqT/w==''; + media-src ''self'' https: data: http://localhost:3000; frame-src ''self'' + https:; manifest-src ''self'' http://localhost:3000; connect-src ''self'' + data: blob: http://localhost:3000 http://localhost:3000 ws://localhost:4000 + ws://localhost:3035 http://localhost:3035; script-src ''self'' ''unsafe-inline'' + ''unsafe-eval'' http://localhost:3000; child-src ''self'' blob: http://localhost:3000; + worker-src ''self'' blob: http://localhost:3000' + Content-Type: + - application/json; charset=utf-8 + Pragma: + - no-cache + Referrer-Policy: + - strict-origin-when-cross-origin + Transfer-Encoding: + - chunked + Vary: + - Accept, Origin + WWW-Authenticate: + - Bearer realm="Doorkeeper", error="invalid_token", error_description="The access + token is invalid" + X-Content-Type-Options: + - nosniff + X-Download-Options: + - noopen + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Request-Id: + - ab1f9d04-149b-431a-b2b0-79921d45f3bc + X-Runtime: + - '0.005292' + X-XSS-Protection: + - 1; mode=block + status: + code: 401 + message: Unauthorized +- request: + body: status=illegal+access+detected + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '30' + Content-Type: + - application/x-www-form-urlencoded + User-Agent: + - tests/v311 + method: POST + uri: http://localhost:3000/api/v1/statuses + response: + body: + string: '{"error":"The access token is invalid"}' + headers: + Cache-Control: + - no-store + Content-Security-Policy: + - 'base-uri ''none''; default-src ''none''; frame-ancestors ''none''; font-src + ''self'' http://localhost:3000; img-src ''self'' https: data: blob: http://localhost:3000; + style-src ''self'' http://localhost:3000 ''nonce-ZTmQUUs9q7lX74Aa3bTgzA==''; + media-src ''self'' https: data: http://localhost:3000; frame-src ''self'' + https:; manifest-src ''self'' http://localhost:3000; connect-src ''self'' + data: blob: http://localhost:3000 http://localhost:3000 ws://localhost:4000 + ws://localhost:3035 http://localhost:3035; script-src ''self'' ''unsafe-inline'' + ''unsafe-eval'' http://localhost:3000; child-src ''self'' blob: http://localhost:3000; + worker-src ''self'' blob: http://localhost:3000' + Content-Type: + - application/json; charset=utf-8 + Pragma: + - no-cache + Referrer-Policy: + - strict-origin-when-cross-origin + Transfer-Encoding: + - chunked + Vary: + - Accept, Origin + WWW-Authenticate: + - Bearer realm="Doorkeeper", error="invalid_token", error_description="The access + token is invalid" + X-Content-Type-Options: + - nosniff + X-Download-Options: + - noopen + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Request-Id: + - 9a7ad262-0d94-4df6-92a5-a670d6515979 + X-Runtime: + - '0.004613' + X-XSS-Protection: + - 1; mode=block + status: + code: 401 + message: Unauthorized +version: 1 diff --git a/tests/test_auth.py b/tests/test_auth.py index c3acb66..a14b4e7 100644 --- a/tests/test_auth.py +++ b/tests/test_auth.py @@ -22,14 +22,37 @@ def test_log_in_none(api_anonymous): with pytest.raises(MastodonIllegalArgumentError): api_anonymous.log_in() - @pytest.mark.vcr() def test_log_in_password(api_anonymous): token = api_anonymous.log_in( username='mastodonpy_test_2@localhost:3000', - password='5fc638e0e53eafd9c4145b6bb852667d') + password='5fc638e0e53eafd9c4145b6bb852667d' + ) assert token +@pytest.mark.vcr() +def test_revoke(api_anonymous): + token = api_anonymous.log_in( + username='mastodonpy_test_2@localhost:3000', + password='5fc638e0e53eafd9c4145b6bb852667d' + ) + api_anonymous.revoke_access_token() + + try: + api_anonymous.toot("illegal access detected") + assert False + except Exception as e: + print(e) + pass + + api_revoked_token = Mastodon(access_token = token) + try: + api_anonymous.toot("illegal access detected") + assert False + except Exception as e: + print(e) + pass + @pytest.mark.vcr() def test_log_in_password_incorrect(api_anonymous): with pytest.raises(MastodonIllegalArgumentError): -- cgit v1.2.3 From 7b9f07fc0c89eb7c5b3b0924201035239cdc138a Mon Sep 17 00:00:00 2001 From: halcy Date: Sun, 13 Nov 2022 22:50:15 +0200 Subject: update changelog --- CHANGELOG.rst | 50 ++++++++++++++++++++++++++++---------------------- 1 file changed, 28 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 1ce779a..c027b91 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -5,38 +5,44 @@ version number. Breaking changes will be indicated by a change in the minor v1.5.3 (in progress) -------------------- * 3.1.3 support - * Add v2 media_post api + * Added v2 media_post api * 3.1.4 support - * Add "remote", "local" and "only_media" parameter for timelines more broadly - * Document updates to instance information api return value + * Added "remote", "local" and "only_media" parameter for timelines more broadly + * Documented updates to instance information api return value * 3.2.0 support - * Add account notes API - * Add thumbnail support to media_post / media_update - * Document new keys in media API + * Added account notes API + * Added thumbnail support to media_post / media_update + * Documented new keys in media API * 3.3.0 support - * Add "notify" parameter for following. - * Add support for timed mutes - * Add support for getting an accounts features tags via account_featured_tags + * Added "notify" parameter for following. + * Added support for timed mutes + * Added support for getting an accounts features tags via account_featured_tags +* Miscelaneous additions + * Added support for paginating by date via converting dates to snowflake IDs (on Mastodon only - thanks to edent for the suggestion) + * Added a method to revoke oauth tokens (thanks fluffy-critter) * Fixes * Various small and big fixes, improving reliablity and test coverage - + * Changed URLs from "tootsuite" to "mastodon" in several places (thanks andypiper) + * Fixed some fields not converting to datetimes (thanks SouthFox-D) + * Improved oauth web flow support + v1.5.2 ------ * BREAKING CHANGE (but to a representation that was intended to be internal): Greatly improve how pagination info is stored (arittner) -* Add "unknown event" handler for streaming (arittner) -* Add support for exclude_types in notifications api (MicroCheapFx) -* Add pagination to bookmarks (arittner) -* Make connecting for streaming more resilient (arittner) -* Allow specifying a user agent header (arittner) -* Add support for tagged and exclude_reblogs on account_statuses api (arittner) -* Add support for reports without attached statuses (arittner) +* Added "unknown event" handler for streaming (arittner) +* Added support for exclude_types in notifications api (MicroCheapFx) +* Added pagination to bookmarks (arittner) +* Made connecting for streaming more resilient (arittner) +* Allowed specifying a user agent header (arittner) +* Addeded support for tagged and exclude_reblogs on account_statuses api (arittner) +* Added support for reports without attached statuses (arittner) * General fixes - * Fix a typo in __json_fruefalse_parse (zen-tools) + * Fixed a typo in __json_fruefalse_parse (zen-tools) * Some non-mastodon related fixes - * Fix a typo in error message for content_type (rinpatch - * Add support for specifying file name when uploading (animeavi) - * Fix several crashes related to gotosocials version string (fwaggle) - * Fix an issue related to hometowns version string + * Fixed a typo in error message for content_type (rinpatch + * Added support for specifying file name when uploading (animeavi) + * Fixed several crashes related to gotosocials version string (fwaggle) + * Fixed an issue related to hometowns version string v1.5.1 ------ -- cgit v1.2.3