Skip to content

Commit fb1d597

Browse files
authored
Merge pull request #435 from metabrainz/mismatch-oauth-url
Provide a user-friendly error if oauth params are incorrect
2 parents a80d647 + 2654722 commit fb1d597

File tree

5 files changed

+98
-9
lines changed

5 files changed

+98
-9
lines changed

critiquebrainz/db/oauth_client.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,12 @@ def create(*, user_id, name, desc, website, redirect_uri):
3030
}
3131
"""
3232
with db.engine.connect() as connection:
33-
connection.execute(sqlalchemy.text("""
33+
row = connection.execute(sqlalchemy.text("""
3434
INSERT INTO oauth_client (client_id, client_secret, redirect_uri,
3535
user_id, name, "desc", website)
3636
VALUES (:client_id, :client_secret, :redirect_uri,
3737
:user_id, :name, :desc, :website)
38+
RETURNING client_id, client_secret, redirect_uri, user_id, name, "desc", website
3839
"""), {
3940
"client_id": generate_string(CLIENT_ID_LENGTH),
4041
"client_secret": generate_string(CLIENT_SECRET_LENGTH),
@@ -44,6 +45,7 @@ def create(*, user_id, name, desc, website, redirect_uri):
4445
"website": website,
4546
"desc": desc,
4647
})
48+
return dict(row.fetchone())
4749

4850

4951
def update(*, client_id, name=None, desc=None, website=None, redirect_uri=None):

critiquebrainz/frontend/testing.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import os
2+
from critiquebrainz.ws.oauth import oauth
23

34
from flask_testing import TestCase
45

@@ -10,6 +11,7 @@ class FrontendTestCase(TestCase):
1011

1112
def create_app(self):
1213
app = create_app(config_path=os.path.join(os.path.dirname(os.path.realpath(__file__)), '..', '..', 'test_config.py'))
14+
oauth.init_app(app)
1315
return app
1416

1517
def setUp(self):

critiquebrainz/frontend/views/oauth.py

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
from flask import Blueprint, render_template, redirect, request
2+
from werkzeug.exceptions import BadRequest
3+
from critiquebrainz.ws.oauth.exceptions import OAuthError
24
from flask_login import login_required, current_user
35

46
import critiquebrainz.db.oauth_client as db_oauth_client
@@ -19,12 +21,18 @@ def authorize_prompt():
1921
state = request.args.get('state')
2022

2123
if request.method == 'GET': # Client requests access
22-
oauth.validate_authorization_request(client_id, response_type, redirect_uri, scope)
23-
client = db_oauth_client.get_client(client_id)
24-
return render_template('oauth/prompt.html', client=client, scope=scope,
25-
cancel_url=build_url(redirect_uri, dict(error='access_denied')))
24+
try:
25+
oauth.validate_authorization_request(client_id, response_type, redirect_uri, scope)
26+
client = db_oauth_client.get_client(client_id)
27+
return render_template('oauth/prompt.html', client=client, scope=scope,
28+
cancel_url=build_url(redirect_uri, dict(error='access_denied')))
29+
except OAuthError as e:
30+
raise BadRequest(e.desc)
2631

