Skip to content

Commit

Permalink
adopt mixin-less setup in google
Browse files Browse the repository at this point in the history
  • Loading branch information
minrk committed Dec 18, 2019
1 parent 4e766dc commit b219f96
Show file tree
Hide file tree
Showing 2 changed files with 55 additions and 60 deletions.
92 changes: 51 additions & 41 deletions oauthenticator/google.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@

import os
import json
import urllib.parse

from tornado import gen
from tornado.httpclient import AsyncHTTPClient
from tornado.auth import GoogleOAuth2Mixin
from tornado.web import HTTPError

Expand All @@ -19,31 +21,27 @@
from .oauth2 import OAuthLoginHandler, OAuthCallbackHandler, OAuthenticator


class GoogleLoginHandler(OAuthLoginHandler, GoogleOAuth2Mixin):
'''An OAuthLoginHandler that provides scope to GoogleOAuth2Mixin's
authorize_redirect.'''
@property
def scope(self):
return self.authenticator.scope


class GoogleOAuthHandler(OAuthCallbackHandler, GoogleOAuth2Mixin):
pass


class GoogleOAuthenticator(OAuthenticator, GoogleOAuth2Mixin):

login_handler = GoogleLoginHandler
callback_handler = GoogleOAuthHandler

@default('scope')
def _scope_default(self):
return ['openid', 'email']

@default("authorize_url")
def _authorize_url_default(self):
return "https://accounts.google.com/o/oauth2/v2/auth"

@default("access_token_url")
def _access_token_url_default(self):
return "https://www.googleapis.com/oauth2/v4/token"

user_info_url = Unicode(
"https://www.googleapis.com/oauth2/v1/userinfo", config=True
)

hosted_domain = List(
Unicode(),
config=True,
help="""List of domains used to restrict sign-in, e.g. mycollege.edu"""
help="""List of domains used to restrict sign-in, e.g. mycollege.edu""",
)

@default('hosted_domain')
Expand Down Expand Up @@ -71,25 +69,35 @@ def _cast_hosted_domain(self, proposal):
login_service = Unicode(
os.environ.get('LOGIN_SERVICE', 'Google'),
config=True,
help="""Google Apps hosted domain string, e.g. My College"""
help="""Google Apps hosted domain string, e.g. My College""",
)

async def authenticate(self, handler, data=None):
code = handler.get_argument("code")
handler.settings['google_oauth'] = {
'key': self.client_id,
'secret': self.client_secret,
'scope': self.scope,
}
user = await handler.get_authenticated_user(
redirect_uri=self.get_callback_url(handler),
code=code)
access_token = str(user['access_token'])
body = urllib.parse.urlencode(
dict(
code=code,
redirect_uri=self.get_callback_url(handler),
client_id=self.client_id,
client_secret=self.client_secret,
grant_type="authorization_code",
)
)

http_client = AsyncHTTPClient()

response = await http_client.fetch(
self.access_token_url,
method="POST",
headers={"Content-Type": "application/x-www-form-urlencoded"},
body=body,
)

http_client = handler.get_auth_http_client()
user = json.loads(response.body.decode("utf-8", "replace"))
access_token = str(user['access_token'])

response = await http_client.fetch(
self._OAUTH_USERINFO_URL + '?access_token=' + access_token
self.user_info_url + '?access_token=' + access_token
)

if not response:
Expand All @@ -102,31 +110,33 @@ async def authenticate(self, handler, data=None):

if not bodyjs['verified_email']:
self.log.warning("Google OAuth unverified email attempt: %s", user_email)
raise HTTPError(403,
"Google email {} not verified".format(user_email)
)
raise HTTPError(403, "Google email {} not verified".format(user_email))

if self.hosted_domain:
if (
user_email_domain not in self.hosted_domain or
bodyjs['hd'] not in self.hosted_domain
user_email_domain not in self.hosted_domain
or bodyjs['hd'] not in self.hosted_domain
):
self.log.warning("Google OAuth unauthorized domain attempt: %s", user_email)
raise HTTPError(403,
"Google account domain @{} not authorized.".format(user_email_domain)
self.log.warning(
"Google OAuth unauthorized domain attempt: %s", user_email
)
raise HTTPError(
403,
"Google account domain @{} not authorized.".format(
user_email_domain
),
)
if len(self.hosted_domain) == 1:
# unambiguous domain, use only base name
username = user_email.split('@')[0]

return {
'name': username,
'auth_state': {
'access_token': access_token,
'google_user': bodyjs,
}
'auth_state': {'access_token': access_token, 'google_user': bodyjs},
}


class LocalGoogleOAuthenticator(LocalAuthenticator, GoogleOAuthenticator):
"""A version that mixes in local system user creation"""

pass
23 changes: 4 additions & 19 deletions oauthenticator/tests/test_google.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,39 +4,24 @@
from pytest import fixture, mark, raises
from tornado.web import Application, HTTPError

from ..google import GoogleOAuthenticator, GoogleOAuthHandler
from ..google import GoogleOAuthenticator

from .mocks import setup_oauth_mock


def user_model(email):
"""Return a user model"""
return {
'email': email,
'hd': email.split('@')[1],
'verified_email': True
}
return {'email': email, 'hd': email.split('@')[1], 'verified_email': True}


@fixture
def google_client(client):
setup_oauth_mock(client,
setup_oauth_mock(
client,
host=['accounts.google.com', 'www.googleapis.com'],
access_token_path=re.compile('^(/o/oauth2/token|/oauth2/v4/token)$'),
user_path='/oauth2/v1/userinfo',
)
original_handler_for_user = client.handler_for_user
# testing Google is harder because it invokes methods inherited from tornado
# classes
def handler_for_user(user):
mock_handler = original_handler_for_user(user)
mock_handler.request.connection = Mock()
real_handler = GoogleOAuthHandler(
application=Application(hub=mock_handler.hub),
request=mock_handler.request,
)
return real_handler
client.handler_for_user = handler_for_user
return client


Expand Down

0 comments on commit b219f96

Please sign in to comment.