Skip to content

Commit

Permalink
✨(user) add user route
Browse files Browse the repository at this point in the history
Add a user api route. The first route is `/user/me` to retrieve session user
info and a JWT access token which could be used by third party application to
make REST requests.
  • Loading branch information
jbpenrath committed Feb 8, 2021
1 parent fb94dd7 commit e9d1ec4
Show file tree
Hide file tree
Showing 7 changed files with 150 additions and 16 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Added

- Add an User API endpoint to fisrt generate a JWT for Authentication purpose
from Third Party Application

## [0.2.1] - 2019-10-10

### Fixed
Expand Down
35 changes: 19 additions & 16 deletions edx-platform/config/lms/docker_run_development.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,21 +10,19 @@
STATIC_ROOT = "/edx/app/edxapp/data/static"
STATIC_URL = "/static/"
MEDIA_ROOT = "/edx/app/edxapp/data/media"
LOG_DIR = '/data/log'
LOG_DIR = "/data/log"

EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
SECRET_KEY = "foo"

ALLOWED_HOSTS = ["*"]

LOGGING['handlers'].update(
local={'class': 'logging.NullHandler'},
tracking={'class': 'logging.NullHandler'},
LOGGING["handlers"].update(
local={"class": "logging.NullHandler"},
tracking={"class": "logging.NullHandler"},
)

INSTALLED_APPS += (
'fonzie',
)
INSTALLED_APPS += ("fonzie",)

ROOT_URLCONF = "fonzie.urls.lms_root"

Expand All @@ -34,12 +32,17 @@

# Django Rest Framework (aka DRF)
REST_FRAMEWORK = {
'ALLOWED_VERSIONS': ('1.0', ),
'DEFAULT_VERSION': '1.0',
'DEFAULT_VERSIONING_CLASS': 'rest_framework.versioning.URLPathVersioning',
"ALLOWED_VERSIONS": ("1.0",),
"DEFAULT_VERSION": "1.0",
"DEFAULT_VERSIONING_CLASS": "rest_framework.versioning.URLPathVersioning",
"DEFAULT_AUTHENTICATION_CLASSES": [
"rest_framework.authentication.SessionAuthentication"
],
}

FEATURES['ENABLE_DISCUSSION_SERVICE'] = False
JWT_PRIVATE_SIGNING_KEY = "ThisIsAnExampleKeyForDevPurposeOnly"

FEATURES["ENABLE_DISCUSSION_SERVICE"] = False

FEATURES["AUTOMATIC_AUTH_FOR_TESTING"] = True
FEATURES["RESTRICT_AUTOMATIC_AUTH"] = False
Expand All @@ -50,9 +53,9 @@
FEATURES["ENABLE_INSTRUCTOR_BACKGROUND_TASKS"] = False

GRADES_DOWNLOAD = {
"STORAGE_CLASS": "django.core.files.storage.FileSystemStorage",
"STORAGE_KWARGS": {
"location": "/data/export",
"base_url": "/api/v1.0/acl/report",
}
"STORAGE_CLASS": "django.core.files.storage.FileSystemStorage",
"STORAGE_KWARGS": {
"location": "/data/export",
"base_url": "/api/v1.0/acl/report",
},
}
2 changes: 2 additions & 0 deletions fonzie/urls/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from ..apps import FonzieConfig
from . import acl as acl_urls
from . import status as status_urls
from . import user as user_urls

app_name = FonzieConfig.name

Expand All @@ -26,4 +27,5 @@
r"{}/acl/".format(API_PREFIX),
include(acl_urls, namespace="acl"),
),
url(r"{}/user/".format(API_PREFIX), include(user_urls, namespace="user")),
]
13 changes: 13 additions & 0 deletions fonzie/urls/user.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# coding: utf-8
"""
API user endpoints
"""
from __future__ import absolute_import, unicode_literals

from django.conf.urls import url

from ..apps import FonzieConfig
from ..views.user import UserSessionView

app_name = FonzieConfig.name
urlpatterns = [url(r"^me/?$", UserSessionView.as_view(), name="me")]
49 changes: 49 additions & 0 deletions fonzie/views/user.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# coding: utf-8
"""
API user views
"""

from __future__ import absolute_import, unicode_literals

from datetime import datetime

import jwt
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.views import APIView

from django.conf import settings


class UserSessionView(APIView):
"""API endpoint to get the authenticated user information."""

permission_classes = [IsAuthenticated]

# pylint: disable=redefined-builtin
def get(self, request, version, format=None):
"""
Retrieve logged in user, then generate a JWT with a claim containing its
username (unique identifier) and its email. The token's expiration is
synchronized with the user session expiration date.
"""
user = request.user
issued_at = datetime.utcnow()
expired_at = request.session.get_expiry_date()
token = jwt.encode(
{
"email": user.email,
"username": user.username,
"exp": expired_at,
"iat": issued_at,
},
getattr(settings, "JWT_PRIVATE_SIGNING_KEY", None),
algorithm="HS256",
)

return Response(
{
"access_token": token,
"username": user.username,
}
)
1 change: 1 addition & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ classifiers =
include_package_data = true
install_requires =
# Add direct dependencies here
djangorestframework-simplejwt
packages = find:
zip_safe = False

Expand Down
61 changes: 61 additions & 0 deletions tests/views/test_user.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# coding: utf-8
"""
Tests for the `fonzie` user module.
"""

# pylint: disable=no-member,import-error
from __future__ import absolute_import, unicode_literals

import jwt
from rest_framework import status
from rest_framework.test import APITestCase

from django.conf import settings
from django.core.urlresolvers import reverse

from student.tests.factories import UserFactory


class UserViewTestCase(APITestCase):
"""Tests for the User API endpoint"""

def setUp(self):
"""
Set common parameters for the test suite.
"""
super(UserViewTestCase, self).setUp()

self.url = reverse("fonzie:user:me", kwargs={"version": "1.0"})

def test_user_me_with_anonymous_user(self):
"""
If user is not authenticated, view should return a 403 status
"""
response = self.client.get(self.url)
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)

def test_user_me_with_logged_in_user(self):
"""
If user is authenticated through Django session, view should return
a JSON object containing the username and a JWT access token
"""
user = UserFactory(username="fonzie", email="arthur_fonzarelli@fun-mooc.fr")
self.client.force_authenticate(user=user)

response = self.client.get(self.url)
token = jwt.decode(
response.data["access_token"],
getattr(
settings,
"JWT_PRIVATE_SIGNING_KEY",
"ThisIsAnExampleKeyForDevPurposeOnly",
),
options={"require": ["exp", "iat", "email", "username"]},
)

self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data["username"], "fonzie")
self.assertEqual(token["username"], "fonzie")
self.assertEqual(token["email"], "arthur_fonzarelli@fun-mooc.fr")
self.assertIsInstance(token["iat"], int)
self.assertIsInstance(token["exp"], int)

0 comments on commit e9d1ec4

Please sign in to comment.