2732
if request.method == 'POST': # User grants access to the client
28-
oauth.validate_authorization_request(client_id, response_type, redirect_uri, scope)
29-
code = oauth.generate_grant(client_id, current_user.id, redirect_uri, scope)
30-
return redirect(build_url(redirect_uri, dict(code=code, state=state)))
33+
try:
34+
oauth.validate_authorization_request(client_id, response_type, redirect_uri, scope)
35+
code = oauth.generate_grant(client_id, current_user.id, redirect_uri, scope)
36+
return redirect(build_url(redirect_uri, dict(code=code, state=state)))
37+
except OAuthError as e:
38+
raise BadRequest(e.desc)
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
from flask import url_for
2+
import critiquebrainz.db.oauth_client as db_oauth_client
3+
import critiquebrainz.db.oauth_grant as db_oauth_grant
4+
import critiquebrainz.db.users as db_users
5+
from critiquebrainz.frontend.testing import FrontendTestCase
6+
7+
from urllib.parse import urlparse, parse_qs
8+
9+
10+
class OauthTestCase(FrontendTestCase):
11+
def setUp(self):
12+
from critiquebrainz.db.user import User
13+
self.user = User(db_users.get_or_create(2, "9371e5c7-5995-4471-a5a9-33481f897f9c", new_user_data={
14+
"display_name": u"User",
15+
}))
16+
self.oauthclient = db_oauth_client.create(
17+
user_id=self.user.id,
18+
name='An oauth app',
19+
desc='This is a great client',
20+
website='https://example.com',
21+
redirect_uri='https://example.com/redirect'
22+
)
23+
24+
25+
def test_invalid_clientid(self):
26+
self.temporary_login(self.user)
27+
response = self.client.get(url_for('oauth.authorize_prompt', response_type='code', client_id='not-an-id', redirect_uri='x', scope='x', state='x'))
28+
assert response.status_code == 400
29+
assert "400 Bad Request: Client authentication failed." in response.text
30+
31+
def test_invalid_redirect_uri(self):
32+
self.temporary_login(self.user)
33+
client_id = self.oauthclient["client_id"]
34+
response = self.client.get(url_for('oauth.authorize_prompt', response_type='code', client_id=client_id, redirect_uri='x', scope='x', state='x'))
35+
assert response.status_code == 400
36+
assert "400 Bad Request: Invalid redirect uri." in response.text
37+
38+
def test_invalid_scope(self):
39+
self.temporary_login(self.user)
40+
client_id = self.oauthclient["client_id"]
41+
redirect_uri = self.oauthclient["redirect_uri"]
42+
response = self.client.get(url_for('oauth.authorize_prompt', response_type='code', client_id=client_id, redirect_uri=redirect_uri, scope='x', state='x'))
43+
assert response.status_code == 400
44+
assert "400 Bad Request: The requested scope is invalid, unknown, or malformed." in response.text
45+
46+
def test_valid_oauth_request(self):
47+
self.temporary_login(self.user)
48+
client_id = self.oauthclient["client_id"]
49+
redirect_uri = self.oauthclient["redirect_uri"]
50+
response = self.client.get(url_for('oauth.authorize_prompt', response_type='code', client_id=client_id, redirect_uri=redirect_uri, scope='review', state='x'))
51+
assert response.status_code == 200
52+
assert "Do you want to give access to your account to An oauth app?" in response.text
53+
54+
def test_approve_invalid_parameter(self):
55+
"""Same endpoint, but a POST with an invalid client id"""
56+
self.temporary_login(self.user)
57+
response = self.client.post(url_for('oauth.authorize_prompt', response_type='code', client_id='x', redirect_uri='y', scope='review', state='x'))
58+
assert response.status_code == 400
59+
assert "400 Bad Request: Client authentication failed." in response.text
60+
61+
def test_approve_application(self):
62+
"""A POST to approve an oauth authorization"""
63+
self.temporary_login(self.user)
64+
client_id = self.oauthclient["client_id"]
65+
redirect_uri = self.oauthclient["redirect_uri"]
66+
response = self.client.post(url_for('oauth.authorize_prompt', response_type='code', client_id=client_id, redirect_uri=redirect_uri, scope='review', state='x'))
67+
assert response.status_code == 302
68+
# This is the redirect URL that we set in the oauth client
69+
assert response.location.startswith('https://example.com/redirect?code=')
70+
71+
redirect_location = urlparse(response.location)
72+
query = parse_qs(redirect_location.query)
73+
assert query['state'] == ['x']
74+
code = query['code'][0]
75+
76+
grants = db_oauth_grant.list_grants(client_id=client_id, code=code)
77+
assert len(grants) == 1

pytest.ini

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
[pytest]
22
testpaths = critiquebrainz
3-
addopts = --cov=critiquebrainz
3+
addopts = --cov=critiquebrainz --no-cov-on-fail

0 commit comments

Comments
 (0)