Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
E
edx-platform
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
edx
edx-platform
Commits
6d5bb9b1
Commit
6d5bb9b1
authored
Nov 05, 2014
by
Greg Price
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #5813 from edx/gprice/login-oauth-token
Add endpoint to log in with OAuth access token
parents
90b40e7d
d2183c58
Hide whitespace changes
Inline
Side-by-side
Showing
6 changed files
with
162 additions
and
8 deletions
+162
-8
common/djangoapps/student/tests/test_login.py
+93
-1
common/djangoapps/student/views.py
+34
-0
common/djangoapps/third_party_auth/pipeline.py
+32
-6
common/djangoapps/third_party_auth/settings.py
+1
-1
lms/urls.py
+1
-0
requirements/edx/base.txt
+1
-0
No files found.
common/djangoapps/student/tests/test_login.py
View file @
6d5bb9b1
...
...
@@ -12,8 +12,14 @@ from django.conf import settings
from
django.core.cache
import
cache
from
django.core.urlresolvers
import
reverse
,
NoReverseMatch
from
django.http
import
HttpResponseBadRequest
,
HttpResponse
import
httpretty
from
social.apps.django_app.default.models
import
UserSocialAuth
from
student.tests.factories
import
UserFactory
,
RegistrationFactory
,
UserProfileFactory
from
student.views
import
_parse_course_id_from_string
,
_get_course_enrollment_domain
from
student.views
import
(
_parse_course_id_from_string
,
_get_course_enrollment_domain
,
login_oauth_token
,
)
from
xmodule.modulestore.tests.factories
import
CourseFactory
from
xmodule.modulestore.tests.django_utils
import
ModuleStoreTestCase
,
mixed_store_config
...
...
@@ -430,3 +436,89 @@ class ExternalAuthShibTest(ModuleStoreTestCase):
self
.
assertEqual
(
shib_response
.
redirect_chain
[
-
2
],
(
'http://testserver{url}'
.
format
(
url
=
TARGET_URL_SHIB
),
302
))
self
.
assertEqual
(
shib_response
.
status_code
,
200
)
@httpretty.activate
class
LoginOAuthTokenMixin
(
object
):
"""
Mixin with tests for the login_oauth_token view. A TestCase that includes
this must define the following:
BACKEND: The name of the backend from python-social-auth
USER_URL: The URL of the endpoint that the backend retrieves user data from
UID_FIELD: The field in the user data that the backend uses as the user id
"""
def
setUp
(
self
):
self
.
client
=
Client
()
self
.
url
=
reverse
(
login_oauth_token
,
kwargs
=
{
"backend"
:
self
.
BACKEND
})
self
.
social_uid
=
"social_uid"
self
.
user
=
UserFactory
()
UserSocialAuth
.
objects
.
create
(
user
=
self
.
user
,
provider
=
self
.
BACKEND
,
uid
=
self
.
social_uid
)
def
_setup_user_response
(
self
,
success
):
"""
Register a mock response for the third party user information endpoint;
success indicates whether the response status code should be 200 or 400
"""
if
success
:
status
=
200
body
=
json
.
dumps
({
self
.
UID_FIELD
:
self
.
social_uid
})
else
:
status
=
400
body
=
json
.
dumps
({})
httpretty
.
register_uri
(
httpretty
.
GET
,
self
.
USER_URL
,
body
=
body
,
status
=
status
,
content_type
=
"application/json"
)
def
_assert_error
(
self
,
response
,
status_code
,
error
):
"""Assert that the given response was a 400 with the given error code"""
self
.
assertEqual
(
response
.
status_code
,
status_code
)
self
.
assertEqual
(
json
.
loads
(
response
.
content
),
{
"error"
:
error
})
self
.
assertNotIn
(
"partial_pipeline"
,
self
.
client
.
session
)
def
test_success
(
self
):
self
.
_setup_user_response
(
success
=
True
)
response
=
self
.
client
.
post
(
self
.
url
,
{
"access_token"
:
"dummy"
})
self
.
assertEqual
(
response
.
status_code
,
204
)
def
test_invalid_token
(
self
):
self
.
_setup_user_response
(
success
=
False
)
response
=
self
.
client
.
post
(
self
.
url
,
{
"access_token"
:
"dummy"
})
self
.
_assert_error
(
response
,
401
,
"invalid_token"
)
def
test_missing_token
(
self
):
response
=
self
.
client
.
post
(
self
.
url
)
self
.
_assert_error
(
response
,
400
,
"invalid_request"
)
def
test_unlinked_user
(
self
):
UserSocialAuth
.
objects
.
all
()
.
delete
()
self
.
_setup_user_response
(
success
=
True
)
response
=
self
.
client
.
post
(
self
.
url
,
{
"access_token"
:
"dummy"
})
self
.
_assert_error
(
response
,
401
,
"invalid_token"
)
def
test_get_method
(
self
):
response
=
self
.
client
.
get
(
self
.
url
,
{
"access_token"
:
"dummy"
})
self
.
assertEqual
(
response
.
status_code
,
405
)
# This is necessary because cms does not implement third party auth
@unittest.skipUnless
(
settings
.
FEATURES
.
get
(
"ENABLE_THIRD_PARTY_AUTH"
),
"third party auth not enabled"
)
class
LoginOAuthTokenTestFacebook
(
LoginOAuthTokenMixin
,
TestCase
):
"""Tests login_oauth_token with the Facebook backend"""
BACKEND
=
"facebook"
USER_URL
=
"https://graph.facebook.com/me"
UID_FIELD
=
"id"
# This is necessary because cms does not implement third party auth
@unittest.skipUnless
(
settings
.
FEATURES
.
get
(
"ENABLE_THIRD_PARTY_AUTH"
),
"third party auth not enabled"
)
class
LoginOAuthTokenTestGoogle
(
LoginOAuthTokenMixin
,
TestCase
):
"""Tests login_oauth_token with the Google backend"""
BACKEND
=
"google-oauth2"
USER_URL
=
"https://www.googleapis.com/oauth2/v1/userinfo"
UID_FIELD
=
"email"
common/djangoapps/student/views.py
View file @
6d5bb9b1
...
...
@@ -39,6 +39,11 @@ from django.template.response import TemplateResponse
from
ratelimitbackend.exceptions
import
RateLimitException
from
requests
import
HTTPError
from
social.apps.django_app
import
utils
as
social_utils
from
social.backends
import
oauth
as
social_oauth
from
edxmako.shortcuts
import
render_to_response
,
render_to_string
from
mako.exceptions
import
TopLevelLookupException
...
...
@@ -1109,6 +1114,35 @@ def login_user(request, error=""): # pylint: disable-msg=too-many-statements,un
})
# TODO: this should be status code 400 # pylint: disable=fixme
@require_POST
@social_utils.strategy
(
"social:complete"
)
def
login_oauth_token
(
request
,
backend
):
"""
Authenticate the client using an OAuth access token by using the token to
retrieve information from a third party and matching that information to an
existing user.
"""
backend
=
request
.
social_strategy
.
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_API
user
=
None
try
:
user
=
backend
.
do_auth
(
request
.
POST
[
"access_token"
])
except
HTTPError
:
pass
# do_auth can return a non-User object if it fails
if
user
and
isinstance
(
user
,
User
):
return
JsonResponse
(
status
=
204
)
else
:
# Ensure user does not re-enter the pipeline
request
.
social_strategy
.
clean_partial_pipeline
()
return
JsonResponse
({
"error"
:
"invalid_token"
},
status
=
401
)
else
:
return
JsonResponse
({
"error"
:
"invalid_request"
},
status
=
400
)
raise
Http404
@ensure_csrf_cookie
def
logout_user
(
request
):
...
...
common/djangoapps/third_party_auth/pipeline.py
View file @
6d5bb9b1
...
...
@@ -66,6 +66,7 @@ from eventtracking import tracker
from
django.contrib.auth.models
import
User
from
django.core.urlresolvers
import
reverse
from
django.http
import
HttpResponseBadRequest
from
django.shortcuts
import
redirect
from
social.apps.django_app.default
import
models
from
social.exceptions
import
AuthException
...
...
@@ -109,11 +110,13 @@ AUTH_ENTRY_DASHBOARD = 'dashboard'
AUTH_ENTRY_LOGIN
=
'login'
AUTH_ENTRY_PROFILE
=
'profile'
AUTH_ENTRY_REGISTER
=
'register'
AUTH_ENTRY_API
=
'api'
_AUTH_ENTRY_CHOICES
=
frozenset
([
AUTH_ENTRY_DASHBOARD
,
AUTH_ENTRY_LOGIN
,
AUTH_ENTRY_PROFILE
,
AUTH_ENTRY_REGISTER
AUTH_ENTRY_REGISTER
,
AUTH_ENTRY_API
,
])
_DEFAULT_RANDOM_PASSWORD_LENGTH
=
12
_PASSWORD_CHARSET
=
string
.
letters
+
string
.
digits
...
...
@@ -396,15 +399,33 @@ def parse_query_params(strategy, response, *args, **kwargs):
'is_register'
:
auth_entry
==
AUTH_ENTRY_REGISTER
,
# Whether the auth pipeline entered from /profile.
'is_profile'
:
auth_entry
==
AUTH_ENTRY_PROFILE
,
# Whether the auth pipeline entered from an API
'is_api'
:
auth_entry
==
AUTH_ENTRY_API
,
}
@partial.partial
def
redirect_to_supplementary_form
(
strategy
,
details
,
response
,
uid
,
is_dashboard
=
None
,
is_login
=
None
,
is_profile
=
None
,
is_register
=
None
,
user
=
None
,
*
args
,
**
kwargs
):
"""Dispatches user to views outside the pipeline if necessary."""
def
ensure_user_information
(
strategy
,
details
,
response
,
uid
,
is_dashboard
=
None
,
is_login
=
None
,
is_profile
=
None
,
is_register
=
None
,
is_api
=
None
,
user
=
None
,
*
args
,
**
kwargs
):
"""
Ensure that we have the necessary information about a user (either an
existing account or registration data) to proceed with the pipeline.
"""
# We're deliberately verbose here to make it clear what the intended
# dispatch behavior is for the
four
pipeline entry points, given the
# dispatch behavior is for the
various
pipeline entry points, given the
# current state of the pipeline. Keep in mind the pipeline is re-entrant
# and values will change on repeated invocations (for example, the first
# time through the login flow the user will be None so we dispatch to the
...
...
@@ -418,6 +439,11 @@ def redirect_to_supplementary_form(strategy, details, response, uid, is_dashboar
user_inactive
=
user
and
not
user
.
is_active
user_unset
=
user
is
None
dispatch_to_login
=
is_login
and
(
user_unset
or
user_inactive
)
reject_api_request
=
is_api
and
(
user_unset
or
user_inactive
)
if
reject_api_request
:
# Content doesn't matter; we just want to exit the pipeline
return
HttpResponseBadRequest
()
if
is_dashboard
or
is_profile
:
return
...
...
@@ -430,7 +456,7 @@ def redirect_to_supplementary_form(strategy, details, response, uid, is_dashboar
@partial.partial
def
set_logged_in_cookie
(
backend
=
None
,
user
=
None
,
request
=
None
,
*
args
,
**
kwargs
):
def
set_logged_in_cookie
(
backend
=
None
,
user
=
None
,
request
=
None
,
is_api
=
None
,
*
args
,
**
kwargs
):
"""This pipeline step sets the "logged in" cookie for authenticated users.
Some installations have a marketing site front-end separate from
...
...
@@ -455,7 +481,7 @@ def set_logged_in_cookie(backend=None, user=None, request=None, *args, **kwargs)
to the next pipeline step.
"""
if
user
is
not
None
and
user
.
is_authenticated
():
if
user
is
not
None
and
user
.
is_authenticated
()
and
not
is_api
:
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
...
...
common/djangoapps/third_party_auth/settings.py
View file @
6d5bb9b1
...
...
@@ -111,7 +111,7 @@ def _set_global_settings(django_settings):
'social.pipeline.social_auth.auth_allowed'
,
'social.pipeline.social_auth.social_user'
,
'social.pipeline.user.get_username'
,
'third_party_auth.pipeline.
redirect_to_supplementary_form
'
,
'third_party_auth.pipeline.
ensure_user_information
'
,
'social.pipeline.user.create_user'
,
'social.pipeline.social_auth.associate_user'
,
'social.pipeline.social_auth.load_extra_data'
,
...
...
lms/urls.py
View file @
6d5bb9b1
...
...
@@ -534,6 +534,7 @@ if settings.FEATURES.get('AUTOMATIC_AUTH_FOR_TESTING'):
if
settings
.
FEATURES
.
get
(
'ENABLE_THIRD_PARTY_AUTH'
):
urlpatterns
+=
(
url
(
r''
,
include
(
'third_party_auth.urls'
)),
url
(
r'^login_oauth_token/(?P<backend>[^/]+)/$'
,
'student.views.login_oauth_token'
),
)
# If enabled, expose the URLs for the new dashboard, account, and profile pages
...
...
requirements/edx/base.txt
View file @
6d5bb9b1
...
...
@@ -44,6 +44,7 @@ git+https://github.com/pmitros/pyfs.git@96e1922348bfe6d99201b9512a9ed946c87b7e0b
GitPython==0.3.2.RC1
glob2==0.3
gunicorn==0.17.4
httpretty==0.8.3
lazy==1.1
lxml==3.3.6
mako==0.9.1
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment