diff --git a/lms/djangoapps/certificates/apis/v0/tests/test_views.py b/lms/djangoapps/certificates/apis/v0/tests/test_views.py
index 38f82c51b8cd91b328207e2bbeb32a3941ca44dd..090dc48af395a5521c21f5a374c68a6148254812 100644
--- a/lms/djangoapps/certificates/apis/v0/tests/test_views.py
+++ b/lms/djangoapps/certificates/apis/v0/tests/test_views.py
@@ -1,7 +1,12 @@
 """
 Tests for the Certificate REST APIs.
 """
+# pylint: disable=missing-docstring
 from datetime import datetime, timedelta
+from enum import Enum
+from itertools import product
+import ddt
+from mock import patch
 
 from django.urls import reverse
 from django.utils import timezone
@@ -10,9 +15,12 @@ from oauth2_provider import models as dot_models
 from rest_framework import status
 from rest_framework.test import APITestCase
 
+from course_modes.models import CourseMode
+from lms.djangoapps.certificates.apis.v0.views import CertificatesDetailView
 from lms.djangoapps.certificates.models import CertificateStatuses
 from lms.djangoapps.certificates.tests.factories import GeneratedCertificateFactory
-from course_modes.models import CourseMode
+from openedx.core.djangoapps.oauth_dispatch.toggles import ENFORCE_JWT_SCOPES
+from openedx.core.lib.token_utils import JwtBuilder
 from student.tests.factories import UserFactory
 from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase
 from xmodule.modulestore.tests.factories import CourseFactory
@@ -20,6 +28,17 @@ from xmodule.modulestore.tests.factories import CourseFactory
 USER_PASSWORD = 'test'
 
 
