Skip to content
Closed
5 changes: 3 additions & 2 deletions common/djangoapps/oauth_exchange/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from provider.oauth2.models import Client
from requests import HTTPError
from social.backends import oauth as social_oauth
from social.exceptions import AuthException

from third_party_auth import pipeline

Expand Down Expand Up @@ -48,7 +49,7 @@ def clean(self):
if self._errors:
return {}

backend = self.request.social_strategy.backend
backend = self.request.backend
if not isinstance(backend, social_oauth.BaseOAuth2):
raise OAuthValidationError(
{
Expand Down Expand Up @@ -83,7 +84,7 @@ def clean(self):
user = None
try:
user = backend.do_auth(self.cleaned_data.get("access_token"))
except HTTPError:
except (HTTPError, AuthException):
pass
if user and isinstance(user, User):
self.cleaned_data["user"] = user
Expand Down
4 changes: 3 additions & 1 deletion common/djangoapps/oauth_exchange/tests/test_forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,10 @@ class AccessTokenExchangeFormTest(AccessTokenExchangeTestMixin):
def setUp(self):
super(AccessTokenExchangeFormTest, self).setUp()
self.request = RequestFactory().post("dummy_url")
redirect_uri = 'dummy_redirect_url'
SessionMiddleware().process_request(self.request)
self.request.social_strategy = social_utils.load_strategy(self.request, self.BACKEND)
self.request.social_strategy = social_utils.load_strategy(self.request)
self.request.backend = social_utils.load_backend(self.request.social_strategy, self.BACKEND, redirect_uri)

def _assert_error(self, data, expected_error, expected_error_description):
form = AccessTokenExchangeForm(request=self.request, data=data)
Expand Down
20 changes: 12 additions & 8 deletions common/djangoapps/student/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -437,7 +437,7 @@ def register_user(request, extra_context=None):
# selected provider.
if third_party_auth.is_enabled() and pipeline.running(request):
running_pipeline = pipeline.get(request)
current_provider = provider.Registry.get_by_backend_name(running_pipeline.get('backend'))
current_provider = provider.Registry.get_from_pipeline(running_pipeline)
overrides = current_provider.get_register_form_data(running_pipeline.get('kwargs'))
overrides['running_pipeline'] = running_pipeline
overrides['selected_provider'] = current_provider.NAME
Expand Down Expand Up @@ -961,10 +961,11 @@ def login_user(request, error=""): # pylint: disable-msg=too-many-statements,un
running_pipeline = pipeline.get(request)
username = running_pipeline['kwargs'].get('username')
backend_name = running_pipeline['backend']
requested_provider = provider.Registry.get_by_backend_name(backend_name)
third_party_uid = running_pipeline['kwargs']['uid']
requested_provider = provider.Registry.get_from_pipeline(running_pipeline)

try:
user = pipeline.get_authenticated_user(username, backend_name)
user = pipeline.get_authenticated_user(requested_provider, username, third_party_uid)
third_party_auth_successful = True
except User.DoesNotExist:
AUDIT_LOG.warning(
Expand Down Expand Up @@ -1152,15 +1153,15 @@ def login_oauth_token(request, backend):
"""
warnings.warn("Please use AccessTokenExchangeView instead.", DeprecationWarning)

backend = request.social_strategy.backend
backend = request.backend
if isinstance(backend, social_oauth.BaseOAuth1) or isinstance(backend, social_oauth.BaseOAuth2):
if "access_token" in request.POST:
# Tell third party auth pipeline that this is an API call
request.session[pipeline.AUTH_ENTRY_KEY] = pipeline.AUTH_ENTRY_LOGIN_API
user = None
try:
user = backend.do_auth(request.POST["access_token"])
except HTTPError:
except (HTTPError, AuthException):
pass
# do_auth can return a non-User object if it fails
if user and isinstance(user, User):
Expand Down Expand Up @@ -1470,7 +1471,10 @@ def create_account_with_params(request, params):

# next, link the account with social auth, if provided
if should_link_with_social_auth:
request.social_strategy = social_utils.load_strategy(backend=params['provider'], request=request)
backend_name = params['provider']
request.social_strategy = social_utils.load_strategy(request)
redirect_uri = reverse('social:complete', args=(backend_name, ))
request.backend = social_utils.load_backend(request.social_strategy, backend_name, redirect_uri)
social_access_token = params.get('access_token')
if not social_access_token:
raise ValidationError({
Expand All @@ -1484,7 +1488,7 @@ def create_account_with_params(request, params):
pipeline_user = None
error_message = ""
try:
pipeline_user = request.social_strategy.backend.do_auth(social_access_token, user=user)
pipeline_user = request.backend.do_auth(social_access_token, user=user)
except AuthAlreadyAssociated:
error_message = _("The provided access_token is already associated with another user.")
except (HTTPError, AuthException):
Expand Down Expand Up @@ -1517,7 +1521,7 @@ def create_account_with_params(request, params):
provider_name = None
if third_party_auth.is_enabled() and pipeline.running(request):
running_pipeline = pipeline.get(request)
current_provider = provider.Registry.get_by_backend_name(running_pipeline.get('backend'))
current_provider = provider.Registry.get_from_pipeline(running_pipeline)
provider_name = current_provider.NAME

analytics.track(
Expand Down
79 changes: 48 additions & 31 deletions common/djangoapps/third_party_auth/pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -199,9 +199,11 @@ class ProviderUserState(object):
lms/templates/dashboard.html.
"""

def __init__(self, enabled_provider, user, state):
def __init__(self, enabled_provider, user, association_id=None):
# UserSocialAuth row ID
self.association_id = association_id
# Boolean. Whether the user has an account associated with the provider
self.has_account = state
self.has_account = association_id is not None
# provider.BaseProvider child. Callers must verify that the provider is
# enabled.
self.provider = enabled_provider
Expand All @@ -218,7 +220,7 @@ def get(request):
return request.session.get('partial_pipeline')


def get_authenticated_user(username, backend_name):
def get_authenticated_user(provider, username, uid):
"""Gets a saved user authenticated by a particular backend.

Between pipeline steps User objects are not saved. We need to reconstitute
Expand All @@ -227,26 +229,26 @@ def get_authenticated_user(username, backend_name):
authenticate().

Args:
provider: the third_party_auth provider in use for the current pipeline.
username: string. Username of user to get.
backend_name: string. The name of the third-party auth backend from
the running pipeline.
uid: string. The user ID according to the third party.

Returns:
User if user is found and has a social auth from the passed
backend_name.
provider.

Raises:
User.DoesNotExist: if no user matching user is found, or the matching
user has no social auth associated with the given backend.
AssertionError: if the user is not authenticated.
"""
user = models.DjangoStorage.user.user_model().objects.get(username=username)
match = models.DjangoStorage.user.get_social_auth_for_user(user, provider=backend_name)
match = models.DjangoStorage.user.get_social_auth(provider=provider.BACKEND_CLASS.name, uid=uid)

if not match:
if not match or match.user.username != username:
raise User.DoesNotExist

user.backend = provider.Registry.get_by_backend_name(backend_name).get_authentication_backend()
user = match.user
user.backend = provider.get_authentication_backend()
return user


Expand All @@ -260,10 +262,13 @@ def _get_enabled_provider_by_name(provider_name):
return enabled_provider


def _get_url(view_name, backend_name, auth_entry=None, redirect_url=None, enroll_course_id=None, email_opt_in=None):
def _get_url(view_name, backend_name, auth_entry=None, redirect_url=None,
enroll_course_id=None, email_opt_in=None, extra_params=None,
url_params=None):
"""Creates a URL to hook into social auth endpoints."""
kwargs = {'backend': backend_name}
url = reverse(view_name, kwargs=kwargs)
url_params = url_params or {}
url_params['backend'] = backend_name
url = reverse(view_name, kwargs=url_params)

query_params = OrderedDict()
if auth_entry:
Expand All @@ -278,6 +283,9 @@ def _get_url(view_name, backend_name, auth_entry=None, redirect_url=None, enroll
if email_opt_in:
query_params[AUTH_EMAIL_OPT_IN_KEY] = email_opt_in

if extra_params:
query_params.update(extra_params)

return u"{url}?{params}".format(
url=url,
params=urllib.urlencode(query_params)
Expand All @@ -297,29 +305,32 @@ def get_complete_url(backend_name):
Raises:
ValueError: if no provider is enabled with the given backend_name.
"""
enabled_provider = provider.Registry.get_by_backend_name(backend_name)

if not enabled_provider:
if not any(provider.Registry.get_enabled_by_backend_name(backend_name)):
raise ValueError('Provider with backend %s not enabled' % backend_name)

return _get_url('social:complete', backend_name)


def get_disconnect_url(provider_name):
def get_disconnect_url(provider_name, association_id):
"""Gets URL for the endpoint that starts the disconnect pipeline.

Args:
provider_name: string. Name of the provider.BaseProvider child you want
to disconnect from.
association_id: int. Optional ID of a specific row in the UserSocialAuth
table to disconnect (useful if multiple providers use a common backend)

Returns:
String. URL that starts the disconnection pipeline.

Raises:
ValueError: if no provider is enabled with the given backend_name.
ValueError: if no provider is enabled with the given name.
"""
enabled_provider = _get_enabled_provider_by_name(provider_name)
return _get_url('social:disconnect', enabled_provider.BACKEND_CLASS.name)
backend_name = _get_enabled_provider_by_name(provider_name).BACKEND_CLASS.name
if association_id:
return _get_url('social:disconnect_individual', backend_name, url_params={'association_id': association_id})
else:
return _get_url('social:disconnect', backend_name)


def get_login_url(provider_name, auth_entry, redirect_url=None, enroll_course_id=None, email_opt_in=None):
Expand Down Expand Up @@ -358,7 +369,8 @@ def get_login_url(provider_name, auth_entry, redirect_url=None, enroll_course_id
auth_entry=auth_entry,
redirect_url=redirect_url,
enroll_course_id=enroll_course_id,
email_opt_in=email_opt_in
email_opt_in=email_opt_in,
extra_params=enabled_provider.get_url_params(),
)


Expand All @@ -374,7 +386,7 @@ def get_duplicate_provider(messages):
unfortunately not in a reusable constant.

Returns:
provider.BaseProvider child instance. The provider of the duplicate
string name of the python-social-auth backend that has the duplicate
account, or None if there is no duplicate (and hence no error).
"""
social_auth_messages = [m for m in messages if m.message.endswith('is already in use.')]
Expand All @@ -383,7 +395,8 @@ def get_duplicate_provider(messages):
return

assert len(social_auth_messages) == 1
return provider.Registry.get_by_backend_name(social_auth_messages[0].extra_tags.split()[1])
backend_name = social_auth_messages[0].extra_tags.split()[1]
return backend_name


def get_provider_user_states(user):
Expand All @@ -397,13 +410,16 @@ def get_provider_user_states(user):
each enabled provider.
"""
states = []
found_user_backends = [
social_auth.provider for social_auth in models.DjangoStorage.user.get_social_auth_for_user(user)
]
found_user_auths = list(models.DjangoStorage.user.get_social_auth_for_user(user))

for enabled_provider in provider.Registry.enabled():
association_id = None
for auth in found_user_auths:
if enabled_provider.match_social_auth(auth):
association_id = auth.id
break

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you like concise code, you could replace these five lines by

association_id = next((auth.id for auth in found_user_auths
                       if enabled_provider.match_social_auth(auth)), None)

What you have is probably more readable, though.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I admire the conciseness of that but I prefer the longer version since I think it's easier to understand.

states.append(
ProviderUserState(enabled_provider, user, enabled_provider.BACKEND_CLASS.name in found_user_backends)
ProviderUserState(enabled_provider, user, association_id)
)

return states
Expand Down Expand Up @@ -445,7 +461,7 @@ def parse_query_params(strategy, response, *args, **kwargs):
"""Reads whitelisted query params, transforms them into pipeline args."""
auth_entry = strategy.session.get(AUTH_ENTRY_KEY)
if not (auth_entry and auth_entry in _AUTH_ENTRY_CHOICES):
raise AuthEntryError(strategy.backend, 'auth_entry missing or invalid')
raise AuthEntryError(strategy.request.backend, 'auth_entry missing or invalid')

return {'auth_entry': auth_entry}

Expand Down Expand Up @@ -526,7 +542,7 @@ def _create_redirect_url(url, strategy):


@partial.partial
def set_logged_in_cookie(backend=None, user=None, request=None, auth_entry=None, *args, **kwargs):
def set_logged_in_cookie(backend=None, user=None, strategy=None, auth_entry=None, *args, **kwargs):
"""This pipeline step sets the "logged in" cookie for authenticated users.

Some installations have a marketing site front-end separate from
Expand All @@ -552,6 +568,7 @@ def set_logged_in_cookie(backend=None, user=None, request=None, auth_entry=None,

"""
if not is_api(auth_entry) and user is not None and user.is_authenticated():
request = strategy.request if strategy else None
if request is not None:
# Check that the cookie isn't already set.
# This ensures that we allow the user to continue to the next
Expand Down Expand Up @@ -692,7 +709,7 @@ def change_enrollment(strategy, auth_entry=None, user=None, *args, **kwargs):


@partial.partial
def associate_by_email_if_login_api(auth_entry, strategy, details, user, *args, **kwargs):
def associate_by_email_if_login_api(auth_entry, backend, details, user, *args, **kwargs):
"""
This pipeline step associates the current social auth with the user with the
same email address in the database. It defers to the social library's associate_by_email
Expand All @@ -701,7 +718,7 @@ def associate_by_email_if_login_api(auth_entry, strategy, details, user, *args,
This association is done ONLY if the user entered the pipeline through a LOGIN API.
"""
if auth_entry == AUTH_ENTRY_LOGIN_API:
association_response = associate_by_email(strategy, details, user, *args, **kwargs)
association_response = associate_by_email(backend, details, user, *args, **kwargs)
if (
association_response and
association_response.get('user') and
Expand Down
Loading