Giter Club home page Giter Club logo

django-lti-provider's Introduction

Actions Status

Documentation

django-lti-provider provides Learning Tools Interoperability (LTI) functionality for the Django web framework. This work began as a port of MIT's LTI Flask Sample, which demonstrates a sample LTI provider for the Flask Framework based on the Python LTI library, PyLTI.

Additional work was completed to provide fuller functionality and support the idiosyncrasies of various LMS systems such as Canvas, Blackboard, Moodle and EdEx.

LTI 1.3 support is currently in progress, using the PyLTI1p3 library.

django-lti-provider offers:

  • an authentication backend to complete an oAuth handshake (optional)
  • a templated view for config.xml generation
  • a templated landing page view for those LMS who do not have a 'launch in new tab' option, i.e. Canvas
  • support for Canvas' embedded tool extensions
  • routing for multiple external assignment end points.

The library is used at Columbia University's Center for Teaching And Learning.

See an example Django app using the library at Django LTI Provider Example.

Installation

You can install django-lti-provider through pip:

$ pip install django-lti-provider

Or, if you're using virtualenv, add django-lti-provider to your requirements.txt.

Add to INSTALLED_APPS in your settings.py::

  'lti_provider',

Dependencies

  • Django
  • nameparser
  • httplib2
  • oauth2
  • oauthlib
  • pylti

Configuration

Basic setup steps

Add the URL route:

path('lti/', include('lti_provider.urls'))

Add the LTIBackend to your AUTHENTICATION_BACKENDS:

AUTHENTICATION_BACKENDS = [
  'django.contrib.auth.backends.ModelBackend',
  'lti_provider.auth.LTIBackend',
]

Complete a migration

./manage.py migrate

Primary LTI config

The LTI_TOOL_CONFIGURATION variable in your settings.py allows you to configure your application's config.xml and set other options for the library. (Edu Apps has good documentation on configuring an lti provider through xml.)

LTI_TOOL_CONFIGURATION = {
    'title': '<your lti provider title>',
    'description': '<your description>',
    'launch_url': 'lti/',
    'embed_url': '<the view endpoint for an embed tool>' or '',
    'embed_icon_url': '<the icon url to use for an embed tool>' or '',
    'embed_tool_id': '<the embed tool id>' or '',
    'landing_url': '<the view landing page>',
    'course_aware': <True or False>,
    'course_navigation': <True or False>,
    'new_tab': <True or False>,
    'frame_width': <width in pixels>,
    'frame_height': <height in pixels>,
    'custom_fields': <dictionary>,
    'allow_ta_access': <True or False>,
    'assignments': {
        '<name>': '<landing_url>',
        '<name>': '<landing_url>',
        '<name>': '<landing_url>',
    },
}

To stash custom properties in your session, populate the LTI_PROPERTY_LIST_EX variable in your settings.py. This is useful for LMS specific custom_x parameters that will be needed later. The default value for LTI_PROPERTY_LIST_EX is: ['custom_canvas_user_login_id', 'context_title', 'lis_course_offering_sourcedid', 'custom_canvas_api_domain'].

LTI_PROPERTY_LIST_EX = ['custom_parameter1', 'custom_parameter2']

Using a cookie based session

For simplest scenarios you can store data for the LTI request in a session cookie. This is the quickest way to get up and running, and due to Django's tamper proof cookie session (assuming a secure secret key) it is a safe option. Please note that you will need to add the following settings in your applications settings.py to make use of cookies:

SESSION_ENGINE = "django.contrib.sessions.backends.signed_cookies"
SESSION_COOKIE_SAMESITE = None
SESSION_COOKIE_SECURE = True

Because Canvas sends the information that we are storing in a POST request on the LTI launch, we need to relax the restriction of cookies only being allowed to be set from the same site. For more information on SESSION_COOKIE_SAMESITE read here.

For more information on why SESSION_COOKIE_SAMESITE and SESSION_COOKIE_SECURE are needed, if you are choosing to make use of cookies, please read here.

Extra LTI Configuration values

To specify a custom username property, add the LTI_PROPERTY_USER_USERNAME variable to your settings.py. By default, LTI_PROPERTY_USER_USERNAME is custom_canvas_user_login_id. This value can vary depending on your LMS.

To pass through extra LTI parameters to your provider, populate the LTI_EXTRA_PARAMETERS variable in your settings.py. This is useful for custom parameters you may specify at installation time.

LTI_EXTRA_PARAMETERS = ['lti_version']  # example

The PYLTI_CONFIG variable in your settings.py configures the application consumers and secrets.

PYLTI_CONFIG = {
    'consumers': {
        '<random number string>': {
            'secret': '<random number string>'
        }
    }
}

Canvas and LTI iframes

