Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
C
course-discovery
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
edx
course-discovery
Commits
aeb4265f
Commit
aeb4265f
authored
Jan 20, 2016
by
Clinton Blackburn
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #17 from edx/clintonb/jwt
Added JWT Authentication
parents
d0951bed
3cf140f8
Show whitespace changes
Inline
Side-by-side
Showing
8 changed files
with
93 additions
and
6 deletions
+93
-6
course_discovery/apps/api/jwt_decode_handler.py
+44
-0
course_discovery/apps/api/v1/tests/test_views.py
+31
-3
course_discovery/apps/api/v1/views.py
+3
-3
course_discovery/apps/core/tests/factories.py
+1
-0
course_discovery/settings/base.py
+9
-0
course_discovery/settings/local.py
+2
-0
course_discovery/settings/test.py
+2
-0
requirements/base.txt
+1
-0
No files found.
course_discovery/apps/api/jwt_decode_handler.py
0 → 100644
View file @
aeb4265f
"""
Custom JWT decoding function for django_rest_framework jwt package.
Adds logging to facilitate debugging of InvalidTokenErrors. Also
requires "exp" and "iat" claims to be present - the base package
doesn't expose settings to enforce this.
"""
import
logging
import
jwt
from
rest_framework_jwt.settings
import
api_settings
logger
=
logging
.
getLogger
(
__name__
)
def
decode
(
token
):
"""
Ensure InvalidTokenErrors are logged for diagnostic purposes, before
failing authentication.
Args:
token (str): JSON web token (JWT) to be decoded.
"""
options
=
{
'verify_exp'
:
api_settings
.
JWT_VERIFY_EXPIRATION
,
'require_exp'
:
True
,
'require_iat'
:
True
,
}
try
:
return
jwt
.
decode
(
token
,
api_settings
.
JWT_SECRET_KEY
,
api_settings
.
JWT_VERIFY
,
options
=
options
,
leeway
=
api_settings
.
JWT_LEEWAY
,
audience
=
api_settings
.
JWT_AUDIENCE
,
issuer
=
api_settings
.
JWT_ISSUER
,
algorithms
=
[
api_settings
.
JWT_ALGORITHM
]
)
except
jwt
.
InvalidTokenError
:
logger
.
exception
(
'JWT decode failed!'
)
raise
course_discovery/apps/api/v1/tests/test_views.py
View file @
aeb4265f
# pylint: disable=redefined-builtin
import
json
import
urllib
from
time
import
time
import
ddt
import
jwt
from
django.conf
import
settings
from
rest_framework.reverse
import
reverse
from
rest_framework.test
import
APITestCase
,
APIRequestFactory
...
...
@@ -59,6 +62,23 @@ class CatalogViewSetTests(ElasticsearchTestMixin, SerializationMixin, APITestCas
self
.
course
=
CourseFactory
(
id
=
'a/b/c'
,
name
=
'ABC Test Course'
)
self
.
refresh_index
()
def
generate_jwt_token_header
(
self
,
user
):
"""Generate a valid JWT token header for authenticated requests."""
now
=
int
(
time
())
ttl
=
5
payload
=
{
'iss'
:
settings
.
JWT_AUTH
[
'JWT_ISSUER'
],
'aud'
:
settings
.
JWT_AUTH
[
'JWT_AUDIENCE'
],
'username'
:
user
.
username
,
'email'
:
user
.
email
,
'iat'
:
now
,
'exp'
:
now
+
ttl
}
token
=
jwt
.
encode
(
payload
,
settings
.
JWT_AUTH
[
'JWT_SECRET_KEY'
])
.
decode
(
'utf-8'
)
return
'JWT {token}'
.
format
(
token
=
token
)
def
test_create_without_authentication
(
self
):
""" Verify authentication is required when creating, updating, or deleting a catalog. """
self
.
client
.
logout
()
...
...
@@ -77,8 +97,7 @@ class CatalogViewSetTests(ElasticsearchTestMixin, SerializationMixin, APITestCas
response
=
getattr
(
self
.
client
,
http_method
)(
url
,
{},
format
=
'json'
)
self
.
assertEqual
(
response
.
status_code
,
403
)
def
test_create
(
self
):
""" Verify the endpoint creates a new catalog. """
def
assert_catalog_created
(
self
,
**
headers
):
name
=
'The Kitchen Sink'
query
=
'*.*'
data
=
{
...
...
@@ -86,7 +105,7 @@ class CatalogViewSetTests(ElasticsearchTestMixin, SerializationMixin, APITestCas
'query'
:
query
}
response
=
self
.
client
.
post
(
reverse
(
'api:v1:catalog-list'
),
data
,
format
=
'json'
)
response
=
self
.
client
.
post
(
reverse
(
'api:v1:catalog-list'
),
data
,
format
=
'json'
,
**
headers
)
self
.
assertEqual
(
response
.
status_code
,
201
)
catalog
=
Catalog
.
objects
.
latest
()
...
...
@@ -94,6 +113,15 @@ class CatalogViewSetTests(ElasticsearchTestMixin, SerializationMixin, APITestCas
self
.
assertEqual
(
catalog
.
name
,
name
)
self
.
assertEqual
(
catalog
.
query
,
query
)
def
test_create_with_session_authentication
(
self
):
""" Verify the endpoint creates a new catalog when the client is authenticated via session authentication. """
self
.
assert_catalog_created
()
def
test_create_with_jwt_authentication
(
self
):
""" Verify the endpoint creates a new catalog when the client is authenticated via JWT authentication. """
self
.
client
.
logout
()
self
.
assert_catalog_created
(
HTTP_AUTHORIZATION
=
self
.
generate_jwt_token_header
(
self
.
user
))
def
test_courses
(
self
):
""" Verify the endpoint returns the list of courses contained in the catalog. """
url
=
reverse
(
'api:v1:catalog-courses'
,
kwargs
=
{
'id'
:
self
.
catalog
.
id
})
...
...
course_discovery/apps/api/v1/views.py
View file @
aeb4265f
...
...
@@ -6,6 +6,7 @@ from rest_framework.authentication import SessionAuthentication
from
rest_framework.decorators
import
detail_route
from
rest_framework.permissions
import
DjangoModelPermissionsOrAnonReadOnly
,
IsAuthenticatedOrReadOnly
from
rest_framework.response
import
Response
from
rest_framework_jwt.authentication
import
JSONWebTokenAuthentication
from
course_discovery.apps.api.pagination
import
ElasticsearchLimitOffsetPagination
from
course_discovery.apps.api.serializers
import
CatalogSerializer
,
CourseSerializer
,
ContainedCoursesSerializer
...
...
@@ -20,8 +21,7 @@ logger = logging.getLogger(__name__)
class
CatalogViewSet
(
viewsets
.
ModelViewSet
):
""" Catalog resource. """
# TODO Add support for JWT
authentication_classes
=
(
SessionAuthentication
,)
authentication_classes
=
(
SessionAuthentication
,
JSONWebTokenAuthentication
,)
permission_classes
=
(
DjangoModelPermissionsOrAnonReadOnly
,)
lookup_field
=
'id'
queryset
=
Catalog
.
objects
.
all
()
...
...
@@ -95,7 +95,7 @@ class CatalogViewSet(viewsets.ModelViewSet):
class
CourseViewSet
(
viewsets
.
ReadOnlyModelViewSet
):
""" Course resource. """
authentication_classes
=
(
SessionAuthentication
,)
authentication_classes
=
(
SessionAuthentication
,
JSONWebTokenAuthentication
,
)
lookup_field
=
'id'
lookup_value_regex
=
COURSE_ID_REGEX
permission_classes
=
(
IsAuthenticatedOrReadOnly
,)
...
...
course_discovery/apps/core/tests/factories.py
View file @
aeb4265f
...
...
@@ -6,6 +6,7 @@ USER_PASSWORD = 'password'
class
UserFactory
(
factory
.
DjangoModelFactory
):
username
=
factory
.
Sequence
(
lambda
n
:
'user_
%
d'
%
n
)
password
=
factory
.
PostGenerationMethodCall
(
'set_password'
,
USER_PASSWORD
)
is_active
=
True
is_superuser
=
False
...
...
course_discovery/settings/base.py
View file @
aeb4265f
...
...
@@ -243,6 +243,15 @@ REST_FRAMEWORK = {
)
}
# NOTE (CCB): JWT_SECRET_KEY is intentionally not set here to avoid production releases with a public value.
# Set a value in a downstream settings file.
JWT_AUTH
=
{
'JWT_ALGORITHM'
:
'HS256'
,
'JWT_AUDIENCE'
:
'course-discovery'
,
'JWT_ISSUER'
:
'course-discovery'
,
'JWT_DECODE_HANDLER'
:
'course_discovery.apps.api.jwt_decode_handler.decode'
,
}
SWAGGER_SETTINGS
=
{
'api_version'
:
'v1'
,
'doc_expansion'
:
'list'
,
...
...
course_discovery/settings/local.py
View file @
aeb4265f
...
...
@@ -55,6 +55,8 @@ SOCIAL_AUTH_EDX_OIDC_ID_TOKEN_DECRYPTION_KEY = SOCIAL_AUTH_EDX_OIDC_SECRET
ENABLE_AUTO_AUTH
=
True
JWT_AUTH
[
'JWT_SECRET_KEY'
]
=
'course-discovery-jwt-secret-key'
#####################################################################
# Lastly, see if the developer has any local overrides.
if
os
.
path
.
isfile
(
join
(
dirname
(
abspath
(
__file__
)),
'private.py'
)):
...
...
course_discovery/settings/test.py
View file @
aeb4265f
...
...
@@ -34,3 +34,5 @@ ELASTICSEARCH = {
'host'
:
os
.
environ
.
get
(
'TEST_ELASTICSEARCH_HOST'
,
'localhost'
),
'index'
:
'course_discovery_test'
,
}
JWT_AUTH
[
'JWT_SECRET_KEY'
]
=
'course-discovery-jwt-secret-key'
requirements/base.txt
View file @
aeb4265f
...
...
@@ -2,6 +2,7 @@ django == 1.8.7
django-extensions == 1.5.9
django-waffle == 0.11
djangorestframework == 3.3.1
djangorestframework-jwt==1.7.2
django-rest-swagger[reST]==0.3.4
edx-auth-backends == 0.1.3
edx-rest-api-client==1.2.1
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment