def authenticate_user_if_possible(request, response, state, user, _):
    """This signs the user in.
    """
    if request.line.uri.startswith(b'/assets/'):
        return

    if not state['website'].db:
        return

    # Cookie and form auth
    # We want to try cookie auth first, but we want form 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_url = request.line.uri.decoded
    if request.method == 'POST':
        body = _get_body(request)
        if body:
            p = sign_in_with_form_data(body, state)
            carry_on = body.pop('log-in.carry-on', None)
            if not p and carry_on:
                p_email = session_p and (
                    session_p.email or session_p.get_any_email()
                )
                if p_email != carry_on:
                    state['log-in.carry-on'] = carry_on
                    raise LoginRequired
            redirect_url = body.get('sign-in.back-to') or redirect_url
    elif request.method == 'GET' and request.qs.get('log-in.id'):
        id = request.qs.pop('log-in.id')
        session_id = request.qs.pop('log-in.key', 1)
        token = request.qs.pop('log-in.token', None)
        p = Participant.authenticate(id, session_id, token)
        if not p and (not session_p or session_p.id != id):
            raise response.error(400, _("This login link is expired or invalid."))
        else:
            qs = '?' + urlencode(request.qs, doseq=True) if request.qs else ''
            redirect_url = request.path.raw + qs
            session_p = p
            session_suffix = '.em'
    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
        if request.body.pop('form.repost', None) != 'true':
            response.redirect(redirect_url, trusted_url=False)
Example #2
0
def sign_in_with_form_data(body, state):
    p = None
    _, website = state['_'], state['website']

    if body.get('log-in.id'):
        id = body.pop('log-in.id')
        password = body.pop('log-in.password', None)
        k = 'email' if '@' in id else 'username'
        if password:
            p = Participant.authenticate(
                k, 'password',
                id, password,
            )
            if not p:
                state['log-in.error'] = _("Bad username or password.")
            if p and p.status == 'closed':
                p.update_status('active')
        elif k == 'username':
            state['log-in.error'] = _("\"{0}\" is not a valid email address.", id)
            return
        else:
            email = id
            p = Participant._from_thing('email', email)
            if p:
                p.start_session()
                qs = {'log-in.id': p.id, 'log-in.token': p.session_token}
                p.send_email(
                    'login_link',
                    email=email,
                    link=p.url('settings/', qs),
                    link_validity=SESSION_TIMEOUT,
                )
                state['log-in.email-sent-to'] = email
            else:
                state['log-in.error'] = _(
                    "We didn't find any account whose primary email address is {0}.",
                    email
                )
            p = None

    elif 'sign-in.email' in body:
        response = state['response']
        kind = body.pop('sign-in.kind')
        if kind not in ('individual', 'organization'):
            raise response.error(400, 'bad kind')
        email = body.pop('sign-in.email')
        if not email:
            raise response.error(400, 'email is required')
        with website.db.get_cursor() as c:
            p = Participant.make_active(
                kind, body.pop('sign-in.username', None),
                body.pop('sign-in.password', None), cursor=c,
            )
            p.set_email_lang(state['request'].headers.get(b'Accept-Language'), cursor=c)
            p.add_email(email, cursor=c)
        p.authenticated = True

    return p
Example #3
0
def sign_in_with_form_data(body, state):
    p = None
    _, website = state['_'], state['website']

    if body.get('log-in.id'):
        id = body.pop('log-in.id')
        k = 'email' if '@' in id else 'username'
        p = Participant.authenticate(
            k, 'password',
            id, body.pop('log-in.password')
        )
        if not p:
            state['sign-in.error'] = _("Bad username or password.")
        if p and p.status == 'closed':
            p.update_status('active')

    elif body.get('sign-in.username'):
        if body.pop('sign-in.terms') != 'agree':
            raise Response(400, 'you have to agree to the terms')
        kind = body.pop('sign-in.kind')
        if kind not in ('individual', 'organization'):
            raise Response(400, 'bad kind')
        with website.db.get_cursor() as c:
            p = Participant.make_active(
                body.pop('sign-in.username'), kind, body.pop('sign-in.password'),
                cursor=c
            )
            p.add_email(body.pop('sign-in.email'), cursor=c)
        p.authenticated = True

    elif body.get('email-login.email'):
        email = body.pop('email-login.email')
        p = Participant._from_thing('email', email)
        if p:
            p.start_session()
            qs = {'log-in.id': p.id, 'log-in.token': p.session_token}
            p.send_email(
                'password_reset',
                email=email,
                link=p.url('settings/', qs),
                link_validity=SESSION_TIMEOUT,
            )
            state['email-login.sent-to'] = email
        else:
            state['sign-in.error'] = _(
                "We didn't find any account whose primary email address is {0}.",
                email
            )
        p = None

    return p