+class AuthType(Enum):
+    session = 1
+    oauth = 2
+    jwt = 3
+    jwt_restricted = 4
+
+
+JWT_AUTH_TYPES = [AuthType.jwt, AuthType.jwt_restricted]
+
+
+@ddt.ddt
 class CertificatesRestApiTest(SharedModuleStoreTestCase, APITestCase):
     """
     Test for the Certificates REST APIs
@@ -58,7 +77,34 @@ class CertificatesRestApiTest(SharedModuleStoreTestCase, APITestCase):
 
         self.namespaced_url = 'certificates_api:v0:certificates:detail'
 
-        # create a configuration for django-oauth-toolkit (DOT)
+    def _assert_certificate_response(self, response):
+        self.assertEqual(
+            response.data,
+            {
+                'username': self.student.username,
+                'status': CertificateStatuses.downloadable,
+                'is_passing': True,
+                'grade': '0.88',
+                'download_url': 'www.google.com',
+                'certificate_type': CourseMode.VERIFIED,
+                'course_id': unicode(self.course.id),
+                'created_date': self.now,
+            }
+        )
+
+    def _get_url(self, username):
+        """
+        Helper function to create the url for certificates
+        """
+        return reverse(
+            self.namespaced_url,
+            kwargs={
+                'course_id': self.course.id,
+                'username': username
+            }
+        )
+
+    def _create_oauth_token(self, user):
         dot_app_user = UserFactory.create(password=USER_PASSWORD)
         dot_app = dot_models.Application.objects.create(
             name='test app',
@@ -67,128 +113,183 @@ class CertificatesRestApiTest(SharedModuleStoreTestCase, APITestCase):
             authorization_grant_type='authorization-code',
             redirect_uris='http://localhost:8079/complete/edxorg/'
         )
-        self.dot_access_token = dot_models.AccessToken.objects.create(
-            user=self.student,
+        return dot_models.AccessToken.objects.create(
+            user=user,
             application=dot_app,
             expires=datetime.utcnow() + timedelta(weeks=1),
             scope='read write',
-            token='16MGyP3OaQYHmpT1lK7Q6MMNAZsjwF'
+            token='test_token',
         )
 
-    def get_url(self, username):
-        """
-        Helper function to create the url for certificates
-        """
-        return reverse(
-            self.namespaced_url,
-            kwargs={
-                'course_id': self.course.id,
-                'username': username
-            }
+    def _create_jwt_token(self, user, auth_type, scopes=None, include_org_filter=True, include_me_filter=False):
+        filters = []
+        if include_org_filter:
+            filters += ['content_org:{}'.format(self.course.id.org)]
+        if include_me_filter:
+            filters += ['user:me']
+
+        if scopes is None:
+            scopes = CertificatesDetailView.required_scopes
+
+        return JwtBuilder(user).build_token(
+            scopes,
+            additional_claims=dict(
+                is_restricted=(auth_type == AuthType.jwt_restricted),
+                filters=filters,
+            ),
         )
 
-    def assert_oauth_status(self, access_token, expected_status):
-        """
-        Helper method for requests with OAUTH token
-        """
-        self.client.logout()
-        auth_header = "Bearer {0}".format(access_token)
-        response = self.client.get(self.get_url(self.student.username), HTTP_AUTHORIZATION=auth_header)
-        self.assertEqual(response.status_code, expected_status)
+    def _get_response(self, requesting_user, auth_type, url=None, token=None):
+        auth_header = None
+        if auth_type == AuthType.session:
+            self.client.login(username=requesting_user.username, password=USER_PASSWORD)
+        elif auth_type == AuthType.oauth:
+            if not token:
+                token = self._create_oauth_token(requesting_user)
+            auth_header = "Bearer {0}".format(token)
+        else:
+            assert auth_type in JWT_AUTH_TYPES
+            if not token:
+                token = self._create_jwt_token(requesting_user, auth_type)
+            auth_header = "JWT {0}".format(token)
 
-    def test_permissions(self):
-        """
-        Test that only the owner of the certificate can access the url
-        """
-        # anonymous user
-        resp = self.client.get(self.get_url(self.student.username))
+        extra = dict(HTTP_AUTHORIZATION=auth_header) if auth_header else {}
+        return self.client.get(
+            url if url else self._get_url(self.student.username),
+            **extra
+        )
+
+    def _assert_in_log(self, text, mock_log_method):
+        self.assertTrue(mock_log_method.called)
+        self.assertIn(text, mock_log_method.call_args_list[0][0][0])
+
+    def test_anonymous_user(self):
+        resp = self.client.get(self._get_url(self.student.username))
         self.assertEqual(resp.status_code, status.HTTP_401_UNAUTHORIZED)
 
-        # another student
-        self.client.login(username=self.student_no_cert.username, password=USER_PASSWORD)
-        resp = self.client.get(self.get_url(self.student.username))
-        # gets 404 instead of 403 for security reasons
+    @ddt.data(*list(AuthType))
+    def test_no_certificate(self, auth_type):
+        resp = self._get_response(
+            self.student_no_cert,
+            auth_type,
+            url=self._get_url(self.student_no_cert.username),
+        )
         self.assertEqual(resp.status_code, status.HTTP_404_NOT_FOUND)
-        self.assertEqual(resp.data, {u'detail': u'Not found.'})
-        self.client.logout()
+        self.assertIn('error_code', resp.data)
+        self.assertEqual(
+            resp.data['error_code'],
+            'no_certificate_for_user',
+        )
 
-        # same student of the certificate
-        self.client.login(username=self.student.username, password=USER_PASSWORD)
-        resp = self.client.get(self.get_url(self.student.username))
-        self.assertEqual(resp.status_code, status.HTTP_200_OK)
-        self.client.logout()
+    @ddt.data(*product(list(AuthType), (True, False)))
+    @ddt.unpack
+    def test_self_user(self, auth_type, scopes_enforced):
+        with ENFORCE_JWT_SCOPES.override(active=scopes_enforced):
+            resp = self._get_response(self.student, auth_type)
+            self.assertEqual(resp.status_code, status.HTTP_200_OK)
+            self._assert_certificate_response(resp)
 
-        # staff user
-        self.client.login(username=self.staff_user.username, password=USER_PASSWORD)
-        resp = self.client.get(self.get_url(self.student.username))
-        self.assertEqual(resp.status_code, status.HTTP_200_OK)
+    @ddt.data(*product(list(AuthType), (True, False)))
+    @ddt.unpack
+    def test_inactive_user(self, auth_type, scopes_enforced):
+        with ENFORCE_JWT_SCOPES.override(active=scopes_enforced):
+            self.student.is_active = False
+            self.student.save()
 
-    def test_inactive_user_access(self):
-        """
-        Verify inactive users - those who have not verified their email addresses -
-        are allowed to access the endpoint.
-        """
-        self.client.login(username=self.student.username, password=USER_PASSWORD)
+            resp = self._get_response(self.student, auth_type)
+            self.assertEqual(resp.status_code, status.HTTP_200_OK)
 
-        self.student.is_active = False
-        self.student.save()
+    @ddt.data(*product(list(AuthType), (True, False)))
+    @ddt.unpack
+    def test_staff_user(self, auth_type, scopes_enforced):
+        with ENFORCE_JWT_SCOPES.override(active=scopes_enforced):
+            resp = self._get_response(self.staff_user, auth_type)
+            self.assertEqual(resp.status_code, status.HTTP_200_OK)
 
-        resp = self.client.get(self.get_url(self.student.username))
-        self.assertEqual(resp.status_code, status.HTTP_200_OK)
+    @patch('edx_rest_framework_extensions.permissions.log')
+    @ddt.data(*product(list(AuthType), (True, False)))
+    @ddt.unpack
+    def test_another_user(self, auth_type, scopes_enforced, mock_log):
+        """ Returns 403 for OAuth and Session auth with IsUserInUrl. """
+        with ENFORCE_JWT_SCOPES.override(active=scopes_enforced):
+            resp = self._get_response(self.student_no_cert, auth_type)
 
-    def test_dot_valid_accesstoken(self):
-        """
-        Verify access with a valid Django Oauth Toolkit access token.
-        """
-        self.assert_oauth_status(self.dot_access_token, status.HTTP_200_OK)
+            # Restricted JWT tokens without the user:me filter have access to other users
+            expected_jwt_access_granted = scopes_enforced and auth_type == AuthType.jwt_restricted
 
-    def test_dot_invalid_accesstoken(self):
-        """
-        Verify the endpoint is inaccessible for authorization
-        attempts made with an invalid OAuth access token.
-        """
-        self.assert_oauth_status("fooooooooooToken", status.HTTP_401_UNAUTHORIZED)
+            self.assertEqual(
+                resp.status_code,
+                status.HTTP_200_OK if expected_jwt_access_granted else status.HTTP_403_FORBIDDEN,
+            )
+            if not expected_jwt_access_granted:
+                self._assert_in_log("IsUserInUrl", mock_log.info)
 
-    def test_dot_expired_accesstoken(self):
-        """
-        Verify the endpoint is inaccessible for authorization
-        attempts made with an expired OAuth access token.
-        """
-        # set the expiration date in the past
-        self.dot_access_token.expires = datetime.utcnow() - timedelta(weeks=1)
-        self.dot_access_token.save()
-        self.assert_oauth_status(self.dot_access_token, status.HTTP_401_UNAUTHORIZED)
+    @patch('edx_rest_framework_extensions.permissions.log')
+    @ddt.data(*product(JWT_AUTH_TYPES, (True, False)))
+    @ddt.unpack
+    def test_jwt_no_scopes(self, auth_type, scopes_enforced, mock_log):
+        """ Returns 403 when scopes are enforced with JwtHasScope. """
+        with ENFORCE_JWT_SCOPES.override(active=scopes_enforced):
+            jwt_token = self._create_jwt_token(self.student, auth_type, scopes=[])
+            resp = self._get_response(self.student, AuthType.jwt, token=jwt_token)
 
-    def test_no_certificate_for_user(self):
-        """
-        Test for case with no certificate available
-        """
-        self.client.login(username=self.student_no_cert.username, password=USER_PASSWORD)
-        resp = self.client.get(self.get_url(self.student_no_cert.username))
-        self.assertEqual(resp.status_code, status.HTTP_404_NOT_FOUND)
-        self.assertIn('error_code', resp.data)
-        self.assertEqual(
-            resp.data['error_code'],
-            'no_certificate_for_user'
-        )
+            is_enforced = scopes_enforced and auth_type == AuthType.jwt_restricted
+            self.assertEqual(resp.status_code, status.HTTP_403_FORBIDDEN if is_enforced else status.HTTP_200_OK)
 
-    def test_certificate_for_user(self):
-        """
-        Tests case user that pulls her own certificate
-        """
-        self.client.login(username=self.student.username, password=USER_PASSWORD)
-        resp = self.client.get(self.get_url(self.student.username))
+            if is_enforced:
+                self._assert_in_log("JwtHasScope", mock_log.warning)
+
+    @patch('edx_rest_framework_extensions.permissions.log')
+    @ddt.data(*product(JWT_AUTH_TYPES, (True, False)))
+    @ddt.unpack
+    def test_jwt_no_filter(self, auth_type, scopes_enforced, mock_log):
+        """ Returns 403 when scopes are enforced with JwtHasContentOrgFilterForRequestedCourse. """
+        with ENFORCE_JWT_SCOPES.override(active=scopes_enforced):
+            jwt_token = self._create_jwt_token(self.student, auth_type, include_org_filter=False)
+            resp = self._get_response(self.student, AuthType.jwt, token=jwt_token)
+
+            is_enforced = scopes_enforced and auth_type == AuthType.jwt_restricted
+            self.assertEqual(resp.status_code, status.HTTP_403_FORBIDDEN if is_enforced else status.HTTP_200_OK)
+
+            if is_enforced:
+                self._assert_in_log("JwtHasContentOrgFilterForRequestedCourse", mock_log.warning)
+
+    @ddt.data(*product(JWT_AUTH_TYPES, (True, False)))
+    @ddt.unpack
+    def test_jwt_on_behalf_of_user(self, auth_type, scopes_enforced):
+        with ENFORCE_JWT_SCOPES.override(active=scopes_enforced):
+            jwt_token = self._create_jwt_token(self.student, auth_type, include_me_filter=True)
+
+            resp = self._get_response(self.student, AuthType.jwt, token=jwt_token)
+            self.assertEqual(resp.status_code, status.HTTP_200_OK)
+
+    @patch('edx_rest_framework_extensions.permissions.log')
+    @ddt.data(*product(JWT_AUTH_TYPES, (True, False)))
+    @ddt.unpack
+    def test_jwt_on_behalf_of_other_user(self, auth_type, scopes_enforced, mock_log):
+        """ Returns 403 when scopes are enforced with JwtHasUserFilterForRequestedUser. """
+        with ENFORCE_JWT_SCOPES.override(active=scopes_enforced):
+            jwt_token = self._create_jwt_token(self.student_no_cert, auth_type, include_me_filter=True)
+            resp = self._get_response(self.student, AuthType.jwt, token=jwt_token)
+
+            self.assertEqual(resp.status_code, status.HTTP_403_FORBIDDEN)
+
+            if scopes_enforced and auth_type == AuthType.jwt_restricted:
+                self._assert_in_log("JwtHasUserFilterForRequestedUser", mock_log.warning)
+            else:
+                self._assert_in_log("IsUserInUrl", mock_log.info)
+
+    def test_valid_oauth_token(self):
+        resp = self._get_response(self.student, AuthType.oauth)
         self.assertEqual(resp.status_code, status.HTTP_200_OK)
-        self.assertEqual(
-            resp.data,
-            {
-                'username': self.student.username,
-                'status': CertificateStatuses.downloadable,
-                'is_passing': True,
-                'grade': '0.88',
-                'download_url': 'www.google.com',
-                'certificate_type': CourseMode.VERIFIED,
-                'course_id': unicode(self.course.id),
-                'created_date': self.now,
-            }
-        )
+
+    def test_invalid_oauth_token(self):
+        resp = self._get_response(self.student, AuthType.oauth, token="fooooooooooToken")
+        self.assertEqual(resp.status_code, status.HTTP_401_UNAUTHORIZED)
+
+    def test_expired_oauth_token(self):
+        token = self._create_oauth_token(self.student)
+        token.expires = datetime.utcnow() - timedelta(weeks=1)
+        token.save()
+        resp = self._get_response(self.student, AuthType.oauth, token=token)
+        self.assertEqual(resp.status_code, status.HTTP_401_UNAUTHORIZED)
diff --git a/lms/djangoapps/certificates/apis/v0/views.py b/lms/djangoapps/certificates/apis/v0/views.py
index ad57f093d542ce4b56b2488bcaa902125125d667..565f86244bc5379fd1b26742ad4e32a9c963e240 100644
--- a/lms/djangoapps/certificates/apis/v0/views.py
+++ b/lms/djangoapps/certificates/apis/v0/views.py
@@ -1,15 +1,19 @@
 """ API v0 views. """
 import logging
 
-from edx_rest_framework_extensions.authentication import JwtAuthentication
-from opaque_keys import InvalidKeyError
-from opaque_keys.edx.keys import CourseKey
 from rest_framework.generics import GenericAPIView
-from rest_framework.permissions import IsAuthenticated
 from rest_framework.response import Response
 
+from edx_rest_framework_extensions import permissions
+from edx_rest_framework_extensions.authentication import JwtAuthentication
 from lms.djangoapps.certificates.api import get_certificate_for_user
-from openedx.core.lib.api import authentication, permissions
+from opaque_keys import InvalidKeyError
+from opaque_keys.edx.keys import CourseKey
+from openedx.core.lib.api.authentication import (
+    OAuth2AuthenticationAllowInactiveUser,
+    SessionAuthenticationAllowInactiveUser
+)
+
 
 log = logging.getLogger(__name__)
 
@@ -70,14 +74,14 @@ class CertificatesDetailView(GenericAPIView):
     """
 
     authentication_classes = (
-        authentication.OAuth2AuthenticationAllowInactiveUser,
-        authentication.SessionAuthenticationAllowInactiveUser,
         JwtAuthentication,
+        OAuth2AuthenticationAllowInactiveUser,
+        SessionAuthenticationAllowInactiveUser,
     )
