Refactor Login Cookies

This commit is contained in:
Nimisha Asthagiri
2018-09-20 15:00:09 -04:00
parent 02418e1401
commit b7deedfb36
11 changed files with 159 additions and 129 deletions

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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