def authenticate_user_if_possible(request, user):
    """This signs the user in.
    """
    if request.line.uri.startswith('/assets/'):
        return
    if 'Authorization' in request.headers:
        header = request.headers['authorization']
        if not header.startswith('Basic '):
            raise Response(401, 'Unsupported authentication method')
        try:
            creds = binascii.a2b_base64(header[len('Basic '):]).split(':', 1)
        except binascii.Error:
            raise Response(400, 'Malformed "Authorization" header')
        participant = Participant.authenticate('id', 'password', *creds)
        if not participant:
            raise Response(401)
        _turn_off_csrf(request)
        return {'user': participant}
    elif SESSION in request.headers.cookie:
        creds = request.headers.cookie[SESSION].value.split(':', 1)
        p = Participant.authenticate('id', 'session', *creds)
        if p:
            return {'user': p}
Example #5
0
def sign_in_with_form_data(body, state):
    p = None
    _, website = state['_'], state['website']

    if body.get('log-in.id'):
        id = body.pop('log-in.id')
        k = 'email' if '@' in id else 'username'
        p = Participant.authenticate(k, 'password', id,
                                     body.pop('log-in.password'))
        if not p:
            state['sign-in.error'] = _("Bad username or password.")
        if p and p.status == 'closed':
            p.update_status('active')

    elif body.get('sign-in.username'):
        if body.pop('sign-in.terms') != 'agree':
            raise Response(400, 'you have to agree to the terms')
        kind = body.pop('sign-in.kind')
        if kind not in ('individual', 'organization'):
            raise Response(400, 'bad kind')
        with website.db.get_cursor() as c:
            p = Participant.make_active(body.pop('sign-in.username'),
                                        kind,
                                        body.pop('sign-in.password'),
                                        cursor=c)
            p.add_email(body.pop('sign-in.email'), cursor=c)
        p.authenticated = True

    elif body.get('email-login.email'):
        email = body.pop('email-login.email')
        p = Participant._from_thing('email', email)
        if p:
            p.start_session()
            qs = {'log-in.id': p.id, 'log-in.token': p.session_token}
            p.send_email(
                'password_reset',
                email=email,
                link=p.url('settings/', qs),
                link_validity=SESSION_TIMEOUT,
            )
            state['email-login.sent-to'] = email
        else:
            state['sign-in.error'] = _(
                "We didn't find any account whose primary email address is {0}.",
                email)
        p = None

    return p
Example #6
0
    def test_email_login(self):
        email = '*****@*****.**'
        alice = self.make_participant('alice')
        alice.add_email(email)
        alice.close(None)

        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')

        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' % (
            alice.id, session.id, session.secret)
        assert qs in last_email['text']

        r = self.client.GxT('/alice/?foo=bar&' + qs)
        assert r.code == 302
        assert r.headers[b'Location'] == b'http://localhost/alice/?foo=bar'
        # ↑ 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 we can change our password
        password = '******'
        r = self.client.POST(
            '/alice/settings/edit',
            {'new-password': password},
            cookies=r.headers.cookie,
            raise_immediately=False,
        )
        assert r.code == 302
        alice2 = Participant.authenticate(alice.id, 0, password)
        assert alice2 and alice2 == alice
Example #7
0
    def test_email_login(self):
        email = '*****@*****.**'
        alice = self.make_participant('alice')
        alice.add_email(email)
        alice.close(None)

        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')

        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' % (
            alice.id, session.id, session.secret
        )
        assert qs in last_email['text']

        r = self.client.GxT('/alice/?foo=bar&' + qs)
        assert r.code == 302
        assert r.headers[b'Location'] == b'http://localhost/alice/?foo=bar'
        # ↑ 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 we can change our password
        password = '******'
        r = self.client.POST(
            '/alice/settings/edit',
            {'new-password': password},
            cookies=r.headers.cookie,
            raise_immediately=False,
        )
        assert r.code == 302
        alice2 = Participant.authenticate(alice.id, 0, password)
        assert alice2 and alice2 == alice
