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
d2183c58
Commit
d2183c58
authored
Oct 30, 2014
by
Greg Price
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Add endpoint to log in with OAuth access token
parent
eaa63da4
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 @
d2183c58
...
...
@@ -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 @
d2183c58
...
...
@@ -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 @
d2183c58
...
...
@@ -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 @
d2183c58
...
...
@@ -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 @
d2183c58
...
...
@@ -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 @
d2183c58
...
...
@@ -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