Authenticate subdomains using single Google OAuth entry.

Previously, we used to create one Google OAuth callback url entry
per subdomain. This commit allows us to authenticate subdomain users
against a single Google OAuth callback url entry.
This commit is contained in:
Umair Khan 2016-10-14 17:12:16 +05:00 committed by Tim Abbott
parent 7a02ed42e7
commit 26646abe8c
4 changed files with 99 additions and 16 deletions

View File

@ -488,7 +488,7 @@ class ResponseMock(object):
class GoogleLoginTest(ZulipTestCase):
def google_oauth2_test(self, token_response, account_response):
# type: (ResponseMock, ResponseMock) -> HttpResponse
result = self.client_get("/accounts/login/google/")
result = self.client_get("/accounts/login/google/send/")
self.assertEquals(result.status_code, 302)
# Now extract the CSRF token from the redirect URL
parsed_url = urllib.parse.urlparse(result.url)
@ -647,7 +647,7 @@ class GoogleLoginTest(ZulipTestCase):
def test_google_oauth2_csrf_badstate(self):
# type: () -> None
with mock.patch("logging.warning") as m:
result = self.client_get("/accounts/login/google/done/?state=badstate:otherbadstate")
result = self.client_get("/accounts/login/google/done/?state=badstate:otherbadstate:")
self.assertEquals(result.status_code, 400)
self.assertEquals(m.call_args_list[0][0][0],
'Google oauth2 CSRF error')

View File

@ -51,6 +51,7 @@ from zproject.backends import password_auth_enabled
from confirmation.models import Confirmation, RealmCreationKey, check_key_is_valid
import requests
import ujson
import calendar
import datetime
@ -63,6 +64,26 @@ import logging
from zproject.jinja2 import render_to_response
def redirect_and_log_into_subdomain(realm, full_name, email_address):
# type: (Realm, text_type, text_type) -> HttpResponse
subdomain_login_uri = ''.join([
realm.uri,
reverse('zerver.views.auth.log_into_subdomain')
])
domain = '.' + settings.EXTERNAL_HOST.split(':')[0]
response = redirect(subdomain_login_uri)
data = {'name': full_name, 'email': email_address, 'subdomain': realm.subdomain}
# Creating a singed cookie so that it cannot be tampered with.
# Cookie and the signature expire in 15 seconds.
response.set_signed_cookie('subdomain.signature',
ujson.dumps(data),
expires=15,
domain=domain,
salt='zerver.views.auth')
return response
@require_post
def accounts_register(request):
# type: (HttpRequest) -> HttpResponse

View File