Example #8
0
    def test_email_login(self):
        email = '*****@*****.**'
        alice = self.make_participant('alice')
        alice.add_email(email)
        alice.close(None)

        data = {'log-in.id': email.upper()}
        r = self.client.POST('/', data, raise_immediately=False)
        alice = alice.refetch()
        assert alice.session_token not in r.headers.raw.decode('ascii')
        assert alice.session_token not in r.body.decode('utf8')

        Participant.dequeue_emails()
        last_email = self.get_last_email()
        assert last_email and last_email['subject'] == 'Log in to Liberapay'
        assert 'log-in.token='+alice.session_token in last_email['text']

        url = '/alice/?foo=bar&log-in.id=%s&log-in.token=%s'
        r = self.client.GxT(url % (alice.id, alice.session_token))
        alice2 = alice.refetch()
        assert alice2.session_token != alice.session_token
        # ↑ this means that the link is only valid once
        assert r.code == 302
        assert r.headers[b'Location'] == b'http://localhost/alice/?foo=bar'
        # ↑ checks that original path and query are preserved

        # Check that we can change our password
        password = '******'
        r = self.client.POST(
            '/alice/settings/edit',
            {'new-password': password},
            cookies=r.headers.cookie,
            raise_immediately=False,
        )
        assert r.code == 302
        alice2 = Participant.authenticate('id', 'password', alice.id, password)
        assert alice2 and alice2 == alice
    def test_email_login(self):
        email = '*****@*****.**'
        alice = self.make_participant('alice')
        alice.add_email(email)
        alice.close(None)

        data = {'log-in.id': email.upper()}
        r = self.client.POST('/', data, raise_immediately=False)
        alice = alice.refetch()
        assert alice.session_token not in r.headers.raw.decode('ascii')
        assert alice.session_token not in r.body.decode('utf8')

        Participant.dequeue_emails()
        last_email = self.get_last_email()
        assert last_email and last_email['subject'] == 'Log in to Liberapay'
        assert 'log-in.token=' + alice.session_token in last_email['text']

        url = '/alice/?foo=bar&log-in.id=%s&log-in.token=%s'
        r = self.client.GxT(url % (alice.id, alice.session_token))
        alice2 = alice.refetch()
        assert alice2.session_token != alice.session_token
        # ↑ this means that the link is only valid once
        assert r.code == 302
        assert r.headers[b'Location'] == b'http://localhost/alice/?foo=bar'
        # ↑ checks that original path and query are preserved

        # Check that we can change our password
        password = '******'
        r = self.client.POST(
            '/alice/settings/edit',
            {'new-password': password},
            cookies=r.headers.cookie,
            raise_immediately=False,
        )
        assert r.code == 302
        alice2 = Participant.authenticate('id', 'password', alice.id, password)
        assert alice2 and alice2 == alice
def sign_in_with_form_data(body, state):
    p = None
    _, website = state['_'], state['website']

    if body.get('log-in.id'):
        id = body.pop('log-in.id')
        password = body.pop('log-in.password', None)
        k = 'email' if '@' in id else 'username'
        if password:
            p = Participant.authenticate(
                k, 'password',
                id, password,
            )
            if not p:
                state['log-in.error'] = _("Bad username or password.")
        elif k == 'username':
            state['log-in.error'] = _("\"{0}\" is not a valid email address.", id)
            return
        else:
            email = id
            p = Participant._from_thing('lower(email)', email.lower())
            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', TooManyLoginEmails)
                website.db.hit_rate_limit('log-in.email', p.id, TooManyLoginEmails)
                p.start_session()
                qs = {'log-in.id': p.id, 'log-in.token': p.session_token}
                p.send_email(
                    'login_link',
                    email,
                    link=p.url('settings/', qs),
                    link_validity=SESSION_TIMEOUT,
                )
                state['log-in.email-sent-to'] = email
                raise LoginRequired
            else:
                state['log-in.error'] = _(
                    "We didn't find any account whose primary email address is {0}.",
                    email
                )
            p = None

    elif 'sign-in.email' in body:
        response = state['response']
        kind = body.pop('sign-in.kind', 'individual')
        if kind not in ('individual', 'organization'):
            raise response.error(400, 'bad kind')
        email = body.pop('sign-in.email')
        if not email:
            raise response.error(400, 'email is required')
        currency = body.pop('sign-in.currency', state.get('currency'))
        if currency and currency not in CURRENCIES:
            raise response.error(400, "`currency` value '%s' is invalid of non-supported" % currency)
        src_addr = state['request'].source
        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.ip-version', src_addr.version, TooManySignUps)
        with website.db.get_cursor() as c:
            p = Participant.make_active(
                kind, body.pop('sign-in.username', None),
                body.pop('sign-in.password', None), currency=currency, cursor=c,
            )
            p.set_email_lang(state['request'].headers.get(b'Accept-Language'), cursor=c)
            p.add_email(email, cursor=c)
        p.authenticated = True

    return p