-    permission_classes = (
-        IsAuthenticated,
-        permissions.IsUserInUrlOrStaff
-    )
+
+    permission_classes = (permissions.JWT_RESTRICTED_APPLICATION_OR_USER_ACCESS,)
+
+    required_scopes = ['certificates:read']
 
     def get(self, request, username, course_id):
         """
diff --git a/lms/djangoapps/grades/api/v1/tests/test_views.py b/lms/djangoapps/grades/api/v1/tests/test_views.py
index 7ffb728376fa44d06355acd82003b29ee892f54a..5fae2e94af0f446f0e61e0c4c1a10e7927f114a8 100644
--- a/lms/djangoapps/grades/api/v1/tests/test_views.py
+++ b/lms/djangoapps/grades/api/v1/tests/test_views.py
@@ -185,7 +185,7 @@ class SingleUserGradesTests(GradeViewTestMixin, APITestCase):
         self.client.logout()
         self.client.login(username=self.other_student.username, password=self.password)
         resp = self.client.get(self.get_url(self.student.username))
-        self.assertEqual(resp.status_code, status.HTTP_404_NOT_FOUND)
+        self.assertEqual(resp.status_code, status.HTTP_403_FORBIDDEN)
 
     def test_self_get_grade_not_enrolled(self):
         """
