From 5a7bc019864cf3887d23d952acdf3ab07e8fee84 Mon Sep 17 00:00:00 2001 From: Clinton Blackburn Date: Fri, 15 Apr 2016 01:21:23 -0400 Subject: [PATCH] JWT authentication updates - Using jwt_decode_handler from edx-drf-extensions - Updated djangorestframework-jwt - Removed feature flag around JWT auth ECOM-4221 --- lms/envs/common.py | 18 +++---- openedx/core/lib/api/jwt_decode_handler.py | 49 ------------------- .../core/lib/api/tests/test_authentication.py | 48 +++--------------- requirements/edx/base.txt | 3 +- 4 files changed, 14 insertions(+), 104 deletions(-) delete mode 100644 openedx/core/lib/api/jwt_decode_handler.py diff --git a/lms/envs/common.py b/lms/envs/common.py index be1befc6d9..f6cdb6ecf2 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -2118,14 +2118,16 @@ SOCIAL_MEDIA_FOOTER_NAMES = [ # JWT Settings JWT_AUTH = { - 'JWT_SECRET_KEY': None, + # TODO Set JWT_SECRET_KEY to a secure value. By default, SECRET_KEY will be used. + # 'JWT_SECRET_KEY': '', 'JWT_ALGORITHM': 'HS256', 'JWT_VERIFY_EXPIRATION': True, - 'JWT_ISSUER': None, - 'JWT_PAYLOAD_GET_USERNAME_HANDLER': lambda d: d.get('username'), + # TODO Set JWT_ISSUER and JWT_AUDIENCE to values specific to your service/organization. + 'JWT_ISSUER': 'change-me', 'JWT_AUDIENCE': None, + 'JWT_PAYLOAD_GET_USERNAME_HANDLER': lambda d: d.get('username'), 'JWT_LEEWAY': 1, - 'JWT_DECODE_HANDLER': 'openedx.core.lib.api.jwt_decode_handler.decode', + 'JWT_DECODE_HANDLER': 'edx_rest_framework_extensions.utils.jwt_decode_handler', } # The footer URLs dictionary maps social footer names @@ -2225,14 +2227,6 @@ if FEATURES.get('CLASS_DASHBOARD'): ENABLE_CREDIT_ELIGIBILITY = True FEATURES['ENABLE_CREDIT_ELIGIBILITY'] = ENABLE_CREDIT_ELIGIBILITY -################ Enable JWT auth #################### -# When this feature flag is set to False, API endpoints using -# JSONWebTokenAuthentication will reject requests using JWT to authenticate, -# even if those tokens are valid. Set this to True only if you need those -# endpoints, and have configured settings 'JWT_AUTH' to override its default -# values with secure values. -FEATURES['ENABLE_JWT_AUTH'] = False - ######################## CAS authentication ########################### if FEATURES.get('AUTH_USE_CAS'): diff --git a/openedx/core/lib/api/jwt_decode_handler.py b/openedx/core/lib/api/jwt_decode_handler.py deleted file mode 100644 index 88d85619f8..0000000000 --- a/openedx/core/lib/api/jwt_decode_handler.py +++ /dev/null @@ -1,49 +0,0 @@ -""" -Custom JWT decoding function for django_rest_framework jwt package. - -Adds logging to facilitate debugging of InvalidTokenErrors. Also -requires "exp" and "iat" claims to be present - the base package -doesn't expose settings to enforce this. -""" -import logging - -from django.conf import settings -import jwt -from rest_framework import exceptions -from rest_framework_jwt.settings import api_settings - - -log = logging.getLogger(__name__) - - -def decode(token): - """ - Ensure InvalidTokenErrors are logged for diagnostic purposes, before - failing authentication. - """ - if not settings.FEATURES.get('ENABLE_JWT_AUTH', False): - msg = 'JWT auth not supported.' - log.error(msg) - raise exceptions.AuthenticationFailed(msg) - - options = { - 'verify_exp': api_settings.JWT_VERIFY_EXPIRATION, - 'require_exp': True, - 'require_iat': True, - } - - try: - return jwt.decode( - token, - api_settings.JWT_SECRET_KEY, - api_settings.JWT_VERIFY, - options=options, - leeway=api_settings.JWT_LEEWAY, - audience=api_settings.JWT_AUDIENCE, - issuer=api_settings.JWT_ISSUER, - algorithms=[api_settings.JWT_ALGORITHM] - ) - except jwt.InvalidTokenError as exc: - exc_type = u'{}.{}'.format(exc.__class__.__module__, exc.__class__.__name__) - log.exception("raised_invalid_token: exc_type=%r, exc_detail=%r", exc_type, exc.message) - raise diff --git a/openedx/core/lib/api/tests/test_authentication.py b/openedx/core/lib/api/tests/test_authentication.py index ab3f077a27..0b29898a9a 100644 --- a/openedx/core/lib/api/tests/test_authentication.py +++ b/openedx/core/lib/api/tests/test_authentication.py @@ -4,40 +4,33 @@ Tests for OAuth2. This module is copied from django-rest-framework-oauth """ from __future__ import unicode_literals -from collections import namedtuple -from datetime import datetime, timedelta + import itertools import json +from collections import namedtuple import ddt -from django.conf import settings +from datetime import datetime, timedelta from django.conf.urls import patterns, url, include from django.contrib.auth.models import User from django.http import HttpResponse from django.test import TestCase from django.utils import unittest from django.utils.http import urlencode -from mock import patch from nose.plugins.attrib import attr from oauth2_provider import models as dot_models -from rest_framework import exceptions +from provider import constants, scope from rest_framework import status from rest_framework.permissions import IsAuthenticated -from rest_framework_oauth import permissions -from rest_framework_oauth.compat import oauth2_provider, oauth2_provider_scope from rest_framework.test import APIRequestFactory, APIClient from rest_framework.views import APIView -from rest_framework_jwt.settings import api_settings +from rest_framework_oauth import permissions +from rest_framework_oauth.compat import oauth2_provider, oauth2_provider_scope from lms.djangoapps.oauth_dispatch import adapters from openedx.core.lib.api import authentication -from openedx.core.lib.api.tests.mixins import JwtMixin -from provider import constants, scope -from student.tests.factories import UserFactory - factory = APIRequestFactory() # pylint: disable=invalid-name -jwt_decode_handler = api_settings.JWT_DECODE_HANDLER # pylint: disable=invalid-name class MockView(APIView): # pylint: disable=missing-docstring @@ -311,32 +304,3 @@ class OAuth2Tests(TestCase): self.assertEqual(response.status_code, scope_statuses.read_status) response = self.post_with_bearer_token('/oauth2-with-scope-test/', token=self.access_token.token) self.assertEqual(response.status_code, scope_statuses.write_status) - - -@attr('shard_2') -@ddt.ddt -@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms') -class TestJWTAuthToggle(JwtMixin, TestCase): - """ Test JWT authentication toggling with feature flag 'ENABLE_JWT_AUTH'.""" - - USERNAME = 'test-username' - - def setUp(self): - self.user = UserFactory.create(username=self.USERNAME) - self.jwt_token = self.generate_id_token(user=self.user) - super(TestJWTAuthToggle, self).setUp() - - @patch.dict('django.conf.settings.FEATURES', {'ENABLE_JWT_AUTH': True}) - def test_enabled_jwt_auth(self): - """ Ensure that the JWT auth works fine when its feature flag - 'ENABLE_JWT_AUTH' is set. - """ - jwt_decode_handler(self.jwt_token) - - @patch.dict('django.conf.settings.FEATURES', {'ENABLE_JWT_AUTH': False}) - def test_disabled_jwt_auth(self): - """ Ensure that the JWT auth raises exception when its feature flag - 'ENABLE_JWT_AUTH' is not set. - """ - with self.assertRaises(exceptions.AuthenticationFailed): - jwt_decode_handler(self.jwt_token) diff --git a/requirements/edx/base.txt b/requirements/edx/base.txt index 6d9589e60f..871d04e725 100644 --- a/requirements/edx/base.txt +++ b/requirements/edx/base.txt @@ -34,9 +34,10 @@ django-method-override==0.1.0 #djangorestframework>=3.1,<3.2 git+https://github.com/edx/django-rest-framework.git@3c72cb5ee5baebc4328947371195eae2077197b0#egg=djangorestframework==3.2.3 django==1.8.12 -djangorestframework-jwt==1.7.2 +djangorestframework-jwt==1.8.0 djangorestframework-oauth==1.1.0 edx-ccx-keys==0.1.2 +edx-drf-extensions==0.5.0 edx-lint==0.4.3 edx-management-commands==0.1.1 edx-django-oauth2-provider==1.0.3