Store Blender ID OAuth scopes in MongoDB + request badge scope too

This also changes the way we treat Blender ID tokens. Before, the Blender ID
token was discarded and a random token was generated & stored. Now the
actual Blender ID token is stored.

The Facebook and Google OAuth code still uses the old approach of generating
a new token. Not sure what the added value is, though, because once the
Django session is gone there is nothing left to authenticate the user and
thus the random token is useless anyway.
This commit is contained in:
Sybren A. Stüvel 2018-09-11 15:36:25 +02:00
parent 6bcce87bb9
commit 0983474e76
6 changed files with 46 additions and 11 deletions

View File

@ -391,6 +391,13 @@ tokens_schema = {
'type': 'string',
},
},
# OAuth scopes granted to this token.
'oauth_scopes': {
'type': 'list',
'default': [],
'schema': {'type': 'string'},
}
}
files_schema = {

View File

@ -235,8 +235,14 @@ def hash_auth_token(token: str) -> str:
return base64.b64encode(digest).decode('ascii')
def store_token(user_id, token: str, token_expiry, oauth_subclient_id=False,
org_roles: typing.Set[str] = frozenset()):
def store_token(user_id,
token: str,
token_expiry,
oauth_subclient_id=False,
*,
org_roles: typing.Set[str] = frozenset(),
oauth_scopes: typing.Optional[typing.List[str]] = None,
):
"""Stores an authentication token.
:returns: the token document from MongoDB
@ -253,6 +259,8 @@ def store_token(user_id, token: str, token_expiry, oauth_subclient_id=False,
token_data['is_subclient_token'] = True
if org_roles:
token_data['org_roles'] = sorted(org_roles)
if oauth_scopes:
token_data['oauth_scopes'] = oauth_scopes
r, _, _, status = current_app.post_internal('tokens', token_data)

View File

@ -1,8 +1,9 @@
import abc
import attr
import json
import logging
import typing
import attr
from rauth import OAuth2Service
from flask import current_app, url_for, request, redirect, session, Response
@ -15,6 +16,8 @@ class OAuthUserResponse:
id = attr.ib(validator=attr.validators.instance_of(str))
email = attr.ib(validator=attr.validators.instance_of(str))
access_token = attr.ib(validator=attr.validators.instance_of(str))
scopes: typing.List[str] = attr.ib(validator=attr.validators.instance_of(list))
class OAuthError(Exception):
@ -127,6 +130,7 @@ class OAuthSignIn(metaclass=abc.ABCMeta):
class BlenderIdSignIn(OAuthSignIn):
provider_name = 'blender-id'
scopes = ['email', 'badge']
def __init__(self):
from urllib.parse import urljoin
@ -145,7 +149,7 @@ class BlenderIdSignIn(OAuthSignIn):
def authorize(self):
return redirect(self.service.get_authorize_url(
scope='email',
scope=' '.join(self.scopes),
response_type='code',
redirect_uri=self.get_callback_url())
)
@ -159,7 +163,11 @@ class BlenderIdSignIn(OAuthSignIn):
session['blender_id_oauth_token'] = access_token
me = oauth_session.get('user').json()
return OAuthUserResponse(str(me['id']), me['email'])
# Blender ID doesn't tell us which scopes were granted by the user, so
# for now assume we got all the scopes we requested.
# (see https://github.com/jazzband/django-oauth-toolkit/issues/644)
return OAuthUserResponse(str(me['id']), me['email'], access_token, self.scopes)
class FacebookSignIn(OAuthSignIn):
@ -189,7 +197,7 @@ class FacebookSignIn(OAuthSignIn):
me = oauth_session.get('me?fields=id,email').json()
# TODO handle case when user chooses not to disclose en email
# see https://developers.facebook.com/docs/graph-api/reference/user/
return OAuthUserResponse(me['id'], me.get('email'))
return OAuthUserResponse(me['id'], me.get('email'), '', [])
class GoogleSignIn(OAuthSignIn):
@ -217,4 +225,4 @@ class GoogleSignIn(OAuthSignIn):
oauth_session = self.make_oauth_session()
me = oauth_session.get('userinfo').json()
return OAuthUserResponse(str(me['id']), me['email'])
return OAuthUserResponse(str(me['id']), me['email'], '', [])

View File

@ -48,6 +48,10 @@ def oauth_authorize(provider):
@blueprint.route('/oauth/<provider>/authorized')
def oauth_callback(provider):
import datetime
from pillar.api.utils.authentication import store_token
from pillar.api.utils import utcnow
if current_user.is_authenticated:
return redirect(url_for('main.homepage'))
@ -65,7 +69,14 @@ def oauth_callback(provider):
user_info = {'id': oauth_user.id, 'email': oauth_user.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)
if oauth_user.access_token:
# TODO(Sybren): make nr of days configurable, or get from OAuthSignIn subclass.
token_expiry = utcnow() + datetime.timedelta(days=15)
token = store_token(db_id, oauth_user.access_token, token_expiry,
oauth_scopes=oauth_user.scopes)
else:
token = generate_and_store_token(db_id)
# Login user
pillar.auth.login_user(token['token'], load_from_db=True)

View File

@ -69,7 +69,7 @@ class AuthenticationTests(AbstractPillarTest):
@responses.activate
def test_validate_token__force_not_logged_in(self):
from pillar.api.utils import authentication as auth
from pillar.auth import UserClass, current_user
from pillar.auth import UserClass
with self.app.test_request_context():
from flask import g
@ -160,8 +160,7 @@ class AuthenticationTests(AbstractPillarTest):
def test_save_own_user(self):
"""Tests that a user can't change their own fields."""
from pillar.api.utils import authentication as auth
from pillar.api.utils import PillarJSONEncoder, remove_private_keys
from pillar.api.utils import remove_private_keys
user_id = self.create_user(roles=['subscriber'], token='token')

View File

@ -62,6 +62,8 @@ class OAuthTests(AbstractPillarTest):
cb = oauth_provider.callback()
self.assertEqual(cb.id, '7')
self.assertEqual(cb.email, 'harry@blender.org')
self.assertEqual(cb.access_token, 'successful-token')
self.assertEqual(cb.scopes, ['email', 'badge'])
@responses.activate
def test_provider_callback_missing_code(self):