def test_connect_success(self, gui, gusi, ft): alice = self.make_participant('alice', elsewhere='twitter') gusi.return_value = self.client.website.platforms.github.extract_user_info({'id': 2}, '') gui.return_value = self.client.website.platforms.github.extract_user_info({'id': 1}, '') ft.return_value = None then = b'/foobar' cookie = b64encode_s(json.dumps(['query_data', 'connect', b64encode_s(then), '2'])) response = self.client.GxT('/on/github/associate?state=deadbeef', auth_as=alice, cookies={'github_deadbeef': cookie}) assert response.code == 302, response.text assert response.headers[b'Location'] == then
def test_unicode_success_message_doesnt_break_edit_page(self): alice = self.make_participant('alice') s = 'épopée' bs = s.encode('utf8') for msg in (s, bs): r = self.client.GET('/alice/edit/username?success='+b64encode_s(msg), auth_as=alice) assert bs in r.body
def test_unicode_success_message_doesnt_break_edit_page(self): alice = self.make_participant('alice') s = 'épopée' bs = s.encode('utf8') for msg in (s, bs): r = self.client.GET('/alice/edit/username?success=' + b64encode_s(msg), auth_as=alice) assert bs in r.body
def asset_etag(path): if path.endswith('.spt'): return '' if path in ETAGS: return ETAGS[path] with open(path, 'rb') as f: h = b64encode_s(md5(f.read()).digest()) ETAGS[path] = h return h
def test_connect_success(self, gui, gusi, ft): alice = self.make_participant('alice', elsewhere='twitter') gusi.return_value = self.client.website.platforms.github.extract_user_info( {'id': 2}, '') gui.return_value = self.client.website.platforms.github.extract_user_info( {'id': 1}, '') ft.return_value = None then = b'/foobar' cookie = b64encode_s( json.dumps(['query_data', 'connect', b64encode_s(then), '2'])) response = self.client.GxT('/on/github/associate?state=deadbeef', auth_as=alice, cookies={'github_deadbeef': cookie}) assert response.code == 302, response.text assert response.headers[b'Location'] == then
def test_connect_failure(self): alice = self.make_participant('alice') error = 'User canceled the Dialog flow' url = '/on/facebook/associate?error_message=%s&state=deadbeef' % error cookie = b64encode_s(json.dumps(['query_data', 'connect', '', '2'])) response = self.client.GxT(url, auth_as=alice, cookies={'facebook_deadbeef': cookie}) assert response.code == 502, response.text assert error in response.text
def asset_etag(path): if path.endswith('.spt'): return '' if path in ETAGS: return ETAGS[path] with open(path, 'rb') as f: h = b64encode_s(md5(f.read()).digest()) ETAGS[path] = h return h
def test_connect_failure(self): alice = self.make_participant('alice') error = 'User canceled the Dialog flow' url = '/on/facebook/associate?error_message=%s&state=deadbeef' % error cookie = b64encode_s(json.dumps(['query_data', 'connect', '', '2'])) response = self.client.GxT(url, auth_as=alice, cookies={'facebook_deadbeef': cookie}) assert response.code == 502, response.text assert error in response.text
def asset_etag(path): if path.endswith('.spt'): return '' mtime = stat(path).st_mtime if path in ETAGS: h, cached_mtime = ETAGS[path] if cached_mtime == mtime: return h with open(path, 'rb') as f: h = b64encode_s(md5(f.read()).digest()) ETAGS[path] = (h, mtime) return h
def test_email_verification_is_backwards_compatible(self): """Test email verification still works with unencoded email in verification link. """ addr = '*****@*****.**' self.hit_email_spt('add-email', addr) nonce = self.alice.get_email(addr).nonce email64 = b64encode_s(addr) url = '/alice/emails/verify.html?email64=%s&nonce=%s' % (email64, nonce) self.client.GET(url, auth_as=self.alice) actual = Participant.from_username('alice').email assert addr == actual
def asset_etag(path): if path.endswith('.spt'): return '' mtime = stat(path).st_mtime if path in ETAGS: h, cached_mtime = ETAGS[path] if cached_mtime == mtime: return h with open(path, 'rb') as f: h = b64encode_s(md5(f.read()).digest()) ETAGS[path] = (h, mtime) return h
def test_connect_might_need_confirmation(self, gui, gusi, ft): alice = self.make_participant('alice') self.make_participant('bob') gusi.return_value = self.client.website.platforms.github.extract_user_info({'id': 2}, '') gui.return_value = self.client.website.platforms.github.extract_user_info({'id': 1}, '') ft.return_value = None cookie = b64encode_s(json.dumps(['query_data', 'connect', '', '2'])) response = self.client.GxT('/on/github/associate?state=deadbeef', auth_as=alice, cookies={'github_deadbeef': cookie}) assert response.code == 302 assert response.headers[b'Location'].startswith(b'/on/confirm.html?id=')
def test_carrying_on_with_form_submission_after_email_login(self): email = '*****@*****.**' alice = self.make_participant('alice', email=email) # Initiate the log-in data = {'log-in.id': email.upper()} r = self.client.POST('/?foo=bar', data, raise_immediately=False, HTTP_ACCEPT=b'text/html') session = self.db.one( "SELECT * FROM user_secrets WHERE participant = %s", (alice.id, )) assert session.secret not in r.headers.raw.decode('ascii') assert session.secret not in r.body.decode('utf8') assert r.headers[b'Content-Type'] == b'text/html; charset=UTF-8' assert "Carry on" in r.text assert 'name="form.repost" value="true"' in r.text # Log in, in another tab qs = '?log-in.id=%i&log-in.key=%i&log-in.token=%s' % ( alice.id, session.id, session.secret) csrf_token = '_ThisIsAThirtyTwoBytesLongToken_' confirmation_token = b64encode_s( blake2b( session.secret.encode(), key=csrf_token.encode(), digest_size=48, ).digest()) r = self.client.GxT( '/alice/' + qs + '&log-in.confirmation=' + confirmation_token, csrf_token=csrf_token, ) assert r.code == 302 assert SESSION in r.headers.cookie # Carry on in the first tab data['form.repost'] = 'true' data['log-in.carry-on'] = email r = self.client.POST( '/?foo=bar', data, auth_as=alice, raise_immediately=False, HTTP_ACCEPT=b'text/html', ) assert r.code == 200
def test_b64encode_s_replaces_slash_with_underscore(self): # TheEnter?prise => VGhlRW50ZXI/cHJpc2U= assert b64encode_s('TheEnter?prise') == 'VGhlRW50ZXI_cHJpc2U~'
def test_email_address_is_base64_encoded_in_sent_verification_link(self): address = '*****@*****.**' encoded = b64encode_s(address) self.hit_email_spt('add-email', address) last_email = self.get_last_email() assert "/alice/emails/verify.html?email64="+encoded in last_email['text']
def authenticate_user_if_possible(request, response, state, user, _): """This signs the user in. """ if request.line.uri.startswith(b'/assets/'): return db = state['website'].db if not db: return # Try to authenticate the user # We want to try cookie auth first, but we want password and email auth to # supersede it. p = None if SESSION in request.headers.cookie: creds = request.headers.cookie[SESSION].value.split(':', 2) if len(creds) == 2: creds = [creds[0], 1, creds[1]] if len(creds) == 3: p = Participant.authenticate(*creds) if p: state['user'] = p session_p, p = p, None session_suffix = '' redirect = False redirect_url = None if request.method == 'POST': # Form auth body = _get_body(request) if body: # Remove email address from blacklist if requested email_address = body.pop('email.unblacklist', None) if email_address: remove_email_address_from_blacklist(email_address, user, request) # Proceed with form auth carry_on = body.pop('log-in.carry-on', None) if carry_on: p_email = session_p and session_p.get_email_address() if p_email != carry_on: state['log-in.carry-on'] = carry_on raise LoginRequired else: p = sign_in_with_form_data(body, state) if p: redirect = body.get('form.repost', None) != 'true' redirect_url = body.get('sign-in.back-to') or request.line.uri.decoded elif request.method == 'GET': if request.qs.get('log-in.id'): # Email auth id = request.qs.get('log-in.id') session_id = request.qs.get('log-in.key') token = request.qs.get('log-in.token') if not (token and token.endswith('.em')): raise response.render('simplates/bad-login-link.spt', state) p = Participant.authenticate(id, session_id, token) if p: redirect = True session_p = p session_suffix = '.em' else: raise response.render('simplates/bad-login-link.spt', state) del request.qs['log-in.id'], request.qs['log-in.key'], request.qs['log-in.token'] # Handle email verification email_id = request.qs.get_int('email.id', default=None) email_nonce = request.qs.get('email.nonce', '') if email_id and not request.path.raw.endswith('/disavow'): email_participant, email_is_already_verified = db.one(""" SELECT p, e.verified FROM emails e JOIN participants p On p.id = e.participant WHERE e.id = %s """, (email_id,), default=(None, None)) if email_participant: result = email_participant.verify_email(email_id, email_nonce, p) state['email.verification-result'] = result request.qs.pop('email.id', None) request.qs.pop('email.nonce', None) if result == EmailVerificationResult.SUCCEEDED: request.qs.add('success', b64encode_s( _("Your email address is now verified.") )) del email_participant # Set up the new session if p: if session_p: session_p.sign_out(response.headers.cookie) if p.status == 'closed': p.update_status('active') if not p.session: p.sign_in(response.headers.cookie, suffix=session_suffix) state['user'] = p # Redirect if appropriate if redirect: if not redirect_url: # Build the redirect URL with the querystring as it is now (we've # probably removed items from it at this point). redirect_url = request.path.raw + request.qs.serialize() response.redirect(redirect_url, trusted_url=False)
def verify_email(self, email, nonce, should_fail=False): # Email address is base64 encoded in url. url = '/alice/emails/verify.html?email64=%s&nonce=%s' % ( b64encode_s(email), nonce) G = self.client.GxT if should_fail else self.client.GET return G(url, auth_as=self.alice)
def verify_email(self, email, nonce, should_fail=False): # Email address is base64 encoded in url. url = '/alice/emails/verify.html?email64=%s&nonce=%s' % (b64encode_s(email), nonce) G = self.client.GxT if should_fail else self.client.GET return G(url, auth_as=self.alice)
def test_unicode_success_message_doesnt_break_edit_page(self): alice = self.make_participant('alice') for msg in ('épopée', b'épopée'): r = self.client.GET('/alice/edit?success=' + b64encode_s(msg), auth_as=alice) assert b'épopée' in r.body
def test_b64encode_s_replaces_equals_with_tilde(self): assert b64encode_s('TheEnterprise') == 'VGhlRW50ZXJwcmlzZQ~~'
def test_safe_base64_transcode_works_with_binary_data(self): utils.b64decode_s(utils.b64encode_s(b'\xff'))
def test_unicode_success_message_doesnt_break_edit_page(self): alice = self.make_participant('alice') for msg in ('épopée', b'épopée'): r = self.client.GET('/alice/edit?success='+b64encode_s(msg), auth_as=alice) assert b'épopée' in r.body
def authenticate_user_if_possible(csrf_token, request, response, state, user, _): """This signs the user in. """ if state.get('etag'): return db = state['website'].db if not db: return # Try to authenticate the user # We want to try cookie auth first, but we want password and email auth to # supersede it. session_p = None if SESSION in request.headers.cookie: creds = request.headers.cookie[SESSION].value.split(':', 2) if len(creds) == 2: creds = [creds[0], 1, creds[1]] if len(creds) == 3: session_p, state[ 'session_status'] = Participant.authenticate_with_session( *creds, allow_downgrade=True, cookies=response.headers.cookie) if session_p: user = state['user'] = session_p p = None session_suffix = '' redirect = False redirect_url = None if request.method == 'POST': # Form auth body = _get_body(request) if body: redirect = body.get('form.repost', None) != 'true' redirect_url = body.get('sign-in.back-to') # Remove email address from blacklist if requested email_address = body.pop('email.unblacklist', None) if email_address: remove_email_address_from_blacklist(email_address, user, request) # Proceed with form auth carry_on = body.pop('log-in.carry-on', None) if carry_on: can_carry_on = (session_p is not None and session_p.session_type != 'ro' and session_p.get_email_address() == carry_on) if not can_carry_on: state['log-in.carry-on'] = carry_on raise LoginRequired else: p = sign_in_with_form_data(body, state) if p: if not p.session: session_suffix = '.pw' # stands for "password" else: redirect = False elif request.method == 'GET': if request.qs.get('log-in.id') or request.qs.get('email.id'): # Prevent email software from messing up an email log-in or confirmation # with a single GET request. Also, show a proper warning to someone trying # to log in while cookies are disabled. require_cookie(state) if request.qs.get('log-in.id'): # Email auth id = request.qs.get_int('log-in.id') session_id = request.qs.get('log-in.key') if not session_id or session_id < '1001' or session_id > '1010': raise response.render('simplates/log-in-link-is-invalid.spt', state) token = request.qs.get('log-in.token') if not (token and token.endswith('.em')): raise response.render('simplates/log-in-link-is-invalid.spt', state) required = request.qs.parse_boolean('log-in.required', default=True) p = Participant.authenticate_with_session( id, session_id, token, allow_downgrade=not required, cookies=response.headers.cookie, )[0] if p: if p.id != user.id: submitted_confirmation_token = request.qs.get( 'log-in.confirmation') if submitted_confirmation_token: expected_confirmation_token = b64encode_s( blake2b( token.encode('ascii'), key=csrf_token.token.encode('ascii'), digest_size=48, ).digest()) confirmation_tokens_match = constant_time_compare( expected_confirmation_token, submitted_confirmation_token) if not confirmation_tokens_match: raise response.invalid_input( submitted_confirmation_token, 'log-in.confirmation', 'querystring') del request.qs['log-in.confirmation'] else: raise response.render( 'simplates/log-in-link-is-valid.spt', state) redirect = True db.run( """ DELETE FROM user_secrets WHERE participant = %s AND id = %s AND mtime = %s """, (p.id, p.session.id, p.session.mtime)) p.session = None session_suffix = '.em' elif required: raise response.render('simplates/log-in-link-is-invalid.spt', state) del request.qs['log-in.id'], request.qs['log-in.key'], request.qs[ 'log-in.token'] request.qs.pop('log-in.required', None) # Handle email verification email_id = request.qs.get_int('email.id', default=None) email_nonce = request.qs.get('email.nonce', '') if email_id and not request.path.raw.endswith('/disavow'): email_participant, email_is_already_verified = db.one( """ SELECT p, e.verified FROM emails e JOIN participants p On p.id = e.participant WHERE e.id = %s """, (email_id, ), default=(None, None)) if email_participant: result = email_participant.verify_email( email_id, email_nonce, p or user, request) state['email.verification-result'] = result request.qs.pop('email.id', None) request.qs.pop('email.nonce', None) if result == EmailVerificationResult.SUCCEEDED: request.qs.add( 'success', b64encode_s(_("Your email address is now verified."))) del email_participant # Set up the new session if p: if p.status == 'closed': p.update_status('active') if session_p: p.regenerate_session(session_p.session, response.headers.cookie, suffix=session_suffix) if not p.session: p.sign_in(response.headers.cookie, suffix=session_suffix) state['user'] = p # Redirect if appropriate if redirect: if not redirect_url: # Build the redirect URL with the querystring as it is now (we've # probably removed items from it at this point). redirect_url = request.path.raw + request.qs.serialize() response.redirect(redirect_url, trusted_url=False)
def test_b64encode_s_replaces_slash_with_underscore(self): # TheEnter?prise => VGhlRW50ZXI/cHJpc2U= assert b64encode_s('TheEnter?prise') == 'VGhlRW50ZXI_cHJpc2U~'
def test_email_login_with_old_unverified_address(self): email = '*****@*****.**' alice = self.make_participant('alice', email=None) alice.add_email(email) Participant.dequeue_emails() self.db.run("UPDATE emails SET nonce = null") # Initiate email log-in data = {'log-in.id': email.upper()} r = self.client.POST('/', data, raise_immediately=False) session = self.db.one( "SELECT * FROM user_secrets WHERE participant = %s", (alice.id, )) assert session.secret not in r.headers.raw.decode('ascii') assert session.secret not in r.body.decode('utf8') # Check the email message Participant.dequeue_emails() last_email = self.get_last_email() assert last_email and last_email['subject'] == 'Log in to Liberapay' email_row = alice.get_email(email) assert email_row.verified is None assert email_row.nonce qs = 'log-in.id=%i&log-in.key=%i&log-in.token=%s&email.id=%s&email.nonce=%s' % ( alice.id, session.id, session.secret, email_row.id, email_row.nonce) assert qs in last_email['text'] # Try to log in without a confirmation code csrf_token = '_ThisIsAThirtyTwoBytesLongToken_' confirmation_token = b64encode_s( blake2b( session.secret.encode(), key=csrf_token.encode(), digest_size=48, ).digest()) r = self.client.GxT('/alice/?' + qs, csrf_token=csrf_token) assert r.code == 200 assert SESSION not in r.headers.cookie assert confirmation_token in r.text # Try to log in with an incorrect confirmation code r = self.client.GxT( '/alice/?' + qs + '&log-in.confirmation=' + ('~' * 64), csrf_token=csrf_token, ) assert r.code == 400 assert SESSION not in r.headers.cookie assert confirmation_token not in r.text # Log in with the correct confirmation code r = self.client.GxT( '/alice/?' + qs + '&log-in.confirmation=' + confirmation_token, csrf_token=csrf_token, ) assert r.code == 302 assert SESSION in r.headers.cookie assert r.headers[b'Location'].startswith(b'http://localhost/alice/') # Check that the email address is now verified email_row = alice.get_email(email) assert email_row.verified alice = alice.refetch() assert alice.email == email
def test_safe_base64_transcode_works_with_binary_data(self): utils.b64decode_s(utils.b64encode_s(b'\xff'))
def test_email_address_is_base64_encoded_in_sent_verification_link(self): address = '*****@*****.**' encoded = b64encode_s(address) self.hit_email_spt('add-email', address) last_email = self.get_last_email() assert "/alice/emails/verify.html?email64="+encoded in last_email['text']
def test_b64encode_s_replaces_equals_with_tilde(self): assert b64encode_s('TheEnterprise') == 'VGhlRW50ZXJwcmlzZQ~~'
def test_email_login(self): email = '*****@*****.**' alice = self.make_participant('alice', email=None) alice.add_email(email) alice.close(None) # Sanity checks email_row = alice.get_email(email) assert email_row.verified is None assert alice.email is None # Initiate email log-in data = {'log-in.id': email.upper()} r = self.client.POST('/', data, raise_immediately=False) session = self.db.one( "SELECT * FROM user_secrets WHERE participant = %s", (alice.id, )) assert session.secret not in r.headers.raw.decode('ascii') assert session.secret not in r.body.decode('utf8') # Check the email message Participant.dequeue_emails() last_email = self.get_last_email() assert last_email and last_email['subject'] == 'Log in to Liberapay' qs = 'log-in.id=%i&log-in.key=%i&log-in.token=%s&email.id=%s&email.nonce=%s' % ( alice.id, session.id, session.secret, email_row.id, email_row.nonce) assert qs in last_email['text'] # Attempt to use the URL in a new browser session (no anti-CSRF cookie yet) r = self.client.GxT('/alice/?foo=bar&' + qs, csrf_token=None) assert r.code == 200 refresh_qs = '?foo=bar&' + qs + '&cookie_sent=true' assert r.headers[b'Refresh'] == b"0;url=" + refresh_qs.encode() assert CSRF_TOKEN in r.headers.cookie # Follow the redirect, still without cookies r = self.client.GxT('/alice/' + refresh_qs, csrf_token=None) assert r.code == 403, r.text assert "Please make sure your browser is configured to allow cookies" in r.text # Log in csrf_token = '_ThisIsAThirtyTwoBytesLongToken_' confirmation_token = b64encode_s( blake2b( session.secret.encode(), key=csrf_token.encode(), digest_size=48, ).digest()) r = self.client.GxT('/alice/' + refresh_qs, csrf_token=csrf_token) assert r.code == 200 assert SESSION not in r.headers.cookie assert confirmation_token in r.text r = self.client.GxT( '/alice/' + refresh_qs + '&log-in.confirmation=' + confirmation_token, csrf_token=csrf_token, ) assert r.code == 302 assert SESSION in r.headers.cookie assert r.headers[b'Location'].startswith( b'http://localhost/alice/?foo=bar&success=') # ↑ checks that original path and query are preserved old_secret = self.db.one( """ SELECT secret FROM user_secrets WHERE participant = %s AND id = %s AND secret = %s """, (alice.id, session.id, session.secret)) assert old_secret is None # ↑ this means that the link is only valid once # Check that the email address is now verified email_row = alice.get_email(email) assert email_row.verified alice = alice.refetch() assert alice.email == email # Check what happens if the user clicks the login link a second time cookies = r.headers.cookie r = self.client.GxT('/alice/?foo=bar&' + qs, cookies=cookies) assert r.code == 400 assert " already logged in," in r.text, r.text # Check that we can change our password password = '******' r = self.client.POST( '/alice/settings/edit', {'new-password': password}, cookies=cookies, raise_immediately=False, ) assert r.code == 302 alice2 = Participant.authenticate(alice.id, 0, password) assert alice2 and alice2 == alice