def authenticate_user_if_possible(request, response, state, user, _):
    """This signs the user in.
    """
    if request.line.uri.startswith('/assets/'):
        return

    if not state['website'].db:
        return

    # HTTP auth
    if b'Authorization' in request.headers:
        header = request.headers[b'Authorization']
        if not header.startswith(b'Basic '):
            raise response.error(401, 'Unsupported authentication method')
        try:
            uid, pwd = binascii.a2b_base64(header[len('Basic '):]).decode('utf8').split(':', 1)
        except (binascii.Error, UnicodeDecodeError, ValueError):
            raise response.error(400, 'Malformed "Authorization" header')
        if not uid.isdigit():
            raise response.error(401, 'Invalid user id: expected an integer, got `%s`' % uid)
        participant = Participant.authenticate('id', 'password', uid, pwd)
        if not participant:
            raise response.error(401, 'Invalid credentials')
        return {'user': participant}

    # Cookie and form auth
    # We want to try cookie auth first, but we want form auth to supersede it
    p = None
    if SESSION in request.headers.cookie:
        creds = request.headers.cookie[SESSION].value.split(':', 1)
        p = Participant.authenticate('id', 'session', *creds)
        if p:
            state['user'] = p
    session_p, p = p, None
    session_suffix = ''
    redirect_url = request.line.uri
    if request.method == 'POST':
        body = _get_body(request)
        if body:
            p = sign_in_with_form_data(body, state)
            carry_on = body.pop('log-in.carry-on', None)
            if not p and carry_on:
                p_email = session_p and (
                    session_p.email or session_p.get_emails()[0].address
                )
                if p_email != carry_on:
                    state['log-in.carry-on'] = carry_on
                    raise LoginRequired
            redirect_url = body.get('sign-in.back-to') or redirect_url
    elif request.method == 'GET' and request.qs.get('log-in.id'):
        id, token = request.qs.pop('log-in.id'), request.qs.pop('log-in.token')
        p = Participant.authenticate('id', 'session', id, token)
        if not p and (not session_p or session_p.id != id):
            raise response.error(400, _("This login link is expired or invalid."))
        else:
            qs = '?' + urlencode(request.qs, doseq=True) if request.qs else ''
            redirect_url = request.path.raw + qs
            session_p = p
            session_suffix = '.em'
    if p:
        if session_p:
            session_p.sign_out(response.headers.cookie)
        if p.status == 'closed':
            p.update_status('active')
        p.sign_in(response.headers.cookie, session_suffix)
        state['user'] = p
        if request.body.pop('form.repost', None) != 'true':
            response.redirect(redirect_url, trusted_url=False)
    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
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
        website.db.hit_rate_limit('log-in.ip-addr', str(src_addr),
                                  TooManyLogInAttempts)
        website.db.hit_rate_limit('log-in.country', src_country,
                                  TooManyLogInAttempts)
        id = body.pop('log-in.id').strip()
        password = body.pop('log-in.password', None)
        k = 'email' if '@' in id else 'username'
        if password:
            id = Participant.get_id_for(k, id)
            p = Participant.authenticate(id, 0, password)
            if not p:
                state['log-in.error'] = _("Bad username or password.")
            else:
                try:
                    p.check_password(password, context='login')
                except Exception as e:
                    website.tell_sentry(e, state)

        elif k == 'username':
            state['log-in.error'] = _("\"{0}\" is not a valid email address.",
                                      id)
            return
        else:
            email = 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)
                p.start_session()
                qs = [('log-in.id', p.id), ('log-in.key', p.session.id),
                      ('log-in.token', p.session.secret)]
                p.send_email(
                    'login_link',
                    email,
                    link=p.url('settings/', qs),
                    link_validity=SESSION_TIMEOUT,
                )
                state['log-in.email-sent-to'] = email
                raise LoginRequired
            else:
                state['log-in.error'] = _(
                    "We didn't find any account whose primary email address is {0}.",
                    email)
            p = None

    elif 'sign-in.email' in body:
        response = state['response']
        kind = body.pop('sign-in.kind', 'individual')
        if kind not in ('individual', 'organization'):
            raise response.error(400, 'bad kind')
        email = body.pop('sign-in.email')
        if not email:
            raise response.error(400, 'email is required')
        currency = body.pop('sign-in.currency', 'EUR')
        if currency not in CURRENCIES:
            raise response.error(
                400,
                "`currency` value '%s' is invalid of non-supported" % currency)
        password = body.pop('sign-in.password', None)
        if password:
            l = len(password)
            if l < PASSWORD_MIN_SIZE or l > PASSWORD_MAX_SIZE:
                raise BadPasswordSize
        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)
        with website.db.get_cursor() as c:
            p = Participant.make_active(
                kind,
                currency,
                body.pop('sign-in.username', None),
                cursor=c,
            )
            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

    return p
    def test_email_login(self):
        email = '*****@*****.**'
        alice = self.make_participant('alice')
        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']

        # Log in
        r = self.client.GxT('/alice/?foo=bar&' + qs)
        assert r.code == 302
        assert r.headers[b'Location'] == b'http://localhost/alice/?foo=bar'
        # ↑ 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
