Commit ae7fcd50 by Tim Babych

Merge pull request #3 from edx/tim/id-token-auth-rebased

TNL-782 each request should have valid ID Token
parents 3e5e4b7b 32a74106
Oleg Marshev <oleg@edx.org> Oleg Marshev <oleg@edx.org>
Tim Babych <tim.babych@gmail.com>
PACKAGES = notesserver notesapi PACKAGES = notesserver notesapi
.PHONY: requirements
validate: test.requirements test coverage validate: test.requirements test coverage
......
import jwt
import logging
from django.conf import settings
from rest_framework.permissions import BasePermission from rest_framework.permissions import BasePermission
logger = logging.getLogger(__name__)
class TokenWrongIssuer(Exception):
pass
class HasAccessToken(BasePermission): class HasAccessToken(BasePermission):
""" """
Allow requests having valid ID Token. Allow requests having valid ID Token.
https://tools.ietf.org/html/draft-ietf-oauth-json-web-token-31
Expected Token:
Header {
"alg": "HS256",
"typ": "JWT"
}
Claims {
"sub": "<USER_ID>",
"exp": <EXPIRATION TIMESTAMP>,
"iat": <ISSUED TIMESTAMP>,
"aud": "<CLIENT ID"
}
Should be signed with CLIENT_SECRET
""" """
def has_permission(self, request, view): def has_permission(self, request, view):
return True if getattr(settings, 'DISABLE_TOKEN_CHECK', False):
return True
token = request.META.get('HTTP_X_ANNOTATOR_AUTH_TOKEN', '')
if not token:
logger.debug("No token found in headers")
return False
try:
data = jwt.decode(token, settings.CLIENT_SECRET)
auth_user = data['sub']
if data['aud'] != settings.CLIENT_ID:
raise TokenWrongIssuer
for request_field in ('GET', 'POST', 'DATA'):
if 'user' in getattr(request, request_field):
req_user = getattr(request, request_field)['user']
if req_user == auth_user:
return True
else:
logger.debug("Token user {auth_user} did not match {field} user {req_user}".format(
auth_user=auth_user, field=request_field, req_user=req_user
))
return False
logger.info("No user was present to compare in GET, POST or DATA")
except jwt.ExpiredSignature:
logger.debug("Token was expired: {}".format(token))
except jwt.DecodeError:
logger.debug("Could not decode token {}".format(token))
except TokenWrongIssuer:
logger.debug("Token has wrong issuer {}".format(token))
return False
\ No newline at end of file
class MockConsumer(object): import jwt
def __init__(self, key='mockconsumer'): from calendar import timegm
self.key = key from datetime import datetime, timedelta
self.secret = 'top-secret' from django.conf import settings
self.ttl = 86400
def get_id_token(user):
class MockUser(object): now = datetime.utcnow()
def __init__(self, id='alice', consumer=None): return jwt.encode({
self.id = id 'aud': settings.CLIENT_ID,
self.consumer = MockConsumer(consumer if consumer is not None else 'mockconsumer') 'sub': user,
self.is_admin = False 'iat': timegm(now.utctimetuple()),
'exp': timegm((now + timedelta(seconds=300)).utctimetuple()),
}, settings.CLIENT_SECRET)
class MockAuthenticator(object):
def request_user(self, request):
return MockUser()
def mock_authorizer(*args, **kwargs):
return True
...@@ -6,7 +6,7 @@ from notesapi.v1.views import AnnotationListView, AnnotationDetailView, Annotati ...@@ -6,7 +6,7 @@ from notesapi.v1.views import AnnotationListView, AnnotationDetailView, Annotati
urlpatterns = patterns( urlpatterns = patterns(
'', '',
url(r'^annotations/$', AnnotationListView.as_view(), name='annotations'), url(r'^annotations/$', AnnotationListView.as_view(), name='annotations'),
url(r'^annotations/(?P<annotation_id>[a-zA-Z0-9_-]+)$', AnnotationDetailView.as_view(), name='annotations_detail'), url(r'^annotations/(?P<annotation_id>[a-zA-Z0-9_-]+)/?$', AnnotationDetailView.as_view(), name='annotations_detail'),
url(r'^search/$', AnnotationSearchView.as_view(), name='annotations_search'), url(r'^search/$', AnnotationSearchView.as_view(), name='annotations_search'),
url(r'^status/$', RedirectView.as_view(url=reverse_lazy('status')), name='status'), url(r'^status/$', RedirectView.as_view(url=reverse_lazy('status')), name='status'),
) )
...@@ -2,7 +2,6 @@ from django.conf import settings ...@@ -2,7 +2,6 @@ from django.conf import settings
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from rest_framework import status from rest_framework import status
from rest_framework.permissions import AllowAny
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.views import APIView from rest_framework.views import APIView
...@@ -16,7 +15,6 @@ class AnnotationSearchView(APIView): ...@@ -16,7 +15,6 @@ class AnnotationSearchView(APIView):
""" """
Search annotations. Search annotations.
""" """
permission_classes = (AllowAny,)
def get(self, *args, **kwargs): # pylint: disable=unused-argument def get(self, *args, **kwargs): # pylint: disable=unused-argument
""" """
...@@ -52,7 +50,6 @@ class AnnotationListView(APIView): ...@@ -52,7 +50,6 @@ class AnnotationListView(APIView):
""" """
List all annotations or create. List all annotations or create.
""" """
permission_classes = (AllowAny,)
def get(self, *args, **kwargs): # pylint: disable=unused-argument def get(self, *args, **kwargs): # pylint: disable=unused-argument
""" """
...@@ -90,7 +87,6 @@ class AnnotationDetailView(APIView): ...@@ -90,7 +87,6 @@ class AnnotationDetailView(APIView):
""" """
Annotation detail view. Annotation detail view.
""" """
permission_classes = (AllowAny,)
UPDATE_FILTER_FIELDS = ('updated', 'created', 'user', 'consumer') UPDATE_FILTER_FIELDS = ('updated', 'created', 'user', 'consumer')
......
...@@ -4,12 +4,18 @@ import sys ...@@ -4,12 +4,18 @@ import sys
DEBUG = False DEBUG = False
TEMPLATE_DEBUG = False TEMPLATE_DEBUG = False
DISABLE_TOKEN_CHECK = False
USE_TZ = True USE_TZ = True
TIME_ZONE = 'UTC' TIME_ZONE = 'UTC'
# This value needs to be overriden in production. # This value needs to be overriden in production.
SECRET_KEY = '*^owi*4%!%9=#h@app!l^$jz8(c*q297^)4&4yn^#_m#fq=z#l' SECRET_KEY = '*^owi*4%!%9=#h@app!l^$jz8(c*q297^)4&4yn^#_m#fq=z#l'
# ID and Secret used for authenticating JWT Auth Tokens
# should match those configured for `edx-notes` Client in EdX's /admin/oauth2/client/
CLIENT_ID = 'edx-notes-id'
CLIENT_SECRET = 'edx-notes-secret'
ROOT_URLCONF = 'notesserver.urls' ROOT_URLCONF = 'notesserver.urls'
MIDDLEWARE_CLASSES = ( MIDDLEWARE_CLASSES = (
...@@ -78,12 +84,17 @@ LOGGING = { ...@@ -78,12 +84,17 @@ LOGGING = {
'level': 'DEBUG', 'level': 'DEBUG',
'propagate': True 'propagate': True
}, },
'notesapi.v1.permissions': {
'handlers': ['console'],
'level': 'DEBUG',
'propagate': True
},
}, },
} }
REST_FRAMEWORK = { REST_FRAMEWORK = {
'DEFAULT_PERMISSION_CLASSES': [ 'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticated' 'notesapi.v1.permissions.HasAccessToken'
] ]
} }
......
...@@ -16,11 +16,11 @@ def root(request): # pylint: disable=unused-argument ...@@ -16,11 +16,11 @@ def root(request): # pylint: disable=unused-argument
}) })
@permission_classes([AllowAny])
class StatusView(APIView): class StatusView(APIView):
""" """
Determine if server is alive. Determine if server is alive.
""" """
permission_classes = (AllowAny,)
def get(self, *args, **kwargs): # pylint: disable=unused-argument def get(self, *args, **kwargs): # pylint: disable=unused-argument
""" """
......
...@@ -5,3 +5,4 @@ django-rest-swagger==0.2.0 ...@@ -5,3 +5,4 @@ django-rest-swagger==0.2.0
elasticsearch==1.2.0 elasticsearch==1.2.0
annotator==0.12.0 annotator==0.12.0
django-cors-headers==0.13 django-cors-headers==0.13
PyJWT==0.3.0
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment