Redirect and Target Link URI Ignored

37 views
Skip to first unread message

Tyler Dean

unread,
Mar 15, 2025, 4:36:31 PMMar 15
to Canvas LMS Users
I've been following this flask LTI example: https://github.com/dmitry-viskov/pylti1.3-flask-example/tree/master/game along with these instructions: https://github.com/dmitry-viskov/pylti1.3/wiki/Configure-Canvas-as-LTI-1.3-Platform.

As per these instructions, see attached for what my dev key settings look like in my instance.  This app is running on port 9001

I've built up my own Flask app from scratch borrowing heavily from the LTI example that I've configured in the same way (see attached).  This app is running on port 5000.

In spite of these near identical configurations, there are differences in how the Canvas instance interacts.  In the LTI example, I see the request made to http://127.0.0.1:9001/login/ with target_link_uri: http://127.0.0.1:9001/launch/ in the payload, which returns a 200 status code.  However, in my from scratch app, I see the request made to http://127.0.0.1:5000/login/ with target_link_uri: http://127.0.0.1:5000 in the payload (notice it's missing the /login/ path), which returns a 302 status code.  

This later leads to a http://canvas.docker/api/lti/authorize call that fails because the redirect uri in the payload (redirect_uri: http://127.0.0.1:5000) once again is missing the path.  Ultimately, this error brings up the following window (see attached): {"status":"bad_request","message":"Invalid redirect_uri"}.

What would cause a difference in redirect behavior if the configurations are purposefully kept nearly identical?

Here's my code for reference, which borrows heavily from the LTI example.




import os

from flask import Flask
from tempfile import mkdtemp
from flask import Flask, jsonify, request, render_template, url_for
from flask_caching import Cache
from werkzeug.exceptions import Forbidden
from pylti1p3.contrib.flask import FlaskOIDCLogin, FlaskMessageLaunch, FlaskRequest, FlaskCacheDataStorage
from pylti1p3.deep_link_resource import DeepLinkResource
from pylti1p3.grade import Grade
from pylti1p3.lineitem import LineItem
from pylti1p3.tool_config import ToolConfJsonFile
from pylti1p3.registration import Registration

app = Flask(__name__, template_folder='templates')
cache = Cache(app)

@app.route("/")
def helloworld():
return "Hello World!"

class ExtendedFlaskMessageLaunch(FlaskMessageLaunch):

    def validate_nonce(self):
        """
        Probably it is bug on "https://lti-ri.imsglobal.org":
        site passes invalid "nonce" value during deep links launch.
        Because of this in case of iss == http://imsglobal.org just skip nonce validation.

        """
        iss = self.get_iss()
        deep_link_launch = self.is_deep_link_launch()
        if iss == "http://imsglobal.org" and deep_link_launch:
            return self
        return super().validate_nonce()

def get_lti_config_path():
return os.path.join(app.root_path, 'configs', 'gwidge.json')

def get_launch_data_storage():
    return FlaskCacheDataStorage(cache)

def get_jwk_from_public_key(key_name):
key_path = os.path.join(app.root_path, '..', 'configs', key_name)
f = open(key_path, 'r')
key_content = f.read()
jwk = Registration.get_jwk(key_content)
f.close()
return jwk

@app.route('/login/', methods=['GET', 'POST'])
def login():
    tool_conf = ToolConfJsonFile(get_lti_config_path())
    launch_data_storage = get_launch_data_storage()

    flask_request = FlaskRequest()
    target_link_uri = flask_request.get_param('target_link_uri')
    if not target_link_uri:
        raise Exception('Missing "target_link_uri" param')

    oidc_login = FlaskOIDCLogin(flask_request, tool_conf, launch_data_storage=launch_data_storage)
    return oidc_login\
        .redirect(target_link_uri)

@app.route('/launch/', methods=['POST'])
def launch():
    tool_conf = ToolConfJsonFile(get_lti_config_path())
    flask_request = FlaskRequest()
    launch_data_storage = get_launch_data_storage()
    message_launch = ExtendedFlaskMessageLaunch(flask_request, tool_conf, launch_data_storage=launch_data_storage)
    message_launch_data = message_launch.get_launch_data()
    pprint.pprint(message_launch_data)


    return render_template('gwidge.html')

@app.route('/jwks/', methods=['GET'])
def get_jwks():
tool_conf = ToolConfJsonFile(get_lti_config_path())
return jsonify({'keys': tool_conf.get_jwks()})

if __name__ == "__main__":
app.run()
My Test settings 2.png
LTI example settings.png
LTI example settings 2.png
My Test settings.png
error window.png

Tyler Dean

unread,
Mar 15, 2025, 4:44:18 PMMar 15
to Canvas LMS Users
It's interesting to note that when I change 

    return oidc_login\
        .redirect(target_link_uri)

with the correct hardcoded value:

    return oidc_login\

        .redirect('http://127.0.0.1:5000/launch/')


I see that correct redirect uri in the http://canvas.docker/api/lti/authorize call payload, but it still responds with an error saying the redirect uri is invalid.

Reply all
Reply to author
Forward
0 new messages