Example #15
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)
def authenticate_user_if_possible(csrf_token, 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.
    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 = Participant.authenticate(*creds)
            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:
            # 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
                    if not p.session:
                        session_suffix = '.pw'  # stands for "password"
    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('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/log-in-link-is-invalid.spt', state)
            p = Participant.authenticate(id, session_id, token)
            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'
            else:
                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']

        # 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 #17
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)
            website.db.hit_rate_limit('hash_password.ip-addr', str(src_addr), TooManyRequests)
            if id_type == 'immutable':
                p_id = Participant.check_id(input_id[1:])
            else:
                p_id = Participant.get_id_for(id_type, input_id)
            p = Participant.authenticate(p_id, 0, password)
            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, state)
        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, state)
        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:
            decode = lambda b: b.decode('ascii', 'backslashreplace')
            request_data = {
                'url': request.line.uri.decoded,
                'headers': {
                    decode(k): decode(b', '.join(v))
                    for k, v in request.headers.items()
                    if k != b'Cookie'
                },
            }
            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')
        # 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
Example #18
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
        website.db.hit_rate_limit('log-in.ip-addr', str(src_addr), TooManyLogInAttempts)
        website.db.hit_rate_limit('log-in.country', src_country, TooManyLogInAttempts)
        id = body.pop('log-in.id').strip()
        password = body.pop('log-in.password', None)
        k = 'email' if '@' in id else 'username'
        if password:
            id = Participant.get_id_for(k, id)
            p = Participant.authenticate(id, 0, password)
            if not p:
                state['log-in.error'] = _("Bad username or password.")
            else:
                try:
                    p.check_password(password, context='login')
                except Exception as e:
                    website.tell_sentry(e, state)

        elif k == 'username':
            state['log-in.error'] = _("\"{0}\" is not a valid email address.", id)
            return
        else:
            email = 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)
                p.start_session()
                qs = [
                    ('log-in.id', p.id),
                    ('log-in.key', p.session.id),
                    ('log-in.token', p.session.secret)
                ]
                p.send_email(
                    'login_link',
                    email,
                    link=p.url('settings/', qs),
                    link_validity=SESSION_TIMEOUT,
                )
                state['log-in.email-sent-to'] = email
                raise LoginRequired
            else:
                state['log-in.error'] = _(
                    "We didn't find any account whose primary email address is {0}.",
                    email
                )
            p = None

    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.pop('sign-in.email')
        if not email:
            raise response.error(400, 'email is required')
        email = normalize_email_address(email)
        check_email_blacklist(email)
        currency = body.pop('sign-in.currency', 'EUR')
        if currency not in CURRENCIES:
            raise response.invalid_input(currency, 'sign-in.currency', 'body')
        password = body.pop('sign-in.password', None)
        if password:
            l = len(password)
            if l < PASSWORD_MIN_SIZE or l > PASSWORD_MAX_SIZE:
                raise BadPasswordSize
        username = body.pop('sign-in.username', None)
        if username:
            username = username.strip()
            Participant.check_username(username)
        session_token = body.pop('sign-in.token', '')
        if session_token:
            Participant.check_session_token(session_token)
        # Check for an existing account
        existing_account = website.db.one("""
            SELECT p, s.secret
              FROM emails e
              JOIN participants p ON p.id = e.participant
         LEFT JOIN user_secrets s ON s.participant = p.id
                                 AND s.id = 1
                                 AND s.mtime < (p.join_time + interval '6 hours')
                                 AND s.mtime > (current_timestamp - interval '6 hours')
             WHERE lower(e.address) = lower(%s)
               AND ( e.verified IS TRUE OR
                     e.added_time > (current_timestamp - interval '1 day') OR
                     s.secret IS NOT NULL OR
                     p.email IS NULL )
          ORDER BY p.join_time DESC
             LIMIT 1
        """, (email,))
        if existing_account:
            p, secret = existing_account
            if secret and constant_time_compare(session_token, secret):
                p.authenticated = True
                p.sign_in(response.headers.cookie, token=session_token)
                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:
            p = Participant.make_active(kind, currency, username, cursor=c)
            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)

    return p