@ -11,17 +11,19 @@ from django.middleware.csrf import get_token
from django.shortcuts import redirect
from django.views.decorators.csrf import csrf_exempt
from django.utils.translation import ugettext as _
from django.core import signing
from six import text_type
from six.moves import urllib
from typing import Any, Dict, Optional
from typing import Any, Dict, Optional, Tuple
from confirmation.models import Confirmation
from zerver.forms import OurAuthenticationForm, WRONG_SUBDOMAIN_ERROR
from zerver.lib.request import REQ, has_request_variables, JsonableError
from zerver.lib.response import json_success, json_error
from zerver.lib.utils import get_subdomain
from zerver.models import PreregistrationUser, UserProfile, remote_user_to_email
from zerver.views import create_homepage_form, create_preregistration_user
from zerver.models import PreregistrationUser, UserProfile, remote_user_to_email, Realm
from zerver.views import create_homepage_form, create_preregistration_user, \
redirect_and_log_into_subdomain
from zproject.backends import password_auth_enabled, dev_auth_enabled, google_auth_enabled
from zproject.jinja2 import render_to_response
@ -31,6 +33,7 @@ import jwt
import logging
import requests
import time
import ujson
def maybe_send_to_registration(request, email, full_name=''):
# type: (HttpRequest, text_type, text_type) -> HttpResponse
@ -149,24 +152,41 @@ def google_oauth2_csrf(request, value):
def start_google_oauth2(request):
# type: (HttpRequest) -> HttpResponse
uri = 'https://accounts.google.com/o/oauth2/auth?'
main_site_uri = ''.join((
settings.EXTERNAL_URI_SCHEME,
settings.EXTERNAL_HOST,
reverse('zerver.views.auth.send_oauth_request_to_google'),
))
params = {'subdomain': get_subdomain(request)}
return redirect(main_site_uri + '?' + urllib.parse.urlencode(params))
def send_oauth_request_to_google(request):
# type: (HttpRequest) -> HttpResponse
subdomain = request.GET.get('subdomain', '')
if settings.REALMS_HAVE_SUBDOMAINS:
if not subdomain or not Realm.objects.filter(subdomain=subdomain).count():
return redirect_to_subdomain_login_url()
google_uri = 'https://accounts.google.com/o/oauth2/auth?'
cur_time = str(int(time.time()))
csrf_state = '{}:{}'.format(
csrf_state = '{}:{}:{}'.format(
cur_time,
google_oauth2_csrf(request, cur_time),
google_oauth2_csrf(request, cur_time + subdomain),
subdomain
)
prams = {
'response_type': 'code',
'client_id': settings.GOOGLE_OAUTH2_CLIENT_ID,
'redirect_uri': ''.join((
settings.EXTERNAL_URI_SCHEME,
request.get_host(),
settings.EXTERNAL_HOST,
reverse('zerver.views.auth.finish_google_oauth2'),
)),
'scope': 'profile email',
'state': csrf_state,
}
return redirect(uri + urllib.parse.urlencode(prams))
return redirect(google_uri + urllib.parse.urlencode(prams))
def finish_google_oauth2(request):
# type: (HttpRequest) -> HttpResponse
@ -178,12 +198,12 @@ def finish_google_oauth2(request):
return HttpResponse(status=400)
csrf_state = request.GET.get('state')
if csrf_state is None or len(csrf_state.split(':')) != 2:
if csrf_state is None or len(csrf_state.split(':')) != 3:
logging.warning('Missing Google oauth2 CSRF state')
return HttpResponse(status=400)
value, hmac_value = csrf_state.split(':')
if hmac_value != google_oauth2_csrf(request, value):
value, hmac_value, subdomain = csrf_state.split(':')
if hmac_value != google_oauth2_csrf(request, value + subdomain):
logging.warning('Google oauth2 CSRF error')
return HttpResponse(status=400)
@ -195,7 +215,7 @@ def finish_google_oauth2(request):
'client_secret': settings.GOOGLE_OAUTH2_CLIENT_SECRET,
'redirect_uri': ''.join((
settings.EXTERNAL_URI_SCHEME,
request.get_host(),
settings.EXTERNAL_HOST,
reverse('zerver.views.auth.finish_google_oauth2'),
)),
'grant_type': 'authorization_code',
@ -234,16 +254,56 @@ def finish_google_oauth2(request):
else:
logging.error('Google oauth2 account email not found: %s' % (body,))
return HttpResponse(status=400)
email_address = email['value']
if not subdomain:
# When request was not initiated from subdomain.
user_profile, return_data = authenticate_remote_user(request, email_address)
invalid_subdomain = bool(return_data.get('invalid_subdomain'))
return login_or_register_remote_user(request, email_address, user_profile,
full_name, invalid_subdomain)
try:
realm = Realm.objects.get(subdomain=subdomain)
except Realm.DoesNotExist:
return redirect_to_subdomain_login_url()
return redirect_and_log_into_subdomain(realm, full_name, email_address)
def authenticate_remote_user(request, email_address):
# type: (HttpRequest, str) -> Tuple[UserProfile, Dict[str, Any]]
return_data = {} # type: Dict[str, bool]
user_profile = authenticate(username=email_address,
realm_subdomain=get_subdomain(request),
use_dummy_backend=True,
return_data=return_data)
return user_profile, return_data
def log_into_subdomain(request):
# type: (HttpRequest) -> HttpResponse
try:
# Discard state if older than 15 seconds
state = request.get_signed_cookie('subdomain.signature',
salt='zerver.views.auth',
max_age=15)
except KeyError:
logging.warning('Missing subdomain signature cookie.')
return HttpResponse(status=400)
except signing.BadSignature:
logging.warning('Subdomain cookie has bad signature.')
return HttpResponse(status=400)
data = ujson.loads(state)
if data['subdomain'] != get_subdomain(request):
logging.warning('Login attemp on invalid subdomain')
return HttpResponse(status=400)
email_address = data['email']
full_name = data['name']
user_profile, return_data = authenticate_remote_user(request, email_address)
invalid_subdomain = bool(return_data.get('invalid_subdomain'))
return login_or_register_remote_user(request, email_address, user_profile, full_name,
invalid_subdomain)
return login_or_register_remote_user(request, email_address, user_profile,
full_name, invalid_subdomain)
def login_page(request, **kwargs):
# type: (HttpRequest, **Any) -> HttpResponse

View File

@ -37,7 +37,9 @@ i18n_urls = [
url(r'^accounts/login/sso/$', 'zerver.views.auth.remote_user_sso', name='login-sso'),
url(r'^accounts/login/jwt/$', 'zerver.views.auth.remote_user_jwt', name='login-jwt'),
url(r'^accounts/login/google/$', 'zerver.views.auth.start_google_oauth2'),
url(r'^accounts/login/google/send/$', 'zerver.views.auth.send_oauth_request_to_google'),
url(r'^accounts/login/google/done/$', 'zerver.views.auth.finish_google_oauth2'),
url(r'^accounts/login/subdomain/$', 'zerver.views.auth.log_into_subdomain'),
url(r'^accounts/login/local/$', 'zerver.views.auth.dev_direct_login'),
# We have two entries for accounts/login to allow reverses on the Django
# view we're wrapping to continue to function.