Improve auth support

This commit is contained in:
halcy 2022-11-13 22:32:04 +02:00
parent 5b328d479c
commit 24c686f6b2
3 changed files with 304 additions and 7 deletions

View File

@ -486,8 +486,7 @@ class Mastodon:
""" """
return Mastodon.__SUPPORTED_MASTODON_VERSION return Mastodon.__SUPPORTED_MASTODON_VERSION
def auth_request_url(self, client_id=None, redirect_uris="urn:ietf:wg:oauth:2.0:oob", 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):
scopes=__DEFAULT_SCOPES, force_login=False):
""" """
Returns the url that a client needs to request an oauth grant from the server. 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 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). 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: if client_id is None:
client_id = self.client_id client_id = self.client_id
@ -515,12 +518,11 @@ class Mastodon:
params['redirect_uri'] = redirect_uris params['redirect_uri'] = redirect_uris
params['scope'] = " ".join(scopes) params['scope'] = " ".join(scopes)
params['force_login'] = force_login params['force_login'] = force_login
params['state'] = state
formatted_params = urlencode(params) formatted_params = urlencode(params)
return "".join([self.api_base_url, "/oauth/authorize?", formatted_params]) return "".join([self.api_base_url, "/oauth/authorize?", formatted_params])
def log_in(self, username=None, password=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):
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. Get the access token for a user.
@ -588,6 +590,25 @@ class Mastodon:
return response['access_token'] 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") @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): def create_account(self, username, password, email, agreement=False, reason=None, locale="en", scopes=__DEFAULT_SCOPES, to_file=None):
""" """

View File

@ -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

View File

@ -22,14 +22,37 @@ def test_log_in_none(api_anonymous):
with pytest.raises(MastodonIllegalArgumentError): with pytest.raises(MastodonIllegalArgumentError):
api_anonymous.log_in() api_anonymous.log_in()
@pytest.mark.vcr() @pytest.mark.vcr()
def test_log_in_password(api_anonymous): def test_log_in_password(api_anonymous):
token = api_anonymous.log_in( token = api_anonymous.log_in(
username='mastodonpy_test_2@localhost:3000', username='mastodonpy_test_2@localhost:3000',
password='5fc638e0e53eafd9c4145b6bb852667d') password='5fc638e0e53eafd9c4145b6bb852667d'
)
assert token 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() @pytest.mark.vcr()
def test_log_in_password_incorrect(api_anonymous): def test_log_in_password_incorrect(api_anonymous):
with pytest.raises(MastodonIllegalArgumentError): with pytest.raises(MastodonIllegalArgumentError):