Add email and profile scopes in JWT Cookies

This commit is contained in:
Nimisha Asthagiri
2018-10-18 00:04:28 -04:00
parent dc56a63e03
commit 45dadca18b
10 changed files with 56 additions and 68 deletions

View File

@@ -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

View File

@@ -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()

View File

@@ -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)

View File

@@ -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):
"""

View File

@@ -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.

View File

@@ -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.

View File

@@ -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

View File

@@ -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,
)

View File

@@ -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)

View File

@@ -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. """