views.py 6.53 KB
Newer Older
1 2 3 4 5 6 7
"""
Views that dispatch processing of OAuth requests to django-oauth2-provider or
django-oauth-toolkit as appropriate.
"""

from __future__ import unicode_literals

8
import hashlib
9 10
import json

11 12 13 14
from Crypto.PublicKey import RSA
from django.conf import settings
from django.core.urlresolvers import reverse
from django.http import JsonResponse
15 16
from django.views.generic import View
from edx_oauth2_provider import views as dop_views  # django-oauth2-provider views
17
from jwkest.jwk import RSAKey
18 19
from oauth2_provider import models as dot_models  # django-oauth-toolkit
from oauth2_provider import views as dot_views
20

21
from openedx.core.djangoapps.auth_exchange import views as auth_exchange_views
22
from openedx.core.lib.token_utils import JwtBuilder
23

24 25 26 27 28 29 30 31 32 33 34 35 36 37
from . import adapters


class _DispatchingView(View):
    """
    Base class that route views to the appropriate provider view.  The default
    behavior routes based on client_id, but this can be overridden by redefining
    `select_backend()` if particular views need different behavior.
    """
    # pylint: disable=no-member

    dot_adapter = adapters.DOTAdapter()
    dop_adapter = adapters.DOPAdapter()

38 39 40 41 42 43 44 45 46
    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():
            return self.dot_adapter
        else:
            return self.dop_adapter

47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62
    def dispatch(self, request, *args, **kwargs):
        """
        Dispatch the request to the selected backend's view.
        """
        backend = self.select_backend(request)
        view = self.get_view_for_backend(backend)
        return view(request, *args, **kwargs)

    def select_backend(self, request):
        """
        Given a request that specifies an oauth `client_id`, return the adapter
        for the appropriate OAuth handling library.  If the client_id is found
        in a django-oauth-toolkit (DOT) Application, use the DOT adapter,
        otherwise use the django-oauth2-provider (DOP) adapter, and allow the
        calls to fail normally if the client does not exist.
        """
63
        return self.get_adapter(request).backend
64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79

    def get_view_for_backend(self, backend):
        """
        Return the appropriate view from the requested backend.
        """
        if backend == self.dot_adapter.backend:
            return self.dot_view.as_view()
        elif backend == self.dop_adapter.backend:
            return self.dop_view.as_view()
        else:
            raise KeyError('Failed to dispatch view. Invalid backend {}'.format(backend))

    def _get_client_id(self, request):
        """
        Return the client_id from the provided request
        """
80 81 82 83
        if request.method == u'GET':
            return request.GET.get('client_id')
        else:
            return request.POST.get('client_id')
84 85 86 87 88 89 90 91 92


class AccessTokenView(_DispatchingView):
    """
    Handle access token requests.
    """
    dot_view = dot_views.TokenView
    dop_view = dop_views.AccessTokenView

93 94 95 96 97 98 99
    def dispatch(self, request, *args, **kwargs):
        response = super(AccessTokenView, self).dispatch(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)

            content = {
100
                'access_token': JwtBuilder(user).build_token(scopes, expires_in),
101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119
                'expires_in': expires_in,
                'token_type': 'JWT',
                'scope': ' '.join(scopes),
            }
            response.content = json.dumps(content)

        return response

    def _decompose_access_token_response(self, request, response):
        """ Decomposes the access token in the request to an expiration date, scopes, and User. """
        content = json.loads(response.content)
        access_token = content['access_token']
        scope = content['scope']
        access_token_obj = self.get_adapter(request).get_access_token(access_token)
        user = access_token_obj.user
        scopes = scope.split(' ')
        expires_in = content['expires_in']
        return expires_in, scopes, user

120 121 122 123 124 125 126 127 128 129 130 131 132 133 134

class AuthorizationView(_DispatchingView):
    """
    Part of the authorization flow.
    """
    dop_view = dop_views.Capture
    dot_view = dot_views.AuthorizationView


class AccessTokenExchangeView(_DispatchingView):
    """
    Exchange a third party auth token.
    """
    dop_view = auth_exchange_views.DOPAccessTokenExchangeView
    dot_view = auth_exchange_views.DOTAccessTokenExchangeView
135 136 137 138 139 140 141


class RevokeTokenView(_DispatchingView):
    """
    Dispatch to the RevokeTokenView of django-oauth-toolkit
    """
    dot_view = dot_views.RevokeTokenView
142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183


class ProviderInfoView(View):
    def get(self, request, *args, **kwargs):
        data = {
            'issuer': settings.JWT_AUTH['JWT_ISSUER'],
            'authorization_endpoint': request.build_absolute_uri(reverse('authorize')),
            'token_endpoint': request.build_absolute_uri(reverse('access_token')),
            'end_session_endpoint': request.build_absolute_uri(reverse('logout')),
            'token_endpoint_auth_methods_supported': ['client_secret_post'],
            # NOTE (CCB): This is not part of the OpenID Connect standard. It is added here since we
            # use JWS for our access tokens.
            'access_token_signing_alg_values_supported': ['RS512', 'HS256'],
            'scopes_supported': ['openid', 'profile', 'email'],
            'claims_supported': ['sub', 'iss', 'name', 'given_name', 'family_name', 'email'],
            'jwks_uri': request.build_absolute_uri(reverse('jwks')),
        }
        response = JsonResponse(data)
        return response


class JwksView(View):
    @staticmethod
    def serialize_rsa_key(key):
        kid = hashlib.md5(key.encode('utf-8')).hexdigest()
        key = RSAKey(kid=kid, key=RSA.importKey(key), use='sig', alg='RS512')
        return key.serialize(private=False)

    def get(self, request, *args, **kwargs):
        secret_keys = []

        if settings.JWT_PRIVATE_SIGNING_KEY:
            secret_keys.append(settings.JWT_PRIVATE_SIGNING_KEY)

        # NOTE: We provide the expired keys in case there are unexpired access tokens
        # that need to have their signatures verified.
        if settings.JWT_EXPIRED_PRIVATE_SIGNING_KEYS:
            secret_keys += settings.JWT_EXPIRED_PRIVATE_SIGNING_KEYS

        return JsonResponse({
            'keys': [self.serialize_rsa_key(key) for key in secret_keys if key],
        })