Example #1
0
    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
Example #2
0
 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
Example #3
0
 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
Example #4
0
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
Example #5
0
    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
Example #6
0
 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
Example #7
0
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
Example #8
0
 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
Example #9
0
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
Example #10
0
 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
Example #11
0
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
Example #14
0
 def test_b64encode_s_replaces_slash_with_underscore(self):
     # TheEnter?prise => VGhlRW50ZXI/cHJpc2U=
     assert b64encode_s('TheEnter?prise') == 'VGhlRW50ZXI_cHJpc2U~'
Example #15
0
 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']
Example #16
0
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)
Example #17
0
 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)
Example #18
0
 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)
Example #19
0
 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
Example #20
0
 def test_b64encode_s_replaces_equals_with_tilde(self):
     assert b64encode_s('TheEnterprise') == 'VGhlRW50ZXJwcmlzZQ~~'
Example #21
0
 def test_safe_base64_transcode_works_with_binary_data(self):
     utils.b64decode_s(utils.b64encode_s(b'\xff'))
Example #22
0
 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
Example #23
0
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)
Example #24
0
 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
Example #26
0
 def test_safe_base64_transcode_works_with_binary_data(self):
     utils.b64decode_s(utils.b64encode_s(b'\xff'))
Example #27
0
 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']
Example #28
0
 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