FastAPI-based LTI 1.3 provider for Canvas LMS integration.
Getting started? Follow the step-by-step Setup Guide to go from zero to a working Canvas launch.
LTI Provider is a FastAPI implementation of the Learning Tools Interoperability (LTI) 1.3 standard, designed to facilitate seamless integration of external learning tools within Learning Management Systems (LMS) like Canvas. This application supports OIDC (OpenID Connect) for secure authentication and JWT (JSON Web Tokens) for message signing, ensuring a robust and secure connection between the LMS and external tools.
Key Features:
- LTI 1.3 Compliance: Secure tool launches and user authentication following the LTI 1.3 standard.
- Multi-tenant platform registry: Platforms are resolved per launch by
(issuer, client_id)from a database registry (LTIPlatform+LTIDeployment), so a single deployment can serve many LMS instances. A default platform can be seeded from environment variables for single-tenant setups. - Secure OIDC flow: One-time-use
state/noncestored server-side with a TTL and consumed at launch, preventing CSRF andid_tokenreplay. - Robust JWT validation: Launch
id_tokensignatures are verified against the platform's JWKS (kid-aware, cached), together withiss/aud/exp/iat/nonce/azp, LTI version, message type and registereddeployment_id. - Persistent signing key: A stable RSA keypair (with an RFC 7638
kid) is loaded or generated once, so the published JWKS keeps verifying the tokens the tool signs. - LTI Advantage: Names and Role Provisioning (NRPS), Assignment and Grade Services (AGS), and Deep Linking, backed by a client_credentials access-token service.
- Local Development: Easy setup for local testing with ngrok support.
-
OIDC Login Initiation
- Canvas initiates login by sending a POST request to
/lti/login - Contains platform info, client_id, and login hints
- Provider validates and redirects to Canvas auth endpoint
- Canvas initiates login by sending a POST request to
-
Authentication & Launch
- Canvas authenticates and sends signed JWT (id_token) to
/lti/launch - Provider validates JWT signature using platform's public JWK
- Claims are verified (issuer, audience, expiry, etc.)
- User is redirected to the tool's interface
- Canvas authenticates and sends signed JWT (id_token) to
-
Security
- Uses RSA key pairs for message signing
- JWT validation ensures message authenticity
- OIDC (OpenID Connect) handles identity verification
- Session management via secure cookies
The Developer Key JSON configuration establishes the security and communication parameters between Canvas and your tool:
{
"title": "Your Tool Name",
"description": "Your tool description",
"target_link_uri": "http://your-domain/lti/launch",
"oidc_initiation_url": "http://your-domain/lti/login",
"public_jwk_url": "http://your-domain/.well-known/jwks.json",
"scopes": [
"https://purl.imsglobal.org/spec/lti-ags/scope/lineitem",
"https://purl.imsglobal.org/spec/lti-ags/scope/result.readonly",
"https://purl.imsglobal.org/spec/lti-nrps/scope/contextmembership.readonly"
]
}-
Authentication Endpoints
target_link_uri: Entry point for tool launchesoidc_initiation_url: Handles OIDC login flow initiationpublic_jwk_url: Provides RSA public keys for JWT verification
-
Security Flow
- Canvas initiates OIDC login via
oidc_initiation_url - Tool validates request and redirects to Canvas
- Canvas sends authenticated launch to
target_link_uri - Tool verifies JWT using keys from
public_jwk_url
- Canvas initiates OIDC login via
-
Scopes
lineitem: Create/manage gradebook columnsresult.readonly: Read student resultscontextmembership.readonly: Access course roster
-
JWT Claims The launch JWT includes:
iss: Canvas instance URLaud: Your Client IDsub: User identifierhttps://purl.imsglobal.org/spec/lti/claim/...: LTI-specific data
-
Security Considerations
- All URLs must use HTTPS in production
- JWKs must be regularly rotated
- Nonce validation prevents replay attacks
- State parameter ensures request continuity
GET /lti/config- Returns tool configuration for Canvas integration
- Defines scopes, URLs, and placement settings
- Used during tool installation in Canvas
-
GET/POST /lti/login- Handles OIDC third-party login initiation (platforms may use either method)
- Resolves the platform, mints a one-time
state/nonce, and redirects to the platform's authorization endpoint
-
POST /lti/launch- Main entry point for resource-link launches and Deep Linking requests
- Consumes the
state, verifies theid_tokensignature and claims, persists the launch - Resource link → redirects to the tool UI with a session token; Deep Linking → returns a signed
LtiDeepLinkingResponse
These are called from the tool UI using the launch session token (?lti=<token> or Authorization: Bearer):
GET /lti/names-and-roles- NRPS: returns the course roster for the launch's context
GET /lti/line-items- AGS: lists the gradebook line items available to the launch's context
GET /.well-known/jwks.json- Serves the tool's stable public JWK for token verification
- Used by the platform to verify tool-signed messages (Deep Linking responses, service assertions)
- Python 3.13+
- uv package manager
- PostgreSQL
Or skip all of the above and use Docker.
The fastest way to run the provider with a database. docker compose starts
PostgreSQL and the app, waits for the database to be healthy, and the app creates
its tables automatically on startup — no manual database setup.
docker compose up --buildThen:
- App: http://localhost:8000 — e.g.
curl http://localhost:8000/.well-known/jwks.json - Config JSON for the Developer Key:
curl http://localhost:8000/lti/config - Expose it for an LMS to reach:
ngrok http 8000
Notes:
DATABASE_URLis wired to the bundleddbservice automatically.- The tool's signing key is generated once and persisted in the
lti_keysvolume. - To register the LMS you connect to, set the
LTI_*variables (e.g. in a.envfile next todocker-compose.yml) — they seed a platform on startup. See .env.example. - Reset everything (including the database) with
docker compose down -v.
-
Clone and install
git clone https://github.com/alejlatorre/lti-provider.git cd lti-provider uv sync -
Environment Configuration
cp .env.example .env # Edit .env with your settings: # - Database URL # - LTI credentials # - Tool URLs
-
Database Setup
# The application will automatically create tables on startup -
Run Development Server
uv run uvicorn app.main:app --reload
-
Expose locally (for Canvas testing)
ngrok http 8000
-
Canvas Configuration
- Add Developer Key in Canvas
- Configure with your tool's URLs (use
GET /lti/configto generate the JSON) - Install in desired course
- Teacher adds tool to Canvas course
- Student clicks tool link
- Canvas sends login request to tool
- Tool validates and redirects to Canvas
- Canvas authenticates and launches tool
- Tool verifies launch and shows interface
lti-provider/
├── app/
│ ├── main.py
│ ├── config.py
│ ├── constants.py # LTI claim URIs, message types, roles, scopes
│ ├── models/
│ │ ├── database.py
│ │ └── lti.py # LTIPlatform, LTIDeployment, LTILoginState, LTILaunch
│ ├── routes/
│ │ ├── home.py
│ │ └── lti.py
│ └── services/
│ ├── keys.py # persistent RSA key + JWKS
│ ├── jwt_service.py # verify platform tokens / sign tool tokens
│ ├── session.py # tool session token
│ ├── lti.py # OIDC login + launch validation + claim parsing
│ ├── access_token.py # client_credentials grant (LTI Advantage)
│ ├── nrps.py # Names and Role Provisioning client
│ ├── ags.py # Assignment and Grade Services client
│ └── deep_linking.py # Deep Linking response signing
├── docs/
│ ├── CONTRIBUTING.md
│ └── SECURITY.md
├── tests/
│ ├── conftest.py
│ ├── test_lti.py # unit tests (keys, roles, claims, JWT, deep linking)
│ ├── test_launch_flow.py # login → launch integration test
│ └── create_test_deployment.py
├── Dockerfile
├── docker-compose.yml # app + PostgreSQL
├── SETUP.md # step-by-step Canvas setup guide
├── pyproject.toml
├── uv.lock
├── .python-version
├── .pre-commit-config.yaml
├── .env.example
├── LICENSE
├── CHANGELOG.md
├── CONTRIBUTING.md
└── SECURITY.md
-
Access Canvas Admin settings
-
Navigate to "Developer Keys"
-
Click "+ Developer Key" and select "LTI Key"
-
Configure the key:
You can get the configuration by making a GET request to:
curl http://your-domain/lti/config
Which returns:
{ "title": "Your Tool Name", "description": "Your tool description", "target_link_uri": "http://your-domain/lti/launch", "oidc_initiation_url": "http://your-domain/lti/login", "public_jwk_url": "http://your-domain/.well-known/jwks.json", "scopes": [ "https://purl.imsglobal.org/spec/lti-ags/scope/lineitem", "https://purl.imsglobal.org/spec/lti-ags/scope/result.readonly", "https://purl.imsglobal.org/spec/lti-nrps/scope/contextmembership.readonly" ], "extensions": [ { "platform": "canvas.instructure.com", "settings": { "platform": "canvas.instructure.com", "text": "Your Tool Name", "placements": [ { "placement": "course_navigation", "enabled": true, "default": "enabled", "message_type": "LtiResourceLinkRequest", "target_link_uri": "http://your-domain/lti/launch" } ] } } ] }Copy this configuration into the Developer Key settings in Canvas.
-
Save and enable the Developer Key
-
Note the Client ID (will be used in .env)
Update your .env file. For a single-tenant setup, the LTI_* variables seed a
platform row on startup; multi-tenant setups insert LTIPlatform / LTIDeployment
rows directly instead.
# Tool
TOOL_URL="https://your-domain" # endpoint URLs are derived from this
DATABASE_URL="postgresql+asyncpg://user:pass@localhost:5432/lti_db"
SECRET_KEY="a-long-random-string"
# Default platform seed (the LMS this tool connects to)
LTI_ISSUER="https://canvas.instructure.com" # platform issuer (iss)
LTI_CLIENT_ID="10000000000001" # from the Developer Key
LTI_DEPLOYMENT_ID="1:df26f9c414..." # created on course install
LTI_AUTH_LOGIN_URL="https://sso.canvaslms.com/api/lti/authorize_redirect"
LTI_AUTH_TOKEN_URL="https://sso.canvaslms.com/login/oauth2/token" # AGS/NRPS
LTI_JWK_URL="https://sso.canvaslms.com/api/lti/security/jwks" # platform JWKSThe tool's own signing key is auto-generated at
keys/private.pemon first run (and reused afterwards). To pin it, setLTI_PRIVATE_KEY(PEM) orLTI_PRIVATE_KEY_PATH. See .env.example for all options.
- Go to Course Settings
- Select "Apps" tab
- Click "+ App"
- Choose "By Client ID"
- Enter the Client ID from Developer Key
- Submit and authorize the installation
- The tool will appear in course navigation
-
Development Environment
# Start your development server uv run uvicorn app.main:app --reload # For local testing, use ngrok to expose your server ngrok http 8000
-
Update Canvas Configuration
- If testing locally, update Developer Key URLs with ngrok domain
- Update .env with corresponding URLs
-
Verify Installation
- Access course as instructor
- Tool should appear in course navigation
- Click to test launch flow
- Check application logs for debugging
Contributions are welcome! See docs/CONTRIBUTING.md to get started.
Apache-2.0 — see LICENSE for details.