Example #19
0
def authenticate_user_if_possible(request, response, state, user, _):
    """This signs the user in.
    """
    if request.line.uri.startswith('/assets/'):
        return

    if not state['website'].db:
        return

    # HTTP auth
    if b'Authorization' in request.headers:
        header = request.headers[b'Authorization']
        if not header.startswith(b'Basic '):
            raise response.error(401, 'Unsupported authentication method')
        try:
            uid, pwd = binascii.a2b_base64(header[len('Basic '):]).decode('utf8').split(':', 1)
        except (binascii.Error, UnicodeDecodeError, ValueError):
            raise response.error(400, 'Malformed "Authorization" header')
        if not uid.isdigit():
            raise response.error(401, 'Invalid user id: expected an integer, got `%s`' % uid)
        participant = Participant.authenticate('id', 'password', uid, pwd)
        if not participant:
            raise response.error(401, 'Invalid credentials')
        return {'user': participant}

    # Cookie and form auth
    # We want to try cookie auth first, but we want form auth to supersede it
    p = None
    if SESSION in request.headers.cookie:
        creds = request.headers.cookie[SESSION].value.split(':', 1)
        p = Participant.authenticate('id', 'session', *creds)
        if p:
            state['user'] = p
    session_p, p = p, None
    session_suffix = ''
    redirect_url = request.line.uri
    if request.method == 'POST':
        body = _get_body(request)
        if body:
            p = sign_in_with_form_data(body, state)
            carry_on = body.pop('log-in.carry-on', None)
            if not p and carry_on:
                p_email = session_p and (
                    session_p.email or session_p.get_emails()[0].address
                )
                if p_email != carry_on:
                    state['log-in.carry-on'] = carry_on
                    raise LoginRequired
            redirect_url = body.get('sign-in.back-to') or redirect_url
    elif request.method == 'GET' and request.qs.get('log-in.id'):
        id, token = request.qs.pop('log-in.id'), request.qs.pop('log-in.token')
        p = Participant.authenticate('id', 'session', id, token)
        if not p and (not session_p or session_p.id != id):
            raise response.error(400, _("This login link is expired or invalid."))
        else:
            qs = '?' + urlencode(request.qs, doseq=True) if request.qs else ''
            redirect_url = request.path.raw + qs
            session_p = p
            session_suffix = '.em'
    if p:
        if session_p:
            session_p.sign_out(response.headers.cookie)
        if p.status == 'closed':
            p.update_status('active')
        p.sign_in(response.headers.cookie, session_suffix)
        state['user'] = p
        if request.body.pop('form.repost', None) != 'true':
            response.redirect(redirect_url, trusted_url=False)