@@ -337,7 +337,7 @@ class CourseGradesViewTest(GradeViewTestMixin, APITestCase):
 
     def test_student(self):
         resp = self.client.get(self.get_url())
-        self.assertEqual(resp.status_code, status.HTTP_404_NOT_FOUND)
+        self.assertEqual(resp.status_code, status.HTTP_403_FORBIDDEN)
 
     def test_course_does_not_exist(self):
         self.client.logout()
diff --git a/lms/djangoapps/grades/api/v1/views.py b/lms/djangoapps/grades/api/v1/views.py
index 36c63396a24b38be81517f0521c83ca90e128cf1..87aa3c12fd99cd0ab1bd71b02edd88cc53165910 100644
--- a/lms/djangoapps/grades/api/v1/views.py
+++ b/lms/djangoapps/grades/api/v1/views.py
@@ -2,25 +2,30 @@
 import logging
 
 from django.contrib.auth import get_user_model
-from opaque_keys import InvalidKeyError
-from opaque_keys.edx.keys import CourseKey
 from rest_framework import status
 from rest_framework.exceptions import AuthenticationFailed
 from rest_framework.generics import GenericAPIView
 from rest_framework.response import Response
 
+from edx_rest_framework_extensions import permissions
+from edx_rest_framework_extensions.authentication import JwtAuthentication
 from enrollment import data as enrollment_data
-from student.models import CourseEnrollment
 from lms.djangoapps.grades.course_grade_factory import CourseGradeFactory
+from opaque_keys import InvalidKeyError
+from opaque_keys.edx.keys import CourseKey
 from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
-from openedx.core.lib.api.permissions import IsUserInUrlOrStaff
-from openedx.core.lib.api.view_utils import DeveloperErrorViewMixin, view_auth_classes
+from openedx.core.lib.api.authentication import (
+    OAuth2AuthenticationAllowInactiveUser,
+    SessionAuthenticationAllowInactiveUser
+)
+from openedx.core.lib.api.view_utils import DeveloperErrorViewMixin
+from student.models import CourseEnrollment
+
 
 log = logging.getLogger(__name__)
 USER_MODEL = get_user_model()
 
 
-@view_auth_classes()
 class GradeViewMixin(DeveloperErrorViewMixin):
     """
     Mixin class for Grades related views.
@@ -147,7 +152,15 @@ class CourseGradesView(GradeViewMixin, GenericAPIView):
             "letter_grade": null,
         }]
     """
