From 182826261df6d3027a56312128f181e3331632c4 Mon Sep 17 00:00:00 2001 From: zubair-arbi Date: Fri, 19 Feb 2016 19:13:57 +0500 Subject: [PATCH] enable jwt auth with feature flag --- lms/envs/common.py | 8 ++++ openedx/core/lib/api/jwt_decode_handler.py | 6 +++ openedx/core/lib/api/tests/mixins.py | 48 +++++++++++++++++++ .../core/lib/api/tests/test_authentication.py | 38 ++++++++++++++- 4 files changed, 98 insertions(+), 2 deletions(-) create mode 100644 openedx/core/lib/api/tests/mixins.py diff --git a/lms/envs/common.py b/lms/envs/common.py index 61b22d9573..8eefee037e 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -2147,6 +2147,14 @@ 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 index dac5fce010..88d85619f8 100644 --- a/openedx/core/lib/api/jwt_decode_handler.py +++ b/openedx/core/lib/api/jwt_decode_handler.py @@ -7,7 +7,9 @@ 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 @@ -19,6 +21,10 @@ 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, diff --git a/openedx/core/lib/api/tests/mixins.py b/openedx/core/lib/api/tests/mixins.py new file mode 100644 index 0000000000..72fdd55f41 --- /dev/null +++ b/openedx/core/lib/api/tests/mixins.py @@ -0,0 +1,48 @@ +""" +Mixins for JWT auth tests. +""" +from time import time + +from django.conf import settings +import jwt + + +JWT_AUTH = 'JWT_AUTH' + + +class JwtMixin(object): + """ Mixin with JWT-related helper functions. """ + + JWT_SECRET_KEY = getattr(settings, JWT_AUTH)['JWT_SECRET_KEY'] if hasattr(settings, JWT_AUTH) else '' + JWT_ISSUER = getattr(settings, JWT_AUTH)['JWT_ISSUER'] if hasattr(settings, JWT_AUTH) else '' + JWT_AUDIENCE = getattr(settings, JWT_AUTH)['JWT_AUDIENCE'] if hasattr(settings, JWT_AUTH) else '' + + def generate_token(self, payload, secret=None): + """ Generate a JWT token with the provided payload.""" + secret = secret or self.JWT_SECRET_KEY + token = jwt.encode(payload, secret) + return token + + def generate_id_token(self, user, ttl=1, **overrides): + """ Generate a JWT id_token that looks like the ones currently + returned by the edx oidc provider. + """ + payload = self.default_payload(user=user, ttl=ttl) + payload.update(overrides) + return self.generate_token(payload) + + def default_payload(self, user, ttl=1): + """ Generate a bare payload, in case tests need to manipulate + it directly before encoding. + """ + now = int(time()) + + return { + "iss": self.JWT_ISSUER, + "aud": self.JWT_AUDIENCE, + "nonce": "dummy-nonce", + "exp": now + ttl, + "iat": now, + "username": user.username, + "email": user.email, + } diff --git a/openedx/core/lib/api/tests/test_authentication.py b/openedx/core/lib/api/tests/test_authentication.py index 63a7aedc84..1e1091b3eb 100644 --- a/openedx/core/lib/api/tests/test_authentication.py +++ b/openedx/core/lib/api/tests/test_authentication.py @@ -10,25 +10,31 @@ import itertools import json import ddt +from django.conf import settings 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 rest_framework import exceptions 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 provider import scope, constants 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 @@ -284,3 +290,31 @@ 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) + + +@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)