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
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:
# 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():
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/'))
#
# 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")
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
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,
+ })
#
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)
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)))
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'),
+ })
#
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
@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]),
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', {