-    permission_classes = (IsUserInUrlOrStaff,)
+    authentication_classes = (
+        JwtAuthentication,
+        OAuth2AuthenticationAllowInactiveUser,
+        SessionAuthenticationAllowInactiveUser,
+    )
+
+    permission_classes = (permissions.JWT_RESTRICTED_APPLICATION_OR_USER_ACCESS,)
+
+    required_scopes = ['grades:read']
 
     def get(self, request, course_id=None):
         """
diff --git a/lms/djangoapps/mobile_api/testutils.py b/lms/djangoapps/mobile_api/testutils.py
index b1af12a70e45fb1bc01047dfd9cd33810deaef88..1a19894316d820911535e6fa57ac75854d2d0be4 100644
--- a/lms/djangoapps/mobile_api/testutils.py
+++ b/lms/djangoapps/mobile_api/testutils.py
@@ -116,7 +116,7 @@ class MobileAuthUserTestMixin(MobileAuthTestMixin):
     """
     def test_invalid_user(self):
         self.login_and_enroll()
-        self.api_response(expected_response_code=404, username='no_user')
+        self.api_response(expected_response_code=403, username='no_user')
 
     def test_other_user(self):
         # login and enroll as the test user
@@ -131,7 +131,7 @@ class MobileAuthUserTestMixin(MobileAuthTestMixin):
 
         # now login and call the API as the test user
         self.login()
-        self.api_response(expected_response_code=404, username=other.username)
+        self.api_response(expected_response_code=403, username=other.username)
 
 
 @ddt.ddt
diff --git a/openedx/core/djangoapps/bookmarks/tests/test_views.py b/openedx/core/djangoapps/bookmarks/tests/test_views.py
index 79ad114512f90e7cee1e31ff3fda4b9dbb76f8db..e6ab3dc3b5168a1eaace6855156b9c949650ddc2 100644
--- a/openedx/core/djangoapps/bookmarks/tests/test_views.py
+++ b/openedx/core/djangoapps/bookmarks/tests/test_views.py
@@ -17,7 +17,6 @@ from .test_api import BookmarkApiEventTestMixin
 from .test_models import BookmarksTestsBase
 
 
-# pylint: disable=no-member
 class BookmarksViewsTestsBase(BookmarksTestsBase, BookmarkApiEventTestMixin):
     """
     Base class for bookmarks views tests.
@@ -397,15 +396,28 @@ class BookmarksDetailViewTests(BookmarksViewsTestsBase):
 
     def test_get_bookmark_that_belongs_to_other_user(self):
         """
-        Test that requesting bookmark that belongs to other user returns 404 status code.
+        Test that requesting bookmark that belongs to other user returns 403 status code.
         """
         self.send_get(
             client=self.client,
             url=reverse(
                 'bookmarks_detail',
-                kwargs={'username': 'other', 'usage_id': unicode(self.vertical_1.location)}
+                kwargs={'username': self.other_user.username, 'usage_id': unicode(self.vertical_1.location)}
             ),
-            expected_status=404
+            expected_status=403
+        )
+
+    def test_get_bookmark_that_belongs_to_nonexistent_user(self):
+        """
+        Test that requesting bookmark that belongs to a non-existent user also returns 403 status code.
+        """
+        self.send_get(
+            client=self.client,
+            url=reverse(
+                'bookmarks_detail',
+                kwargs={'username': 'non-existent', 'usage_id': unicode(self.vertical_1.location)}
+            ),
+            expected_status=403
         )
 
     def test_get_bookmark_that_does_not_exist(self):
@@ -482,7 +494,7 @@ class BookmarksDetailViewTests(BookmarksViewsTestsBase):
 
     def test_delete_bookmark_that_belongs_to_other_user(self):
         """
-        Test that delete bookmark that belongs to other user returns 404.
+        Test that delete bookmark that belongs to other user returns 403.
         """
         self.send_delete(
             client=self.client,
@@ -490,7 +502,7 @@ class BookmarksDetailViewTests(BookmarksViewsTestsBase):
                 'bookmarks_detail',
                 kwargs={'username': 'other', 'usage_id': unicode(self.vertical_1.location)}
             ),
-            expected_status=404
+            expected_status=403
         )
 
     def test_delete_bookmark_that_does_not_exist(self):
diff --git a/openedx/core/djangoapps/profile_images/tests/test_views.py b/openedx/core/djangoapps/profile_images/tests/test_views.py
index 3d8e6d599896c6943d625a52de6045fab928c262..09898364f9aff9deca8e74c7d6fdd38778e10f57 100644
--- a/openedx/core/djangoapps/profile_images/tests/test_views.py
+++ b/openedx/core/djangoapps/profile_images/tests/test_views.py
@@ -243,6 +243,18 @@ class ProfileImageViewPostTestCase(ProfileImageEndpointMixin, APITestCase):
         self.assertFalse(mock_log.info.called)
         self.assert_no_events_were_emitted()
 
+    def test_upload_nonexistent_user(self, mock_log):
+        """
+        Test that an authenticated user who POSTs to a non-existent user's upload
+        endpoint gets an indistinguishable 403.
+        """
+        nonexistent_user_url = reverse(self._view_name, kwargs={'username': 'nonexistent'})
+
+        with make_image_file() as image_file:
+            response = self.client.post(nonexistent_user_url, {'file': image_file}, format='multipart')
+            self.check_response(response, 403)
+        self.assertFalse(mock_log.info.called)
+
     def test_upload_other(self, mock_log):
         """
         Test that an authenticated user cannot POST to another user's upload
@@ -255,7 +267,7 @@ class ProfileImageViewPostTestCase(ProfileImageEndpointMixin, APITestCase):
         different_client.login(username=different_user.username, password=TEST_PASSWORD)
         with make_image_file() as image_file:
             response = different_client.post(self.url, {'file': image_file}, format='multipart')
-            self.check_response(response, 404)
+            self.check_response(response, 403)
             self.check_images(False)
             self.check_has_profile_image(False)
         self.assertFalse(mock_log.info.called)
