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