Esempio n. 1
0
def sign_in_with_form_data(body, state):
    p = None
    _, website = state['_'], state['website']

    if body.get('log-in.id'):
        request = state['request']
        src_addr, src_country = request.source, request.country
        input_id = body['log-in.id'].strip()
        password = body.pop('log-in.password', None)
        id_type = None
        if input_id.find('@') > 0:
            id_type = 'email'
        elif input_id.startswith('~'):
            id_type = 'immutable'
        elif set(input_id).issubset(ASCII_ALLOWED_IN_USERNAME):
            id_type = 'username'
        if password and id_type:
            website.db.hit_rate_limit('log-in.password.ip-addr', str(src_addr),
                                      TooManyLogInAttempts)
            if id_type == 'immutable':
                p_id = Participant.check_id(input_id[1:])
            else:
                p_id = Participant.get_id_for(id_type, input_id)
            try:
                p = Participant.authenticate_with_password(p_id, password)
            except AccountIsPasswordless:
                if id_type == 'email':
                    state['log-in.email'] = input_id
                else:
                    state['log-in.error'] = _(
                        "The submitted password is incorrect.")
                return
            if not p:
                state['log-in.error'] = (_(
                    "The submitted password is incorrect."
                ) if p_id is not None else _(
                    "“{0}” is not a valid account ID.", input_id
                ) if id_type == 'immutable' else _(
                    "No account has the username “{username}”.",
                    username=input_id
                ) if id_type == 'username' else _(
                    "No account has “{email_address}” as its primary email address.",
                    email_address=input_id))
            else:
                website.db.decrement_rate_limit('log-in.password.ip-addr',
                                                str(src_addr))
                try:
                    p.check_password(password, context='login')
                except Exception as e:
                    website.tell_sentry(e)
        elif id_type == 'email':
            website.db.hit_rate_limit('log-in.email.ip-addr', str(src_addr),
                                      TooManyLogInAttempts)
            website.db.hit_rate_limit('log-in.email.ip-net',
                                      get_ip_net(src_addr),
                                      TooManyLogInAttempts)
            website.db.hit_rate_limit('log-in.email.country', src_country,
                                      TooManyLogInAttempts)
            email = input_id
            p = Participant.from_email(email)
            if p and p.kind == 'group':
                state['log-in.error'] = _(
                    "{0} is linked to a team account. It's not possible to log in as a team.",
                    email)
            elif p:
                if not p.get_email(email).verified:
                    website.db.hit_rate_limit('log-in.email.not-verified',
                                              email, TooManyLoginEmails)
                website.db.hit_rate_limit('log-in.email', p.id,
                                          TooManyLoginEmails)
                email_row = p.get_email(email)
                p.send_email('login_link',
                             email_row,
                             link_validity=SESSION_TIMEOUT)
                state['log-in.email-sent-to'] = email
                raise LoginRequired
            else:
                state['log-in.error'] = _(
                    "No account has “{email_address}” as its primary email address.",
                    email_address=email)
            p = None
        else:
            state['log-in.error'] = _("\"{0}\" is not a valid email address.",
                                      input_id)
            return

    elif 'sign-in.email' in body:
        response = state['response']
        # Check the submitted data
        kind = body.pop('sign-in.kind', 'individual')
        if kind not in ('individual', 'organization'):
            raise response.invalid_input(kind, 'sign-in.kind', 'body')
        email = body['sign-in.email']
        if not email:
            raise response.error(400, 'email is required')
        email = normalize_and_check_email_address(email)
        currency = (body.get('sign-in.currency') or body.get('currency')
                    or state.get('currency') or 'EUR')
        if currency not in CURRENCIES:
            raise response.invalid_input(currency, 'sign-in.currency', 'body')
        password = body.get('sign-in.password')
        if password:
            l = len(password)
            if l < PASSWORD_MIN_SIZE or l > PASSWORD_MAX_SIZE:
                raise BadPasswordSize
        username = body.get('sign-in.username')
        if username:
            username = username.strip()
            Participant.check_username(username)
        session_token = body.get('sign-in.token', '')
        if session_token:
            Participant.check_session_token(session_token)
        # Check for an existing account
        is_duplicate_request = website.db.hit_rate_limit(
            'sign-up.email', email) is None
        for i in range(5):
            existing_account = website.db.one(
                """
                SELECT p
                  FROM emails e
                  JOIN participants p ON p.id = e.participant
                 WHERE lower(e.address) = lower(%s)
                   AND ( e.verified IS TRUE OR
                         e.added_time > (current_timestamp - interval '1 day') OR
                         p.email IS NULL )
              ORDER BY p.join_time DESC
                 LIMIT 1
            """, (email, ))
            if is_duplicate_request and not existing_account:
                # The other thread hasn't created the account yet.
                sleep(1)
            else:
                break
        if existing_account:
            session = website.db.one(
                """
                SELECT id, secret, mtime
                  FROM user_secrets
                 WHERE participant = %s
                   AND id = 1
                   AND mtime < (%s + interval '6 hours')
                   AND mtime > (current_timestamp - interval '6 hours')
            """, (existing_account.id, existing_account.join_time))
            if session and constant_time_compare(session_token,
                                                 session.secret.split('.')[0]):
                p = existing_account
                p.authenticated = True
                p.sign_in(response.headers.cookie, session=session)
                return p
            else:
                raise EmailAlreadyTaken(email)
        username_taken = website.db.one(
            """
            SELECT count(*)
              FROM participants p
             WHERE p.username = %s
        """, (username, ))
        if username_taken:
            raise UsernameAlreadyTaken(username)
        # Rate limit
        request = state['request']
        src_addr, src_country = request.source, request.country
        website.db.hit_rate_limit('sign-up.ip-addr', str(src_addr),
                                  TooManySignUps)
        website.db.hit_rate_limit('sign-up.ip-net', get_ip_net(src_addr),
                                  TooManySignUps)
        website.db.hit_rate_limit('sign-up.country', src_country,
                                  TooManySignUps)
        website.db.hit_rate_limit('sign-up.ip-version', src_addr.version,
                                  TooManySignUps)
        # Okay, create the account
        with website.db.get_cursor() as c:
            request_data = {
                'url': request.line.uri.decoded,
                'headers': get_recordable_headers(request),
            }
            p = Participant.make_active(kind,
                                        currency,
                                        username,
                                        cursor=c,
                                        request_data=request_data)
            p.set_email_lang(state['locale'].language, cursor=c)
            p.add_email(email, cursor=c)
        if password:
            p.update_password(password)
            p.check_password(password, context='login')
        p.authenticated = True
        p.sign_in(response.headers.cookie, token=session_token, suffix='.in')
        website.logger.info(f"a new participant has joined: ~{p.id}")
        # We're done, we can clean up the body now
        body.pop('sign-in.email')
        body.pop('sign-in.currency', None)
        body.pop('sign-in.password', None)
        body.pop('sign-in.username', None)
        body.pop('sign-in.token', None)

    return p
    def test_email_login(self):
        email = '*****@*****.**'
        alice = self.make_participant('alice', email=None)
        alice.add_email(email)
        alice.close()

        # 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_with_password(alice.id, password)
        assert alice2 and alice2 == alice