@@ -407,7 +419,7 @@ class ProfileImageViewDeleteTestCase(ProfileImageEndpointMixin, APITestCase):
         different_client = APIClient()
         different_client.login(username=different_user.username, password=TEST_PASSWORD)
         response = different_client.delete(self.url)
-        self.check_response(response, 404)
+        self.check_response(response, 403)
         self.check_images(True)  # thumbnails should remain intact.
         self.check_has_profile_image(True)
         self.assertFalse(mock_log.info.called)
diff --git a/openedx/core/djangoapps/user_api/preferences/api.py b/openedx/core/djangoapps/user_api/preferences/api.py
index 540801834981c27c4508b02ab6a2f6a2402d5348..ecf2dfff0d60affdc1a16f9ebd461cc024df3cd4 100644
--- a/openedx/core/djangoapps/user_api/preferences/api.py
+++ b/openedx/core/djangoapps/user_api/preferences/api.py
@@ -315,12 +315,13 @@ def _get_authorized_user(requesting_user, username=None, allow_staff=False):
             # Otherwise, treat this as a request against a separate user
             username = requesting_user.username
 
+    _check_authorized(requesting_user, username, allow_staff)
+
     try:
         existing_user = User.objects.get(username=username)
     except ObjectDoesNotExist:
         raise UserNotFound()
 
-    _check_authorized(requesting_user, username, allow_staff)
     return existing_user
 
 
diff --git a/openedx/core/djangoapps/user_api/preferences/tests/test_api.py b/openedx/core/djangoapps/user_api/preferences/tests/test_api.py
index 4109aa583f032cfc0e080ef604a8d162b5556858..5acaca5afe39a9e4d6adbadf4f63897907f2e70d 100644
--- a/openedx/core/djangoapps/user_api/preferences/tests/test_api.py
+++ b/openedx/core/djangoapps/user_api/preferences/tests/test_api.py
@@ -77,7 +77,7 @@ class TestPreferenceAPI(CacheIsolationTestCase):
         """
         Verifies that get_user_preference returns appropriate errors.
         """
-        with self.assertRaises(UserNotFound):
+        with self.assertRaises(UserNotAuthorized):
             get_user_preference(self.user, self.test_preference_key, username="no_such_user")
 
         with self.assertRaises(UserNotFound):
@@ -100,7 +100,7 @@ class TestPreferenceAPI(CacheIsolationTestCase):
         """
         Verifies that get_user_preferences returns appropriate errors.
         """
-        with self.assertRaises(UserNotFound):
+        with self.assertRaises(UserNotAuthorized):
             get_user_preferences(self.user, username="no_such_user")
 
         with self.assertRaises(UserNotFound):
@@ -125,7 +125,7 @@ class TestPreferenceAPI(CacheIsolationTestCase):
         """
         Verifies that set_user_preference returns appropriate errors.
         """
-        with self.assertRaises(UserNotFound):
+        with self.assertRaises(UserNotAuthorized):
             set_user_preference(self.user, self.test_preference_key, "new_value", username="no_such_user")
 
         with self.assertRaises(UserNotFound):
@@ -227,7 +227,7 @@ class TestPreferenceAPI(CacheIsolationTestCase):
         update_data = {
             self.test_preference_key: "new_value"
         }
-        with self.assertRaises(UserNotFound):
+        with self.assertRaises(UserNotAuthorized):
             update_user_preferences(self.user, update_data, user="no_such_user")
 
         with self.assertRaises(UserNotFound):
@@ -303,7 +303,7 @@ class TestPreferenceAPI(CacheIsolationTestCase):
         """
         Verifies that delete_user_preference returns appropriate errors.
         """
-        with self.assertRaises(UserNotFound):
+        with self.assertRaises(UserNotAuthorized):
             delete_user_preference(self.user, self.test_preference_key, username="no_such_user")
 
         with self.assertRaises(UserNotFound):
diff --git a/openedx/core/djangoapps/user_api/preferences/tests/test_views.py b/openedx/core/djangoapps/user_api/preferences/tests/test_views.py
index eabc42e315ab3581e7547215758c5e7823132288..0d4e17eb2cb5d42a4c9e1c6700f5eb5dcc4a4fbb 100644
--- a/openedx/core/djangoapps/user_api/preferences/tests/test_views.py
+++ b/openedx/core/djangoapps/user_api/preferences/tests/test_views.py
@@ -52,7 +52,7 @@ class TestPreferencesAPI(UserAPITestCase):
         Test that a client (logged in) cannot get the preferences information for a different client.
         """
         self.different_client.login(username=self.different_user.username, password=TEST_PASSWORD)
-        self.send_get(self.different_client, expected_status=404)
+        self.send_get(self.different_client, expected_status=403)
 
     @ddt.data(
         ("client", "user"),
@@ -61,11 +61,11 @@ class TestPreferencesAPI(UserAPITestCase):
     @ddt.unpack
     def test_get_unknown_user(self, api_client, username):
         """
-        Test that requesting a user who does not exist returns a 404.
+        Test that requesting a user who does not exist returns a 404 for staff users, but 403 for others.
         """
         client = self.login_client(api_client, username)
         response = client.get(reverse(self.url_endpoint_name, kwargs={'username': "does_not_exist"}))
-        self.assertEqual(404, response.status_code)
+        self.assertEqual(404 if username == "staff_user" else 403, response.status_code)
 
     def test_get_preferences_default(self):
         """
