diff --git a/lms/djangoapps/certificates/apis/v0/views.py b/lms/djangoapps/certificates/apis/v0/views.py index ad57f093d542..70b810c4a465 100644 --- a/lms/djangoapps/certificates/apis/v0/views.py +++ b/lms/djangoapps/certificates/apis/v0/views.py @@ -2,6 +2,7 @@ import logging from edx_rest_framework_extensions.authentication import JwtAuthentication +from edx_rest_framework_extensions.permissions import JWTRestrictedApplicationPermission from opaque_keys import InvalidKeyError from opaque_keys.edx.keys import CourseKey from rest_framework.generics import GenericAPIView @@ -68,6 +69,7 @@ class CertificatesDetailView(GenericAPIView): "grade": "0.98" } """ + required_scopes = ['certificates:read'] authentication_classes = ( authentication.OAuth2AuthenticationAllowInactiveUser, @@ -76,7 +78,7 @@ class CertificatesDetailView(GenericAPIView): ) permission_classes = ( IsAuthenticated, - permissions.IsUserInUrlOrStaff + JWTRestrictedApplicationPermission ) def get(self, request, username, course_id): @@ -100,6 +102,14 @@ def get(self, request, username, course_id): data={'error_code': 'course_id_not_valid'} ) + if hasattr(request, 'auth') and hasattr(request, 'oauth_scopes_filters'): + if 'content_org' in request.oauth_scopes_filters.keys(): + if course_key.org not in request.oauth_scopes_filters['content_org']: + return Response( + status=403, + data={'error_code': 'course_org_not_associated_with_calling_application'} + ) + user_cert = get_certificate_for_user(username=username, course_key=course_key) if user_cert is None: return Response( diff --git a/lms/djangoapps/grades/api/views.py b/lms/djangoapps/grades/api/views.py index 81f05ad7dc95..4288cf4b3ef1 100644 --- a/lms/djangoapps/grades/api/views.py +++ b/lms/djangoapps/grades/api/views.py @@ -9,7 +9,10 @@ from rest_framework.exceptions import AuthenticationFailed from rest_framework.generics import GenericAPIView, ListAPIView from rest_framework.response import Response - +from rest_framework.permissions import IsAuthenticated +from edx_rest_framework_extensions.permissions import JWTRestrictedApplicationPermission +from edx_rest_framework_extensions.authentication import JwtAuthentication +from openedx.core.lib.api.authentication import OAuth2AuthenticationAllowInactiveUser from courseware.access import has_access from lms.djangoapps.courseware import courses from lms.djangoapps.courseware.exceptions import CourseAccessRedirect @@ -160,6 +163,18 @@ class UserGradeView(GradeViewMixin, GenericAPIView): }] """ + authentication_classes = ( + JwtAuthentication, + OAuth2AuthenticationAllowInactiveUser, + ) + permission_classes = (IsAuthenticated, JWTRestrictedApplicationPermission,) + + # needed for passing JWTRestrictedApplicationPermission checks + # for RestrictedApplications (only). A RestrictedApplication can + # only call this method if it is allowed to receive a 'grades:read' + # scope + required_scopes = ['grades:read'] + def get(self, request, course_id): """ Gets a course progress status. @@ -171,6 +186,18 @@ def get(self, request, course_id): Return: A JSON serialized representation of the requesting user's current grade status. """ + # See if the request has an explicit sattr(request, 'allowed_organizations')) + # which limits which OAuth2 clients can see the courses + # based on the association with a RestrictedApplication + if hasattr(request, 'auth') and hasattr(request, 'oauth_scopes_filters'): + course_key = CourseKey.from_string(course_id) + if 'content_org' in request.oauth_scopes_filters.keys(): + if course_key.org not in request.oauth_scopes_filters['content_org']: + return self.make_error_response( + status_code=status.HTTP_403_FORBIDDEN, + developer_message='The OAuth2 RestrictedApplication is not associated with org.', + error_code='course_org_not_associated_with_calling_application' + ) course = self._get_course(course_id, request.user, 'load') if isinstance(course, Response): diff --git a/lms/envs/common.py b/lms/envs/common.py index 8a1b384f85ff..63a7fbfe85e7 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -498,6 +498,7 @@ OAUTH2_PROVIDER = { 'OAUTH2_VALIDATOR_CLASS': 'openedx.core.djangoapps.oauth_dispatch.dot_overrides.validators.EdxOAuth2Validator', 'REFRESH_TOKEN_EXPIRE_SECONDS': 20160, + 'SCOPES_BACKEND_CLASS':'openedx.core.djangoapps.oauth_dispatch.scopes.DynamicScopes', 'SCOPES': { 'read': 'Read access', 'write': 'Write access', @@ -505,13 +506,14 @@ # conform profile scope message that is presented to end-user # to lms/templates/provider/authorize.html. This may be revised later. 'profile': 'Know your name and username', + 'grades:read': 'Retrieve your grades for your enrolled courses', + 'certificates:read': 'Retrieve your course certificates' }, 'REQUEST_APPROVAL_PROMPT': 'auto_even_if_expired', } # This is required for the migrations in oauth_dispatch.models # otherwise it fails saying this attribute is not present in Settings -OAUTH2_PROVIDER_APPLICATION_MODEL = 'oauth2_provider.Application' - +OAUTH2_PROVIDER_APPLICATION_MODEL = 'oauth_dispatch.OauthRestrictedApplication' ################################## TEMPLATE CONFIGURATION ##################################### # Mako templating import tempfile @@ -2402,6 +2404,8 @@ def _make_locale_paths(settings): "reddit", ] +DEFAULT_JWT_ISSUER = 'test-issuer-1', +DEFAULT_RESTRICTED_JWT_ISSUER = 'test-issuer-2' # JWT Settings JWT_AUTH = { # TODO Set JWT_SECRET_KEY to a secure value. By default, SECRET_KEY will be used. @@ -2416,6 +2420,18 @@ def _make_locale_paths(settings): 'JWT_DECODE_HANDLER': 'edx_rest_framework_extensions.utils.jwt_decode_handler', # Number of seconds before JWT tokens expire 'JWT_EXPIRATION': 30, + 'JWT_ISSUERS': [ + { + 'ISSUER':'test-issuer-1', + 'SECRET_KEY':'test-secret-key-1', + 'AUDIENCE':'test-audience-1', + }, + { + 'ISSUER':'test-issuer-2', + 'SECRET_KEY':'test-secret-key-2', + 'AUDIENCE':'test-audience-2', + } + ] } # The footer URLs dictionary maps social footer names diff --git a/openedx/core/djangoapps/oauth_dispatch/admin.py b/openedx/core/djangoapps/oauth_dispatch/admin.py index 2c01b289d15c..79813a65a25f 100644 --- a/openedx/core/djangoapps/oauth_dispatch/admin.py +++ b/openedx/core/djangoapps/oauth_dispatch/admin.py @@ -5,7 +5,7 @@ from django.contrib.admin import ModelAdmin, site from oauth2_provider import models -from .models import RestrictedApplication +from .models import RestrictedApplication, OauthRestrictOrganization, OauthRestrictedApplication def reregister(model_class): @@ -52,17 +52,6 @@ class DOTRefreshTokenAdmin(ModelAdmin): search_fields = [u'token', u'user__username', u'access_token__token'] -@reregister(models.Application) -class DOTApplicationAdmin(ModelAdmin): - """ - Custom Application Admin - """ - list_display = [u'name', u'user', u'client_type', u'authorization_grant_type', u'client_id'] - list_filter = [u'client_type', u'authorization_grant_type'] - raw_id_fields = [u'user'] - search_fields = [u'name', u'user__username', u'client_id'] - - @reregister(models.Grant) class DOTGrantAdmin(ModelAdmin): """ @@ -79,7 +68,28 @@ class RestrictedApplicationAdmin(ModelAdmin): """ ModelAdmin for the Restricted Application """ - list_display = [u'application'] - + list_display = [u'application', u'_org_associations'] site.register(RestrictedApplication, RestrictedApplicationAdmin) + + +@reregister(OauthRestrictedApplication) +class OauthRestrictedApplicationAdmin(ModelAdmin): + """ + ModelAdmin for the Restricted Application + """ + list_display = [u'name', u'user', u'client_type', u'authorization_grant_type', u'client_id'] + list_filter = [u'client_type', u'authorization_grant_type'] + raw_id_fields = [u'user'] + search_fields = [u'name', u'user__username', u'client_id'] + + +class OauthRestrictOrganizationAdmin(ModelAdmin): + """ + ModelAdmin for the Restricted Application + """ + list_display = [u'application','organization_type'] + filter_horizontal = ('_org_associations',) + +site.register(OauthRestrictOrganization, OauthRestrictOrganizationAdmin) + diff --git a/openedx/core/djangoapps/oauth_dispatch/dot_overrides/validators.py b/openedx/core/djangoapps/oauth_dispatch/dot_overrides/validators.py index c77d9924a16f..a79873f4bb75 100644 --- a/openedx/core/djangoapps/oauth_dispatch/dot_overrides/validators.py +++ b/openedx/core/djangoapps/oauth_dispatch/dot_overrides/validators.py @@ -13,9 +13,9 @@ from oauth2_provider.oauth2_validators import OAuth2Validator from pytz import utc from ratelimitbackend.backends import RateLimitMixin - -from ..models import RestrictedApplication - +from django.conf import settings +from ..models import RestrictedApplication, OauthRestrictedApplication +from openedx.core.djangoapps.oauth_dispatch.utils import is_oauth_scope_enforcement_enabled @receiver(pre_save, sender=AccessToken) def on_access_token_presave(sender, instance, *args, **kwargs): # pylint: disable=unused-argument @@ -25,12 +25,9 @@ def on_access_token_presave(sender, instance, *args, **kwargs): # pylint: disab We do this as a pre-save hook on the ORM """ - - is_application_restricted = RestrictedApplication.objects.filter(application=instance.application).exists() - if is_application_restricted: + if not is_oauth_scope_enforcement_enabled(): RestrictedApplication.set_access_token_as_expired(instance) - # TODO: Remove Django 1.11 upgrade shim # SHIM: Allow users that are inactive to still authenticate while keeping rate-limiting functionality. if django.VERSION < (1, 10): @@ -112,8 +109,7 @@ def save_bearer_token(self, token, request, *args, **kwargs): super(EdxOAuth2Validator, self).save_bearer_token(token, request, *args, **kwargs) - is_application_restricted = RestrictedApplication.objects.filter(application=request.client).exists() - if is_application_restricted: + if not is_oauth_scope_enforcement_enabled(): # Since RestrictedApplications will override the DOT defined expiry, so that access_tokens # are always expired, we need to re-read the token from the database and then calculate the # expires_in (in seconds) from what we stored in the database. This value should be a negative @@ -122,7 +118,6 @@ def save_bearer_token(self, token, request, *args, **kwargs): access_token = AccessToken.objects.get(token=token['access_token']) utc_now = datetime.utcnow().replace(tzinfo=utc) expires_in = (access_token.expires - utc_now).total_seconds() - # assert that RestrictedApplications only issue expired tokens # blow up processing if we see otherwise assert expires_in < 0 diff --git a/openedx/core/djangoapps/oauth_dispatch/dot_overrides/views.py b/openedx/core/djangoapps/oauth_dispatch/dot_overrides/views.py index 363e6e86ebd4..37782ccd959a 100644 --- a/openedx/core/djangoapps/oauth_dispatch/dot_overrides/views.py +++ b/openedx/core/djangoapps/oauth_dispatch/dot_overrides/views.py @@ -10,6 +10,32 @@ from oauth2_provider.settings import oauth2_settings from oauth2_provider.views import AuthorizationView +from oauth2_provider import models as dot_models +from openedx.core.djangoapps.oauth_dispatch.models import ScopedOrganization + +def is_org_associated_with_appication(request): + dot_id = dot_models.get_application_model().objects.get(client_id = request.GET.get('client_id')).id + if ScopedOrganization.objects.filter(application_id=dot_id).exists(): + return True + else: + return False + +def get_associated_application_orgs(request): + if is_org_associated_with_appication(request): + orgs = ScopedOrganization.objects.get(application_id=dot_id).org_associations + return orgs + else: + return None + +def associate_org_with_scope_description(scopes,all_scopes,org): + org_scopes = ['grades:read','certificates:read'] + scope_descriptions = [] + for scope in scopes: + if scope in org_scopes: + scope_descriptions.append(all_scopes[scope] + ' with '+ org[0] ) + else: + scope_descriptions.append(all_scopes[scope]) + return scope_descriptions # TODO (ARCH-83) remove once we have full support of OAuth Scopes class EdxOAuth2AuthorizationView(AuthorizationView): @@ -38,7 +64,12 @@ def get(self, request, *args, **kwargs): scopes, credentials = self.validate_authorization_request(request) all_scopes = get_scopes_backend().get_all_scopes() - kwargs["scopes_descriptions"] = [all_scopes[scope] for scope in scopes] + if is_org_associated_with_appication: + org = get_associated_application_orgs(request) + kwargs["scopes_descriptions"] = associate_org_with_scope_description(scopes,all_scopes,org) + else: + kwargs["scopes_descriptions"] = [all_scopes[scope] for scope in scopes] + kwargs['scopes'] = scopes # at this point we know an Application instance with such client_id exists in the database diff --git a/openedx/core/djangoapps/oauth_dispatch/migrations/0002_auto_20180515_1350.py b/openedx/core/djangoapps/oauth_dispatch/migrations/0002_auto_20180515_1350.py new file mode 100644 index 000000000000..47a0a0ca5fca --- /dev/null +++ b/openedx/core/djangoapps/oauth_dispatch/migrations/0002_auto_20180515_1350.py @@ -0,0 +1,54 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.12 on 2018-05-15 17:50 +from __future__ import unicode_literals + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import oauth2_provider.generators +import oauth2_provider.validators + + +class Migration(migrations.Migration): + + dependencies = [ + ('organizations', '0006_auto_20171207_0259'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + migrations.swappable_dependency(settings.OAUTH2_PROVIDER_APPLICATION_MODEL), + ('oauth_dispatch', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='OauthRestrictedApplication', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('client_id', models.CharField(db_index=True, default=oauth2_provider.generators.generate_client_id, max_length=100, unique=True)), + ('redirect_uris', models.TextField(blank=True, help_text='Allowed URIs list, space separated', validators=[oauth2_provider.validators.validate_uris])), + ('client_type', models.CharField(choices=[('confidential', 'Confidential'), ('public', 'Public')], max_length=32)), + ('authorization_grant_type', models.CharField(choices=[('authorization-code', 'Authorization code'), ('implicit', 'Implicit'), ('password', 'Resource owner password-based'), ('client-credentials', 'Client credentials')], max_length=32)), + ('client_secret', models.CharField(blank=True, db_index=True, default=oauth2_provider.generators.generate_client_secret, max_length=255)), + ('name', models.CharField(blank=True, max_length=255)), + ('skip_authorization', models.BooleanField(default=False)), + ('allowed_scope', models.TextField(blank=True)), + ('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='oauth_dispatch_oauthrestrictedapplication', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='OauthRestrictOrganization', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('organization_type', models.CharField(choices=[(b'content_provider', 'Content Provider'), (b'user_provider', 'User Provider')], default=b'content_provider', max_length=32)), + ('_org_associations', models.ManyToManyField(to='organizations.Organization')), + ('application', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.OAUTH2_PROVIDER_APPLICATION_MODEL)), + ], + ), + migrations.AlterField( + model_name='restrictedapplication', + name='application', + field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='restricted_application', to=settings.OAUTH2_PROVIDER_APPLICATION_MODEL), + ), + ] diff --git a/openedx/core/djangoapps/oauth_dispatch/models.py b/openedx/core/djangoapps/oauth_dispatch/models.py index 406b69fcc6e3..0c1df1422fe9 100644 --- a/openedx/core/djangoapps/oauth_dispatch/models.py +++ b/openedx/core/djangoapps/oauth_dispatch/models.py @@ -7,6 +7,15 @@ from django.db import models from oauth2_provider.settings import oauth2_settings from pytz import utc +from oauth2_provider.models import AccessToken +from organizations.models import Organization +from django.utils.translation import ugettext_lazy as _ +from oauth2_provider.models import AbstractApplication +from oauth2_provider.scopes import get_scopes_backend + +# define default separator used to store lists +# IMPORTANT: Do not change this after data has been populated in database +_DEFAULT_SEPARATOR = ' ' class RestrictedApplication(models.Model): @@ -43,3 +52,73 @@ def verify_access_token_as_expired(cls, access_token): is set at the beginning of the epoch which is Jan. 1, 1970 """ return access_token.expires == datetime(1970, 1, 1, tzinfo=utc) + + +class ScopedApplication(AbstractApplication): + """ + Application model for use with Django OAuth Toolkit that allows the scopes + available to an application to be restricted on a per-application basis. + """ + allowed_scope = models.TextField(blank = True) + + def _get_list_from_delimited_string(self, delimited_string, separator=_DEFAULT_SEPARATOR): + """ + Helper to return a list from a delimited string + """ + + return delimited_string.split(separator) if delimited_string else [] + + @classmethod + def is_token_oauth_restricted_application(cls, token): + """ + Returns if token is issued to a RestriectedApplication + """ + + if isinstance(token, basestring): + # if string is passed in, do the look up + token_obj = AccessToken.objects.get(token=token) + else: + token_obj = token + + return cls.get_restricted_application(token_obj.application) is not None + + @classmethod + def get_restricted_application(cls, application): + """ + For a given application, get the related restricted application + """ + return OauthRestrictedApplication.objects.filter(id=application.id) + + @property + def allowed_scopes(self): + """ + Translate space delimited string to a list + """ + all_scopes = set(get_scopes_backend().get_all_scopes().keys()) + app_scopes = set(self._get_list_from_delimited_string(self.allowed_scope)) + return app_scopes.intersection(all_scopes) + + +class ScopedOrganization(models.Model): + + CONTENT_PROVIDER = 'content_provider' + USER_PROVIDER = 'user_provider' + ORGANIZATION_PROVIDER_TYPES = ( + (CONTENT_PROVIDER, _('Content Provider')), + (USER_PROVIDER, _('User Provider')), + ) + application = models.ForeignKey(oauth2_settings.APPLICATION_MODEL, null=False) + + _org_associations = models.ManyToManyField(Organization) + + organization_type = models.CharField(max_length=32, choices=ORGANIZATION_PROVIDER_TYPES, default=CONTENT_PROVIDER) + + @property + def org_associations(self): + """ + Translate space delimited string to a list + """ + org_associations_list = [] + for each in self._org_associations.all(): + org_associations_list.append(each.name) + return org_associations_list diff --git a/openedx/core/djangoapps/oauth_dispatch/scopes.py b/openedx/core/djangoapps/oauth_dispatch/scopes.py new file mode 100644 index 000000000000..5aa22e6abd11 --- /dev/null +++ b/openedx/core/djangoapps/oauth_dispatch/scopes.py @@ -0,0 +1,20 @@ +""" +Django OAuth Toolkit scopes backend for the dot-dynamic-scopes package. +""" + +from django.conf import settings +from django.utils import module_loading + +from oauth2_provider.scopes import SettingsScopes + + +class DynamicScopes(SettingsScopes): + """ + Scopes backend that provides scopes from a Django model. + """ + def get_all_scopes(self): + return settings.OAUTH2_PROVIDER['SCOPES'] + + def get_available_scopes(self, application = None, request = None, *args, **kwargs): + return list(self.get_all_scopes().keys()) + diff --git a/openedx/core/djangoapps/oauth_dispatch/utils.py b/openedx/core/djangoapps/oauth_dispatch/utils.py new file mode 100644 index 000000000000..6b1c50389f69 --- /dev/null +++ b/openedx/core/djangoapps/oauth_dispatch/utils.py @@ -0,0 +1,10 @@ +"""Utilities to assist with oauth scopes enforcement""" +from waffle import switch_is_active + + +def is_oauth_scope_enforcement_enabled(): + """ + Returns True if switch is enabled + """ + return switch_is_active('ENABLE_OAUTH_SCOPE_ENFORCEMENT') + diff --git a/openedx/core/djangoapps/oauth_dispatch/views.py b/openedx/core/djangoapps/oauth_dispatch/views.py index 910a6d724b06..968ca959bdff 100644 --- a/openedx/core/djangoapps/oauth_dispatch/views.py +++ b/openedx/core/djangoapps/oauth_dispatch/views.py @@ -22,7 +22,7 @@ from openedx.core.djangoapps.auth_exchange import views as auth_exchange_views from openedx.core.lib.token_utils import JwtBuilder - +from .models import RestrictedApplication, OauthRestrictOrganization,OauthRestrictedApplication from . import adapters from .dot_overrides import views as dot_overrides_views @@ -42,7 +42,7 @@ def get_adapter(self, request): """ Returns the appropriate adapter based on the OAuth client linked to the request. """ - if dot_models.Application.objects.filter(client_id=self._get_client_id(request)).exists(): + if dot_models.get_application_model().objects.filter(client_id=self._get_client_id(request)).exists(): return self.dot_adapter else: return self.dop_adapter @@ -85,7 +85,41 @@ def _get_client_id(self, request): else: return request.POST.get('client_id') + def _get_application_id(self, request): + """ + Return application id from provided request + """ + return dot_models.get_application_model().objects.get(client_id=self._get_client_id(request)).id + + def is_application_scopes_restricted(self, request): + """ + Returns the appropriate adapter based on the OAuth client linked to the request. + """ + dot_id = self._get_application_id(request) + + if OauthRestrictedApplication.objects.filter(id=dot_id).exists(): + return True + else: + return False + + def get_associated_orgs(self, request): + """ + Returns the appropriate adapter based on the OAuth client linked to the request. + """ + dot_id = self._get_application_id(request) + + if OauthRestrictOrganization.objects.filter(application_id=dot_id).exists(): + return OauthRestrictOrganization.objects.get(application_id=dot_id).org_associations + else: + return None + def get_application_grant_type(self, request): + """ + Returns grant type of the application + """ + return dot_models.get_application_model().objects.get(client_id=self._get_client_id(request)).authorization_grant_type + + class AccessTokenView(RatelimitMixin, _DispatchingView): """ Handle access token requests. @@ -102,9 +136,11 @@ def dispatch(self, request, *args, **kwargs): if response.status_code == 200 and request.POST.get('token_type', '').lower() == 'jwt': expires_in, scopes, user = self._decompose_access_token_response(request, response) - + orgs = self.get_associated_orgs(request) + is_application_scopes_restricted = self.is_application_scopes_restricted(request) + application_grant_type = self.get_application_grant_type(request) content = { - 'access_token': JwtBuilder(user).build_token(scopes, expires_in), + 'access_token': JwtBuilder(user, is_application_scopes_restricted=is_application_scopes_restricted).build_token(scopes, expires_in, orgs=orgs, application_grant_type=application_grant_type), 'expires_in': expires_in, 'token_type': 'JWT', 'scope': ' '.join(scopes), diff --git a/openedx/core/lib/api/view_utils.py b/openedx/core/lib/api/view_utils.py index ed552ae11c6f..cff681d9ba4f 100644 --- a/openedx/core/lib/api/view_utils.py +++ b/openedx/core/lib/api/view_utils.py @@ -13,7 +13,7 @@ from rest_framework.request import clone_request from rest_framework.response import Response from six import text_type - +from edx_rest_framework_extensions.permissions import JWTRestrictedApplicationPermission from openedx.core.lib.api.authentication import ( OAuth2AuthenticationAllowInactiveUser, SessionAuthenticationAllowInactiveUser @@ -103,6 +103,8 @@ def _decorator(func_or_class): func_or_class.permission_classes += (IsAuthenticated,) if is_user: func_or_class.permission_classes += (IsUserInUrl,) + # always check access by restricted OAuth2 applications + func_or_class.permission_classes += (JWTRestrictedApplicationPermission, ) return func_or_class return _decorator diff --git a/openedx/core/lib/token_utils.py b/openedx/core/lib/token_utils.py index edbd52905e6b..38e281d9e37f 100644 --- a/openedx/core/lib/token_utils.py +++ b/openedx/core/lib/token_utils.py @@ -29,13 +29,24 @@ class JwtBuilder(object): secret (string): Overrides configured JWT secret (signing) key. Unused if an asymmetric signature is requested. """ - def __init__(self, user, asymmetric=False, secret=None): + def __init__(self, user, asymmetric=False, secret=None,is_application_scopes_restricted=None): + self.user = user self.asymmetric = asymmetric self.secret = secret self.jwt_auth = configuration_helpers.get_value('JWT_AUTH', settings.JWT_AUTH) + self.default_jwt_issuer = configuration_helpers.get_value('DEFAULT_JWT_ISSUER', settings.DEFAULT_JWT_ISSUER) + self.default_restricted_jwt_issuer = configuration_helpers.get_value('DEFAULT_RESTRICTED_JWT_ISSUER', settings.DEFAULT_RESTRICTED_JWT_ISSUER) + if is_application_scopes_restricted: + for issuer in self.jwt_auth['JWT_ISSUERS']: + if self.default_restricted_jwt_issuer == issuer['ISSUER']: + self.issuer = issuer + else: + for issuer in self.jwt_auth['JWT_ISSUERS']: + if self.default_jwt_issuer == issuer['ISSUER']: + self.issuer = issuer - def build_token(self, scopes, expires_in=None, aud=None, additional_claims=None): + def build_token(self, scopes, expires_in=None, aud=None, additional_claims=None, orgs=None, application_grant_type=None): """Returns a JWT access token. Arguments: @@ -51,14 +62,23 @@ def build_token(self, scopes, expires_in=None, aud=None, additional_claims=None) """ now = int(time()) expires_in = expires_in or self.jwt_auth['JWT_EXPIRATION'] + filters = [] + if orgs: + for org in orgs: + filters.append('content_org:'+org) + if application_grant_type != u'client-credentials': + filters.append('user:me') + payload = { # TODO Consider getting rid of this claim since we don't use it. - 'aud': aud if aud else self.jwt_auth['JWT_AUDIENCE'], + 'aud': aud if aud else self.issuer['AUDIENCE'], 'exp': now + expires_in, 'iat': now, - 'iss': self.jwt_auth['JWT_ISSUER'], + 'iss': self.issuer['ISSUER'], 'preferred_username': self.user.username, 'scopes': scopes, + 'filters': filters, + 'version': '1.0', 'sub': anonymous_id_for_user(self.user, None), } @@ -109,7 +129,7 @@ def encode(self, payload): keys.add(RSAKey(key=RSA.importKey(settings.JWT_PRIVATE_SIGNING_KEY))) algorithm = 'RS512' else: - key = self.secret if self.secret else self.jwt_auth['JWT_SECRET_KEY'] + key = self.secret if self.secret else self.issuer['SECRET_KEY'] keys.add({'key': key, 'kty': 'oct'}) algorithm = self.jwt_auth['JWT_ALGORITHM']