From 8c8f389d7f0604c4e22ff91fde9c7df20c00444b Mon Sep 17 00:00:00 2001 From: Viktor Roytman Date: Thu, 16 Feb 2017 10:35:10 -0500 Subject: [PATCH 1/4] Simplify test commands --- tests/__init__.py | 0 tests/python/__init__.py | 0 tests/python/coverage_run.sh | 2 +- 3 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 tests/__init__.py create mode 100644 tests/python/__init__.py diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/python/__init__.py b/tests/python/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/python/coverage_run.sh b/tests/python/coverage_run.sh index 9b057c7..6960082 100755 --- a/tests/python/coverage_run.sh +++ b/tests/python/coverage_run.sh @@ -2,6 +2,6 @@ set -e bash -c "psql -d minigrid -c 'drop schema if exists minigrid_test cascade;' -U postgres 1&>/dev/null" coverage erase -coverage run --source=minigrid,server.py --branch -m unittest ${@:-discover tests.python} +coverage run --source=minigrid,server.py --branch -m unittest coverage html coverage report -m From 861c4faa0a87aecd5167a19f8a630f603b81c99e Mon Sep 17 00:00:00 2001 From: Viktor Roytman Date: Thu, 16 Feb 2017 10:37:01 -0500 Subject: [PATCH 2/4] Update version to 0.1.5 --- prod/docker-compose.yml | 2 +- prod/install.sh | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/prod/docker-compose.yml b/prod/docker-compose.yml index 6e1dd2e..2d437e7 100644 --- a/prod/docker-compose.yml +++ b/prod/docker-compose.yml @@ -1,7 +1,7 @@ version: '2' services: minigrid: - image: selcolumbia/minigrid-server:0.1.4 + image: selcolumbia/minigrid-server:0.1.5 command: ./prod/run.sh --db_host=db --redis_url=redis://redis:6379/0 --minigrid-website-url=https://www.example.com depends_on: - redis diff --git a/prod/install.sh b/prod/install.sh index 5f9a858..2f23dd1 100755 --- a/prod/install.sh +++ b/prod/install.sh @@ -1,5 +1,5 @@ #!/usr/bin/env sh -# Minigrid Server installer for version 0.1.4 +# Minigrid Server installer for version 0.1.5 set -e # Do you have docker installed? @@ -108,8 +108,8 @@ $SUDO openssl dhparam -out /etc/letsencrypt/live/$LETSENCRYPT_DIR/dhparam.pem 20 printf "========================================\n" printf " Generating configuration \n" printf "========================================\n" -$CURL -L https://raw.githubusercontent.com/SEL-Columbia/minigrid-server/0.1.4/prod/docker-compose.yml > docker-compose.yml -$CURL -L https://raw.githubusercontent.com/SEL-Columbia/minigrid-server/0.1.4/prod/nginx.conf > nginx.conf +$CURL -L https://raw.githubusercontent.com/SEL-Columbia/minigrid-server/0.1.5/prod/docker-compose.yml > docker-compose.yml +$CURL -L https://raw.githubusercontent.com/SEL-Columbia/minigrid-server/0.1.5/prod/nginx.conf > nginx.conf sed -i s/www.example.com/$LETSENCRYPT_DIR/g docker-compose.yml sed -i s/www.example.com/$LETSENCRYPT_DIR/g nginx.conf From 93344e3e6b801358801569bc6a00e87cdaeb8f45 Mon Sep 17 00:00:00 2001 From: Viktor Roytman Date: Thu, 16 Feb 2017 13:03:14 -0500 Subject: [PATCH 3/4] Use asyncio-portier --- minigrid/error.py | 2 + minigrid/handlers.py | 30 +++- minigrid/portier.py | 150 ----------------- minigrid/templates/index-logged-out.html | 2 +- requirements.txt | 1 + tests/__init__.py | 1 + tests/python/__init__.py | 1 + tests/python/test_handlers.py | 8 +- tests/python/test_portier.py | 203 ----------------------- tests/python/util.py | 4 +- 10 files changed, 36 insertions(+), 366 deletions(-) delete mode 100644 minigrid/portier.py delete mode 100644 tests/python/test_portier.py diff --git a/minigrid/error.py b/minigrid/error.py index 1b31ba6..2309d07 100644 --- a/minigrid/error.py +++ b/minigrid/error.py @@ -15,6 +15,8 @@ def __init__(self, reason, status_code, template_name, **template_kwargs): self.template_name = template_name self.log_message = None self.template_kwargs = template_kwargs + if 'next_page' not in template_kwargs: + template_kwargs['next_page'] = '/' class LoginError(MinigridHTTPError): diff --git a/minigrid/handlers.py b/minigrid/handlers.py index 9c62d0f..5f5e70d 100644 --- a/minigrid/handlers.py +++ b/minigrid/handlers.py @@ -3,6 +3,10 @@ from urllib.parse import urlencode from uuid import uuid4 +from asyncio_portier import get_verified_email + +import redis + from sqlalchemy.dialects.postgresql import insert from sqlalchemy.exc import DataError, IntegrityError from sqlalchemy.orm.exc import NoResultFound, UnmappedInstanceError @@ -12,7 +16,10 @@ import minigrid.error import minigrid.models as models from minigrid.options import options -from minigrid.portier import get_verified_email, redis_kv + + +cache = redis.StrictRedis.from_url(options.redis_url) +broker_url = 'https://broker.portier.io' class BaseHandler(tornado.web.RequestHandler): @@ -65,12 +72,15 @@ def get(self): self.render( 'index-minigrid-list.html', system=system, minigrids=minigrids) return - self.render('index-logged-out.html') + self.render( + 'index-logged-out.html', next_page=self.get_argument('next', '/')) def post(self): """Send login information to the portier broker.""" nonce = uuid4().hex - redis_kv.setex(nonce, timedelta(minutes=15), '') + next_page = self.get_argument('next', '/') + expiration = timedelta(minutes=15) + cache.set('portier:nonce:{}'.format(nonce), next_page, expiration) query_args = urlencode({ 'login_hint': self.get_argument('email'), 'scope': 'openid email', @@ -79,7 +89,7 @@ def post(self): 'response_mode': 'form_post', 'client_id': options.minigrid_website_url, 'redirect_uri': options.minigrid_website_url + '/verify'}) - self.redirect('https://broker.portier.io/auth?' + query_args) + self.redirect(broker_url + '/auth?' + query_args) class TariffsHandler(BaseHandler): @@ -360,7 +370,15 @@ async def post(self): raise minigrid.error.LoginError( reason=f'Broker Error: {error}: {description}') token = self.get_argument('id_token') - email = await get_verified_email(token) + try: + email, next_page = await get_verified_email( + broker_url, + token, + options.minigrid_website_url, + broker_url, + cache) + except ValueError as exc: + raise minigrid.error.LoginError(exc) try: user = ( self.session @@ -373,7 +391,7 @@ async def post(self): self.set_secure_cookie( 'user', str(user.user_id), httponly=True, secure=options.minigrid_https) - self.redirect(self.get_argument('next', '/')) + self.redirect(next_page) class LogoutHandler(BaseHandler): diff --git a/minigrid/portier.py b/minigrid/portier.py deleted file mode 100644 index 40c6a2d..0000000 --- a/minigrid/portier.py +++ /dev/null @@ -1,150 +0,0 @@ -"""Utility functions for portier login.""" -from base64 import urlsafe_b64decode -from datetime import timedelta -import re - -from cryptography.hazmat.backends import default_backend -from cryptography.hazmat.primitives.asymmetric import rsa - -import jwt - -import redis - -from tornado.escape import json_decode -from tornado.httpclient import AsyncHTTPClient - -import minigrid.error -from minigrid.options import options - - -redis_kv = redis.StrictRedis.from_url(options.redis_url) - - -def b64dec(string): - """Decode unpadded URL-safe Base64 strings. - - Base64 values in JWTs and JWKs have their padding '=' characters stripped - during serialization. Before decoding, we must re-append padding characters - so that the encoded value's final length is evenly divisible by 4. - - Taken from - github.com/portier/demo-rp/blob/6aee99fe126eceda527cae1f6da3f02a68401b6e - /server.py#L176-L184 - """ - padding = '=' * ((4 - len(string) % 4) % 4) - return urlsafe_b64decode(string + padding) - - -async def get_verified_email(token): - """Validate an Identity Token (JWT) and return its subject (email address). - - In Portier, the subject field contains the user's verified email address. - - This functions checks the authenticity of the JWT with the following steps: - - 1. Verify that the JWT has a valid signature from a trusted broker. - 2. Validate that all claims are present and conform to expectations: - - * ``aud`` (audience) must match this website's origin. - * ``iss`` (issuer) must match the broker's origin. - * ``exp`` (expires) must be in the future. - * ``iat`` (issued at) must be in the past. - * ``sub`` (subject) must be an email address. - * ``nonce`` (cryptographic nonce) must not have been seen previously. - - 3. If present, verify that the ``nbf`` (not before) claim is in the past. - - Timestamps are allowed a few minutes of leeway to account for clock skew. - - This demo relies on the `PyJWT`_ library to check signatures and validate - all claims except for ``sub`` and ``nonce``. Those are checked separately. - - .. _PyJWT: https://github.com/jpadilla/pyjwt - - Taken from - github.com/portier/demo-rp/blob/6aee99fe126eceda527cae1f6da3f02a68401b6e - /server.py#L240-L296 - """ - keys = await discover_keys('https://broker.portier.io') - raw_header, _, _ = token.partition('.') - header = json_decode(b64dec(raw_header)) - try: - pub_key = keys[header['kid']] - except KeyError: - raise minigrid.error.LoginError( - reason=f"Cannot find public key with ID {header['kid']}") - try: - payload = jwt.decode( - token, pub_key, - algorithms=['RS256'], - audience=options.minigrid_website_url, - issuer='https://broker.portier.io', - leeway=3*60, - ) - except Exception as exc: - raise minigrid.error.LoginError(reason=f'Invalid JWT: {exc}') - if not re.match('.+@.+', payload['sub']): - raise minigrid.error.LoginError( - reason=f"Invalid e-mail address: {payload['sub']}") - if not redis_kv.delete(payload['nonce']): - raise minigrid.error.LoginError( - reason='Invalid, expired, or re-used nonce') - return payload['sub'] - - -def jwk_to_rsa(key): - """Convert a deserialized JWK into an RSA Public Key instance. - - Taken from - github.com/portier/demo-rp/blob/6aee99fe126eceda527cae1f6da3f02a68401b6e - /server.py#L233-L237 - """ - e = int.from_bytes(b64dec(key['e']), 'big') - n = int.from_bytes(b64dec(key['n']), 'big') - return rsa.RSAPublicNumbers(e, n).public_key(default_backend()) - - -async def discover_keys(broker): - """Discover and return a Broker's public keys. - - Returns a dict mapping from Key ID strings to Public Key instances. - - Portier brokers implement the `OpenID Connect Discovery`_ specification. - This function follows that specification to discover the broker's current - cryptographic public keys: - - 1. Fetch the Discovery Document from ``/.well-known/openid-configuration``. - 2. Parse it as JSON and read the ``jwks_uri`` property. - 3. Fetch the URL referenced by ``jwks_uri`` to retrieve a `JWK Set`_. - 4. Parse the JWK Set as JSON and extract keys from the ``keys`` property. - - Portier currently only supports keys with the ``RS256`` algorithm type. - - .. _OpenID Connect Discovery: - https://openid.net/specs/openid-connect-discovery-1_0.html - .. _JWK Set: https://tools.ietf.org/html/rfc7517#section-5 - - Taken from - github.com/portier/demo-rp/blob/6aee99fe126eceda527cae1f6da3f02a68401b6e - /server.py#L187-L206 - """ - cache_key = 'jwks:' + broker - raw_jwks = redis_kv.get(cache_key) - if not raw_jwks: - http_client = AsyncHTTPClient() - url = broker + '/.well-known/openid-configuration' - response = await http_client.fetch(url) - discovery = json_decode(response.body) - if 'jwks_uri' not in discovery: - raise minigrid.error.LoginError( - reason='No jwks_uri in discovery document') - raw_jwks = (await http_client.fetch(discovery['jwks_uri'])).body - redis_kv.setex(cache_key, timedelta(minutes=5), raw_jwks) - jwks = json_decode(raw_jwks) - if 'keys' not in jwks: - raise minigrid.error.LoginError(reason='No keys found in JWK Set') - return { - key['kid']: jwk_to_rsa(key) - for key in jwks['keys'] - if key['alg'] == 'RS256' - } diff --git a/minigrid/templates/index-logged-out.html b/minigrid/templates/index-logged-out.html index 1d38dbb..8170b34 100644 --- a/minigrid/templates/index-logged-out.html +++ b/minigrid/templates/index-logged-out.html @@ -4,7 +4,7 @@ {% if message is not None %}

