Commit 16972468 authored by Christophe de Vienne's avatar Christophe de Vienne
Browse files

Allow oauth2 session token to be submitted by the webclient.

When using facebook javascript SDK the oauth2 negociation is done client-side.
This patch makes it possible to send the oauth2 token back to the server.

Related to #3957660
parent 19bd8d5a43c4
......@@ -100,6 +100,11 @@ class FakeRauthService(object):
s.access_token = self.tokens.next()
return s
def get_session(self, token):
s = FakeRequestRession(self.base_url)
s.access_token = token
return s
class FakeServiceAdapter(ServiceAdapter):
__select__ = ServiceAdapter.__select__ & yes()
......@@ -146,7 +151,11 @@ class OAuthTC(CubicWebTestTC):
self.vreg.unregister(FakeServiceAdapter)
super(OAuthTC, self).tearDown()
def login(self):
def login(self, oauthtoken=None):
if oauthtoken:
return self.webapp.get(
'?__externalauthprovider=facebook'
'&__externalauth_oauth2_token=%s' % oauthtoken)
response = self.webapp.get('?__externalauthprovider=facebook')
location = response.headers['location']
......@@ -297,3 +306,20 @@ class OAuthTC(CubicWebTestTC):
with self.admin_access.repo_cnx() as cnx:
rset = cnx.execute('Any X WHERE X login "zeuser"')
self.assertEqual(1, rset.rowcount)
def test_directlogin(self):
with self.admin_access.repo_cnx() as cnx:
user = cnx.create_entity(
'CWUser', login=u"zeuser", upassword='',
in_group=cnx.find('CWGroup', name='users').one())
cnx.create_entity(
'ExternalIdentity',
identity_of=user, provider=cnx.find(
'ExternalAuthProvider', spid='facebook').one(),
uid=u'1111')
rset = cnx.execute('Any X WHERE X login "zeuser"')
self.assertEqual(1, rset.rowcount)
cnx.commit()
self.login(oauthtoken="SOMETOKEN")
self.assertEqual('zeuser', self.webapp.cookies['__data_session'][:6])
......@@ -32,6 +32,8 @@ from cubicweb.web import Redirect, formfields, formwidgets
from cubicweb.web import InvalidSession
from cubicweb import NoResultError, MultipleResultsError
from cubes.oauth.authplugin import EXT_TOKEN
_ = unicode
......@@ -53,6 +55,20 @@ def get_next_login(login):
return '{}.{}'.format(login, index)
class ExternalAuthMixin(object):
def get_service(self, req, cnx, providername):
try:
return cnx.execute(
'ExternalAuthService S WHERE S provider P, P spid %(spid)s',
{'spid': providername}
).one()
except (NoResultError, MultipleResultsError):
self.exception('no service for provider: %s' % providername)
raise Redirect(req.build_url(
__message='no service for provider %s' % providername
))
class ExternalAuthConfirm(Controller):
__regid__ = 'externalauth-confirm'
__select__ = match_form_params('code', '__externalauth_negociationid')
......@@ -87,7 +103,8 @@ class ExternalAuthError(Controller):
% (form.get('error_code'), form.get('error_message', ''))))
class ExternalAuthRetrieverStart(authentication.WebAuthInfoRetriever):
class ExternalAuthRetrieverStart(
ExternalAuthMixin, authentication.WebAuthInfoRetriever):
"""External authentication first step.
Will only redirect on the demanded service.
......@@ -115,18 +132,7 @@ class ExternalAuthRetrieverStart(authentication.WebAuthInfoRetriever):
vreg = self._cw # YES, see cw/web/views/authentication.py ~106
repo = vreg.config.repository(vreg)
with repo.internal_cnx() as cnx:
servicerset = cnx.execute(
'ExternalAuthService S WHERE S provider P, P spid %(spid)s',
{'spid': providername}
)
if not servicerset:
self.error('no service for provider: %s' % providername)
raise Redirect(req.build_url(
__message='no service for provider %s' % providername
))
service = servicerset.one()
service = self.get_service(req, cnx, providername)
path = req.relative_path(includeparams=True)
# Remove the 'vid' parameter if the selected view is 'loggedout'
if '?' in path:
......@@ -155,7 +161,127 @@ class ExternalAuthRetrieverStart(authentication.WebAuthInfoRetriever):
raise Redirect(url)
class ExternalAuthRetrieverFinish(authentication.WebAuthInfoRetriever):
class ExternalAuthRetrieverMixin(ExternalAuthMixin):
def _authentication_information(
self, req, cnx, adapter, service, oauth2_session):
try:
infos = adapter.provider.retrieve_info(oauth2_session)
extid_rset = cnx.execute(
'ExternalIdentity SI WHERE SI provider P, '
'P eid %(peid)s, SI uid %(uid)s',
{'peid': service.provider[0].eid, 'uid': infos['uid']}
)
assert extid_rset.rowcount < 2
if extid_rset:
external_identity = extid_rset.get_entity(0, 0)
else:
external_identity = cnx.create_entity(
'ExternalIdentity', provider=service.provider[0],
uid=infos['uid']
)
# deactivate any previous access_token
cnx.execute(
'SET X active FALSE WHERE '
'X is OAuth2Session, '
'S external_identity SI, SI eid %(sieid)s, '
'S service SE, SE eid %(seid)s',
{'sieid': external_identity.eid, 'seid': service.eid}
)
oauth2_session = cnx.create_entity(
'OAuth2Session', service=service,
external_identity=external_identity,
access_token=oauth2_session.access_token,
active=True
)
cnx.commit(free_cnxset=False)
except Exception, e:
self.exception("Cannot get the oauth2_session")
raise authentication.NoAuthInfo(str(e))
if external_identity.identity_of:
user = external_identity.identity_of[0]
self.debug('identified %s on %s as %s' % (
user.login, service.provider[0].name, infos['uid']))
login = user.login
else:
self.debug(
"Unknown identity %s on %s: will create a local account",
infos['uid'], service.provider[0].name)
login = self.forge_login(cnx, infos)
password = make_uid()
# TODO insert more informations from the external service
# XXX We should loop around the call_service and retry with a
# different login (w. get_next_login) when it raises a
# ValidationError _or_ if the commit() raise a duplicate key
# error.
try:
cnx.call_service(
'register_user',
login=login,
password=password,
email=infos.get('email', None),
firstname=infos.get('firstname', u''),
surname=infos.get('lastname', u''))
cnx.execute(
'SET X identity_of U '
'WHERE X eid %(sieid)s, '
'U is CWUser, U login %(login)s',
{'sieid': external_identity.eid,
'login': login}
)
cnx.commit()
except:
self.exception("Cannot create user %s" % login)
raise
return login, {'__externalauth_directauth': EXT_TOKEN}
class ExternalAuthRetrieverDirect(
ExternalAuthRetrieverMixin, authentication.WebAuthInfoRetriever):
__regid__ = 'externalauth-direct'
order = ExternalAuthRetrieverStart.order - 1
def request_has_auth_info(self, req):
return (
'__externalauthprovider' in req.form
and '__externalauth_oauth2_token' in req.form)
def revalidate_login(self, req):
# Always invalidate the current session
raise InvalidSession()
def authentication_information(self, req):
if not self.request_has_auth_info(req):
raise authentication.NoAuthInfo
spid = req.form.get('__externalauthprovider')
oauth2_token = req.form.get('__externalauth_oauth2_token')
vreg = self._cw # YES, see cw/web/views/authentication.py ~106
repo = vreg.config.repository(vreg)
with repo.internal_cnx() as cnx:
try:
service = self.get_service(req, cnx, spid)
adapter = service.cw_adapt_to('externalauth.service')
oauth2_session = adapter.oauth2_service.get_session(
oauth2_token)
except:
self.error("Cannot get a auth2session")
raise authentication.NoAuthInfo
return self._authentication_information(
req, cnx, adapter, service, oauth2_session)
class ExternalAuthRetrieverFinish(
ExternalAuthRetrieverMixin, authentication.WebAuthInfoRetriever):
__regid__ = 'externalauth-success'
order = ExternalAuthRetrieverStart.order - 1
......@@ -196,83 +322,11 @@ class ExternalAuthRetrieverFinish(authentication.WebAuthInfoRetriever):
'code': code,
'redirect_uri': nego['redirect_uri']
})
infos = adapter.provider.retrieve_info(oauth2_session)
extid_rset = cnx.execute(
'ExternalIdentity SI WHERE SI provider P, '
'P eid %(peid)s, SI uid %(uid)s',
{'peid': service.provider[0].eid, 'uid': infos['uid']}
)
assert extid_rset.rowcount < 2
if extid_rset:
external_identity = extid_rset.get_entity(0, 0)
else:
external_identity = cnx.create_entity(
'ExternalIdentity', provider=service.provider[0],
uid=infos['uid']
)
# deactivate any previous access_token
cnx.execute(
'SET X active FALSE WHERE '
'X is OAuth2Session, '
'S external_identity SI, SI eid %(sieid)s, '
'S service SE, SE eid %(seid)s',
{'sieid': external_identity.eid, 'seid': service.eid}
)
oauth2_session = cnx.create_entity(
'OAuth2Session', service=service,
external_identity=external_identity,
access_token=oauth2_session.access_token,
active=True
)
cnx.commit(free_cnxset=False)
except Exception, e:
self.exception("Cannot get the oauth2_session")
raise authentication.NoAuthInfo(str(e))
if external_identity.identity_of:
user = external_identity.identity_of[0]
self.debug('identified %s on %s as %s' % (
user.login, service.provider[0].name, infos['uid']))
login = user.login
else:
self.debug(
"Unknown identity %s on %s: will create a local account",
infos['uid'], service.provider[0].name)
login = self.forge_login(cnx, infos)
password = make_uid()
# TODO insert more informations from the external service
# XXX We should loop around the call_service and retry with a
# different login (w. get_next_login) when it raises a
# ValidationError _or_ if the commit() raise a duplicate key
# error.
try:
cnx.call_service(
'register_user',
login=login,
password=password,
email=infos.get('email', None),
firstname=infos.get('firstname', u''),
surname=infos.get('lastname', u''))
cnx.execute(
'SET X identity_of U '
'WHERE X eid %(sieid)s, '
'U is CWUser, U login %(login)s',
{'sieid': external_identity.eid,
'login': login}
)
cnx.commit()
except:
self.exception("Cannot create user %s" % login)
raise
return login, {'__externalauth_directauth': EXT_TOKEN}
except:
self.error("Cannot get a auth2session")
raise authentication.NoAuthInfo
return self._authentication_information(
req, cnx, adapter, service, oauth2_session)
def make_unique_login(self, cnx, login):
"""Return a login that does not yet exists in the database,
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment