A minimal working example of an LTI 1.3 tool built with Django, using pylti1p3next. It connects to Moodle and renders a simple "Hello World" page inside an iframe — the shortest path from zero to a working LTI integration.
- Prerequisites
- Project Setup
- RSA Key Generation
- Django Configuration
- LTI Views
- Template
- Moodle Configuration
- Running the Server
- How It Works
- Appendix — Standard Moodle URLs
- Python 3.10+
- pip
- openssl
- A running and accessible Moodle instance
mkdir lti-hello && cd lti-hello
python -m venv venv
source venv/bin/activate # Linux/macOS
# venv\Scripts\activate # Windows
pip install django
pip install git+https://github.com/pymsglobal/pylti1p3next.git
django-admin startproject config .
python manage.py startapp lti
mkdir -p lti/templates/ltiExpected project structure:
lti-hello/
├── config/
│ ├── settings.py
│ └── urls.py
├── lti/
│ ├── views.py
│ ├── urls.py
│ └── templates/lti/hello.html
├── private.key
├── public.key
└── manage.py
LTI 1.3 uses RSA key pairs to sign and verify JWT tokens exchanged with Moodle:
openssl genrsa -out private.key 4096
openssl rsa -in private.key -pubout -out public.keySecurity note: Never commit
private.keyto version control. Add it to.gitignore.
Add 'lti' to INSTALLED_APPS and set the following options:
INSTALLED_APPS = [
...
'lti',
]
# Required for rendering inside a Moodle iframe
X_FRAME_OPTIONS = 'ALLOWALL'
PYLTI1P3_TOOL_CONF = {
"private_key_file": BASE_DIR / "private.key",
"public_key_file": BASE_DIR / "public.key",
}from django.urls import path, include
urlpatterns = [
path('lti/', include('lti.urls')),
]from django.urls import path
from . import views
urlpatterns = [
path('login/', views.login, name='lti-login'),
path('launch/', views.launch, name='lti-launch'),
path('jwks/', views.jwks, name='lti-jwks'),
]Create lti/views.py. Replace CLIENT_ID_MOODLE with the Client ID provided by Moodle after registering the tool (see Moodle Configuration).
from django.http import JsonResponse
from django.views.decorators.csrf import csrf_exempt
from django.shortcuts import render
from pylti1p3.contrib.django import DjangoOIDCLogin, DjangoMessageLaunch
from pylti1p3.tool_config import ToolConfDict
from django.conf import settings
def get_tool_conf():
return ToolConfDict({
"http://localhost": [{
"default": True,
"client_id": "CLIENT_ID_MOODLE",
"auth_login_url": "http://localhost/mod/lti/auth.php",
"auth_token_url": "http://localhost/mod/lti/token.php",
"key_set_url": "http://localhost/mod/lti/certs.php",
"key_set": None,
"private_key_file": str(settings.BASE_DIR / "private.key"),
"public_key_file": str(settings.BASE_DIR / "public.key"),
"deployment_ids": ["1"]
}]
})
@csrf_exempt
def login(request):
tool_conf = get_tool_conf()
oidc_login = DjangoOIDCLogin(request, tool_conf)
return oidc_login.redirect("http://localhost:8000/lti/launch/")
@csrf_exempt
def launch(request):
tool_conf = get_tool_conf()
message_launch = DjangoMessageLaunch(request, tool_conf)
message_launch.validate()
return render(request, 'lti/hello.html')
def jwks(request):
tool_conf = get_tool_conf()
return JsonResponse(tool_conf.get_jwks())Create lti/templates/lti/hello.html:
<!DOCTYPE html>
<html>
<head><title>LTI Hello World</title></head>
<body>
<h1>Hello from Django LTI!</h1>
<p>The connection with Moodle is working correctly.</p>
</body>
</html>In Moodle, go to: Site administration → Plugins → Activity modules → Manage tools → Configure a tool manually
| Field | Value |
|---|---|
| Tool name | Django Hello World |
| Tool URL | http://localhost:8000/lti/launch/ |
| LTI version | LTI 1.3 |
| Initiate login URI | http://localhost:8000/lti/login/ |
| Redirection URI(s) | http://localhost:8000/lti/launch/ |
| Public keyset URL | http://localhost:8000/lti/jwks/ |
After saving, Moodle will display a Client ID. Copy it into get_tool_conf() in views.py to replace CLIENT_ID_MOODLE.
python manage.py migrate
python manage.py runserverThen add the tool as an activity in a Moodle course.
The LTI 1.3 launch follows the OpenID Connect third-party login flow:
Moodle Django
│ │
│── OIDC login request ────────>│ /lti/login/
│ │ (DjangoOIDCLogin)
│<── redirect to Moodle ────────│
│ │
│── ID token (POST) ──────────>│ /lti/launch/
│ │ (DjangoMessageLaunch.validate())
│<── Hello World page ──────────│
- Moodle sends an OIDC login request to
/lti/login/ - Django redirects back to Moodle for authentication
- Moodle POSTs a signed JWT to
/lti/launch/ - Django validates the token and renders the Hello World page inside the iframe
Replace <moodle> with your Moodle or your LMS instance hostname or IP.
| Parameter | URL |
|---|---|
auth_login_url |
http://<moodle>/mod/lti/auth.php |
auth_token_url |
http://<moodle>/mod/lti/token.php |
key_set_url |
http://<moodle>/mod/lti/certs.php |