diff options
-rw-r--r-- | mastodon/Mastodon.py | 31 | ||||
-rw-r--r-- | tests/cassettes/test_revoke.yaml | 253 | ||||
-rw-r--r-- | tests/test_auth.py | 27 |
3 files changed, 304 insertions, 7 deletions
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: | |||
486 | """ | 486 | """ |
487 | return Mastodon.__SUPPORTED_MASTODON_VERSION | 487 | return Mastodon.__SUPPORTED_MASTODON_VERSION |
488 | 488 | ||
489 | def auth_request_url(self, client_id=None, redirect_uris="urn:ietf:wg:oauth:2.0:oob", | 489 | 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): |
490 | scopes=__DEFAULT_SCOPES, force_login=False): | ||
491 | """ | 490 | """ |
492 | Returns the url that a client needs to request an oauth grant from the server. | 491 | Returns the url that a client needs to request an oauth grant from the server. |
493 | 492 | ||
@@ -501,6 +500,10 @@ class Mastodon: | |||
501 | 500 | ||
502 | Pass force_login if you want the user to always log in even when already logged | 501 | Pass force_login if you want the user to always log in even when already logged |
503 | into web mastodon (i.e. when registering multiple different accounts in an app). | 502 | into web mastodon (i.e. when registering multiple different accounts in an app). |
503 | |||
504 | State is the oauth `state`parameter to pass to the server. It is strongly suggested | ||
505 | to use a random, nonguessable value (i.e. nothing meaningful and no incrementing ID) | ||
506 | to preserve security guarantees. It can be left out for non-web login flows. | ||
504 | """ | 507 | """ |
505 | if client_id is None: | 508 | if client_id is None: |
506 | client_id = self.client_id | 509 | client_id = self.client_id |
@@ -515,12 +518,11 @@ class Mastodon: | |||
515 | params['redirect_uri'] = redirect_uris | 518 | params['redirect_uri'] = redirect_uris |
516 | params['scope'] = " ".join(scopes) | 519 | params['scope'] = " ".join(scopes) |
517 | params['force_login'] = force_login | 520 | params['force_login'] = force_login |
521 | params['state'] = state | ||
518 | formatted_params = urlencode(params) | 522 | formatted_params = urlencode(params) |
519 | return "".join([self.api_base_url, "/oauth/authorize?", formatted_params]) | 523 | return "".join([self.api_base_url, "/oauth/authorize?", formatted_params]) |
520 | 524 | ||
521 | def log_in(self, username=None, password=None, | 525 | 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): |
522 | code=None, redirect_uri="urn:ietf:wg:oauth:2.0:oob", refresh_token=None, | ||
523 | scopes=__DEFAULT_SCOPES, to_file=None): | ||
524 | """ | 526 | """ |
525 | Get the access token for a user. | 527 | Get the access token for a user. |
526 | 528 | ||
@@ -588,6 +590,25 @@ class Mastodon: | |||
588 | 590 | ||
589 | return response['access_token'] | 591 | return response['access_token'] |
590 | 592 | ||
593 | def revoke_access_token(self): | ||
594 | """ | ||
595 | Revoke the oauth token the user is currently authenticated with, effectively removing | ||
596 | the apps access and requiring the user to log in again. | ||
597 | """ | ||
598 | if self.access_token is None: | ||
599 | raise MastodonIllegalArgumentError("Not logged in, do not have a token to revoke.") | ||
600 | if self.client_id is None or self.client_secret is None: | ||
601 | raise MastodonIllegalArgumentError("Client authentication (id + secret) is required to revoke tokens.") | ||
602 | params = collections.OrderedDict([]) | ||
603 | params['client_id'] = self.client_id | ||
604 | params['client_secret'] = self.client_secret | ||
605 | params['token'] = self.access_token | ||
606 | self.__api_request('POST', '/oauth/revoke', params) | ||
607 | |||
608 | # We are now logged out, clear token and logged in id | ||
609 | self.access_token = None | ||
610 | self.__logged_in_id = None | ||
611 | |||
591 | @api_version("2.7.0", "2.7.0", "2.7.0") | 612 | @api_version("2.7.0", "2.7.0", "2.7.0") |
592 | def create_account(self, username, password, email, agreement=False, reason=None, locale="en", scopes=__DEFAULT_SCOPES, to_file=None): | 613 | def create_account(self, username, password, email, agreement=False, reason=None, locale="en", scopes=__DEFAULT_SCOPES, to_file=None): |
593 | """ | 614 | """ |
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 @@ | |||
1 | interactions: | ||
2 | - request: | ||
3 | 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 | ||
4 | headers: | ||
5 | Accept: | ||
6 | - '*/*' | ||
7 | Accept-Encoding: | ||
8 | - gzip, deflate | ||
9 | Connection: | ||
10 | - keep-alive | ||
11 | Content-Length: | ||
12 | - '271' | ||
13 | Content-Type: | ||
14 | - application/x-www-form-urlencoded | ||
15 | User-Agent: | ||
16 | - tests/v311 | ||
17 | method: POST | ||
18 | uri: http://localhost:3000/oauth/token | ||
19 | response: | ||
20 | body: | ||
21 | string: '{"access_token":"s3ZSxpaa2Uhe9EcHankvkfaQZQGiWpdEWIhX7GuhDlk","token_type":"Bearer","scope":"read | ||
22 | write follow push","created_at":1668370881}' | ||
23 | headers: | ||
24 | Cache-Control: | ||
25 | - no-store | ||
26 | Content-Security-Policy: | ||
27 | - 'base-uri ''none''; default-src ''none''; frame-ancestors ''none''; font-src | ||
28 | ''self'' http://localhost:3000; img-src ''self'' https: data: blob: http://localhost:3000; | ||
29 | style-src ''self'' http://localhost:3000 ''nonce-pCi2AQ9aKYXwS29cp2OHAg==''; | ||
30 | media-src ''self'' https: data: http://localhost:3000; frame-src ''self'' | ||
31 | https:; manifest-src ''self'' http://localhost:3000; connect-src ''self'' | ||
32 | data: blob: http://localhost:3000 http://localhost:3000 ws://localhost:4000 | ||
33 | ws://localhost:3035 http://localhost:3035; script-src ''self'' ''unsafe-inline'' | ||
34 | ''unsafe-eval'' http://localhost:3000; child-src ''self'' blob: http://localhost:3000; | ||
35 | worker-src ''self'' blob: http://localhost:3000' | ||
36 | Content-Type: | ||
37 | - application/json; charset=utf-8 | ||
38 | ETag: | ||
39 | - W/"b4c5259bc2edbe94aab5df0f3b8ba79a" | ||
40 | Pragma: | ||
41 | - no-cache | ||
42 | Referrer-Policy: | ||
43 | - strict-origin-when-cross-origin | ||
44 | Transfer-Encoding: | ||
45 | - chunked | ||
46 | Vary: | ||
47 | - Accept, Origin | ||
48 | X-Content-Type-Options: | ||
49 | - nosniff | ||
50 | X-Download-Options: | ||
51 | - noopen | ||
52 | X-Frame-Options: | ||
53 | - SAMEORIGIN | ||
54 | X-Permitted-Cross-Domain-Policies: | ||
55 | - none | ||
56 | X-Request-Id: | ||
57 | - 086350f4-00fc-4d82-a5ce-2b557df59682 | ||
58 | X-Runtime: | ||
59 | - '0.040539' | ||
60 | X-XSS-Protection: | ||
61 | - 1; mode=block | ||
62 | status: | ||
63 | code: 200 | ||
64 | message: OK | ||
65 | - request: | ||
66 | body: client_id=__MASTODON_PY_TEST_CLIENT_ID&client_secret=__MASTODON_PY_TEST_CLIENT_SECRET&token=s3ZSxpaa2Uhe9EcHankvkfaQZQGiWpdEWIhX7GuhDlk | ||
67 | headers: | ||
68 | Accept: | ||
69 | - '*/*' | ||
70 | Accept-Encoding: | ||
71 | - gzip, deflate | ||
72 | Authorization: | ||
73 | - Bearer s3ZSxpaa2Uhe9EcHankvkfaQZQGiWpdEWIhX7GuhDlk | ||
74 | Connection: | ||
75 | - keep-alive | ||
76 | Content-Length: | ||
77 | - '135' | ||
78 | Content-Type: | ||
79 | - application/x-www-form-urlencoded | ||
80 | User-Agent: | ||
81 | - tests/v311 | ||
82 | method: POST | ||
83 | uri: http://localhost:3000/oauth/revoke | ||
84 | response: | ||
85 | body: | ||
86 | string: '{}' | ||
87 | headers: | ||
88 | Cache-Control: | ||
89 | - max-age=0, private, must-revalidate | ||
90 | Content-Security-Policy: | ||
91 | - 'base-uri ''none''; default-src ''none''; frame-ancestors ''none''; font-src | ||
92 | ''self'' http://localhost:3000; img-src ''self'' https: data: blob: http://localhost:3000; | ||
93 | style-src ''self'' http://localhost:3000 ''nonce-PqM4ChK427oGZ5jIKprkYQ==''; | ||
94 | media-src ''self'' https: data: http://localhost:3000; frame-src ''self'' | ||
95 | https:; manifest-src ''self'' http://localhost:3000; connect-src ''self'' | ||
96 | data: blob: http://localhost:3000 http://localhost:3000 ws://localhost:4000 | ||
97 | ws://localhost:3035 http://localhost:3035; script-src ''self'' ''unsafe-inline'' | ||
98 | ''unsafe-eval'' http://localhost:3000; child-src ''self'' blob: http://localhost:3000; | ||
99 | worker-src ''self'' blob: http://localhost:3000' | ||
100 | Content-Type: | ||
101 | - application/json; charset=utf-8 | ||
102 | ETag: | ||
103 | - W/"44136fa355b3678a1146ad16f7e8649e" | ||
104 | Referrer-Policy: | ||
105 | - strict-origin-when-cross-origin | ||
106 | Transfer-Encoding: | ||
107 | - chunked | ||
108 | Vary: | ||
109 | - Accept | ||
110 | X-Content-Type-Options: | ||
111 | - nosniff | ||
112 | X-Download-Options: | ||
113 | - noopen | ||
114 | X-Frame-Options: | ||
115 | - SAMEORIGIN | ||
116 | X-Permitted-Cross-Domain-Policies: | ||
117 | - none | ||
118 | X-Request-Id: | ||
119 | - 36ec7e63-b15b-487a-aa4a-c396db945794 | ||
120 | X-Runtime: | ||
121 | - '0.010431' | ||
122 | X-XSS-Protection: | ||
123 | - 1; mode=block | ||
124 | status: | ||
125 | code: 200 | ||
126 | message: OK | ||
127 | - request: | ||
128 | body: status=illegal+access+detected | ||
129 | headers: | ||
130 | Accept: | ||
131 | - '*/*' | ||
132 | Accept-Encoding: | ||
133 | - gzip, deflate | ||
134 | Connection: | ||
135 | - keep-alive | ||
136 | Content-Length: | ||
137 | - '30' | ||
138 | Content-Type: | ||
139 | - application/x-www-form-urlencoded | ||
140 | User-Agent: | ||
141 | - tests/v311 | ||
142 | method: POST | ||
143 | uri: http://localhost:3000/api/v1/statuses | ||
144 | response: | ||
145 | body: | ||
146 | string: '{"error":"The access token is invalid"}' | ||
147 | headers: | ||
148 | Cache-Control: | ||
149 | - no-store | ||
150 | Content-Security-Policy: | ||
151 | - 'base-uri ''none''; default-src ''none''; frame-ancestors ''none''; font-src | ||
152 | ''self'' http://localhost:3000; img-src ''self'' https: data: blob: http://localhost:3000; | ||
153 | style-src ''self'' http://localhost:3000 ''nonce-UClOh6+Y0zf3a4O/ysqT/w==''; | ||
154 | media-src ''self'' https: data: http://localhost:3000; frame-src ''self'' | ||
155 | https:; manifest-src ''self'' http://localhost:3000; connect-src ''self'' | ||
156 | data: blob: http://localhost:3000 http://localhost:3000 ws://localhost:4000 | ||
157 | ws://localhost:3035 http://localhost:3035; script-src ''self'' ''unsafe-inline'' | ||
158 | ''unsafe-eval'' http://localhost:3000; child-src ''self'' blob: http://localhost:3000; | ||
159 | worker-src ''self'' blob: http://localhost:3000' | ||
160 | Content-Type: | ||
161 | - application/json; charset=utf-8 | ||
162 | Pragma: | ||
163 | - no-cache | ||
164 | Referrer-Policy: | ||
165 | - strict-origin-when-cross-origin | ||
166 | Transfer-Encoding: | ||
167 | - chunked | ||
168 | Vary: | ||
169 | - Accept, Origin | ||
170 | WWW-Authenticate: | ||
171 | - Bearer realm="Doorkeeper", error="invalid_token", error_description="The access | ||
172 | token is invalid" | ||
173 | X-Content-Type-Options: | ||
174 | - nosniff | ||
175 | X-Download-Options: | ||
176 | - noopen | ||
177 | X-Frame-Options: | ||
178 | - SAMEORIGIN | ||
179 | X-Permitted-Cross-Domain-Policies: | ||
180 | - none | ||
181 | X-Request-Id: | ||
182 | - ab1f9d04-149b-431a-b2b0-79921d45f3bc | ||
183 | X-Runtime: | ||
184 | - '0.005292' | ||
185 | X-XSS-Protection: | ||
186 | - 1; mode=block | ||
187 | status: | ||
188 | code: 401 | ||
189 | message: Unauthorized | ||
190 | - request: | ||
191 | body: status=illegal+access+detected | ||
192 | headers: | ||
193 | Accept: | ||
194 | - '*/*' | ||
195 | Accept-Encoding: | ||
196 | - gzip, deflate | ||
197 | Connection: | ||
198 | - keep-alive | ||
199 | Content-Length: | ||
200 | - '30' | ||
201 | Content-Type: | ||
202 | - application/x-www-form-urlencoded | ||
203 | User-Agent: | ||
204 | - tests/v311 | ||
205 | method: POST | ||
206 | uri: http://localhost:3000/api/v1/statuses | ||
207 | response: | ||
208 | body: | ||
209 | string: '{"error":"The access token is invalid"}' | ||
210 | headers: | ||
211 | Cache-Control: | ||
212 | - no-store | ||
213 | Content-Security-Policy: | ||
214 | - 'base-uri ''none''; default-src ''none''; frame-ancestors ''none''; font-src | ||
215 | ''self'' http://localhost:3000; img-src ''self'' https: data: blob: http://localhost:3000; | ||
216 | style-src ''self'' http://localhost:3000 ''nonce-ZTmQUUs9q7lX74Aa3bTgzA==''; | ||
217 | media-src ''self'' https: data: http://localhost:3000; frame-src ''self'' | ||
218 | https:; manifest-src ''self'' http://localhost:3000; connect-src ''self'' | ||
219 | data: blob: http://localhost:3000 http://localhost:3000 ws://localhost:4000 | ||
220 | ws://localhost:3035 http://localhost:3035; script-src ''self'' ''unsafe-inline'' | ||
221 | ''unsafe-eval'' http://localhost:3000; child-src ''self'' blob: http://localhost:3000; | ||
222 | worker-src ''self'' blob: http://localhost:3000' | ||
223 | Content-Type: | ||
224 | - application/json; charset=utf-8 | ||
225 | Pragma: | ||
226 | - no-cache | ||
227 | Referrer-Policy: | ||
228 | - strict-origin-when-cross-origin | ||
229 | Transfer-Encoding: | ||
230 | - chunked | ||
231 | Vary: | ||
232 | - Accept, Origin | ||
233 | WWW-Authenticate: | ||
234 | - Bearer realm="Doorkeeper", error="invalid_token", error_description="The access | ||
235 | token is invalid" | ||
236 | X-Content-Type-Options: | ||
237 | - nosniff | ||
238 | X-Download-Options: | ||
239 | - noopen | ||
240 | X-Frame-Options: | ||
241 | - SAMEORIGIN | ||
242 | X-Permitted-Cross-Domain-Policies: | ||
243 | - none | ||
244 | X-Request-Id: | ||
245 | - 9a7ad262-0d94-4df6-92a5-a670d6515979 | ||
246 | X-Runtime: | ||
247 | - '0.004613' | ||
248 | X-XSS-Protection: | ||
249 | - 1; mode=block | ||
250 | status: | ||
251 | code: 401 | ||
252 | message: Unauthorized | ||
253 | 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,15 +22,38 @@ def test_log_in_none(api_anonymous): | |||
22 | with pytest.raises(MastodonIllegalArgumentError): | 22 | with pytest.raises(MastodonIllegalArgumentError): |
23 | api_anonymous.log_in() | 23 | api_anonymous.log_in() |
24 | 24 | ||
25 | |||
26 | @pytest.mark.vcr() | 25 | @pytest.mark.vcr() |
27 | def test_log_in_password(api_anonymous): | 26 | def test_log_in_password(api_anonymous): |
28 | token = api_anonymous.log_in( | 27 | token = api_anonymous.log_in( |
29 | username='mastodonpy_test_2@localhost:3000', | 28 | username='mastodonpy_test_2@localhost:3000', |
30 | password='5fc638e0e53eafd9c4145b6bb852667d') | 29 | password='5fc638e0e53eafd9c4145b6bb852667d' |
30 | ) | ||
31 | assert token | 31 | assert token |
32 | 32 | ||
33 | @pytest.mark.vcr() | 33 | @pytest.mark.vcr() |
34 | def test_revoke(api_anonymous): | ||
35 | token = api_anonymous.log_in( | ||
36 | username='mastodonpy_test_2@localhost:3000', | ||
37 | password='5fc638e0e53eafd9c4145b6bb852667d' | ||
38 | ) | ||
39 | api_anonymous.revoke_access_token() | ||
40 | |||
41 | try: | ||
42 | api_anonymous.toot("illegal access detected") | ||
43 | assert False | ||
44 | except Exception as e: | ||
45 | print(e) | ||
46 | pass | ||
47 | |||
48 | api_revoked_token = Mastodon(access_token = token) | ||
49 | try: | ||
50 | api_anonymous.toot("illegal access detected") | ||
51 | assert False | ||
52 | except Exception as e: | ||
53 | print(e) | ||
54 | pass | ||
55 | |||
56 | @pytest.mark.vcr() | ||
34 | def test_log_in_password_incorrect(api_anonymous): | 57 | def test_log_in_password_incorrect(api_anonymous): |
35 | with pytest.raises(MastodonIllegalArgumentError): | 58 | with pytest.raises(MastodonIllegalArgumentError): |
36 | api_anonymous.log_in( | 59 | api_anonymous.log_in( |