Commit 862d847c by Tim Babych

TNL-782 Check for valid ID Token

parent 3e5e4b7b
Oleg Marshev <oleg@edx.org>
Tim Babych <tim.babych@gmail.com>
PACKAGES = notesserver notesapi
.PHONY: requirements
validate: test.requirements test coverage
......
import jwt
import logging
from django.conf import settings
from rest_framework.permissions import BasePermission
logger = logging.getLogger(__name__)
class TokenWrongIssuer(Exception):
pass
class HasAccessToken(BasePermission):
"""
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):
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):
def __init__(self, key='mockconsumer'):
self.key = key
self.secret = 'top-secret'
self.ttl = 86400
class MockUser(object):
def __init__(self, id='alice', consumer=None):
self.id = id
self.consumer = MockConsumer(consumer if consumer is not None else 'mockconsumer')
self.is_admin = False
class MockAuthenticator(object):
def request_user(self, request):
return MockUser()
def mock_authorizer(*args, **kwargs):
return True
import jwt
from calendar import timegm
from datetime import datetime, timedelta
from django.conf import settings
def get_id_token(user):
now = datetime.utcnow()
return jwt.encode({
'aud': settings.CLIENT_ID,
'sub': user,
'iat': timegm(now.utctimetuple()),
'exp': timegm((now + timedelta(seconds=300)).utctimetuple()),
}, settings.CLIENT_SECRET)
......@@ -2,7 +2,6 @@ from django.conf import settings
from django.core.urlresolvers import reverse
from rest_framework import status
from rest_framework.permissions import AllowAny
from rest_framework.response import Response
from rest_framework.views import APIView
......@@ -16,7 +15,6 @@ class AnnotationSearchView(APIView):
"""
Search annotations.
"""
permission_classes = (AllowAny,)
def get(self, *args, **kwargs): # pylint: disable=unused-argument
"""
......@@ -52,7 +50,6 @@ class AnnotationListView(APIView):
"""
List all annotations or create.
"""
permission_classes = (AllowAny,)
def get(self, *args, **kwargs): # pylint: disable=unused-argument
"""
......@@ -90,7 +87,6 @@ class AnnotationDetailView(APIView):
"""
Annotation detail view.
"""
permission_classes = (AllowAny,)
UPDATE_FILTER_FIELDS = ('updated', 'created', 'user', 'consumer')
......
......@@ -4,12 +4,18 @@ import sys
DEBUG = False
TEMPLATE_DEBUG = False
DISABLE_TOKEN_CHECK = False
USE_TZ = True
TIME_ZONE = 'UTC'
# 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'
# 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'
MIDDLEWARE_CLASSES = (
......@@ -78,12 +84,17 @@ LOGGING = {
'level': 'DEBUG',
'propagate': True
},
'notesapi.v1.permissions': {
'handlers': ['console'],
'level': 'DEBUG',
'propagate': True
},
},
}
REST_FRAMEWORK = {
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticated'
'notesapi.v1.permissions.HasAccessToken'
]
}
......
......@@ -16,11 +16,11 @@ def root(request): # pylint: disable=unused-argument
})
@permission_classes([AllowAny])
class StatusView(APIView):
"""
Determine if server is alive.
"""
permission_classes = (AllowAny,)
def get(self, *args, **kwargs): # pylint: disable=unused-argument
"""
......
......@@ -5,3 +5,4 @@ django-rest-swagger==0.2.0
elasticsearch==1.2.0
annotator==0.12.0
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