diff --git a/c2corg_api/tests/views/test_sso.py b/c2corg_api/tests/views/test_sso.py index 27ceb7968..5dbac0e46 100644 --- a/c2corg_api/tests/views/test_sso.py +++ b/c2corg_api/tests/views/test_sso.py @@ -340,20 +340,39 @@ def setUp(self): self.session.flush() def test_no_token(self): - self.app.get(self._url, status=403) + body = self.app_post_json(self._url, status=400).json + errors = body.get('errors') + self.assertEqual('token', errors[0].get('name')) + self.assertEqual('Required', errors[0].get('description')) def test_invalid_token(self): - self.app.get(self._url, - params={'token': 'bad_token'}, - status=403) + body = self.app_post_json(self._url, + {'token': 'bad_token'}, + status=403).json + errors = body.get('errors') + self.assertEqual('token', errors[0].get('name')) + self.assertEqual('Invalid', errors[0].get('description')) def test_expired_token(self): self.sso_external_id.expire = localized_now() self.session.flush() - self.app.get(self._url, - params={'token': 'good_token'}, - status=403) + body = self.app_post_json(self._url, + params={'token': 'good_token'}, + status=403).json + errors = body.get('errors') + self.assertEqual('token', errors[0].get('name')) + self.assertEqual('Invalid', errors[0].get('description')) + + def test_success(self): + self.sso_external_id.expire = sso_expire_from_now() + self.session.flush() + + body = self.app_post_json(self._url, + {'discourse': True, + 'token': 'good_token'}, + status=200).json + self.assertTrue('token' in body) @patch( 'c2corg_api.views.sso.get_discourse_client', @@ -367,11 +386,11 @@ def test_success_discourse_up(self, discourse_mock): self.sso_external_id.expire = sso_expire_from_now() self.session.flush() - r = self.app.get(self._url, - params={'token': 'good_token'}, - status=302) - - self.assertEqual('https://discourse_redirect', r.location) + body = self.app_post_json(self._url, + {'discourse': True, + 'token': 'good_token'}, + status=200).json + self.assertTrue('token' in body) @patch( 'c2corg_api.views.sso.get_discourse_client', @@ -384,6 +403,8 @@ def test_success_discourse_down(self, discourse_mock): self.sso_external_id.expire = sso_expire_from_now() self.session.flush() - self.app.get(self._url, - params={'token': 'good_token'}, - status=200) + body = self.app_post_json(self._url, + {'discourse': True, + 'token': 'good_token'}, + status=200).json + self.assertTrue('token' in body) diff --git a/c2corg_api/views/sso.py b/c2corg_api/views/sso.py index a655e36d1..7c4fac1d8 100644 --- a/c2corg_api/views/sso.py +++ b/c2corg_api/views/sso.py @@ -10,8 +10,6 @@ from cornice.validators import colander_body_validator from pydiscourse.exceptions import DiscourseClientError from pyramid.httpexceptions import ( - HTTPForbidden, - HTTPFound, HTTPInternalServerError, ) @@ -32,6 +30,7 @@ json_view, ) from c2corg_api.views.user import ( + token_to_response, validate_unique_attribute, validate_forum_username, ) @@ -204,8 +203,9 @@ def post(self): group_id, discourse_userid) return { - 'url': '{}?{}'.format(request.route_url('ssologinrest'), - urlencode({'token': sso_external_id.token})) + 'url': '{}/sso-login?no_redirect&{}'.format( + request.registry.settings['ui.url'], + urlencode({'token': sso_external_id.token})) } @@ -239,21 +239,26 @@ def sso_expire_from_now(): return (localized_now() + timedelta(minutes=CONST_EXPIRE_AFTER_MINUTES)) +class SsoLoginSchema(colander.MappingSchema): + token = colander.SchemaNode(colander.String()) + +sso_login_schema = SsoLoginSchema() + + def validate_token(request, **kwargs): - token = request.params.get('token', None) - if token is None: - log.warning('Attempt to use sso_login without token from {}' - .format(request.client_addr)) - raise HTTPForbidden('Invalid token') + if 'token' not in request.validated: + return # validated by colander schema sso_external_id = DBSession.query(SsoExternalId). \ - filter(SsoExternalId.token == token). \ + filter(SsoExternalId.token == request.validated['token']). \ filter(SsoExternalId.expire > localized_now()). \ one_or_none() if sso_external_id is None: log.warning('Attempt to use sso_login with bad token from {}' .format(request.client_addr)) - raise HTTPForbidden('Invalid token') + request.errors.status = 403 + request.errors.add('body', 'token', 'Invalid') + return request.validated['sso_user'] = sso_external_id.user @@ -262,20 +267,21 @@ class SsoLoginRest(object): def __init__(self, request): self.request = request - @view(validators=[validate_token]) - def get(self): + @view( + schema=sso_login_schema, + validators=[colander_body_validator, validate_token]) + def post(self): user = self.request.validated['sso_user'] - log_validated_user_i_know_what_i_do(user, self.request) - client = get_discourse_client(self.request.registry.settings) - try: - r = client.redirect_without_nonce(user) - except: - # Any error with discourse should not prevent login - log.warning( - 'Error logging into discourse for %d', user.id, - exc_info=True) - else: - return HTTPFound(r) - return { - 'success': True - } + token = log_validated_user_i_know_what_i_do(user, self.request) + response = token_to_response(user, token, self.request) + if 'discourse' in self.request.json: + client = get_discourse_client(self.request.registry.settings) + try: + r = client.redirect_without_nonce(user) + response['redirect_internal'] = r + except: + # Any error with discourse should not prevent login + log.warning( + 'Error logging into discourse for %d', user.id, + exc_info=True) + return response diff --git a/common.ini.in b/common.ini.in index d1f8839ce..dbe96d9ad 100644 --- a/common.ini.in +++ b/common.ini.in @@ -72,6 +72,8 @@ discourse.api_key = {discourse_api_key} discourse.sso_secret = {discourse_sso_secret} discourse.category = {discourse_category} +ui.url = {ui_url} + mail.validate_register_url_template = {mail_validate_register_url_template} mail.request_password_change_url_template = {mail_request_password_change_url_template} mail.validate_change_email_url_template = {mail_validate_change_email_url_template}