Since LTI tools live within an iframe on Canvas, you might need adjust your X_FRAME_OPTIONS setting to allow for the LTI tool to be opened within the iframe. To the best of our knowledge you probably don't have to adjust this setting, as Canvas has built a workaround. For more info read here

This ensures that the Django application will allow requests from your orgs Canvas instance. For more on X_FRAME_OPTIONS please consult here.

If you are using a load balancer

If you happen to have a deployment scenario where you have load balancer listening on https and routing traffic to nodes that are listening to HTTP, you will need to add the following line of configuration in settings.py:

SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')

This ensures the correct launch_url is generated for the LTI tool. For more on this setting, read here.

Assignments

To support multiple assignments:

  • Create multiple endpoint views
  • Add the assignment urls to the LTI_TOOL_CONFIGURATION['assignments'] map
  • Add an assignment, using the External Tool option.
  • Update the URL to be https://<your domain name>/lti/assignment/<assignment_name>
  • The assignment_name variable should match a landing_url in the LTI_TOOL_CONFIGURATION dict.
  • Full example here: Django LTI Provider Example.

OR

  • Create a single named endpoint that accepts an id
  • On Post, django-lti-provider will attempt to reverse the assignment_name/id and then redirect to that view.

django-lti-provider's People

Contributors

arthurian avatar cclauss avatar dependabot-preview[bot] avatar dependabot[bot] avatar dopename avatar evan-ctl avatar kyle-tc avatar nbuonin avatar ndittren avatar nicholasgotchtufts avatar nikolas avatar pyup-bot avatar sdreher avatar thedpws avatar thraxil avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar

django-lti-provider's Issues

deprecation of x-frame-options allow-from directive

https://github.com/ccnmtl/django-lti-provider/blame/master/README.md#L129

https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options

When I put this addition together a few months back I didn't realize it was going to be deprecated. I'm currently unsure of what the fix for LTI tools is. But the LTI I've been working on loads correctly even with the deprecation error. I think this snippet can just be removed from the README.md.

Have you all seen any issues like this?

Custom User

It looks like there isn't any support for custom user models (i.e. imports into auth.py go directly to django.contrib.auth.models.User). Is there anyway it could support using custom user models through get_user_model() (django.contrib.auth.get_user_model).

I don't know if there are any negative implications of doing so, but thought I'd bring it up!

Please let me know your thoughts, thank you.

There are some errors in the Readme for the part of "Using a cookie based session".

There is an error in the Readme related to the Django setting SESSION_COOKIE_SAMESITE. According to the Django documentation, SESSION_COOKIE_SAMESITE should be a string.

The correct setting should be:

SESSION_ENGINE = "django.contrib.sessions.backends.signed_cookies"
SESSION_COOKIE_SAMESITE = 'None'
SESSION_COOKIE_SECURE = True

get username not encrypt

Hi Susan,
I linked my django project with Moodle and I want to get the username of the user but not encrypt, is-it possible ?
Regards
Nicolas

DB Errors

