diff --git a/pillar/api/eve_settings.py b/pillar/api/eve_settings.py index 40671d09..a475856a 100644 --- a/pillar/api/eve_settings.py +++ b/pillar/api/eve_settings.py @@ -97,7 +97,7 @@ users_schema = { 'schema': { 'provider': { 'type': 'string', - 'allowed': ['blender-id', 'local', 'facebook'], + 'allowed': ['local', 'blender-id', 'facebook', 'google'], }, 'user_id': { 'type': 'string' diff --git a/pillar/auth/oauth.py b/pillar/auth/oauth.py index 06ae7a3d..8e3a73d8 100644 --- a/pillar/auth/oauth.py +++ b/pillar/auth/oauth.py @@ -1,7 +1,7 @@ import json from rauth import OAuth2Service -from flask import current_app, url_for, request, redirect, session +from flask import current_app, url_for, request, redirect class OAuthSignIn(object): @@ -33,6 +33,53 @@ class OAuthSignIn(object): return cls.providers[provider_name] +class BlenderIdSignIn(OAuthSignIn): + def __init__(self): + super(BlenderIdSignIn, self).__init__('blender-id') + + base_url = current_app.config['OAUTH_CREDENTIALS']['blender-id'].get( + 'base_url', 'https://www.blender.org/id/') + + self.service = OAuth2Service( + name='blender-id', + client_id=self.consumer_id, + client_secret=self.consumer_secret, + authorize_url='%soauth/authorize' % base_url, + access_token_url='%soauth/token' % base_url, + base_url='%sapi/' % base_url + ) + + def authorize(self): + return redirect(self.service.get_authorize_url( + scope='email', + response_type='code', + redirect_uri=self.get_callback_url()) + ) + + def callback(self): + def decode_json(payload): + return json.loads(payload.decode('utf-8')) + + if 'code' not in request.args: + return None, None, None + oauth_session = self.service.get_auth_session( + data={'code': request.args['code'], + 'grant_type': 'authorization_code', + 'redirect_uri': self.get_callback_url()}, + decoder=decode_json + ) + + # TODO handle exception for failed oauth or not authorized + + me = oauth_session.get('user').json() + # TODO handle case when user chooses not to disclose en email + return ( + me['id'], + me.get('email'), + oauth_session.access_token + ) + + class FacebookSignIn(OAuthSignIn): def __init__(self): super(FacebookSignIn, self).__init__('facebook') @@ -69,4 +116,45 @@ class FacebookSignIn(OAuthSignIn): return ( me['id'], me.get('email'), + None + ) + + +class GoogleSignIn(OAuthSignIn): + def __init__(self): + super(GoogleSignIn, self).__init__('google') + self.service = OAuth2Service( + name='google', + client_id=self.consumer_id, + client_secret=self.consumer_secret, + authorize_url='https://accounts.google.com/o/oauth2/auth', + access_token_url='https://accounts.google.com/o/oauth2/token', + base_url='https://www.googleapis.com/oauth2/v1/' + ) + + def authorize(self): + return redirect(self.service.get_authorize_url( + scope='https://www.googleapis.com/auth/userinfo.email', + response_type='code', + redirect_uri=self.get_callback_url()) + ) + + def callback(self): + def decode_json(payload): + return json.loads(payload.decode('utf-8')) + + if 'code' not in request.args: + return None, None, None + oauth_session = self.service.get_auth_session( + data={'code': request.args['code'], + 'grant_type': 'authorization_code', + 'redirect_uri': self.get_callback_url()}, + decoder=decode_json + ) + me = oauth_session.get('userinfo').json() + # TODO handle case when user chooses not to disclose en email + return ( + me['id'], + me.get('email'), + None ) diff --git a/pillar/web/users/routes.py b/pillar/web/users/routes.py index fe6dd121..31bc3b82 100644 --- a/pillar/web/users/routes.py +++ b/pillar/web/users/routes.py @@ -36,22 +36,38 @@ def oauth_authorize(provider): return oauth.authorize() -@blueprint.route('/callback/') +@blueprint.route('/oauth//authorized') def oauth_callback(provider): if not current_user.is_anonymous: return redirect(url_for('main.homepage')) oauth = OAuthSignIn.get_provider(provider) - social_id, email = oauth.callback() + social_id, email, access_token = oauth.callback() if social_id is None: - flash('Authentication failed.') + log.debug('Authentication failed for user with {}'.format(provider)) return redirect(url_for('main.homepage')) - # Find or create user - user_info = {'id': social_id, 'email': email, 'full_name': ''} - db_user = find_user_in_db(user_info, provider=provider) - db_id, status = upsert_user(db_user) - token = generate_and_store_token(db_id) - # Login user - pillar.auth.login_user(token['token']) + # If login from Blender ID we use the token to create the user + if provider == 'blender-id': + session['blender_id_oauth_token'] = (access_token, '') + pillar.auth.login_user(access_token) + + if current_user is not None: + # Check with the store for user roles. If the user has an active + # subscription, we apply the 'subscriber' role + api = system_util.pillar_api(token=access_token) + api.get('bcloud/update-subscription') + else: + # Find or create user + user_info = {'id': social_id, 'email': email, 'full_name': ''} + db_user = find_user_in_db(user_info, provider=provider) + db_id, status = upsert_user(db_user) + token = generate_and_store_token(db_id) + # Login user + pillar.auth.login_user(token['token']) + + next_after_login = session.pop('next_after_login', None) + if next_after_login: + log.debug('Redirecting user to %s', next_after_login) + return redirect(next_after_login) return redirect(url_for('main.homepage')) @@ -61,41 +77,6 @@ def login(): return render_template('login.html') -@blueprint.route('/oauth/blender-id/authorized') -def blender_id_authorized(): - check_oauth_provider(current_app.oauth_blender_id) - try: - oauth_resp = current_app.oauth_blender_id.authorized_response() - if isinstance(oauth_resp, OAuthException): - raise oauth_resp - except OAuthException as ex: - log.warning('Error parsing BlenderID OAuth response. data=%s; message=%s', - ex.data, ex.message) - raise wz_exceptions.Forbidden('Access denied, sorry!') - - if oauth_resp is None: - msg = 'Access denied: reason=%s error=%s' % ( - request.args.get('error_reason'), request.args.get('error_description')) - log.warning('Access denied to user because oauth_resp=None: %s', msg) - return wz_exceptions.Forbidden(msg) - - session['blender_id_oauth_token'] = (oauth_resp['access_token'], '') - - pillar.auth.login_user(oauth_resp['access_token']) - - if current_user is not None: - # Check with the store for user roles. If the user has an active - # subscription, we apply the 'subscriber' role - api = system_util.pillar_api(token=oauth_resp['access_token']) - api.get('bcloud/update-subscription') - - next_after_login = session.pop('next_after_login', None) - if next_after_login: - log.debug('Redirecting user to %s', next_after_login) - return redirect(next_after_login) - return redirect(url_for('main.homepage')) - - @blueprint.route('/login/local', methods=['GET', 'POST']) def login_local(): """Login with a local account, as an alternative to OAuth.