Refactor Login Cookies
This commit is contained in:
@@ -29,7 +29,7 @@ from openedx.core.djangoapps.content.course_overviews.models import CourseOvervi
|
||||
from openedx.core.djangoapps.content.course_overviews.tests.factories import CourseOverviewFactory
|
||||
from openedx.core.djangoapps.site_configuration.tests.test_util import with_site_configuration_context
|
||||
from pyquery import PyQuery as pq
|
||||
from openedx.core.djangoapps.user_authn.cookies import get_user_info_cookie_data
|
||||
from openedx.core.djangoapps.user_authn.cookies import _get_user_info_cookie_data
|
||||
from student.helpers import DISABLE_UNENROLL_CERT_STATES
|
||||
from student.models import CourseEnrollment, UserProfile
|
||||
from student.signals import REFUND_ORDER
|
||||
@@ -301,7 +301,7 @@ class StudentDashboardTests(SharedModuleStoreTestCase, MilestonesTestCaseMixin,
|
||||
|
||||
request = RequestFactory().get(self.path)
|
||||
request.user = self.user
|
||||
expected = json.dumps(get_user_info_cookie_data(request))
|
||||
expected = json.dumps(_get_user_info_cookie_data(request, self.user))
|
||||
self.client.get(self.path)
|
||||
actual = self.client.cookies[settings.EDXMKTG_USER_INFO_COOKIE_NAME].value
|
||||
self.assertEqual(actual, expected)
|
||||
|
||||
@@ -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_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_user_info_cookie(response, request)
|
||||
_set_deprecated_user_info_cookie(response, request, user) # pylint: disable=protected-access
|
||||
return response
|
||||
|
||||
@@ -6,7 +6,6 @@ django-oauth-toolkit as appropriate.
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import json
|
||||
import logging
|
||||
|
||||
from django.conf import settings
|
||||
from django.views.generic import View
|
||||
@@ -24,8 +23,6 @@ from . import adapters
|
||||
from .dot_overrides import views as dot_overrides_views
|
||||
from .toggles import ENFORCE_JWT_SCOPES
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class _DispatchingView(View):
|
||||
"""
|
||||
@@ -159,15 +156,9 @@ class AccessTokenView(RatelimitMixin, _DispatchingView):
|
||||
# (asymmetric) key.
|
||||
# TODO: ARCH-162
|
||||
use_asymmetric_key = ENFORCE_JWT_SCOPES.is_enabled() and is_client_restricted
|
||||
monitoring_utils.set_custom_metric('oauth_asymmetric_jwt', use_asymmetric_key)
|
||||
|
||||
log.info("Using Asymmetric JWT: %s", use_asymmetric_key)
|
||||
|
||||
return JwtBuilder(
|
||||
user,
|
||||
asymmetric=use_asymmetric_key,
|
||||
secret=settings.JWT_AUTH['JWT_SECRET_KEY'],
|
||||
issuer=settings.JWT_AUTH['JWT_ISSUER'],
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -13,12 +13,70 @@ from django.urls import NoReverseMatch, reverse
|
||||
from django.dispatch import Signal
|
||||
from django.utils.http import cookie_date
|
||||
|
||||
from edx_rest_framework_extensions.auth.jwt import cookies as jwt_cookies
|
||||
from openedx.core.djangoapps.user_api.accounts.utils import retrieve_last_sitewide_block_completed
|
||||
from openedx.core.djangoapps.user_authn.waffle import JWT_COOKIES_FLAG
|
||||
from student.models import CourseEnrollment
|
||||
|
||||
|
||||
CREATE_LOGON_COOKIE = Signal(providing_args=['user', 'response'])
|
||||
|
||||
|
||||
JWT_COOKIE_NAMES = (
|
||||
# Header and payload sections of a JSON Web Token containing user
|
||||
# information and used as an access token.
|
||||
jwt_cookies.jwt_cookie_header_payload_name(),
|
||||
|
||||
# Signature section of a JSON Web Token.
|
||||
jwt_cookies.jwt_cookie_signature_name(),
|
||||
|
||||
# Refresh token, which can be used to get a new JSON Web Token.
|
||||
jwt_cookies.jwt_refresh_cookie_name(),
|
||||
)
|
||||
|
||||
# TODO (ARCH-245): Remove the following deprecated cookies.
|
||||
DEPRECATED_LOGGED_IN_COOKIE_NAMES = (
|
||||
# Set to 'true' if the user is logged in.
|
||||
settings.EDXMKTG_LOGGED_IN_COOKIE_NAME,
|
||||
|
||||
# JSON-encoded dictionary with user information.
|
||||
settings.EDXMKTG_USER_INFO_COOKIE_NAME,
|
||||
)
|
||||
|
||||
ALL_LOGGED_IN_COOKIE_NAMES = JWT_COOKIE_NAMES + DEPRECATED_LOGGED_IN_COOKIE_NAMES
|
||||
|
||||
|
||||
def is_logged_in_cookie_set(request):
|
||||
""" Check whether the request has logged in cookies set. """
|
||||
if JWT_COOKIES_FLAG.is_enabled():
|
||||
expected_cookie_names = ALL_LOGGED_IN_COOKIE_NAMES
|
||||
else:
|
||||
expected_cookie_names = DEPRECATED_LOGGED_IN_COOKIE_NAMES
|
||||
|
||||
return all(
|
||||
cookie_name in request.COOKIES
|
||||
for cookie_name in expected_cookie_names
|
||||
)
|
||||
|
||||
|
||||
def delete_logged_in_cookies(response):
|
||||
"""
|
||||
Delete cookies indicating that the user is logged in.
|
||||
Arguments:
|
||||
response (HttpResponse): The response sent to the client.
|
||||
Returns:
|
||||
HttpResponse
|
||||
"""
|
||||
for cookie_name in ALL_LOGGED_IN_COOKIE_NAMES:
|
||||
response.delete_cookie(
|
||||
cookie_name.encode('utf-8'),
|
||||
path='/',
|
||||
domain=settings.SESSION_COOKIE_DOMAIN
|
||||
)
|
||||
|
||||
return response
|
||||
|
||||
|
||||
def standard_cookie_settings(request):
|
||||
""" Returns the common cookie settings (e.g. expiration time). """
|
||||
|
||||
@@ -27,8 +85,7 @@ def standard_cookie_settings(request):
|
||||
expires = None
|
||||
else:
|
||||
max_age = request.session.get_expiry_age()
|
||||
expires_time = time.time() + max_age
|
||||
expires = cookie_date(expires_time)
|
||||
expires = _cookie_expiration_based_on_max_age(max_age)
|
||||
|
||||
cookie_settings = {
|
||||
'max_age': max_age,
|
||||
@@ -38,21 +95,69 @@ def standard_cookie_settings(request):
|
||||
'httponly': None,
|
||||
}
|
||||
|
||||
# In production, TLS should be enabled so that this cookie is encrypted
|
||||
# when we send it. We also need to set "secure" to True so that the browser
|
||||
# will transmit it only over secure connections.
|
||||
#
|
||||
# In non-production environments (acceptance tests, devstack, and sandboxes),
|
||||
# we still want to set this cookie. However, we do NOT want to set it to "secure"
|
||||
# because the browser won't send it back to us. This can cause an infinite redirect
|
||||
# loop in the third-party auth flow, which calls `is_logged_in_cookie_set` to determine
|
||||
# whether it needs to set the cookie or continue to the next pipeline stage.
|
||||
cookie_settings['secure'] = request.is_secure()
|
||||
|
||||
return cookie_settings
|
||||
|
||||
|
||||
def set_logged_in_cookies(request, response, user):
|
||||
"""
|
||||
Set cookies indicating that the user is logged in.
|
||||
Set cookies at the time of user login. See ALL_LOGGED_IN_COOKIE_NAMES to see
|
||||
which cookies are set.
|
||||
|
||||
Some installations have an external marketing site configured
|
||||
that displays a different UI when the user is logged in
|
||||
(e.g. a link to the student dashboard instead of to the login page)
|
||||
Arguments:
|
||||
request (HttpRequest): The request to the view, used to calculate
|
||||
the cookie's expiration date based on the session expiration date.
|
||||
response (HttpResponse): The response on which the cookie will be set.
|
||||
user (User): The currently logged in user.
|
||||
|
||||
Currently, two cookies are set:
|
||||
Returns:
|
||||
HttpResponse
|
||||
|
||||
* EDXMKTG_LOGGED_IN_COOKIE_NAME: Set to 'true' if the user is logged in.
|
||||
* EDXMKTG_USER_INFO_COOKIE_VERSION: JSON-encoded dictionary with user information (see below).
|
||||
"""
|
||||
# Note: The user may not yet be set on the request object by this time,
|
||||
# especially during third party authentication. So use the user object
|
||||
# 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)
|
||||
_set_jwt_cookies(response, request, user)
|
||||
CREATE_LOGON_COOKIE.send(sender=None, user=user, response=response)
|
||||
|
||||
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):
|
||||
"""
|
||||
Sets the user info cookie on the response.
|
||||
|
||||
The user info cookie has the following format:
|
||||
{
|
||||
@@ -66,91 +171,26 @@ def set_logged_in_cookies(request, response, user):
|
||||
"logout": "https://example.com/logout"
|
||||
}
|
||||
}
|
||||
|
||||
Arguments:
|
||||
request (HttpRequest): The request to the view, used to calculate
|
||||
the cookie's expiration date based on the session expiration date.
|
||||
response (HttpResponse): The response on which the cookie will be set.
|
||||
user (User): The currently logged in user.
|
||||
|
||||
Returns:
|
||||
HttpResponse
|
||||
|
||||
"""
|
||||
cookie_settings = standard_cookie_settings(request)
|
||||
|
||||
# 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',
|
||||
secure=None,
|
||||
**cookie_settings
|
||||
)
|
||||
|
||||
set_user_info_cookie(response, request)
|
||||
|
||||
# give signal receivers a chance to add cookies
|
||||
CREATE_LOGON_COOKIE.send(sender=None, user=user, response=response)
|
||||
|
||||
return response
|
||||
|
||||
|
||||
def set_user_info_cookie(response, request):
|
||||
""" Sets the user info cookie on the response. """
|
||||
cookie_settings = standard_cookie_settings(request)
|
||||
|
||||
# In production, TLS should be enabled so that this cookie is encrypted
|
||||
# when we send it. We also need to set "secure" to True so that the browser
|
||||
# will transmit it only over secure connections.
|
||||
#
|
||||
# In non-production environments (acceptance tests, devstack, and sandboxes),
|
||||
# we still want to set this cookie. However, we do NOT want to set it to "secure"
|
||||
# because the browser won't send it back to us. This can cause an infinite redirect
|
||||
# loop in the third-party auth flow, which calls `is_logged_in_cookie_set` to determine
|
||||
# whether it needs to set the cookie or continue to the next pipeline stage.
|
||||
user_info_cookie_is_secure = request.is_secure()
|
||||
user_info = get_user_info_cookie_data(request)
|
||||
|
||||
user_info = _get_user_info_cookie_data(request, user)
|
||||
response.set_cookie(
|
||||
settings.EDXMKTG_USER_INFO_COOKIE_NAME.encode('utf-8'),
|
||||
json.dumps(user_info),
|
||||
secure=user_info_cookie_is_secure,
|
||||
**cookie_settings
|
||||
)
|
||||
|
||||
|
||||
def set_experiments_is_enterprise_cookie(request, response, experiments_is_enterprise):
|
||||
""" Sets the experiments_is_enterprise cookie on the response.
|
||||
This cookie can be used for tests or minor features,
|
||||
but should not be used for payment related or other critical work
|
||||
since users can edit their cookies
|
||||
"""
|
||||
cookie_settings = standard_cookie_settings(request)
|
||||
# In production, TLS should be enabled so that this cookie is encrypted
|
||||
# when we send it. We also need to set "secure" to True so that the browser
|
||||
# will transmit it only over secure connections.
|
||||
#
|
||||
# In non-production environments (acceptance tests, devstack, and sandboxes),
|
||||
# we still want to set this cookie. However, we do NOT want to set it to "secure"
|
||||
# because the browser won't send it back to us. This can cause an infinite redirect
|
||||
# loop in the third-party auth flow, which calls `is_logged_in_cookie_set` to determine
|
||||
# whether it needs to set the cookie or continue to the next pipeline stage.
|
||||
cookie_is_secure = request.is_secure()
|
||||
|
||||
response.set_cookie(
|
||||
'experiments_is_enterprise',
|
||||
json.dumps(experiments_is_enterprise),
|
||||
secure=cookie_is_secure,
|
||||
**cookie_settings
|
||||
)
|
||||
def _set_jwt_cookies(response, request, user): # pylint: disable=unused-argument
|
||||
""" Sets a cookie containing a JWT on the response. """
|
||||
if not JWT_COOKIES_FLAG.is_enabled():
|
||||
return
|
||||
# TODO (ARCH-236)
|
||||
|
||||
|
||||
def get_user_info_cookie_data(request):
|
||||
""" Returns information that wil populate the user info cookie. """
|
||||
user = request.user
|
||||
def _get_user_info_cookie_data(request, user):
|
||||
""" Returns information that will populate the user info cookie. """
|
||||
|
||||
# Set a cookie with user info. This can be used by external sites
|
||||
# to customize content based on user information. Currently,
|
||||
@@ -189,30 +229,6 @@ def get_user_info_cookie_data(request):
|
||||
return user_info
|
||||
|
||||
|
||||
def delete_logged_in_cookies(response):
|
||||
"""
|
||||
Delete cookies indicating that the user is logged in.
|
||||
|
||||
Arguments:
|
||||
response (HttpResponse): The response sent to the client.
|
||||
|
||||
Returns:
|
||||
HttpResponse
|
||||
|
||||
"""
|
||||
for cookie_name in [settings.EDXMKTG_LOGGED_IN_COOKIE_NAME, settings.EDXMKTG_USER_INFO_COOKIE_NAME]:
|
||||
response.delete_cookie(
|
||||
cookie_name.encode('utf-8'),
|
||||
path='/',
|
||||
domain=settings.SESSION_COOKIE_DOMAIN
|
||||
)
|
||||
|
||||
return response
|
||||
|
||||
|
||||
def is_logged_in_cookie_set(request):
|
||||
"""Check whether the request has logged in cookies set. """
|
||||
return (
|
||||
settings.EDXMKTG_LOGGED_IN_COOKIE_NAME in request.COOKIES and
|
||||
settings.EDXMKTG_USER_INFO_COOKIE_NAME in request.COOKIES
|
||||
)
|
||||
def _cookie_expiration_based_on_max_age(max_age):
|
||||
expires_time = time.time() + max_age
|
||||
return cookie_date(expires_time)
|
||||
|
||||
@@ -6,7 +6,7 @@ from django.conf import settings
|
||||
from django.urls import reverse
|
||||
from django.test import RequestFactory
|
||||
|
||||
from openedx.core.djangoapps.user_authn.cookies import get_user_info_cookie_data
|
||||
from openedx.core.djangoapps.user_authn.cookies import _get_user_info_cookie_data
|
||||
from openedx.core.djangoapps.user_api.accounts.utils import retrieve_last_sitewide_block_completed
|
||||
from student.models import CourseEnrollment
|
||||
from student.tests.factories import UserFactory
|
||||
@@ -47,7 +47,7 @@ class CookieTests(SharedModuleStoreTestCase):
|
||||
request = RequestFactory().get('/')
|
||||
request.user = self.user
|
||||
|
||||
actual = get_user_info_cookie_data(request)
|
||||
actual = _get_user_info_cookie_data(request, self.user)
|
||||
|
||||
expected = {
|
||||
'version': settings.EDXMKTG_USER_INFO_COOKIE_VERSION,
|
||||
|
||||
@@ -9,5 +9,8 @@ _WAFFLE_FLAG_NAMESPACE = WaffleFlagNamespace(_WAFFLE_NAMESPACE)
|
||||
|
||||
# Flags
|
||||
|
||||
# TODO (ARCH-247)
|
||||
# Intended as a temporary toggle for roll-out of jwt cookies feature.
|
||||
# Satisfies Use Case #3 "Ops - Monitored Rollout" from
|
||||
# https://open-edx-proposals.readthedocs.io/en/latest/oep-0017-bp-feature-toggles.html
|
||||
JWT_COOKIES_FLAG = WaffleFlag(_WAFFLE_FLAG_NAMESPACE, u'jwt_cookies')
|
||||
|
||||
@@ -7,6 +7,7 @@ from django.utils.functional import cached_property
|
||||
from jwkest import jwk
|
||||
from jwkest.jws import JWS
|
||||
|
||||
from edx_django_utils.monitoring import set_custom_metric
|
||||
from student.models import UserProfile, anonymous_id_for_user
|
||||
|
||||
|
||||
@@ -51,6 +52,8 @@ class JwtBuilder(object):
|
||||
"""
|
||||
now = int(time())
|
||||
expires_in = expires_in or self.jwt_auth['JWT_EXPIRATION']
|
||||
set_custom_metric('jwt_expires_in', expires_in)
|
||||
|
||||
payload = {
|
||||
# TODO Consider getting rid of this claim since we don't use it.
|
||||
'aud': aud if aud else self.jwt_auth['JWT_AUDIENCE'],
|
||||
@@ -104,6 +107,7 @@ class JwtBuilder(object):
|
||||
|
||||
def encode(self, payload):
|
||||
"""Encode the provided payload."""
|
||||
set_custom_metric('jwt_asymmetric', self.asymmetric)
|
||||
keys = jwk.KEYS()
|
||||
|
||||
if self.asymmetric:
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
|
||||
import six
|
||||
from django.conf import settings
|
||||
@@ -9,7 +10,7 @@ from django.utils.translation import ugettext as _
|
||||
import third_party_auth
|
||||
from third_party_auth import pipeline
|
||||
|
||||
from openedx.core.djangoapps.user_authn.cookies import set_experiments_is_enterprise_cookie
|
||||
from openedx.core.djangoapps.user_authn.cookies import standard_cookie_settings
|
||||
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
|
||||
from openedx.core.djangolib.markup import HTML, Text
|
||||
|
||||
@@ -188,7 +189,7 @@ def handle_enterprise_cookies_for_logistration(request, response, context):
|
||||
# This cookie can be used for tests or minor features,
|
||||
# but should not be used for payment related or other critical work
|
||||
# since users can edit their cookies
|
||||
set_experiments_is_enterprise_cookie(request, response, context['enable_enterprise_sidebar'])
|
||||
_set_experiments_is_enterprise_cookie(request, response, context['enable_enterprise_sidebar'])
|
||||
|
||||
# Remove enterprise cookie so that subsequent requests show default login page.
|
||||
response.delete_cookie(
|
||||
@@ -197,6 +198,21 @@ def handle_enterprise_cookies_for_logistration(request, response, context):
|
||||
)
|
||||
|
||||
|
||||
def _set_experiments_is_enterprise_cookie(request, response, experiments_is_enterprise):
|
||||
""" Sets the experiments_is_enterprise cookie on the response.
|
||||
This cookie can be used for tests or minor features,
|
||||
but should not be used for payment related or other critical work
|
||||
since users can edit their cookies
|
||||
"""
|
||||
cookie_settings = standard_cookie_settings(request)
|
||||
|
||||
response.set_cookie(
|
||||
'experiments_is_enterprise',
|
||||
json.dumps(experiments_is_enterprise),
|
||||
**cookie_settings
|
||||
)
|
||||
|
||||
|
||||
def update_account_settings_context_for_enterprise(context, enterprise_customer):
|
||||
"""
|
||||
Take processed context for account settings page and update it taking enterprise customer into account.
|
||||
|
||||
@@ -117,7 +117,7 @@ edx-django-oauth2-provider==1.3.5
|
||||
edx-django-release-util==0.3.1
|
||||
edx-django-sites-extensions==2.3.1
|
||||
edx-django-utils==1.0.1
|
||||
edx-drf-extensions==1.7.0
|
||||
edx-drf-extensions==1.9.0
|
||||
edx-enterprise==0.73.5
|
||||
edx-i18n-tools==0.4.6
|
||||
edx-milestones==0.1.13
|
||||
|
||||
@@ -136,7 +136,7 @@ edx-django-oauth2-provider==1.3.5
|
||||
edx-django-release-util==0.3.1
|
||||
edx-django-sites-extensions==2.3.1
|
||||
edx-django-utils==1.0.1
|
||||
edx-drf-extensions==1.7.0
|
||||
edx-drf-extensions==1.9.0
|
||||
edx-enterprise==0.73.5
|
||||
edx-i18n-tools==0.4.6
|
||||
edx-lint==0.5.5
|
||||
|
||||
@@ -131,7 +131,7 @@ edx-django-oauth2-provider==1.3.5
|
||||
edx-django-release-util==0.3.1
|
||||
edx-django-sites-extensions==2.3.1
|
||||
edx-django-utils==1.0.1
|
||||
edx-drf-extensions==1.7.0
|
||||
edx-drf-extensions==1.9.0
|
||||
edx-enterprise==0.73.5
|
||||
edx-i18n-tools==0.4.6
|
||||
edx-lint==0.5.5
|
||||
|
||||
Reference in New Issue
Block a user