@@ -83,8 +83,7 @@ class TestPreferencesAPI(UserAPITestCase):
     @ddt.unpack
     def test_get_preferences(self, api_client, user):
         """
-        Test that a client (logged in) can get her own preferences information. Also verifies that a "is_staff"
-        user can get the preferences information for other users.
+        Test that a client (logged in) can get her own preferences information.
         """
         # Create some test preferences values.
         set_user_preference(self.user, "dict_pref", {"int_key": 10})
@@ -104,14 +103,14 @@ class TestPreferencesAPI(UserAPITestCase):
     @ddt.unpack
     def test_patch_unknown_user(self, api_client, user):
         """
-        Test that trying to update preferences for a user who does not exist returns a 404.
+        Test that trying to update preferences for a user who does not exist returns a 403.
         """
         client = self.login_client(api_client, user)
         response = client.patch(
             reverse(self.url_endpoint_name, kwargs={'username': "does_not_exist"}),
             data=json.dumps({"string_pref": "value"}), content_type="application/merge-patch+json"
         )
-        self.assertEqual(404, response.status_code)
+        self.assertEqual(403, response.status_code)
 
     def test_patch_bad_content_type(self):
         """
@@ -168,7 +167,7 @@ class TestPreferencesAPI(UserAPITestCase):
                 "dict_pref": {"int_key": 10},
                 "string_pref": "value",
             },
-            expected_status=403 if user == "staff_user" else 404,
+            expected_status=403,
         )
 
     def test_update_preferences(self):
@@ -311,7 +310,7 @@ class TestPreferencesAPI(UserAPITestCase):
                 "new_pref": "new_value",
                 "extra_pref": None,
             },
-            expected_status=403 if user == "staff_user" else 404
+            expected_status=403
         )
 
 
@@ -405,9 +404,9 @@ class TestPreferencesDetailAPI(UserAPITestCase):
         Test that a client (logged in) cannot manipulate a preference for a different client.
         """
         self.different_client.login(username=self.different_user.username, password=TEST_PASSWORD)
