Skip to content

Commit

Permalink
Merge pull request #2 from Pearson-Advance/vue/PADV-262
Browse files Browse the repository at this point in the history
PADV-262: Import required utilities for LTI 1.3 authentication
  • Loading branch information
kuipumu authored Dec 29, 2022
2 parents 016bbcb + a7f25f3 commit 997fa57
Show file tree
Hide file tree
Showing 16 changed files with 267 additions and 39 deletions.
7 changes: 1 addition & 6 deletions openedx_lti_tool_plugin/apps.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,4 @@
"""
App configuration for `openedx_lti_tool_plugin`.
For more information on this file, see:
https://docs.djangoproject.com/en/3.2/ref/applications
"""
"""App configuration for `openedx_lti_tool_plugin`."""
from django.apps import AppConfig


Expand Down
24 changes: 24 additions & 0 deletions openedx_lti_tool_plugin/auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
"""Authentication for `openedx_lti_tool_plugin`."""
from django.contrib.auth.backends import ModelBackend

from .models import LtiProfile # pylint: disable=unused-import


class LtiAuthenticationBackend(ModelBackend):
"""Custom LTI 1.3 Django authentication backend.
Returns a user platform if any LTI profile instance matches
with the requested LTI user identity claims (iss, aud, sub).
Returns None if no user profile is found.
"""

# pylint: disable=arguments-renamed
def authenticate(self, request, iss=None, aud=None, sub=None, **kwargs):
"""Authenticate using LTI launch claims corresponding to a LTIProfile instance.
Args:
request: HTTP request object
iss (str, optional): LTI issuer claim. Defaults to None.
aud (str, optional): LTI audience claim. Defaults to None.
sub (str, optional): LTI subject claim. Defaults to None.
"""
22 changes: 22 additions & 0 deletions openedx_lti_tool_plugin/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
"""Models for `openedx_lti_tool_plugin`."""
from django.contrib.auth import get_user_model
from django.db import models


class LtiProfileManager(models.Manager):
"""LTI 1.3 profile model manager."""


class LtiProfile(models.Model):
"""LTI 1.3 profile for Open edX users.
A unique representation of the LTI subject
that initiated an LTI launch.
"""

objects = LtiProfileManager()
user = models.OneToOneField(get_user_model(), on_delete=models.CASCADE)

def __str__(self):
"""Get a string representation of this model instance."""
return f'<Lti1p3Profile, ID: {self.id}>'
2 changes: 2 additions & 0 deletions openedx_lti_tool_plugin/settings/test.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,5 @@
'NAME': 'db.sqlite3',
},
}

DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
7 changes: 1 addition & 6 deletions openedx_lti_tool_plugin/tests/test_apps.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,4 @@
"""
Tests for the `openedx_lti_tool_plugin` apps module.
For more information on this file, see:
https://docs.python.org/3/library/unittest.html
"""
"""Tests for the `openedx_lti_tool_plugin` apps module."""
from django.test import TestCase
from jsonschema import validate

Expand Down
11 changes: 11 additions & 0 deletions openedx_lti_tool_plugin/tests/test_auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
"""Tests for the `openedx_lti_tool_plugin` auth module."""
from django.test import TestCase

from openedx_lti_tool_plugin.auth import LtiAuthenticationBackend # pylint: disable=unused-import


class TestLtiAuthenticationBackend(TestCase):
"""Test LTI 1.3 profile authentication backend."""

def test_authenticate(self):
"""Test authenticate method."""
12 changes: 12 additions & 0 deletions openedx_lti_tool_plugin/tests/test_models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
"""Tests for the `openedx_lti_tool_plugin` models module."""
from django.test import TestCase

from openedx_lti_tool_plugin.models import LtiProfile, LtiProfileManager # pylint: disable=unused-import


class TestLtiProfileManager(TestCase):
"""Test LTI profile model manager."""


class TestLtiProfile(TestCase):
"""Test LTI 1.3 profile model."""
36 changes: 36 additions & 0 deletions openedx_lti_tool_plugin/tests/test_views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
"""Tests for the `openedx_lti_tool_plugin` views module."""
from django.test import TestCase

from openedx_lti_tool_plugin.views import ( # pylint: disable=unused-import
LtiToolBaseView,
LtiToolLaunchView,
LtiToolLoginView,
)


class TestLtiToolBaseView(TestCase):
"""Test base LTI 1.3 view."""


class TestLtiToolLoginView(TestCase):
"""Test LTI 1.3 third-party login view."""

def test_get(self):
"""Test GET method."""

def test_post(self):
"""Test POST method."""


class TestLtiToolLaunchView(TestCase):
"""Test LTI 1.3 platform tool launch view."""

def test_authenticate_and_login(self):
"""Test LTI 1.3 launch user authentication and authorization."""


class TestLtiToolJwksView(TestCase):
"""Test LTI 1.3 JSON Web Key Sets view."""