Example #20
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_url = request.line.uri.decoded
    if request.method == 'POST':
        # Password auth
        body = _get_body(request)
        if body:
            p = sign_in_with_form_data(body, state)
            carry_on = body.pop('log-in.carry-on', None)
            if not p and 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
            redirect_url = body.get('sign-in.back-to') or redirect_url
    elif request.method == 'GET' and request.qs.get('log-in.id'):
        # Email auth
        id = request.qs.pop('log-in.id')
        session_id = request.qs.pop('log-in.key', 1)
        token = request.qs.pop('log-in.token', None)
        if not (token and token.endswith('.em')):
            raise response.error(400,
                                 _("This login link is expired or invalid."))
        p = Participant.authenticate(id, session_id, token)
        if not p and (not session_p or session_p.id != id):
            raise response.error(400,
                                 _("This login link is expired or invalid."))
        else:
            qs = '?' + urlencode(request.qs, doseq=True) if request.qs else ''
            redirect_url = request.path.raw + qs
            session_p = p
            session_suffix = '.em'

    # 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 = db.one(
            """
            SELECT p
              FROM emails e
              JOIN participants p On p.id = e.participant
             WHERE e.id = %s
        """, (email_id, ))
        if email_participant:
            result = email_participant.verify_email(email_id, email_nonce, p)
            state['email.verification-result'] = result
            if result == EmailVerificationResult.SUCCEEDED:
                del request.qs['email.id'], request.qs['email.nonce']
                qs = '?' + urlencode(request.qs,
                                     doseq=True) if request.qs else ''
                redirect_url = request.path.raw + qs
        del email_participant

    # Finish up
    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
        if request.body.pop('form.repost', None) != 'true':
            response.redirect(redirect_url, trusted_url=False)
Example #21
0
def authenticate_user_if_possible(request, state, user, _):
    """This signs the user in.
    """
    if request.line.uri.startswith('/assets/'):
        return

    # HTTP auth
    if 'Authorization' in request.headers:
        header = request.headers['authorization']
        if not header.startswith('Basic '):
            raise Response(401, 'Unsupported authentication method')
        try:
            creds = binascii.a2b_base64(header[len('Basic '):]).split(':', 1)
        except binascii.Error:
            raise Response(400, 'Malformed "Authorization" header')
        participant = Participant.authenticate('id', 'password', *creds)
        if not participant:
            raise Response(401)
        return {'user': participant}

    # Cookie and form auth
    # We want to try cookie auth first, but we want form auth to supersede it
    p = None
    response = state.setdefault('response', Response())
    if SESSION in request.headers.cookie:
        creds = request.headers.cookie[SESSION].value.split(':', 1)
        p = Participant.authenticate('id', 'session', *creds)
        if p:
            state['user'] = p
    session_p, p = p, None
    session_suffix = ''
    redirect_url = request.line.uri
    if request.method == 'POST':
        body = _get_body(request)
        if body:
            p = sign_in_with_form_data(body, state)
            carry_on = body.pop('email-login.carry-on', None)
            if not p and carry_on:
                p_email = session_p and (session_p.email
                                         or session_p.get_emails()[0].address)
                if p_email != carry_on:
                    state['email-login.carry-on'] = carry_on
                    raise AuthRequired
            redirect_url = body.get('sign-in.back-to') or redirect_url
    elif request.method == 'GET' and request.qs.get('log-in.id'):
        id, token = request.qs.pop('log-in.id'), request.qs.pop('log-in.token')
        p = Participant.authenticate('id', 'session', id, token)
        if not p and (not session_p or session_p.id != id):
            raise Response(400, _("This login link is expired or invalid."))
        else:
            qs = '?' + urlencode(request.qs, doseq=True) if request.qs else ''
            redirect_url = request.path.raw + qs
            session_p = p
            session_suffix = '.em'
    if p:
        if session_p:
            session_p.sign_out(response.headers.cookie)
        p.sign_in(response.headers.cookie, session_suffix)
        state['user'] = p
        if request.body.pop('form.repost', None) != 'true':
            response.redirect(redirect_url)
Example #22
0
def sign_in_with_form_data(body, state):
    p = None
    _, website = state['_'], state['website']

    if body.get('log-in.id'):
        id = body.pop('log-in.id')
        password = body.pop('log-in.password', None)
        k = 'email' if '@' in id else 'username'
        if password:
            p = Participant.authenticate(
                k, 'password',
                id, password,
            )
            if not p:
                state['log-in.error'] = _("Bad username or password.")
        elif k == 'username':
            state['log-in.error'] = _("\"{0}\" is not a valid email address.", id)
            return
        else:
            email = id
            p = Participant._from_thing('lower(email)', email.lower())
            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', TooManyLoginEmails)
                website.db.hit_rate_limit('log-in.email', p.id, TooManyLoginEmails)
                p.start_session()
                qs = {'log-in.id': p.id, 'log-in.token': p.session_token}
                p.send_email(
                    'login_link',
                    email,
                    link=p.url('settings/', qs),
                    link_validity=SESSION_TIMEOUT,
                )
                state['log-in.email-sent-to'] = email
                raise LoginRequired
            else:
                state['log-in.error'] = _(
                    "We didn't find any account whose primary email address is {0}.",
                    email
                )
            p = None

    elif 'sign-in.email' in body:
        response = state['response']
        kind = body.pop('sign-in.kind', 'individual')
        if kind not in ('individual', 'organization'):
            raise response.error(400, 'bad kind')
        email = body.pop('sign-in.email')
        if not email:
            raise response.error(400, 'email is required')
        currency = body.pop('sign-in.currency', state.get('currency'))
        if currency and currency not in CURRENCIES:
            raise response.error(400, "`currency` value '%s' is invalid of non-supported" % currency)
        src_addr = state['request'].source
        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.ip-version', src_addr.version, TooManySignUps)
        with website.db.get_cursor() as c:
            p = Participant.make_active(
                kind, body.pop('sign-in.username', None),
                body.pop('sign-in.password', None), currency=currency, cursor=c,
            )
            p.set_email_lang(state['request'].headers.get(b'Accept-Language'), cursor=c)
            p.add_email(email, cursor=c)
        p.authenticated = True

    return p