-        self.send_get(self.different_client, expected_status=404)
-        self.send_put(self.different_client, "new_value", expected_status=404)
-        self.send_delete(self.different_client, expected_status=404)
+        self.send_get(self.different_client, expected_status=403)
+        self.send_put(self.different_client, "new_value", expected_status=403)
+        self.send_delete(self.different_client, expected_status=403)
 
     @ddt.data(
         ("client", "user"),
@@ -416,13 +415,13 @@ class TestPreferencesDetailAPI(UserAPITestCase):
     @ddt.unpack
     def test_get_unknown_user(self, api_client, username):
         """
-        Test that requesting a user who does not exist returns a 404.
+        Test that requesting a user who does not exist returns a 404 for staff users, but 403 for others.
         """
         client = self.login_client(api_client, username)
         response = client.get(
             reverse(self.url_endpoint_name, kwargs={'username': "does_not_exist", 'preference_key': self.test_pref_key})
         )
-        self.assertEqual(404, response.status_code)
+        self.assertEqual(404 if username == "staff_user" else 403, response.status_code)
 
     def test_get_preference_does_not_exist(self):
         """
@@ -532,7 +531,7 @@ class TestPreferencesDetailAPI(UserAPITestCase):
         self._set_url("new_key")
         client = self.login_client(api_client, user)
         new_value = "new value"
-        self.send_put(client, new_value, expected_status=403 if user == "staff_user" else 404)
+        self.send_put(client, new_value, expected_status=403)
 
     @ddt.data(
         (u"new value",),
@@ -560,7 +559,7 @@ class TestPreferencesDetailAPI(UserAPITestCase):
         """
         client = self.login_client(api_client, user)
         new_value = "new value"
-        self.send_put(client, new_value, expected_status=403 if user == "staff_user" else 404)
+        self.send_put(client, new_value, expected_status=403)
 
     @ddt.data(
         (None,),
@@ -607,4 +606,4 @@ class TestPreferencesDetailAPI(UserAPITestCase):
         Test that a client (logged in) cannot delete a preference for another user.
         """
         client = self.login_client(api_client, user)
-        self.send_delete(client, expected_status=403 if user == "staff_user" else 404)
+        self.send_delete(client, expected_status=403)
diff --git a/openedx/core/lib/api/permissions.py b/openedx/core/lib/api/permissions.py
index 84684fd7e6eecb38fd5be95c249532dcb303518c..f5520923cfdf000203273acf8c4ad2d0d4634466 100644
--- a/openedx/core/lib/api/permissions.py
+++ b/openedx/core/lib/api/permissions.py
@@ -6,8 +6,10 @@ from django.conf import settings
 from django.http import Http404
 from opaque_keys import InvalidKeyError
 from opaque_keys.edx.keys import CourseKey
+from rest_condition import C
 from rest_framework import permissions
 
+from edx_rest_framework_extensions.permissions import IsStaff, IsUserInUrl
 from openedx.core.lib.log_utils import audit_log
 from student.roles import CourseInstructorRole, CourseStaffRole
 
@@ -20,7 +22,6 @@ class ApiKeyHeaderPermission(permissions.BasePermission):
     def has_permission(self, request, view):
         """
         Check for permissions by matching the configured API key and header
-
         Allow the request if and only if settings.EDX_API_KEY is set and
         the X-Edx-Api-Key HTTP header is present in the request and
         matches the setting.
@@ -39,7 +40,6 @@ class ApiKeyHeaderPermission(permissions.BasePermission):
 class ApiKeyHeaderPermissionIsAuthenticated(ApiKeyHeaderPermission, permissions.IsAuthenticated):
     """
     Allow someone to access the view if they have the API key OR they are authenticated.
-
     See ApiKeyHeaderPermission for more information how the API key portion is implemented.
     """
 
@@ -50,29 +50,6 @@ class ApiKeyHeaderPermissionIsAuthenticated(ApiKeyHeaderPermission, permissions.
         return api_permissions or is_authenticated_permissions
 
 
-class IsUserInUrl(permissions.BasePermission):
-    """
-    Permission that checks to see if the request user matches the user in the URL.
-    """
-
-    def has_permission(self, request, view):
-        """
-        Returns true if the current request is by the user themselves.
-
-        Note: a 404 is returned for non-staff instead of a 403. This is to prevent
-        users from being able to detect the existence of accounts.
-        """
-        url_username = (
-            request.parser_context.get('kwargs', {}).get('username') or
-            request.GET.get('username', '')
-        )
-        if request.user.username.lower() != url_username.lower():
-            if request.user.is_staff:
-                return False  # staff gets 403
-            raise Http404()
-        return True
-
-
 class IsCourseStaffInstructor(permissions.BasePermission):
     """
     Permission to check that user is a course instructor or staff of
@@ -118,26 +95,9 @@ class IsMasterCourseStaffInstructor(permissions.BasePermission):
         return False
 
 
-class IsStaff(permissions.BasePermission):
-    """
-    Permission that checks to see if the request user has is_staff access.
-    """
-
+class IsUserInUrlOrStaff(permissions.BasePermission):
     def has_permission(self, request, view):
-        if request.user.is_staff:
-            return True
-
-
-class IsUserInUrlOrStaff(IsUserInUrl):
-    """
-    Permission that checks to see if the request user matches the user in the URL or has is_staff access.
-    """
-
-    def has_permission(self, request, view):
-        if request.user.is_staff:
-            return True
-
-        return super(IsUserInUrlOrStaff, self).has_permission(request, view)
+        return C(IsStaff) | IsUserInUrl
 
 
 class IsStaffOrReadOnly(permissions.BasePermission):
diff --git a/requirements/edx/base.in b/requirements/edx/base.in
index 15b13bc3feb43773f69ff17b9660ffc95338e5fd..cd98b89a441a7ecb4b952fb1abc2a1cb590e5848 100644
--- a/requirements/edx/base.in
+++ b/requirements/edx/base.in
@@ -125,6 +125,7 @@ python-openid
 python-saml==2.4.0
 pyuca==1.1                          # For more accurate sorting of translated country names in django-countries
 reportlab==3.1.44                   # Used for shopping cart's pdf invoice/receipt generation
+rest-condition                      # DRF's recommendation for supporting complex permissions
 rfc6266-parser                      # Used to generate Content-Disposition headers.
 social-auth-app-django
 social-auth-core
diff --git a/requirements/edx/base.txt b/requirements/edx/base.txt
index d45d8cbec812a89a418377a1069ac74997244a96..820aa36915c359a08dcf0fee34ef76ff629d1aea 100644
--- a/requirements/edx/base.txt
+++ b/requirements/edx/base.txt
@@ -115,7 +115,7 @@ edx-completion==0.1.7
 edx-django-oauth2-provider==1.2.5
 edx-django-release-util==0.3.1
 edx-django-sites-extensions==2.3.1
-edx-drf-extensions==1.5.1
+edx-drf-extensions==1.5.2
 edx-enterprise==0.70.1
 edx-i18n-tools==0.4.5
 edx-milestones==0.1.13
@@ -209,7 +209,7 @@ redis==2.10.6
 reportlab==3.1.44
 requests-oauthlib==0.6.1
 requests==2.9.1
-rest-condition==1.0.3     # via edx-drf-extensions
+rest-condition==1.0.3
 rfc6266-parser==0.0.5.post2
 rules==1.3
 s3transfer==0.1.13        # via boto3
diff --git a/requirements/edx/development.txt b/requirements/edx/development.txt
index 9469bcb600a4102d316aefdac0a9344ddbc2ea06..d173ec26bd23b3ff76f8c0a41764e1a31d4193c0 100644
--- a/requirements/edx/development.txt
+++ b/requirements/edx/development.txt
@@ -135,7 +135,7 @@ edx-completion==0.1.7
 edx-django-oauth2-provider==1.2.5
 edx-django-release-util==0.3.1
 edx-django-sites-extensions==2.3.1
-edx-drf-extensions==1.5.1
+edx-drf-extensions==1.5.2
 edx-enterprise==0.70.1
 edx-i18n-tools==0.4.5
 edx-lint==0.5.5
diff --git a/requirements/edx/testing.txt b/requirements/edx/testing.txt
index eaac239684b29adeb1756c2c3304725013fd1303..afcc19a1a8a8616a2e947f4a8f3bc178c025d092 100644
--- a/requirements/edx/testing.txt
+++ b/requirements/edx/testing.txt
@@ -130,7 +130,7 @@ edx-completion==0.1.7
 edx-django-oauth2-provider==1.2.5
 edx-django-release-util==0.3.1
 edx-django-sites-extensions==2.3.1
-edx-drf-extensions==1.5.1
+edx-drf-extensions==1.5.2
 edx-enterprise==0.70.1
 edx-i18n-tools==0.4.5
 edx-lint==0.5.5
@@ -228,8 +228,8 @@ pluggy==0.6.0             # via pytest, tox
 polib==1.1.0
 psutil==1.2.1
 py2neo==3.1.2
-py==1.5.3                 # via pytest, tox
-pyasn1-modules==0.2.1     # via service-identity
+py==1.5.4                 # via pytest, tox
+pyasn1-modules==0.2.2     # via service-identity
 pyasn1==0.4.3             # via pyasn1-modules, service-identity
 pycodestyle==2.3.1
 pycontracts==1.7.1