def test_get(self):
"""Test GET method."""
14 changes: 8 additions & 6 deletions openedx_lti_tool_plugin/urls.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
"""
URL configuration for `openedx_lti_tool_plugin`.
"""URL configuration for `openedx_lti_tool_plugin`."""
from django.urls import path

For more information on this file, see:
https://docs.djangoproject.com/en/3.2/topics/http/urls/
"""
from openedx_lti_tool_plugin import views

urlpatterns = []
urlpatterns = [
path('1.3/login/', views.LtiToolLoginView.as_view(), name='lti1p3-login'),
path('1.3/launch/', views.LtiToolLaunchView.as_view(), name='lti1p3-launch'),
path('1.3/pub/jwks/', views.LtiToolJwksView.as_view(), name='lti1p3-pub-jwks'),
]
55 changes: 55 additions & 0 deletions openedx_lti_tool_plugin/views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
"""Views for `openedx_lti_tool_plugin`."""
from django.contrib.auth import authenticate # pylint: disable=unused-import
from django.views.generic.base import TemplateResponseMixin, View
from pylti1p3.contrib.django import DjangoCacheDataStorage # pylint: disable=unused-import
from pylti1p3.contrib.django import DjangoDbToolConf # pylint: disable=unused-import
from pylti1p3.contrib.django import DjangoMessageLaunch # pylint: disable=unused-import
from pylti1p3.contrib.django import DjangoOIDCLogin # pylint: disable=unused-import
from pylti1p3.exception import LtiException # pylint: disable=unused-import
from pylti1p3.exception import OIDCException # pylint: disable=unused-import


class LtiToolBaseView(View):
"""Base LTI view initializing common LTI tool attributes."""

def setup(self, request, *args, **kwargs):
"""Initialize attributes shared by all LTI views."""


class LtiToolLoginView(LtiToolBaseView):
"""
LTI 1.3 third-party login view.
The LTI platform will start the OpenID Connect flow by redirecting the User
Agent (UA) to this view. The redirect may be a form POST or a GET. On
success the view should redirect the UA to the LTI platform's authentication
URL.
"""

def get(self, request):
"""Get request."""
return self.post(request)

def post(self, request):
"""Initialize 3rd-party login requests to redirect."""


class LtiToolLaunchView(TemplateResponseMixin, LtiToolBaseView):
"""LTI 1.3 platform tool launch view.
Returns a rendered view of a requested XBlock LTI launch,
unless authentication or authorization fails.
"""

def _authenticate_and_login(self):
"""Authenticate and authorize the user for this LTI message launch."""


class LtiToolJwksView(LtiToolBaseView):
"""LTI 1.3 JSON Web Key Sets view.
Returns the LTI tool public key.
"""

def get(self, request):
"""Return the public JWKS."""
3 changes: 2 additions & 1 deletion requirements/base.in
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@

-c constraints.txt

Django # Web application framework
Django # Web application framework
pylti1p3 # LTI 1.3 Advantage Tool implementation
32 changes: 30 additions & 2 deletions requirements/base.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,41 @@
#
# pip-compile --output-file=requirements/base.txt requirements/base.in
#
asgiref==3.5.2
asgiref==3.6.0
# via django
certifi==2022.12.7
# via requests
cffi==1.15.1
# via cryptography
charset-normalizer==2.1.1
# via requests
cryptography==38.0.4
# via jwcrypto
deprecated==1.2.13
# via jwcrypto
django==3.2.16
# via
# -c requirements/constraints.txt
# -r requirements/base.in
pytz==2022.6
idna==3.4
# via requests
jwcrypto==1.4.2
# via pylti1p3
pycparser==2.21
# via cffi
pyjwt==2.6.0
# via pylti1p3
pylti1p3==1.10.0
# via
# -c requirements/constraints.txt
# -r requirements/base.in
pytz==2022.7
# via django
requests==2.28.1
# via pylti1p3
sqlparse==0.4.3
# via django
urllib3==1.26.13
# via requests
wrapt==1.14.1
# via deprecated
3 changes: 3 additions & 0 deletions requirements/constraints.txt
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,6 @@ tox<4.0.0
# pylint==2.15.0 includes changes that makes pylint-django to break:
# https://github.com/PyCQA/pylint-django/issues/370
pylint<2.15.0

# use edx-platform version pylti1p3==1.10.0.
pylti1p3<1.11.0
2 changes: 1 addition & 1 deletion requirements/pip-tools.txt
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ pip==22.3.1
# via
# -r requirements/pip-tools.in
# pip-tools
pip-tools==6.12.0
pip-tools==6.12.1
# via -r requirements/pip-tools.in
setuptools==59.8.0
# via
Expand Down
1 change: 0 additions & 1 deletion requirements/test.in
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,4 @@ isort # to standardize order of imports

tox # Virtualenv management for tests
pytest-django # pytest extension for better Django support
codecov # Code coverage reporting
jsonschema # JSON Schema data validation
Loading

0 comments on commit 997fa57

Please sign in to comment.