Login unsuccessful: {{ message }}

{% end %} -
+ {% module xsrf_form_html() %} E-mail: diff --git a/requirements.txt b/requirements.txt index cdc7a94..561e044 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ +asyncio-portier==0.1.0.post1 cryptography==1.5.3 psycopg2==2.6.2 PyJWT==1.4.2 diff --git a/tests/__init__.py b/tests/__init__.py index e69de29..72e5a0d 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -0,0 +1 @@ +"""Tests for minigrid-server.""" diff --git a/tests/python/__init__.py b/tests/python/__init__.py index e69de29..93d4eb3 100644 --- a/tests/python/__init__.py +++ b/tests/python/__init__.py @@ -0,0 +1 @@ +"""Python tests for minigrid-server.""" diff --git a/tests/python/test_handlers.py b/tests/python/test_handlers.py index a90fc50..7a0ba4e 100644 --- a/tests/python/test_handlers.py +++ b/tests/python/test_handlers.py @@ -11,7 +11,7 @@ from tests.python.util import HTTPTest, CoroMock from minigrid import models -from minigrid.portier import redis_kv +from minigrid.handlers import cache from server import Application @@ -796,12 +796,12 @@ def test_login_success(self): self.assertResponseCode(response, 302) query = parse_qs(urlparse(response.headers['Location']).query) self.assertEqual(query['login_hint'][0], 'a@a.com') - self.assertIn(query['nonce'][0].encode(), redis_kv) + self.assertIn(('portier:nonce:' + query['nonce'][0]).encode(), cache) self.assertTrue(query['redirect_uri'][0].endswith('/verify')) @patch('minigrid.handlers.get_verified_email', new_callable=CoroMock) def test_verify(self, get_verified_email): - get_verified_email.coro.return_value = 'a@a.com' + get_verified_email.coro.return_value = 'a@a.com', '' self.create_user() response = self.fetch( '/verify?id_token=', method='POST', body='', follow_redirects=False @@ -815,7 +815,7 @@ def test_verify(self, get_verified_email): @patch('minigrid.handlers.get_verified_email', new_callable=CoroMock) def test_verify_user_does_not_exist(self, get_verified_email): - get_verified_email.coro.return_value = 'a@a.com' + get_verified_email.coro.return_value = 'a@a.com', '' with ExpectLog('tornado.access', '400'): response = self.fetch('/verify?id_token=', method='POST', body='') self.assertResponseCode(response, 400) diff --git a/tests/python/test_portier.py b/tests/python/test_portier.py deleted file mode 100644 index 6c8ff9f..0000000 --- a/tests/python/test_portier.py +++ /dev/null @@ -1,203 +0,0 @@ -from base64 import b64encode -import binascii -from uuid import uuid4 -from unittest.mock import Mock, patch - -from cryptography.hazmat.backends.openssl.rsa import _RSAPublicKey - -from tornado.escape import json_encode -from tornado.testing import gen_test - -from tests.python.util import Test, HTTPTest, CoroMock - -from minigrid.error import LoginError -from minigrid import portier - - -class TestUtil(Test): - def test_b64dec_incorrect_padding(self): - self.assertRaises(binascii.Error, portier.b64dec, 'a') - - def test_b64dec_success(self): - result = portier.b64dec('aaaa') - self.assertEqual(result, b'i\xa6\x9a') - - def test_jwk_to_rsa(self): - result = portier.jwk_to_rsa({ - 'e': b64encode('3'.encode()).decode(), - 'n': b64encode('4'.encode()).decode(), - }) - self.assertIsInstance(result, _RSAPublicKey) - - -class TestGetVerifiedEmail(HTTPTest): - @patch('minigrid.portier.discover_keys', new_callable=CoroMock) - @patch('minigrid.portier.jwt.decode') - @gen_test - async def test_get_verified_email_success(self, jwt_decode, discover_keys): - discover_keys.coro.return_value = {'a': 'key'} - nonce = uuid4().hex - portier.redis_kv.setex(nonce, 1, '') - jwt_decode.return_value = { - 'sub': 'a@a.com', - 'nonce': nonce, - } - token = b64encode(json_encode({ - 'kid': 'a', - }).encode()) - result = await portier.get_verified_email(token.decode()) - self.assertEqual(result, 'a@a.com') - - @patch('minigrid.portier.discover_keys', new_callable=CoroMock) - @gen_test - async def test_get_verified_email_missing_key(self, discover_keys): - discover_keys.coro.return_value = {'a': 'key'} - token = b64encode(json_encode({ - 'kid': 'b', - }).encode()) - with self.assertRaises(LoginError) as missing_key: - await portier.get_verified_email(token.decode()) - self.assertEqual( - missing_key.exception.reason, 'Cannot find public key with ID b') - - @patch('minigrid.portier.discover_keys', new_callable=CoroMock) - @gen_test - async def test_get_verified_invalid_jwt(self, discover_keys): - discover_keys.coro.return_value = {'a': 'key'} - token = b64encode(json_encode({ - 'kid': 'a', - }).encode()) - with self.assertRaises(LoginError) as invalid_jwt: - await portier.get_verified_email(token.decode()) - self.assertEqual( - invalid_jwt.exception.reason, 'Invalid JWT: Not enough segments') - - @patch('minigrid.portier.discover_keys', new_callable=CoroMock) - @patch('minigrid.portier.jwt.decode') - @gen_test - async def test_get_verified_email_invalid_email( - self, jwt_decode, discover_keys): - discover_keys.coro.return_value = {'a': 'key'} - nonce = uuid4().hex - portier.redis_kv.setex(nonce, 1, '') - jwt_decode.return_value = { - 'sub': 'a#a.com', - 'nonce': nonce, - } - token = b64encode(json_encode({ - 'kid': 'a', - }).encode()) - with self.assertRaises(LoginError) as invalid_email: - await portier.get_verified_email(token.decode()) - self.assertEqual( - invalid_email.exception.reason, 'Invalid e-mail address: a#a.com') - - @patch('minigrid.portier.discover_keys', new_callable=CoroMock) - @patch('minigrid.portier.jwt.decode') - @gen_test - async def test_get_verified_email_invalid_nonce( - self, jwt_decode, discover_keys): - discover_keys.coro.return_value = {'a': 'key'} - jwt_decode.return_value = { - 'sub': 'a@a.com', - 'nonce': 'invalid nonce', - } - token = b64encode(json_encode({ - 'kid': 'a', - }).encode()) - with self.assertRaises(LoginError) as invalid_nonce: - await portier.get_verified_email(token.decode()) - self.assertEqual( - invalid_nonce.exception.reason, - 'Invalid, expired, or re-used nonce' - ) - - -class TestDiscoverKeys(HTTPTest): - @patch('minigrid.portier.AsyncHTTPClient.fetch', new_callable=CoroMock) - @gen_test - async def test_discover_keys_no_cache_hit_success(self, fetch): - response = Mock() - rsa_numbers = { - 'e': b64encode('3'.encode()).decode(), - 'n': b64encode('4'.encode()).decode(), - } - response.body = json_encode({'jwks_uri': 'a', 'keys': [ - { - 'alg': 'RS256', - 'kid': 'included', - **rsa_numbers, - }, - {'alg': 'notRS256', 'kid': 'excluded'}, - ]}) - fetch.coro.return_value = response - result = await portier.discover_keys('a') - self.assertListEqual(list(result.keys()), ['included']) - self.assertIsInstance(result['included'], _RSAPublicKey) - - @gen_test - async def test_discover_keys_cache_hit_success(self): - rsa_numbers = { - 'e': b64encode('3'.encode()).decode(), - 'n': b64encode('4'.encode()).decode(), - } - raw_jwks = json_encode({'jwks_uri': 'a', 'keys': [ - { - 'alg': 'RS256', - 'kid': 'included', - **rsa_numbers, - }, - {'alg': 'notRS256', 'kid': 'excluded'}, - ]}) - portier.redis_kv.setex('jwks:a', 1, raw_jwks) - result = await portier.discover_keys('a') - self.assertListEqual(list(result.keys()), ['included']) - self.assertIsInstance(result['included'], _RSAPublicKey) - - @patch('minigrid.portier.AsyncHTTPClient.fetch', new_callable=CoroMock) - @gen_test - async def test_discover_keys_missing_jwks_uri(self, fetch): - response = Mock() - rsa_numbers = { - 'e': b64encode('3'.encode()).decode(), - 'n': b64encode('4'.encode()).decode(), - } - response.body = json_encode({'not_jwks_uri': 'a', 'keys': [ - { - 'alg': 'RS256', - 'kid': 'included', - **rsa_numbers, - }, - {'alg': 'notRS256', 'kid': 'excluded'}, - ]}) - fetch.coro.return_value = response - with self.assertRaises(LoginError) as jwks_uri_missing: - await portier.discover_keys('a') - self.assertEqual( - jwks_uri_missing.exception.reason, - 'No jwks_uri in discovery document' - ) - - @patch('minigrid.portier.AsyncHTTPClient.fetch', new_callable=CoroMock) - @gen_test - async def test_discover_keys_missing_keys(self, fetch): - response = Mock() - rsa_numbers = { - 'e': b64encode('3'.encode()).decode(), - 'n': b64encode('4'.encode()).decode(), - } - response.body = json_encode({'jwks_uri': 'a', 'not_keys': [ - { - 'alg': 'RS256', - 'kid': 'included', - **rsa_numbers, - }, - {'alg': 'notRS256', 'kid': 'excluded'}, - ]}) - fetch.coro.return_value = response - with self.assertRaises(LoginError) as jwks_uri_missing: - await portier.discover_keys('a') - self.assertEqual( - jwks_uri_missing.exception.reason, - 'No keys found in JWK Set' - ) diff --git a/tests/python/util.py b/tests/python/util.py index 1d64d2f..e21a1b9 100644 --- a/tests/python/util.py +++ b/tests/python/util.py @@ -28,7 +28,7 @@ class Dummy: DummyRedis.StrictRedis = Dummy DummyRedis.StrictRedis.from_url = lambda _: fakeredis.FakeStrictRedis() sys.modules['redis'] = DummyRedis -from minigrid import portier # noqa +import minigrid.handlers # noqa from server import Application # noqa @@ -71,7 +71,7 @@ def tearDown(self): for tbl in reversed(models.Base.metadata.sorted_tables): engine.execute(tbl.delete()) shutil.rmtree(path('../tests/python/tmp'), ignore_errors=True) - portier.redis_kv.flushall() + minigrid.handlers.cache.flushall() super().tearDown() From fef9a856841af9e418159f4315575297d7569969 Mon Sep 17 00:00:00 2001 From: Viktor Roytman Date: Thu, 16 Feb 2017 13:19:43 -0500 Subject: [PATCH 4/4] Round out tests --- minigrid/error.py | 3 +-- minigrid/handlers.py | 2 +- tests/python/test_handlers.py | 13 +++++++++++++ 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/minigrid/error.py b/minigrid/error.py index 2309d07..6c88194 100644 --- a/minigrid/error.py +++ b/minigrid/error.py @@ -15,8 +15,7 @@ def __init__(self, reason, status_code, template_name, **template_kwargs): self.template_name = template_name self.log_message = None self.template_kwargs = template_kwargs - if 'next_page' not in template_kwargs: - template_kwargs['next_page'] = '/' + template_kwargs['next_page'] = '/' class LoginError(MinigridHTTPError): diff --git a/minigrid/handlers.py b/minigrid/handlers.py index 5f5e70d..392b37a 100644 --- a/minigrid/handlers.py +++ b/minigrid/handlers.py @@ -378,7 +378,7 @@ async def post(self): broker_url, cache) except ValueError as exc: - raise minigrid.error.LoginError(exc) + raise minigrid.error.LoginError(reason=str(exc)) try: user = ( self.session diff --git a/tests/python/test_handlers.py b/tests/python/test_handlers.py index 7a0ba4e..14dff9e 100644 --- a/tests/python/test_handlers.py +++ b/tests/python/test_handlers.py @@ -799,6 +799,19 @@ def test_login_success(self): self.assertIn(('portier:nonce:' + query['nonce'][0]).encode(), cache) self.assertTrue(query['redirect_uri'][0].endswith('/verify')) + @patch('minigrid.handlers.get_verified_email', new_callable=CoroMock) + def test_verify_value_error(self, get_verified_email): + get_verified_email.coro.side_effect = ValueError('error') + self.create_user() + with ExpectLog('tornado.access', '400'): + response = self.fetch( + '/verify?id_token=', + method='POST', body='', follow_redirects=False + ) + self.assertResponseCode(response, 400) + self.assertEqual( + response.error.message, 'error') + @patch('minigrid.handlers.get_verified_email', new_callable=CoroMock) def test_verify(self, get_verified_email): get_verified_email.coro.return_value = 'a@a.com', ''