Example #23
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
        website.db.hit_rate_limit('log-in.ip-addr', str(src_addr), TooManyLogInAttempts)
        website.db.hit_rate_limit('log-in.country', src_country, TooManyLogInAttempts)
        id = body['log-in.id'].strip()
        password = body.pop('log-in.password', None)
        k = 'email' if '@' in id else 'username'
        if password:
            id = Participant.get_id_for(k, id)
            p = Participant.authenticate(id, 0, password)
            if not p:
                state['log-in.error'] = _("Bad username or password.")
            else:
                try:
                    p.check_password(password, context='login')
                except Exception as e:
                    website.tell_sentry(e, state)
        elif k == 'username':
            state['log-in.error'] = _("\"{0}\" is not a valid email address.", id)
            return
        else:
            email = 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'] = _(
                    "We didn't find any account whose primary email address is {0}.",
                    email
                )
            p = None

    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, state)
        currency = body.get('sign-in.currency', '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):
                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:
            p = Participant.make_active(kind, currency, username, cursor=c)
            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)
        # 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
Example #24
0
def authenticate_user_if_possible(request, state, user, _):
    """This signs the user in.
    """
    if request.line.uri.startswith('/assets/'):
        return

    # HTTP auth
    if 'Authorization' in request.headers:
        header = request.headers['authorization']
        if not header.startswith('Basic '):
            raise Response(401, 'Unsupported authentication method')
        try:
            creds = binascii.a2b_base64(header[len('Basic '):]).split(':', 1)
        except binascii.Error:
            raise Response(400, 'Malformed "Authorization" header')
        participant = Participant.authenticate('id', 'password', *creds)
        if not participant:
            raise Response(401)
        return {'user': participant}

    # Cookie and form auth
    # We want to try cookie auth first, but we want form auth to supersede it
    p = None
    response = state.setdefault('response', Response())
    if SESSION in request.headers.cookie:
        creds = request.headers.cookie[SESSION].value.split(':', 1)
        p = Participant.authenticate('id', 'session', *creds)
        if p:
            state['user'] = p
    session_p, p = p, None
    session_suffix = ''
    redirect_url = request.line.uri
    if request.method == 'POST':
        body = _get_body(request)
        if body:
            p = sign_in_with_form_data(body, state)
            carry_on = body.pop('email-login.carry-on', None)
            if not p and carry_on:
                p_email = session_p and (
                    session_p.email or session_p.get_emails()[0].address
                )
                if p_email != carry_on:
                    state['email-login.carry-on'] = carry_on
                    raise AuthRequired
            redirect_url = body.get('sign-in.back-to') or redirect_url
    elif request.method == 'GET' and request.qs.get('log-in.id'):
        id, token = request.qs.pop('log-in.id'), request.qs.pop('log-in.token')
        p = Participant.authenticate('id', 'session', id, token)
        if not p and (not session_p or session_p.id != id):
            raise Response(400, _("This login link is expired or invalid."))
        else:
            qs = '?' + urlencode(request.qs, doseq=True) if request.qs else ''
            redirect_url = request.path.raw + qs
            session_p = p
            session_suffix = '.em'
    if p:
        if session_p:
            session_p.sign_out(response.headers.cookie)
        p.sign_in(response.headers.cookie, session_suffix)
        state['user'] = p
        if request.body.pop('form.repost', None) != 'true':
            response.redirect(redirect_url)