mirror of
https://github.com/postgres/pgweb.git
synced 2025-07-25 16:02:27 +00:00
Use encrypted cookie instead of session for oauth state data
During oauth logins we need to store some temporary data related to the users session. Previously we did this in the django session, but thanks to AI bots trying millions of logins every day (and never completing the process) we end up with many abandoned sessions in the db. To work around this, instead store the temporary data in an encrypted cookie passed to the browser. Since this cookie can be limited in scope to just the auth part of the site, the slightly larger cookie size doesn't matter, and we don't need to store any data at all server-side.
This commit is contained in:
@ -5,8 +5,14 @@ from django.views.decorators.http import require_POST, require_GET
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
import base64
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import urllib.parse
|
||||
from Cryptodome import Random
|
||||
from Cryptodome.Cipher import AES
|
||||
|
||||
from pgweb.util.misc import get_client_ip
|
||||
from pgweb.util.decorators import queryparams
|
||||
@ -28,7 +34,54 @@ def configure():
|
||||
os.environ['OAUTHLIB_RELAX_TOKEN_SCOPE'] = '1'
|
||||
|
||||
|
||||
def _perform_oauth_login(request, provider, email, firstname, lastname):
|
||||
_cookie_key = hashlib.sha512(settings.SECRET_KEY.encode()).digest()
|
||||
|
||||
|
||||
def set_encrypted_oauth_cookie_on(response, cookiecontent, path=None):
|
||||
cookiedata = json.dumps(cookiecontent)
|
||||
r = Random.new()
|
||||
nonce = r.read(16)
|
||||
encryptor = AES.new(_cookie_key, AES.MODE_SIV, nonce=nonce)
|
||||
cipher, tag = encryptor.encrypt_and_digest(cookiedata.encode('ascii'))
|
||||
response.set_cookie(
|
||||
'pgweb_oauth',
|
||||
urllib.parse.urlencode({
|
||||
'n': base64.urlsafe_b64encode(nonce),
|
||||
'c': base64.urlsafe_b64encode(cipher),
|
||||
't': base64.urlsafe_b64encode(tag),
|
||||
}),
|
||||
secure=settings.SESSION_COOKIE_SECURE,
|
||||
httponly=True,
|
||||
path=path or '/account/login/',
|
||||
)
|
||||
return response
|
||||
|
||||
|
||||
def get_encrypted_oauth_cookie(request):
|
||||
if 'pgweb_oauth' not in request.COOKIES:
|
||||
raise OAuthException("Secure cookie missing")
|
||||
|
||||
parts = urllib.parse.parse_qs(request.COOKIES['pgweb_oauth'])
|
||||
|
||||
decryptor = AES.new(
|
||||
_cookie_key,
|
||||
AES.MODE_SIV,
|
||||
base64.urlsafe_b64decode(parts['n'][0]),
|
||||
)
|
||||
s = decryptor.decrypt_and_verify(
|
||||
base64.urlsafe_b64decode(parts['c'][0]),
|
||||
base64.urlsafe_b64decode(parts['t'][0]),
|
||||
)
|
||||
|
||||
return json.loads(s)
|
||||
|
||||
|
||||
def delete_encrypted_oauth_cookie_on(response):
|
||||
response.delete_cookie('pgweb_oauth')
|
||||
return response
|
||||
|
||||
|
||||
def _perform_oauth_login(request, provider, email, firstname, lastname, nexturl):
|
||||
try:
|
||||
user = User.objects.get(email=email)
|
||||
except User.DoesNotExist:
|
||||
@ -36,11 +89,12 @@ def _perform_oauth_login(request, provider, email, firstname, lastname):
|
||||
|
||||
# Offer the user a chance to sign up. The full flow is
|
||||
# handled elsewhere, so store the details we got from
|
||||
# the oauth login in the session, and pass the user on.
|
||||
request.session['oauth_email'] = email
|
||||
request.session['oauth_firstname'] = firstname or ''
|
||||
request.session['oauth_lastname'] = lastname or ''
|
||||
return HttpResponseRedirect('/account/signup/oauth/')
|
||||
# the oauth login in a secure cookie, and pass the user on.
|
||||
return set_encrypted_oauth_cookie_on(HttpResponseRedirect('/account/signup/oauth/'), {
|
||||
'oauth_email': email,
|
||||
'oauth_firstname': firstname or '',
|
||||
'oauth_lastname': lastname or '',
|
||||
}, '/account/signup/oauth/')
|
||||
|
||||
log.info("Oauth signin of {0} using {1} from {2}.".format(email, provider, get_client_ip(request)))
|
||||
if UserProfile.objects.filter(user=user).exists():
|
||||
@ -50,11 +104,7 @@ def _perform_oauth_login(request, provider, email, firstname, lastname):
|
||||
|
||||
user.backend = settings.AUTHENTICATION_BACKENDS[0]
|
||||
django_login(request, user)
|
||||
n = request.session.pop('login_next')
|
||||
if n:
|
||||
return HttpResponseRedirect(n)
|
||||
else:
|
||||
return HttpResponseRedirect('/account/')
|
||||
return delete_encrypted_oauth_cookie_on(HttpResponseRedirect(nexturl or '/account/'))
|
||||
|
||||
|
||||
#
|
||||
@ -76,7 +126,9 @@ def _login_oauth(request, provider, authurl, tokenurl, scope, authdatafunc):
|
||||
|
||||
# Receiving a login request from the provider, so validate data
|
||||
# and log the user in.
|
||||
if request.GET.get('state', '') != request.session.pop('oauth_state'):
|
||||
oauthdata = get_encrypted_oauth_cookie(request)
|
||||
|
||||
if request.GET.get('state', '') != oauthdata['oauth_state']:
|
||||
log.warning("Invalid state received in {0} oauth2 step from {1}".format(provider, get_client_ip(request)))
|
||||
raise OAuthException("Invalid OAuth state received")
|
||||
|
||||
@ -93,7 +145,7 @@ def _login_oauth(request, provider, authurl, tokenurl, scope, authdatafunc):
|
||||
log.warning("Oauth signing using {0} was missing data: {1}".format(provider, e))
|
||||
return HttpResponse('OAuth login was missing critical data. To log in, you need to allow access to email, first name and last name!')
|
||||
|
||||
return _perform_oauth_login(request, provider, email, firstname, lastname)
|
||||
return _perform_oauth_login(request, provider, email, firstname, lastname, oauthdata['next'])
|
||||
else:
|
||||
log.info("Initiating {0} oauth2 step from {1}".format(provider, get_client_ip(request)))
|
||||
# First step is redirect to provider
|
||||
@ -101,10 +153,10 @@ def _login_oauth(request, provider, authurl, tokenurl, scope, authdatafunc):
|
||||
authurl,
|
||||
prompt='consent',
|
||||
)
|
||||
request.session['login_next'] = request.GET.get('next', '')
|
||||
request.session['oauth_state'] = state
|
||||
request.session.modified = True
|
||||
return HttpResponseRedirect(authorization_url)
|
||||
return set_encrypted_oauth_cookie_on(HttpResponseRedirect(authorization_url), {
|
||||
'next': request.POST.get('next', ''),
|
||||
'oauth_state': state,
|
||||
})
|
||||
|
||||
|
||||
#
|
||||
@ -124,8 +176,10 @@ def _login_oauth1(request, provider, requesturl, accessurl, baseauthurl, authdat
|
||||
r = oa.parse_authorization_response(request.build_absolute_uri())
|
||||
verifier = r.get('oauth_verifier')
|
||||
|
||||
ro_key = request.session.pop('ro_key')
|
||||
ro_secret = request.session.pop('ro_secret')
|
||||
oauthdata = get_encrypted_oauth_cookie(request)
|
||||
|
||||
ro_key = oauthdata['ro_key']
|
||||
ro_secret = oauthdata['ro_secret']
|
||||
|
||||
oa = OAuth1Session(client_id, client_secret, ro_key, ro_secret, verifier=verifier)
|
||||
tokens = oa.fetch_access_token(accessurl)
|
||||
@ -137,7 +191,7 @@ def _login_oauth1(request, provider, requesturl, accessurl, baseauthurl, authdat
|
||||
log.warning("Oauth1 signing using {0} was missing data: {1}".format(provider, e))
|
||||
return HttpResponse('OAuth login was missing critical data. To log in, you need to allow access to email, first name and last name!')
|
||||
|
||||
return _perform_oauth_login(request, provider, email, firstname, lastname)
|
||||
return _perform_oauth_login(request, provider, email, firstname, lastname, oauthdata['next'])
|
||||
else:
|
||||
log.info("Initiating {0} oauth1 step from {1}".format(provider, get_client_ip(request)))
|
||||
|
||||
@ -145,11 +199,11 @@ def _login_oauth1(request, provider, requesturl, accessurl, baseauthurl, authdat
|
||||
fr = oa.fetch_request_token(requesturl)
|
||||
authorization_url = oa.authorization_url(baseauthurl)
|
||||
|
||||
request.session['login_next'] = request.GET.get('next', '')
|
||||
request.session['ro_key'] = fr.get('oauth_token')
|
||||
request.session['ro_secret'] = fr.get('oauth_token_secret')
|
||||
request.session.modified = True
|
||||
return HttpResponseRedirect(authorization_url)
|
||||
return set_encrypted_oauth_cookie_on(HttpResponseRedirect(authorization_url), {
|
||||
'next': request.POST.get('next', ''),
|
||||
'ro_key': fr.get('oauth_token'),
|
||||
'ro_secret': fr.get('oauth_token_secret'),
|
||||
})
|
||||
|
||||
|
||||
#
|
||||
|
@ -44,6 +44,7 @@ from .forms import CommunityAuthConsentForm
|
||||
from .forms import SignupForm, SignupOauthForm
|
||||
from .forms import UserForm, UserProfileForm, ContributorForm
|
||||
from .forms import AddEmailForm, PgwebPasswordResetForm
|
||||
from .oauthclient import get_encrypted_oauth_cookie, delete_encrypted_oauth_cookie_on
|
||||
|
||||
import logging
|
||||
|
||||
@ -541,61 +542,55 @@ def signup_complete(request):
|
||||
@transaction.atomic
|
||||
@queryparams('do_abort')
|
||||
def signup_oauth(request):
|
||||
if 'oauth_email' not in request.session \
|
||||
or 'oauth_firstname' not in request.session \
|
||||
or 'oauth_lastname' not in request.session:
|
||||
cookiedata = get_encrypted_oauth_cookie(request)
|
||||
|
||||
if 'oauth_email' not in cookiedata \
|
||||
or 'oauth_firstname' not in cookiedata \
|
||||
or 'oauth_lastname' not in cookiedata:
|
||||
return HttpSimpleResponse(request, "OAuth error", 'Invalid redirect received')
|
||||
|
||||
# Is this email already on a different account as a secondary one?
|
||||
if SecondaryEmail.objects.filter(email=request.session['oauth_email'].lower()).exists():
|
||||
if SecondaryEmail.objects.filter(email=cookiedata['oauth_email'].lower()).exists():
|
||||
return HttpSimpleResponse(request, "OAuth error", 'This email address is already attached to a different account')
|
||||
|
||||
if request.method == 'POST':
|
||||
# Second stage, so create the account. But verify that the
|
||||
# nonce matches.
|
||||
data = request.POST.copy()
|
||||
data['email'] = request.session['oauth_email'].lower()
|
||||
data['first_name'] = request.session['oauth_firstname']
|
||||
data['last_name'] = request.session['oauth_lastname']
|
||||
data['email'] = cookiedata['oauth_email'].lower()
|
||||
data['first_name'] = cookiedata['oauth_firstname']
|
||||
data['last_name'] = cookiedata['oauth_lastname']
|
||||
form = SignupOauthForm(data=data)
|
||||
if form.is_valid():
|
||||
log.info("Creating user for {0} from {1} from oauth signin of email {2}".format(form.cleaned_data['username'], get_client_ip(request), request.session['oauth_email']))
|
||||
log.info("Creating user for {0} from {1} from oauth signin of email {2}".format(form.cleaned_data['username'], get_client_ip(request), cookiedata['oauth_email']))
|
||||
|
||||
user = User.objects.create_user(form.cleaned_data['username'].lower(),
|
||||
request.session['oauth_email'].lower(),
|
||||
cookiedata['oauth_email'].lower(),
|
||||
last_login=datetime.now())
|
||||
user.first_name = request.session['oauth_firstname']
|
||||
user.last_name = request.session['oauth_lastname']
|
||||
user.first_name = cookiedata['oauth_firstname']
|
||||
user.last_name = cookiedata['oauth_lastname']
|
||||
user.password = OAUTH_PASSWORD_STORE
|
||||
user.save()
|
||||
|
||||
# Clean up our session
|
||||
del request.session['oauth_email']
|
||||
del request.session['oauth_firstname']
|
||||
del request.session['oauth_lastname']
|
||||
request.session.modified = True
|
||||
|
||||
# We can immediately log the user in because their email
|
||||
# is confirmed.
|
||||
user.backend = settings.AUTHENTICATION_BACKENDS[0]
|
||||
django_login(request, user)
|
||||
|
||||
# Redirect to the sessions page, or to the account page
|
||||
# Redirect to the page stored in the cookie, or to the account page
|
||||
# if none was given.
|
||||
return HttpResponseRedirect(request.session.pop('login_next', '/account/'))
|
||||
return delete_encrypted_oauth_cookie_on(
|
||||
HttpResponseRedirect(cookiedata.get('login_next', '/account/'))
|
||||
)
|
||||
elif 'do_abort' in request.GET:
|
||||
del request.session['oauth_email']
|
||||
del request.session['oauth_firstname']
|
||||
del request.session['oauth_lastname']
|
||||
request.session.modified = True
|
||||
return HttpResponseRedirect(request.session.pop('login_next', '/'))
|
||||
return delete_encrypted_oauth_cookie_on(HttpResponseRedirect(cookiedata.get('login_next', '/')))
|
||||
else:
|
||||
# Generate possible new username
|
||||
suggested_username = request.session['oauth_email'].replace('@', '.')[:30]
|
||||
suggested_username = cookiedata['oauth_email'].replace('@', '.')[:30]
|
||||
|
||||
# Auto generation requires firstname and lastname to be specified
|
||||
f = request.session['oauth_firstname'].lower()
|
||||
l = request.session['oauth_lastname'].lower()
|
||||
f = cookiedata['oauth_firstname'].lower()
|
||||
l = cookiedata['oauth_lastname'].lower()
|
||||
if f and l:
|
||||
for u in itertools.chain([
|
||||
"{0}{1}".format(f, l[0]),
|
||||
@ -607,9 +602,9 @@ def signup_oauth(request):
|
||||
|
||||
form = SignupOauthForm(initial={
|
||||
'username': suggested_username,
|
||||
'email': request.session['oauth_email'].lower(),
|
||||
'first_name': request.session['oauth_firstname'][:30],
|
||||
'last_name': request.session['oauth_lastname'][:30],
|
||||
'email': cookiedata['oauth_email'].lower(),
|
||||
'first_name': cookiedata['oauth_firstname'][:30],
|
||||
'last_name': cookiedata['oauth_lastname'][:30],
|
||||
})
|
||||
|
||||
return render_pgweb(request, 'account', 'account/signup_oauth.html', {
|
||||
|
Reference in New Issue
Block a user