Hello,
I'm trying to add django lti provider to my project.
When i launched the migrate operation, i had this error :
Running migrations:
Rendering model states... DONE
Applying lti_provider.0001_initial... OK
Applying lti_provider.0002_auto_20151231_1107... OK
Applying lti_provider.0003_auto_20151231_1109... OK
Applying lti_provider.0004_lticoursecontext_enable... OK
Applying lti_provider.0005_auto_20171009_1234...Traceback (most recent call last):
File "manage.py", line 10, in
execute_from_command_line(sys.argv)
File "/home/nico/.virtualenvs/django_pod/local/lib/python2.7/site-packages/django/core/management/init.py", line 354, in execute_from_command_line
utility.execute()
File "/home/nico/.virtualenvs/django_pod/local/lib/python2.7/site-packages/django/core/management/init.py", line 346, in execute
self.fetch_command(subcommand).run_from_argv(self.argv)
File "/home/nico/.virtualenvs/django_pod/local/lib/python2.7/site-packages/django/core/management/base.py", line 394, in run_from_argv
self.execute(*args, **cmd_options)
File "/home/nico/.virtualenvs/django_pod/local/lib/python2.7/site-packages/django/core/management/base.py", line 445, in execute
output = self.handle(*args, **options)
File "/home/nico/.virtualenvs/django_pod/local/lib/python2.7/site-packages/django/core/management/commands/migrate.py", line 222, in handle
executor.migrate(targets, plan, fake=fake, fake_initial=fake_initial)
File "/home/nico/.virtualenvs/django_pod/local/lib/python2.7/site-packages/django/db/migrations/executor.py", line 110, in migrate
self.apply_migration(states[migration], migration, fake=fake, fake_initial=fake_initial)
File "/home/nico/.virtualenvs/django_pod/local/lib/python2.7/site-packages/django/db/migrations/executor.py", line 148, in apply_migration
state = migration.apply(state, schema_editor)
File "/home/nico/.virtualenvs/django_pod/local/lib/python2.7/site-packages/django/db/migrations/migration.py", line 115, in apply
operation.database_forwards(self.app_label, schema_editor, old_state, project_state)
File "/home/nico/.virtualenvs/django_pod/local/lib/python2.7/site-packages/django/db/migrations/operations/fields.py", line 62, in database_forwards
field,
File "/home/nico/.virtualenvs/django_pod/local/lib/python2.7/site-packages/django/db/backends/mysql/schema.py", line 43, in add_field
super(DatabaseSchemaEditor, self).add_field(model, field)
File "/home/nico/.virtualenvs/django_pod/local/lib/python2.7/site-packages/django/db/backends/base/schema.py", line 398, in add_field
self.execute(sql, params)
File "/home/nico/.virtualenvs/django_pod/local/lib/python2.7/site-packages/django/db/backends/base/schema.py", line 111, in execute
cursor.execute(sql, params)
File "/home/nico/.virtualenvs/django_pod/local/lib/python2.7/site-packages/django/db/backends/utils.py", line 79, in execute
return super(CursorDebugWrapper, self).execute(sql, params)
File "/home/nico/.virtualenvs/django_pod/local/lib/python2.7/site-packages/django/db/backends/utils.py", line 64, in execute
return self.cursor.execute(sql, params)
File "/home/nico/.virtualenvs/django_pod/local/lib/python2.7/site-packages/django/db/utils.py", line 98, in exit
six.reraise(dj_exc_type, dj_exc_value, traceback)
File "/home/nico/.virtualenvs/django_pod/local/lib/python2.7/site-packages/django/db/backends/utils.py", line 64, in execute
return self.cursor.execute(sql, params)
File "/home/nico/.virtualenvs/django_pod/local/lib/python2.7/site-packages/django/db/backends/mysql/base.py", line 124, in execute
return self.cursor.execute(query, args)
File "/home/nico/.virtualenvs/django_pod/local/lib/python2.7/site-packages/MySQLdb/cursors.py", line 205, in execute
self.errorhandler(self, exc, value)
File "/home/nico/.virtualenvs/django_pod/local/lib/python2.7/site-packages/MySQLdb/connections.py", line 36, in defaulterrorhandler
raise errorclass, errorvalue
django.db.utils.OperationalError: (1170, "BLOB/TEXT column 'lms_course_context' used in key specification without a key length")

Can you help me ?
Regards

deprecation of 3rd party cookies most web browsers pretty soon

https://github.com/ccnmtl/django-lti-provider/blame/master/README.md#L96-L104

It looks like there will be inconvenient from a user perspective to use signed cookies to store information about the Canvas POST request in the Django session. 3rd party cookies seemed to be be blocked by default in Chrome now. And they will be phased out entirely in a couple years:

https://www.theverge.com/2020/1/14/21064698/google-third-party-cookies-chrome-two-years-privacy-safari-firefox

Server-side sessions should probably be recommended in the README instead, perhaps with an example of using sqlite. Maybe that already exists in the examples? Just wanted to flag it.

Software license?

Normally Django plugin (python packages in general) are licensed MIT/BSD/ISC/etc.

