Add email and profile scopes in JWT Cookies
This commit is contained in:
@@ -44,7 +44,7 @@ from openedx.features.enterprise_support.api import get_dashboard_consent_notifi
|
||||
from openedx.features.journals.api import journals_enabled
|
||||
from shoppingcart.api import order_history
|
||||
from shoppingcart.models import CourseRegistrationCode, DonationConfiguration
|
||||
from openedx.core.djangoapps.user_authn.cookies import _set_deprecated_user_info_cookie
|
||||
from openedx.core.djangoapps.user_authn.cookies import set_deprecated_user_info_cookie
|
||||
from student.helpers import cert_info, check_verify_status_by_course
|
||||
from student.models import (
|
||||
CourseEnrollment,
|
||||
@@ -848,5 +848,5 @@ def student_dashboard(request):
|
||||
})
|
||||
|
||||
response = render_to_response('dashboard.html', context)
|
||||
_set_deprecated_user_info_cookie(response, request, user) # pylint: disable=protected-access
|
||||
set_deprecated_user_info_cookie(response, request, user) # pylint: disable=protected-access
|
||||
return response
|
||||
|
||||
@@ -103,7 +103,3 @@ class DOTAdapterMixin(object):
|
||||
def test_single_access_token(self):
|
||||
# TODO: Single access tokens not supported yet for DOT (See MA-2122)
|
||||
super(DOTAdapterMixin, self).test_single_access_token()
|
||||
|
||||
@skip("Not supported yet (See MA-2123)")
|
||||
def test_scopes(self):
|
||||
super(DOTAdapterMixin, self).test_scopes()
|
||||
|
||||
@@ -60,7 +60,7 @@ class AccessTokenExchangeViewTest(AccessTokenExchangeTestMixin):
|
||||
timedelta(seconds=int(content["expires_in"])),
|
||||
provider.constants.EXPIRE_DELTA_PUBLIC
|
||||
)
|
||||
self.assertEqual(content["scope"], self.oauth2_adapter.normalize_scopes(expected_scopes))
|
||||
self.assertEqual(content["scope"], ' '.join(expected_scopes))
|
||||
token = self.oauth2_adapter.get_access_token(token_string=content["access_token"])
|
||||
self.assertEqual(token.user, self.user)
|
||||
self.assertEqual(self.oauth2_adapter.get_client_for_token(token), self.oauth_client)
|
||||
|
||||
@@ -21,6 +21,7 @@ from oauth2_provider.settings import oauth2_settings
|
||||
from oauth2_provider.views.base import TokenView as DOTAccessTokenView
|
||||
from oauthlib.oauth2.rfc6749.tokens import BearerToken
|
||||
from provider import constants
|
||||
from provider import scope as dop_scope
|
||||
from provider.oauth2.views import AccessTokenView as DOPAccessTokenView
|
||||
from rest_framework import permissions
|
||||
from rest_framework.exceptions import AuthenticationFailed
|
||||
@@ -111,7 +112,8 @@ class DOTAccessTokenExchangeView(AccessTokenExchangeBase, DOTAccessTokenView):
|
||||
"""
|
||||
Create and return a new access token.
|
||||
"""
|
||||
return create_dot_access_token(request, user, client, scope=scope)
|
||||
scopes = dop_scope.to_names(scope)
|
||||
return create_dot_access_token(request, user, client, scopes=scopes)
|
||||
|
||||
def access_token_response(self, token):
|
||||
"""
|
||||
|
||||
@@ -69,12 +69,6 @@ class DOPAdapter(object):
|
||||
expires=expires,
|
||||
)
|
||||
|
||||
def normalize_scopes(self, scopes):
|
||||
"""
|
||||
Given a list of scopes, return a space-separated list of those scopes.
|
||||
"""
|
||||
return ' '.join(scopes)
|
||||
|
||||
def get_token_scope_names(self, token):
|
||||
"""
|
||||
Given an access token object, return its scopes.
|
||||
|
||||
@@ -79,14 +79,6 @@ class DOTAdapter(object):
|
||||
expires=expires,
|
||||
)
|
||||
|
||||
def normalize_scopes(self, scopes):
|
||||
"""
|
||||
Given a list of scopes, return a space-separated list of those scopes.
|
||||
"""
|
||||
if not scopes:
|
||||
scopes = ['default']
|
||||
return ' '.join(scopes)
|
||||
|
||||
def get_token_scope_names(self, token):
|
||||
"""
|
||||
Given an access token object, return its scopes.
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
import json
|
||||
from django.conf import settings
|
||||
|
||||
from edx_oauth2_provider.constants import SCOPE_VALUE_DICT
|
||||
from oauthlib.oauth2.rfc6749.errors import OAuth2Error
|
||||
from oauthlib.oauth2.rfc6749.tokens import BearerToken
|
||||
from oauth2_provider.models import AccessToken as dot_access_token
|
||||
@@ -22,7 +21,7 @@ def destroy_oauth_tokens(user):
|
||||
dot_refresh_token.objects.filter(user=user.id).delete()
|
||||
|
||||
|
||||
def create_dot_access_token(request, user, client, expires_in=None, scope=None):
|
||||
def create_dot_access_token(request, user, client, expires_in=None, scopes=None):
|
||||
"""
|
||||
Create and return a new (persisted) access token, including a refresh token.
|
||||
The token is returned in the form of a Dict:
|
||||
@@ -31,17 +30,15 @@ def create_dot_access_token(request, user, client, expires_in=None, scope=None):
|
||||
u'refresh_token': u'another string',
|
||||
u'token_type': u'Bearer',
|
||||
u'expires_in': 36000,
|
||||
u'scope': u'default',
|
||||
u'scope': u'profile email',
|
||||
},
|
||||
"""
|
||||
# TODO (ARCH-204) the 'scope' argument may not really be needed by callers.
|
||||
|
||||
expires_in = _get_expires_in_value(expires_in)
|
||||
token_generator = BearerToken(
|
||||
expires_in=expires_in,
|
||||
request_validator=dot_settings.OAUTH2_VALIDATOR_CLASS(),
|
||||
)
|
||||
_populate_create_access_token_request(request, user, client, scope)
|
||||
_populate_create_access_token_request(request, user, client, scopes)
|
||||
return token_generator.create_token(request, refresh_token=True)
|
||||
|
||||
|
||||
@@ -72,17 +69,15 @@ def _get_expires_in_value(expires_in):
|
||||
return expires_in or dot_settings.ACCESS_TOKEN_EXPIRE_SECONDS
|
||||
|
||||
|
||||
def _populate_create_access_token_request(request, user, client, scope=None):
|
||||
def _populate_create_access_token_request(request, user, client, scopes):
|
||||
"""
|
||||
django-oauth-toolkit expects certain non-standard attributes to
|
||||
be present on the request object. This function modifies the
|
||||
request object to match these expectations
|
||||
"""
|
||||
if scope is None:
|
||||
scope = 0
|
||||
request.user = user
|
||||
request.scopes = [SCOPE_VALUE_DICT[scope]]
|
||||
request.client = client
|
||||
request.scopes = scopes or ''
|
||||
request.state = None
|
||||
request.refresh_token = None
|
||||
request.extra_credentials = None
|
||||
|
||||
@@ -45,7 +45,7 @@ class TestOAuthDispatchAPI(TestCase):
|
||||
{
|
||||
u'token_type': u'Bearer',
|
||||
u'expires_in': EXPECTED_DEFAULT_EXPIRES_IN,
|
||||
u'scope': u'default',
|
||||
u'scope': u'',
|
||||
},
|
||||
token,
|
||||
)
|
||||
@@ -58,7 +58,9 @@ class TestOAuthDispatchAPI(TestCase):
|
||||
|
||||
def test_create_token_overrides(self):
|
||||
expires_in = 4800
|
||||
token = api.create_dot_access_token(HttpRequest(), self.user, self.client, expires_in=expires_in, scope=2)
|
||||
token = api.create_dot_access_token(
|
||||
HttpRequest(), self.user, self.client, expires_in=expires_in, scopes=['profile'],
|
||||
)
|
||||
self.assertDictContainsSubset({u'scope': u'profile'}, token)
|
||||
self.assertDictContainsSubset({u'expires_in': expires_in}, token)
|
||||
|
||||
@@ -69,7 +71,7 @@ class TestOAuthDispatchAPI(TestCase):
|
||||
{
|
||||
u'token_type': u'Bearer',
|
||||
u'expires_in': EXPECTED_DEFAULT_EXPIRES_IN,
|
||||
u'scope': u'default',
|
||||
u'scope': u'',
|
||||
},
|
||||
new_token,
|
||||
)
|
||||
|
||||
@@ -135,9 +135,13 @@ def set_logged_in_cookies(request, response, user):
|
||||
# that is passed in when needed.
|
||||
if user.is_authenticated and not user.is_anonymous:
|
||||
|
||||
_set_deprecated_logged_in_cookie(response, request)
|
||||
_set_deprecated_user_info_cookie(response, request, user)
|
||||
_create_and_set_jwt_cookies(response, request, user)
|
||||
# JWT cookies expire at the same time as other login-related cookies
|
||||
# so that cookie-based login determination remains consistent.
|
||||
cookie_settings = standard_cookie_settings(request)
|
||||
|
||||
_set_deprecated_logged_in_cookie(response, cookie_settings)
|
||||
set_deprecated_user_info_cookie(response, request, user, cookie_settings)
|
||||
_create_and_set_jwt_cookies(response, request, cookie_settings, user=user)
|
||||
CREATE_LOGON_COOKIE.send(sender=None, user=user, response=response)
|
||||
|
||||
return response
|
||||
@@ -153,29 +157,14 @@ def refresh_jwt_cookies(request, response):
|
||||
refresh_token = request.COOKIES[jwt_cookies.jwt_refresh_cookie_name()]
|
||||
except KeyError:
|
||||
raise AuthFailedError(u"JWT Refresh Cookie not found in request.")
|
||||
_create_and_set_jwt_cookies(response, request, refresh_token=refresh_token)
|
||||
|
||||
# TODO don't extend the cookie expiration - reuse value from existing cookie
|
||||
cookie_settings = standard_cookie_settings(request)
|
||||
_create_and_set_jwt_cookies(response, request, cookie_settings, refresh_token=refresh_token)
|
||||
return response
|
||||
|
||||
|
||||
def _set_deprecated_logged_in_cookie(response, request):
|
||||
""" Sets the logged in cookie on the response. """
|
||||
|
||||
# Backwards compatibility: set the cookie indicating that the user
|
||||
# is logged in. This is just a boolean value, so it's not very useful.
|
||||
# In the future, we should be able to replace this with the "user info"
|
||||
# cookie set below.
|
||||
cookie_settings = standard_cookie_settings(request)
|
||||
|
||||
response.set_cookie(
|
||||
settings.EDXMKTG_LOGGED_IN_COOKIE_NAME.encode('utf-8'),
|
||||
'true',
|
||||
**cookie_settings
|
||||
)
|
||||
|
||||
return response
|
||||
|
||||
|
||||
def _set_deprecated_user_info_cookie(response, request, user):
|
||||
def set_deprecated_user_info_cookie(response, request, user, cookie_settings=None):
|
||||
"""
|
||||
Sets the user info cookie on the response.
|
||||
|
||||
@@ -192,8 +181,7 @@ def _set_deprecated_user_info_cookie(response, request, user):
|
||||
}
|
||||
}
|
||||
"""
|
||||
cookie_settings = standard_cookie_settings(request)
|
||||
|
||||
cookie_settings = cookie_settings or standard_cookie_settings(request)
|
||||
user_info = _get_user_info_cookie_data(request, user)
|
||||
response.set_cookie(
|
||||
settings.EDXMKTG_USER_INFO_COOKIE_NAME.encode('utf-8'),
|
||||
@@ -202,6 +190,22 @@ def _set_deprecated_user_info_cookie(response, request, user):
|
||||
)
|
||||
|
||||
|
||||
def _set_deprecated_logged_in_cookie(response, cookie_settings):
|
||||
""" Sets the logged in cookie on the response. """
|
||||
|
||||
# Backwards compatibility: set the cookie indicating that the user
|
||||
# is logged in. This is just a boolean value, so it's not very useful.
|
||||
# In the future, we should be able to replace this with the "user info"
|
||||
# cookie set below.
|
||||
response.set_cookie(
|
||||
settings.EDXMKTG_LOGGED_IN_COOKIE_NAME.encode('utf-8'),
|
||||
'true',
|
||||
**cookie_settings
|
||||
)
|
||||
|
||||
return response
|
||||
|
||||
|
||||
def _get_user_info_cookie_data(request, user):
|
||||
""" Returns information that will populate the user info cookie. """
|
||||
|
||||
@@ -242,15 +246,11 @@ def _get_user_info_cookie_data(request, user):
|
||||
return user_info
|
||||
|
||||
|
||||
def _create_and_set_jwt_cookies(response, request, user=None, refresh_token=None):
|
||||
def _create_and_set_jwt_cookies(response, request, cookie_settings, user=None, refresh_token=None):
|
||||
""" Sets a cookie containing a JWT on the response. """
|
||||
if not JWT_COOKIES_FLAG.is_enabled():
|
||||
return
|
||||
|
||||
# JWT cookies expire at the same time as other login-related cookies
|
||||
# so that cookie-based login determination remains consistent.
|
||||
cookie_settings = standard_cookie_settings(request)
|
||||
|
||||
# For security reasons, the JWT that is embedded inside the cookie expires
|
||||
# much sooner than the cookie itself, per the following setting.
|
||||
expires_in = settings.JWT_AUTH['JWT_IN_COOKIE_EXPIRATION']
|
||||
@@ -262,7 +262,7 @@ def _create_and_set_jwt_cookies(response, request, user=None, refresh_token=None
|
||||
)
|
||||
else:
|
||||
access_token = create_dot_access_token(
|
||||
request, user, oauth_application, expires_in=expires_in,
|
||||
request, user, oauth_application, expires_in=expires_in, scopes=['email', 'profile'],
|
||||
)
|
||||
jwt = create_jwt_from_token(access_token, DOTAdapter(), use_asymmetric_key=True)
|
||||
jwt_header_and_payload, jwt_signature = _parse_jwt(jwt)
|
||||
|
||||
@@ -8,6 +8,7 @@ from django.http import HttpResponse
|
||||
from django.urls import reverse
|
||||
from django.test import RequestFactory, TestCase
|
||||
|
||||
from edx_rest_framework_extensions.auth.jwt.decoder import jwt_decode_handler
|
||||
from edx_rest_framework_extensions.auth.jwt.middleware import JwtAuthCookieMiddleware
|
||||
from openedx.core.djangoapps.user_authn import cookies as cookies_api
|
||||
from openedx.core.djangoapps.user_authn.tests.utils import setup_login_oauth_client
|
||||
@@ -57,8 +58,10 @@ class CookieTests(TestCase):
|
||||
|
||||
def _assert_recreate_jwt_from_cookies(self, response, can_recreate):
|
||||
"""
|
||||
Verifies that a JWT can be properly recreated from the 2 separate
|
||||
JWT-related cookies using the JwtAuthCookieMiddleware middleware.
|
||||
If can_recreate is True, verifies that a JWT can be properly recreated
|
||||
from the 2 separate JWT-related cookies using the
|
||||
JwtAuthCookieMiddleware middleware and returns the recreated JWT.
|
||||
If can_recreate is False, verifies that a JWT cannot be recreated.
|
||||
"""
|
||||
self._copy_cookies_to_request(response, self.request)
|
||||
JwtAuthCookieMiddleware().process_request(self.request)
|
||||
@@ -66,6 +69,10 @@ class CookieTests(TestCase):
|
||||
cookies_api.jwt_cookies.jwt_cookie_name() in self.request.COOKIES,
|
||||
can_recreate,
|
||||
)
|
||||
if can_recreate:
|
||||
jwt_string = self.request.COOKIES[cookies_api.jwt_cookies.jwt_cookie_name()]
|
||||
jwt = jwt_decode_handler(jwt_string)
|
||||
self.assertEqual(jwt['scopes'], ['email', 'profile'])
|
||||
|
||||
def _assert_cookies_present(self, response, expected_cookies):
|
||||
""" Verify all expected_cookies are present in the response. """
|
||||
|
||||
Reference in New Issue
Block a user