From 439b86fe3bf12ffe2686162bed9c1778e78c9706 Mon Sep 17 00:00:00 2001 From: Jason Michalski Date: Wed, 28 Jan 2015 23:59:41 -0800 Subject: [PATCH] Migrate the google SSO from openid to oauth2 (imported from commit 6938c1cc5d245cc5642043279470365ff04df903) --- templates/zerver/accounts_home.html | 2 +- templates/zerver/login.html | 2 +- zerver/views/__init__.py | 82 +++++++++++++++++++++++++++++ zproject/local_settings.py | 8 +++ zproject/urls.py | 2 + 5 files changed, 94 insertions(+), 2 deletions(-) diff --git a/templates/zerver/accounts_home.html b/templates/zerver/accounts_home.html index c84ce9535a..a8ac7155f2 100644 --- a/templates/zerver/accounts_home.html +++ b/templates/zerver/accounts_home.html @@ -41,7 +41,7 @@ $(function () { company email address to sign up. Otherwise, we won’t be able to connect you with your coworkers.
- Sign up with Google + Sign up with Google
diff --git a/templates/zerver/login.html b/templates/zerver/login.html index 0af13f6155..0f37a8b3c1 100644 --- a/templates/zerver/login.html +++ b/templates/zerver/login.html @@ -92,7 +92,7 @@ autofocus('#id_username'); {% if not desktop_sso_dispatch %}
- or + or
{% endif %} diff --git a/zerver/views/__init__.py b/zerver/views/__init__.py index aef86ae525..4da41bf2d9 100644 --- a/zerver/views/__init__.py +++ b/zerver/views/__init__.py @@ -15,6 +15,7 @@ from django.contrib.auth.views import login as django_login_page, \ logout_then_login as django_logout_then_login from django.db.models import Q, F from django.core.mail import send_mail, EmailMessage +from django.middleware.csrf import get_token from django.db import transaction from zerver.models import Message, UserProfile, Stream, Subscription, \ Recipient, Realm, UserMessage, bulk_get_recipients, \ @@ -74,6 +75,8 @@ from zproject.backends import password_auth_enabled from confirmation.models import Confirmation +import requests + import subprocess import calendar import datetime @@ -86,6 +89,8 @@ import time import logging import os import jwt +import hashlib +import hmac from collections import defaultdict from zerver.lib.rest import rest_dispatch as _rest_dispatch @@ -697,6 +702,83 @@ def handle_openid_errors(request, issue, openid_response=None): def process_openid_login(request): return login_complete(request, render_failure=handle_openid_errors) +def google_oauth2_csrf(request, value): + return hmac.new(get_token(request).encode('utf-8'), value, hashlib.sha256).hexdigest() + +def start_google_oauth2(request): + uri = 'https://accounts.google.com/o/oauth2/auth?' + cur_time = str(int(time.time())) + csrf_state = '{}:{}'.format( + cur_time, + google_oauth2_csrf(request, cur_time), + ) + prams = { + 'response_type': 'code', + 'client_id': settings.GOOGLE_OAUTH2_CLIENT_ID, + 'redirect_uri': ''.join(( + settings.EXTERNAL_URI_SCHEME, + settings.EXTERNAL_HOST, + reverse('zerver.views.finish_google_oauth2'), + )), + 'scope': 'profile email', + 'state': csrf_state, + } + return redirect(uri + urllib.urlencode(prams)) + +def finish_google_oauth2(request): + error = request.GET.get('error') + if error == 'access_denied': + return redirect('/') + elif error is not None: + logging.error('Error from google oauth2 login %r', request.GET) + return HttpResponse(status=400) + + value, hmac_value = request.GET.get('state').split(':') + if hmac_value != google_oauth2_csrf(request, value): + raise Exception('Google oauth2 CSRF error') + + resp = requests.post( + 'https://www.googleapis.com/oauth2/v3/token', + data={ + 'code': request.GET.get('code'), + 'client_id': settings.GOOGLE_OAUTH2_CLIENT_ID, + 'client_secret': settings.GOOGLE_OAUTH2_CLIENT_SECRET, + 'redirect_uri': ''.join(( + settings.EXTERNAL_URI_SCHEME, + settings.EXTERNAL_HOST, + reverse('zerver.views.finish_google_oauth2'), + )), + 'grant_type': 'authorization_code', + }, + ) + if resp.status_code != 200: + raise Exception('Could not convert google pauth2 code to access_token\r%r' % resp.text) + access_token = resp.json['access_token'] + + resp = requests.get( + 'https://www.googleapis.com/plus/v1/people/me', + params={'access_token': access_token} + ) + if resp.status_code != 200: + raise Exception('Google login failed making API call\r%r' % resp.text) + body = resp.json + + try: + full_name = body['name']['formatted'] + except KeyError: + # Only google+ users have a formated name. I am ignoring i18n here. + full_name = '{} {}'.format( + body['name']['givenName'], body['name']['familyName'] + ) + for email in body['emails']: + if email['type'] == 'account': + break + else: + raise Exception('Google oauth2 account email not found %r' % body) + email_address = email['value'] + user_profile = authenticate(username=email_address, use_dummy_backend=True) + return login_or_register_remote_user(request, email_address, user_profile, full_name) + def login_page(request, **kwargs): template_response = django_login_page( request, authentication_form=OurAuthenticationForm, **kwargs) diff --git a/zproject/local_settings.py b/zproject/local_settings.py index 2b5e0c3b49..b122ce82b7 100644 --- a/zproject/local_settings.py +++ b/zproject/local_settings.py @@ -143,6 +143,14 @@ else: GOOGLE_CLIENT_ID = "835904834568-77mtr5mtmpgspj9b051del9i9r5t4g4n.apps.googleusercontent.com" +if DEPLOYED: + GOOGLE_OAUTH2_CLIENT_ID = '' + GOOGLE_OAUTH2_CLIENT_SECRET = '' +else: + # Google OAUTH2 for dev with the redirect uri set to http://localhost:9991/accounts/login/google/done/ + GOOGLE_OAUTH2_CLIENT_ID = '607830223128-4qgthc7ofdqce232dk690t5jgkm1ce33.apps.googleusercontent.com' + GOOGLE_OAUTH2_CLIENT_SECRET = 'xxxxxxxxxxxxxxxxxxxxxxxx' + # Administrator domain for this install ADMIN_DOMAIN = "zulip.com" diff --git a/zproject/urls.py b/zproject/urls.py index d8e99387e3..fbeca190d7 100644 --- a/zproject/urls.py +++ b/zproject/urls.py @@ -24,6 +24,8 @@ urlpatterns = patterns('', url(r'^accounts/login/openid/done/$', 'django_openid_auth.views.login_complete', name='openid-complete'), url(r'^accounts/login/sso/$', 'zerver.views.remote_user_sso', name='login-sso'), url(r'^accounts/login/jwt/$', 'zerver.views.remote_user_jwt', name='login-jwt'), + url(r'^accounts/login/google/$', 'zerver.views.start_google_oauth2'), + url(r'^accounts/login/google/done/$', 'zerver.views.finish_google_oauth2'), # We have two entries for accounts/login to allow reverses on the Django # view we're wrapping to continue to function. url(r'^accounts/login/', 'zerver.views.login_page', {'template_name': 'zerver/login.html'}),