This GPL license makes it tricky to incorporate (in fact in my case I'd end up writing something totally new)

https://github.com/ccnmtl/django-lti-provider/blob/master/LICENSE

django-lti-provider depends on several other free open-source software as
dependencies, which come with other licenses (compatible with the GPL)
specified in [ test_reqs.txt ] including Django
(http://www.djangoproject.com/) which is BSD-licensed.

It is true the BSD license django uses is compatible with GPL. So if
a GPL project were to want to incorporate this, it would probably be
okay. The issue is two things in my case:

  1. As a django application, it's meant to be incorporated in a custom django project, which in
    many cases (I guess not CTL's? but for django developers in general)
    will be closed source (and probably permissively licensed if it was
    an open source project based on django-lti-provider).
  2. BSD license is compatible with GPL, but not in the other direction.
    So if this package was licensed BSD, for instance, a GPL project
    is welcome to use it. But in the case of my project, it's not worth
    introducing it to the codebase (of a proprietary project) or downstream
    to an open source project.

(I think?) this may be true in general for other prospective users of this package.

403 error while opening multiple tabs

I am getting this 403 error when i try to open the lti in multiple tabs at the same time. When i checked the provider logs this is what i got from the logs.

The request's session was deleted before the request completed. The user may have logged out in a concurrent request, for example.

Did anyone face the same issue? can anybody help me on this?

request scheme set incorrectly for launch_url in a load balancer scenario

Hi folks ๐Ÿ‘‹

I just wanted to open a small tracking issue for something I came across today. While working on an LTI that is being deployed behind an AWS ALB, I found that in the rendered /lti/config.xml, I was getting http://mydomain.net/lti/ instead of what I expected https://mydomain.net/lti/".

I mention the ALB because it's relevant to why this was happening. I wanted to use SSL termination at the load balancer. And then have the traffic from the load balancer to the application as http traffic.

Taking a look at the code, I realized that the scheme is getting set by request.scheme.

launch_url = '%s://%s/%s' % (
self.request.scheme, domain,
settings.LTI_TOOL_CONFIGURATION.get('launch_url'))

I was puzzled by how I could get around this. I think all that's is required in this scenario is to make use of the following:

https://docs.djangoproject.com/en/3.1/ref/settings/#secure-proxy-ssl-header

Initially I was considering implement a PR that would allow for you to override the url scheme, by adding a new config item in the LTI_TOOL_CONFIGURATION called use_https_for_launch (or something). But now I would like just propose that we could add some documentation into the README (If the secure-proxy-ssl-header approach does indeed work).

Retrieve all LTI post parameters

I've noticed that not all the post parameters (custom_canvas_*) are coming through in the LTI object for later views to access.

On line 74 in lti_provider/views.py I added a loop for go through the POST items and add it to the SESSION. This gives me access to all the LTI parameters later on.

def post(self, request, assignment_name=None):
        for key, value in request.POST.items():
            request.session[key] = value
        ...
        if request.POST.get('ext_content_intended_use', '') == 'embed':

Am I going about this the wrong way and I am just not accessing the custom lti parameters the correct way?

Handle "Score is not between 0 and 1" canvas error, in PostGrade view

The response can look like this:

'<?xml version="1.0" encoding="UTF-8"?>\n<imsx_POXEnvelopeResponse xmlns="http://www.imsglobal.org/services/ltiv1p1/xsd/imsoms_v1p0">\n        <imsx_POXHeader>\n          <imsx_POXResponseHeaderInfo>\n            <imsx_version>V1.0</imsx_version>\n            <imsx_messageIdentifier/>\n            <imsx_statusInfo>\n              <imsx_codeMajor>failure</imsx_codeMajor>\n              <imsx_severity>status</imsx_severity>\n              <imsx_description>Score is not between 0 and 1\n[EID_13960006980949683]</imsx_description>\n              <imsx_messageRefIdentifier>1512060794</imsx_messageRefIdentifier>\n              <imsx_operationRefIdentifier>replaceResult</imsx_operationRefIdentifier>\n            </imsx_statusInfo>\n          </imsx_POXResponseHeaderInfo>\n        </imsx_POXHeader>\n        <imsx_POXBody><replaceResultResponse/></imsx_POXBody>\n      </imsx_POXEnvelopeResponse>\n'

and our library just returns "Post grade failed". Give more detail here

Authentication when contained in iframe

Thanks for making this excellent library available!

I'm working on a test LTI producer (Django 2.2, Python3) that leverages this library for LTI connectivity to an Open edX-based consumer course I'm composing on https://studio.edge.edx.org/.

If I access the producer via the "new window" option in Studio, it works fine and I can see the default landing page and a launch button. However, if I use the "inline" option, which uses an iframe to load the producer, I get the standard django-lti-provider "Authentication Failed" page.

I've set X_FRAME_OPTIONS to 'ALLOW-FROM https://studio.edge.edx.org/', but that didn't seem to help. Any suggestions on other Django configurations that may be blocking iframe access but allowing direct access?

Example of passing grade back

Are there any examples of best practice for passing back a grade to the consumer?

Is there a standard approach to handling grading logic in our own app-specific views and then redirecting the result with score post variable to /grade?

The approach outlined in django-lti-provider-example looks something like this

  <form action="{% url 'lti-post-grade' %}" method="post">
        {% csrf_token %}
        <input type="input" name="score" value=".55"/>
        <input type="hidden" name="next" value="{% url 'main:assignment-success' %}"/>
        <input type="submit" value="submit"/>
      </form>

but that approach wouldn't allow for app-specific logic before determining a grade.

(When I do try this basic approach, the oauth2 library complains that "Base URL for request is not set." and causes a 500 error. I've traced that down to the fact that a normalized_url isn't set in Oauth when the request to the consumer goes out...I'm wondering if that means I haven't configured my producer right? I have set my LTI_TOOL_CONFIGURATION per the example...)

Any thoughts are much appreciated!

Supported LTI version

Hello. What version of the LTI standard is supported in this library, 1.1 and/or 1.3? thank you.

Initial Update

Hi ๐Ÿ‘Š

This is my first visit to this fine repo, but it seems you have been working hard to keep all dependencies updated so far.

Once you have closed this issue, I'll create separate pull requests for every update as soon as I find one.

That's it for now!

Happy merging! ๐Ÿค–

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.