-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #103 from uw-it-aca/feature/additive-1-point-3
Feature/additive 1 point 3
- Loading branch information
Showing
40 changed files
with
1,458 additions
and
803 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,49 +1,101 @@ | ||
# Django BLTI Provider | ||
|
||
[](https://github.com/uw-it-aca/django-blti/actions) | ||
[](https://coveralls.io/github/uw-it-aca/django-blti?branch=main) | ||
[](https://pypi.python.org/pypi/django-blti) | ||
 | ||
|
||
# Documentation | ||
|
||
A Django application on which to build IMS BLTI Tool Providers | ||
|
||
Installation | ||
------------ | ||
|
||
**Project directory** | ||
|
||
Install django-blti in your project. | ||
django-blti is a Django web framework application intended so serve | ||
as a base for [IMS LTI 1.3](https://www.imsglobal.org/spec/lti/v1p3) | ||
Tool projects. It implements common class-based views providing launch | ||
authentication, payload normalization, and role based authorization. | ||
It also includes optional endpoints for tool development based on | ||
mock payloads. We understand and regret that the ``b`` in the package | ||
name is a little misleading, but it is what it is. | ||
|
||
$ cd [project] | ||
## Installation | ||
``` | ||
$ pip install django-blti | ||
|
||
Project settings.py | ||
------------------ | ||
|
||
**INSTALLED_APPS** | ||
|
||
'blti', | ||
|
||
**MIDDLEWARE_CLASSES** | ||
|
||
'django.middleware.common.CommonMiddleware', | ||
'blti.middleware.CSRFHeaderMiddleware', | ||
'blti.middleware.SessionHeaderMiddleware', | ||
'django.middleware.csrf.CsrfViewMiddleware', | ||
'django.contrib.sessions.middleware.SessionMiddleware', | ||
|
||
**Additional settings** | ||
|
||
# BLTI consumer key:secret pairs | ||
LTI_CONSUMERS = { | ||
'<unique_consumer_key>': '<32_or_more_bytes_of_entropy>' | ||
} | ||
|
||
# BLTI session object encryption values | ||
BLTI_AES_KEY = b'<AES128_KEY>' | ||
BLTI_AES_IV = b'<AES128_INIT_VECTOR>' | ||
|
||
Project urls.py | ||
--------------- | ||
``` | ||
## Django Configuration | ||
It should be sufficient to add the app and supporting settings to ``project/settings.py``: | ||
``` | ||
INSTALLED_APPS += ['blti'] | ||
# add session authentication based on lauch authentication | ||
MIDDLEWARE_CLASSES += [ | ||
'blti.middleware.SessionHeaderMiddleware', | ||
'blti.middleware.CSRFHeaderMiddleware', | ||
'blti.middleware.SameSiteMiddleware' | ||
'blti.middleware.LTISessionAuthenticationMiddleware',] | ||
# relax samesite requirements, limit casual snooping | ||
SESSION_COOKIE_SAMESITE = 'None' | ||
SESSION_COOKIE_SECURE = True | ||
CSRF_COOKIE_SECURE = True | ||
# only necessary when running behind ingress proxy | ||
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_SCHEME', 'https') | ||
``` | ||
and expose the necessary authentication endpoints in ``project/urls.py``: | ||
``` | ||
url(r'^blti/', include('blti.urls')), | ||
``` | ||
## Class Based View | ||
A tool is implemented by subclassing ``blti.views.BLTILaunchView``. | ||
|
||
After successful launch authentication, the instance variable | ||
``self.blti`` will hold the normalized payload data provided by | ||
the class's ``self.launch_data_model`` method. The default method | ||
provides a normalized data model for the Canvas LTI Platform. | ||
|
||
Access control is applied based on the view's class variable: | ||
``` | ||
authorized_role = 'member' | ||
``` | ||
In addition to LTI-defined roles the following rollup roles are | ||
also supported: | ||
* public - no access restrictions | ||
* member - viewable by staff, instructors, students, and observers | ||
* admin - viewable by staff, instructors, and content developers | ||
## LTI Tool Configuration | ||
Deployed tool configuration is defined in the JSON file named | ||
``tool.json`` in the location defined by the environment variable: | ||
``` | ||
LTI_CONFIG_DIRECTORY = /etc/lti-config | ||
``` | ||
The configuration file content is documented in the | ||
[pylti1p3 README](https://github.com/dmitry-viskov/pylti1.3?tab=readme-ov-file#configuration). | ||
|
||
In addition, a management command is available to simplify key | ||
pair generation during configuration. | ||
``` | ||
# python manage.py generate_credentials private.key public.key jwt.json | ||
``` | ||
## Tool Development | ||
This app also provides an optional development environment activated by | ||
defining the environment variable: | ||
``` | ||
LTI_DEVELOP_APP=my_app | ||
``` | ||
and launch url named: | ||
``` | ||
urlpatterns = [ | ||
re_path(r'^$', MyToolLaunchView.as_view(), name="lti-launch"), | ||
] | ||
``` | ||
And finally, to initiate the launch sequence, point your browser at ``/blti/dev`` | ||
|
||
A mocked JWT payload for Canvas is provided, but can be overridden by | ||
creating the file ``resources/lti1p3/file/jwt.json`` in your tool's | ||
app directory. django-blti will walk the list of ``INSTALLED_APPS``, | ||
and use the first file by that name discovered. | ||
### Project Examples | ||
Visit [uw-id-aca/info-hub-lti](https://github.com/uw-it-aca/info-hub-lti) or | ||
[uw-id-aca/library-guides-lti](https://github.com/uw-it-aca/library-guides-lti) or | ||
to see LTI Tool examples based on launch views and mocked local development | ||
environment. | ||
## Legacy Support | ||
LTI 1.1 launch authentication, authorization, and payload normalization is | ||
also supported for the time being, but is no longer documented here. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
# Copyright 2024 UW-IT, University of Washington | ||
# SPDX-License-Identifier: Apache-2.0 | ||
|
||
|
||
import os | ||
from django.conf import settings | ||
from importlib import resources | ||
from pylti1p3.tool_config import ToolConfJsonFile | ||
from pylti1p3.contrib.django import DjangoCacheDataStorage | ||
|
||
|
||
LTI1P3_CONFIG_DIRECTORY_NAME = 'lti_config' | ||
LTI1P3_CONFIG_FILE_NAME = 'tool.json' | ||
|
||
|
||
def get_tool_conf(): | ||
return ToolConfJsonFile(get_lti_config_path()) | ||
|
||
|
||
def get_launch_data_storage(): | ||
return DjangoCacheDataStorage() | ||
|
||
|
||
def get_lti_config_directory(): | ||
return os.environ.get( | ||
'LTI_CONFIG_DIRECTORY', | ||
os.path.join(settings.BASE_DIR, LTI1P3_CONFIG_DIRECTORY_NAME)) | ||
|
||
|
||
def get_lti_config_path(): | ||
return os.path.join(get_lti_config_directory(), LTI1P3_CONFIG_FILE_NAME) | ||
|
||
|
||
def get_lti_public_key_path(key_name): | ||
return os.path.join(get_lti_config_directory(), key_name) |
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
# Copyright 2024 UW-IT, University of Washington | ||
# SPDX-License-Identifier: Apache-2.0 | ||
|
||
|
||
class BLTIException(Exception): | ||
pass |
Empty file.
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
# Copyright 2024 UW-IT, University of Washington | ||
# SPDX-License-Identifier: Apache-2.0 | ||
|
||
from django.core.management.base import BaseCommand, CommandError | ||
from jwcrypto.jwk import JWK | ||
from Crypto.PublicKey import RSA | ||
import json | ||
|
||
|
||
KEY_LENGTH = 4096 | ||
|
||
|
||
class Command(BaseCommand): | ||
help = 'Generate RSA keys and JWK for JWT signing' | ||
|
||
def add_arguments(self, parser): | ||
parser.add_argument('private_key_file', type=str, nargs=1) | ||
parser.add_argument('public_key_file', type=str, nargs=1) | ||
parser.add_argument('jwt_file', type=str, nargs=1) | ||
|
||
def create_keys(self): | ||
private_key = RSA.generate(KEY_LENGTH) | ||
return (private_key.exportKey(format='PEM'), | ||
private_key.publickey().exportKey(format='PEM')) | ||
|
||
def create_jwk(self, public_key): | ||
jwk_obj = JWK.from_pem(public_key) | ||
public_jwk = json.loads(jwk_obj.export_public()) | ||
public_jwk['alg'] = 'RS256' | ||
public_jwk['use'] = 'sig' | ||
return json.dumps(public_jwk) | ||
|
||
def handle(self, *args, **options): | ||
private_key, public_key = self.create_keys() | ||
public_jwk = self.create_jwk(public_key) | ||
|
||
with open(options['private_key_file'][0], 'w') as f: | ||
f.write(private_key.decode('utf-8')) | ||
|
||
with open(options['public_key_file'][0], 'w') as f: | ||
f.write(public_key.decode('utf-8')) | ||
|
||
with open(options['jwt_file'][0], 'w') as f: | ||
f.write(public_jwk) |
Oops, something went wrong.