view Sources/LTI/LTIRoutes.cpp @ 77:80b663d5f8fe default tip

replaced boost::math::iround() by Orthanc::Math::llround()
author Sebastien Jodogne <s.jodogne@gmail.com>
date Tue, 27 Jan 2026 17:05:03 +0100
parents 0f8c46d755e2
children
line wrap: on
line source

/**
 * SPDX-FileCopyrightText: 2024-2026 Sebastien Jodogne, EPL UCLouvain, Belgium
 * SPDX-License-Identifier: AGPL-3.0-or-later
 */

/**
 * Orthanc for Education
 * Copyright (C) 2024-2026 Sebastien Jodogne, EPL UCLouvain, Belgium
 *
 * This program is free software: you can redistribute it and/or
 * modify it under the terms of the GNU Affero General Public License
 * as published by the Free Software Foundation, either version 3 of
 * the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
 **/


#include "LTIRoutes.h"

#include "../EducationConfiguration.h"
#include "../HttpToolbox.h"
#include "../OrthancDatabase.h"
#include "../ProjectPermissionContext.h"
#include "../RestApiRouter.h"
#include "../Security/PlatformKeysRegistry.h"

#include <OrthancPluginCppWrapper.h>
#include <SerializationToolbox.h>

#include <cassert>


static const char* const COOKIE_LTI = "orthanc-education-lti";

static std::unique_ptr<PlatformKeysRegistry> platformKeysRegistry_;


static void CheckState(const std::map<std::string, std::string>& form,
                       const OrthancPluginHttpRequest* request)
{
  std::string cookieHeader;
  if (!HttpToolbox::LookupCDictionary(cookieHeader, "cookie", true, request->headersCount, request->headersKeys, request->headersValues))
  {
    throw Orthanc::OrthancException(Orthanc::ErrorCode_Unauthorized);
  }

  const std::string formState = HttpToolbox::ReadMandatoryString(form, "state");

  if (!EducationConfiguration::GetInstance().GetLtiContext().CheckSession(cookieHeader, formState))
  {
    throw Orthanc::OrthancException(Orthanc::ErrorCode_Unauthorized);
  }
}


void ServeJwks(OrthancPluginRestOutput* output,
               const std::string& url,
               const OrthancPluginHttpRequest* request,
               const AuthenticatedUser& user)
{
  assert(user.GetRole() == Role_Guest);

  Json::Value key;

  {
    LTIContext::Lock lock(EducationConfiguration::GetInstance().GetLtiContext());
    lock.GetPrivateKey().Export_JWKS_RS256(key, lock.GetKeyId());
  }

  Json::Value keys = Json::arrayValue;
  keys.append(key);

  Json::Value answer = Json::objectValue;
  answer["keys"] = keys;

  HttpToolbox::AnswerJson(output, answer);
}


static bool IsSameUrl(const std::string& a,
                      const std::string& b)
{
  HttpToolbox::CheckUrlScheme(a);
  HttpToolbox::CheckUrlScheme(b);
  return (HttpToolbox::RemoveTrailingSlashes(a) == HttpToolbox::RemoveTrailingSlashes(b));
}


void ServeOidc(OrthancPluginRestOutput* output,
               const std::string& url,
               const OrthancPluginHttpRequest* request,
               const AuthenticatedUser& user)
{
  assert(user.GetRole() == Role_Guest);

  // This route tests bidirectional communication between Orthanc and Moodle

  if (request->method != OrthancPluginHttpMethod_Post)
  {
    OrthancPluginSendMethodNotAllowed(OrthancPlugins::GetGlobalContext(), output, "POST");
  }
  else
  {
    std::map<std::string, std::string> form;
    HttpToolbox::ParseFormUrlEncoded(form, request->body, request->bodySize);

    const std::string iss = HttpToolbox::ReadMandatoryString(form, "iss");
    const std::string login_hint = HttpToolbox::ReadMandatoryString(form, "login_hint");
    const std::string target_link_uri = HttpToolbox::ReadMandatoryString(form, "target_link_uri");
    const std::string lti_deployment_id = HttpToolbox::ReadMandatoryString(form, "lti_deployment_id");

    const std::string platformUrl = EducationConfiguration::GetInstance().GetLtiPlatformUrl();
    if (!IsSameUrl(iss, platformUrl))
    {
      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadRequest, "Bad value of \"iss\": \"" + iss +
                                      "\" instead of \"" + platformUrl + "\"");
    }

    std::string cookieHeader;
    if (!HttpToolbox::LookupCDictionary(cookieHeader, "cookie", true, request->headersCount, request->headersKeys, request->headersValues))
    {
      cookieHeader.clear();
    }

    std::string state, nonce;

    {
      const bool secureCookies = EducationConfiguration::GetInstance().IsSecureCookies();
      EducationConfiguration::GetInstance().GetLtiContext().EnterSession(output, state, nonce, cookieHeader, secureCookies);
    }

    std::map<std::string, std::string> arguments;
    arguments["client_id"] = EducationConfiguration::GetInstance().GetLtiClientId();
    arguments["login_hint"] = login_hint;
    arguments["lti_deployment_id"] = lti_deployment_id;
    arguments["lti_message_hint"] = HttpToolbox::ReadOptionalString(form, "lti_message_hint", "");
    arguments["nonce"] = nonce;
    arguments["prompt"] = "none";
    arguments["redirect_uri"] = target_link_uri;
    arguments["response_mode"] = "form_post";
    arguments["response_type"] = "id_token";
    arguments["scope"] = "openid";
    arguments["state"] = state;

    std::string redirection;
    HttpToolbox::FormatRedirectionUrl(redirection, EducationConfiguration::GetInstance().GetLtiPlatformRedirectionUrl(), arguments);

    // We manually reimplement "OrthancPluginRedirect()", otherwise "Set-Cookie" has no effect
    OrthancPluginSetHttpHeader(OrthancPlugins::GetGlobalContext(), output, "Location", redirection.c_str());
    OrthancPluginSendHttpStatusCode(OrthancPlugins::GetGlobalContext(), output, 302);
  }
}


void ServeLaunch(OrthancPluginRestOutput* output,
                 const std::string& url,
                 const OrthancPluginHttpRequest* request,
                 const AuthenticatedUser& guestUser)
{
  assert(platformKeysRegistry_.get() != NULL);
  assert(guestUser.GetRole() == Role_Guest);

  if (request->method != OrthancPluginHttpMethod_Post)
  {
    OrthancPluginSendMethodNotAllowed(OrthancPlugins::GetGlobalContext(), output, "POST");
  }
  else
  {
    /**
     * This route does not check that the learner has access to the
     * DICOM resources of interest per se. It only redirects the Web
     * browser to the DICOM viewer. Access will be checked afterward,
     * as the viewer downloads the DICOM resources.
     **/

    std::map<std::string, std::string> form;
    HttpToolbox::ParseFormUrlEncoded(form, request->body, request->bodySize);

    CheckState(form, request);

    JWT jwt(HttpToolbox::ReadMandatoryString(form, "id_token"));
    platformKeysRegistry_->VerifyJWT(jwt, EducationConfiguration::GetInstance().GetLtiPlatformKeysUrl(), 60 /* must be short-lived */);

    std::map<std::string, std::string> custom;
    Orthanc::SerializationToolbox::ReadMapOfStrings(custom, jwt.GetPayload(), "https://purl.imsglobal.org/spec/lti/claim/custom");

    const std::string redirection = HttpToolbox::ReadMandatoryString(custom, "orthanc_url");

    std::unique_ptr<AuthenticatedUser> user;

    {
      ProjectPermissionContext context;
      user.reset(AuthenticatedUser::FromLti(context, jwt.GetPayload()));
    }

    std::string token;
    user->ForgeJWT(token, EducationConfiguration::GetInstance().GetLtiContext(),
                   EducationConfiguration::GetInstance().GetMaxLoginAgeSeconds());

    {
      const bool secureCookies = EducationConfiguration::GetInstance().IsSecureCookies();
      HttpToolbox::SetCookie(output, COOKIE_LTI, token, CookieSameSite_Lax, secureCookies);
    }

    // We manually reimplement "OrthancPluginRedirect()", to redirect the POST to a GET, and to set JWT cookie
    OrthancPluginSetHttpHeader(OrthancPlugins::GetGlobalContext(), output, "Location", redirection.c_str());
    OrthancPluginSendHttpStatusCode(OrthancPlugins::GetGlobalContext(), output, 303);  // 303 means "See Other"
  }
}


void ServeDeep(OrthancPluginRestOutput* output,
               const std::string& url,
               const OrthancPluginHttpRequest* request,
               const AuthenticatedUser& guestUser)
{
  assert(platformKeysRegistry_.get() != NULL);
  assert(guestUser.GetRole() == Role_Guest);

  if (request->method != OrthancPluginHttpMethod_Post)
  {
    OrthancPluginSendMethodNotAllowed(OrthancPlugins::GetGlobalContext(), output, "POST");
  }
  else
  {
    std::map<std::string, std::string> form;
    HttpToolbox::ParseFormUrlEncoded(form, request->body, request->bodySize);

    CheckState(form, request);

    JWT jwt(HttpToolbox::ReadMandatoryString(form, "id_token"));
    platformKeysRegistry_->VerifyJWT(jwt, EducationConfiguration::GetInstance().GetLtiPlatformKeysUrl(), 60 /* must be short-lived */);

    std::unique_ptr<AuthenticatedUser> user;

    {
      ProjectPermissionContext context;
      user.reset(AuthenticatedUser::FromLti(context, jwt.GetPayload()));
    }

    std::string orthancEducationJwt;
    user->ForgeJWT(orthancEducationJwt, EducationConfiguration::GetInstance().GetLtiContext(),
                   EducationConfiguration::GetInstance().GetMaxLoginAgeSeconds());

    std::string s;
    HttpToolbox::GetWebApplicationResource(s, "deep.html");

    const Json::Value& payload = jwt.GetPayload();

    Json::Value settings;
    if (!HttpToolbox::LookupJsonObject(settings, payload, "https://purl.imsglobal.org/spec/lti-dl/claim/deep_linking_settings"))
    {
      throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol);
    }

    // The info below is copied by "deep.js", then sent to "CreateDeepLink()" or "RedirectToViewer()"

    Json::Value info;
    info["aud"] = Orthanc::SerializationToolbox::ReadString(payload, "iss");
    info["data"] = Orthanc::SerializationToolbox::ReadString(settings, "data", "");
    info["deployment-id"] = Orthanc::SerializationToolbox::ReadString(payload, "https://purl.imsglobal.org/spec/lti/claim/deployment_id");
    info["nonce"] = Orthanc::SerializationToolbox::ReadString(payload, "nonce");
    info["return-url"] = Orthanc::SerializationToolbox::ReadString(settings, "deep_link_return_url");
    info["title"] = Orthanc::SerializationToolbox::ReadString(settings, "title", "This is my title");
    info["orthanc-education-jwt"] = orthancEducationJwt;

    std::string encoded;
    Orthanc::Toolbox::WriteFastJson(encoded, info);

    std::string base64;
    Orthanc::Toolbox::EncodeBase64(base64, encoded);
    boost::replace_all(s, "${INFO}", base64);

    OrthancPluginAnswerBuffer(OrthancPlugins::GetGlobalContext(), output, s.c_str(), s.size(), "text/html");
  }
}


void ServeDeepJavaScript(OrthancPluginRestOutput* output,
                         const std::string& url,
                         const OrthancPluginHttpRequest* request,
                         const AuthenticatedUser& user)
{
  assert(user.GetRole() == Role_Guest);

  std::string content;
  HttpToolbox::GetWebApplicationResource(content, "deep.js");

  OrthancPluginAnswerBuffer(OrthancPlugins::GetGlobalContext(), output,
                            content.c_str(), content.size(), Orthanc::EnumerationToString(Orthanc::MimeType_JavaScript));
}


static void CheckUserPermission(const std::map<std::string, std::string>& args,
                                const AuthenticatedUser& user)
{
  const std::string levelString = HttpToolbox::ReadMandatoryString(args, "level");
  const Orthanc::ResourceType level = Orthanc::StringToResourceType(levelString.c_str());
  const std::string resourceId = HttpToolbox::ReadMandatoryString(args, "resource-id");

  ProjectPermissionContext::Granter granter(user);

  if (!OrthancDatabase::IsGrantedResource(granter, level, resourceId))
  {
    throw Orthanc::OrthancException(Orthanc::ErrorCode_ForbiddenAccess);
  }
}


void CreateDeepLink(OrthancPluginRestOutput* output,
                    const std::string& url,
                    const OrthancPluginHttpRequest* request,
                    const AuthenticatedUser& user)
{
  if (request->method != OrthancPluginHttpMethod_Post)
  {
    OrthancPluginSendMethodNotAllowed(OrthancPlugins::GetGlobalContext(), output, "POST");
  }
  else
  {
    std::map<std::string, std::string> args;
    HttpToolbox::ParseFormUrlEncoded(args, request->body, request->bodySize);

    Json::Value link;
    link["type"] = "ltiResourceLink";
    link["url"] = Orthanc::Toolbox::JoinUri(EducationConfiguration::GetInstance().GetLtiOrthancUrl(), "education/lti/launch");

    std::string linkUrl;
    const std::string type = HttpToolbox::ReadMandatoryString(args, "link-type");

    if (type == "viewer")
    {
      CheckUserPermission(args, user);

      const ViewerType viewer = ParseViewerType(HttpToolbox::ReadMandatoryString(args, "viewer"));

      link["title"] = HttpToolbox::ReadMandatoryString(args, "title");
      linkUrl = Orthanc::Toolbox::JoinUri("../..", OrthancDatabase::GenerateViewerUrl(viewer, args));
    }
    else if (type == "project")
    {
      if (user.GetRole() == Role_Administrator ||
          user.IsInstructorOfProject(user.GetLtiProjectId()))
      {
        link["title"] = "DICOM resources available in this course";
        linkUrl = "../app/list-projects.html?open-project-id=" + user.GetLtiProjectId();
      }
      else
      {
        throw Orthanc::OrthancException(Orthanc::ErrorCode_ForbiddenAccess);
      }
    }
    else
    {
      throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol);
    }

    // WARNING: LTI does not like dashes "-" in the keys, so we use underscores "_"
    link["custom"]["orthanc_url"] = linkUrl;

    Json::Value content = Json::arrayValue;
    content.append(link);

    Json::Value payload;
    payload["aud"] = HttpToolbox::ReadMandatoryString(args, "aud");
    payload["iss"] = EducationConfiguration::GetInstance().GetLtiClientId();
    payload["nonce"] = HttpToolbox::ReadMandatoryString(args, "nonce");

    payload["https://purl.imsglobal.org/spec/lti-dl/claim/content_items"] = content;
    payload["https://purl.imsglobal.org/spec/lti-dl/claim/data"] = HttpToolbox::ReadMandatoryString(args, "data");
    payload["https://purl.imsglobal.org/spec/lti/claim/deployment_id"] = HttpToolbox::ReadMandatoryString(args, "deployment-id");
    payload["https://purl.imsglobal.org/spec/lti/claim/message_type"] = "LtiDeepLinkingResponse";
    payload["https://purl.imsglobal.org/spec/lti/claim/version"] = "1.3.0";

    std::string jwt;
    EducationConfiguration::GetInstance().GetLtiContext().ForgeJWT(jwt, payload, 60 /* expires in 1 minute */);

    OrthancPluginAnswerBuffer(OrthancPlugins::GetGlobalContext(), output, jwt.c_str(), jwt.size(), "application/jwt");
  }
}



void RedirectToViewer(OrthancPluginRestOutput* output,
                      const std::string& url,
                      const OrthancPluginHttpRequest* request,
                      const AuthenticatedUser& user)
{
  if (request->method != OrthancPluginHttpMethod_Get)
  {
    OrthancPluginSendMethodNotAllowed(OrthancPlugins::GetGlobalContext(), output, "GET");
  }
  else
  {
    std::map<std::string, std::string> args;
    HttpToolbox::ConvertDictionaryFromC(args, false, request->getCount, request->getKeys, request->getValues);

    std::map<std::string, std::string>::const_iterator bearer = args.find("bearer");

    if (bearer != args.end())
    {
      std::unique_ptr<AuthenticatedUser> check(AuthenticatedUser::FromJWT(EducationConfiguration::GetInstance().GetLtiContext(), bearer->second));
      CheckUserPermission(args, *check);

      // Setting the cookie is needed for "RedirectToViewer()" to work, as long as no deep link has been created
      {
        const bool secureCookies = EducationConfiguration::GetInstance().IsSecureCookies();
        HttpToolbox::SetCookie(output, COOKIE_LTI, bearer->second, CookieSameSite_Lax, secureCookies);
      }
    }
    else
    {
      CheckUserPermission(args, user);
    }

    const ViewerType viewer = ParseViewerType(HttpToolbox::ReadMandatoryString(args, "viewer"));
    const std::string viewerUrl = Orthanc::Toolbox::JoinUri("../..", OrthancDatabase::GenerateViewerUrl(viewer, args));

    // We manually reimplement "OrthancPluginRedirect()", otherwise "Set-Cookie" has no effect
    OrthancPluginSetHttpHeader(OrthancPlugins::GetGlobalContext(), output, "Location", viewerUrl.c_str());
    OrthancPluginSendHttpStatusCode(OrthancPlugins::GetGlobalContext(), output, 302);
  }
}


void ListProjectResourcesForLti(OrthancPluginRestOutput* output,
                                const std::string& url,
                                const OrthancPluginHttpRequest* request,
                                const AuthenticatedUser& user)
{
  if (user.GetRole() == Role_Administrator)
  {
    // LTI authentication can never generate an administrator-level access
    throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
  }

  const std::string& projectId = user.GetLtiProjectId();

  std::unique_ptr<Project> project(ProjectPermissionContext::GetProjects().CloneDocument<Project>(projectId));
  assert(project.get() != NULL);

  // Is the user an instructor for this project?
  if (ProjectPermissionContext::GetProjectAccessMode(user, projectId, *project) == ProjectAccessMode_Writable)
  {
    Json::Value answer;
    OrthancDatabase::FormatProjectWithResources(answer, projectId, *project);

    HttpToolbox::AnswerJson(output, answer);
  }
  else
  {
    // This route is only available to the instructors of the project
    throw Orthanc::OrthancException(Orthanc::ErrorCode_ForbiddenAccess);
  }
}


void ServeRegister(OrthancPluginRestOutput* output,
                   const std::string& url,
                   const OrthancPluginHttpRequest* request,
                   const AuthenticatedUser& user)
{
  assert(platformKeysRegistry_.get() != NULL);
  assert(user.GetRole() == Role_Guest);

  // This is "LTI Dynamic Registration Specification"
  // https://www.imsglobal.org/spec/lti-dr/v1p0

  std::map<std::string, std::string> form;
  HttpToolbox::ConvertDictionaryFromC(form, false, request->getCount, request->getKeys, request->getValues);

  const std::string oid_url = HttpToolbox::ReadMandatoryString(form, "openid_configuration");
  const std::string token = HttpToolbox::ReadMandatoryString(form, "registration_token");

  Json::Value platform;

  {
    OrthancPlugins::HttpClient client;
    client.SetTimeout(5);  // 5 seconds to avoid freezing
    client.SetUrl(oid_url);
    OrthancPlugins::HttpHeaders headers;
    client.Execute(headers, platform);
  }

  const std::string issuer = Orthanc::SerializationToolbox::ReadString(platform, "issuer");
  const std::string platformKeysUrl = Orthanc::SerializationToolbox::ReadString(platform, "jwks_uri");
  const std::string platformRedirectionUrl = Orthanc::SerializationToolbox::ReadString(platform, "authorization_endpoint");

  const std::string platformUrl = EducationConfiguration::GetInstance().GetLtiPlatformUrl();
  if (!IsSameUrl(issuer, platformUrl))
  {
    throw Orthanc::OrthancException(Orthanc::ErrorCode_BadRequest, "Bad value of \"issuer\": \"" + issuer +
                                    "\" instead of \"" + platformUrl + "\"");
  }

  JWT jwt(token);
  platformKeysRegistry_->VerifyJWT(jwt, platformKeysUrl, 60 /* must be short-lived */);

  const std::string orthancUrl = EducationConfiguration::GetInstance().GetLtiOrthancUrl();

  Json::Value messages = Json::arrayValue;

  {
    Json::Value message;
    message["type"] = "LtiDeepLinkingRequest";
    message["target_link_uri"] = Orthanc::Toolbox::JoinUri(orthancUrl, "education/lti/deep");
    message["label"] = "Add a link to Orthanc";
    message["supported_types"].append("ltiResourceLink");
    messages.append(message);
  }

  Json::Value tool;
  tool["domain"] = EducationConfiguration::GetInstance().GetLtiOrthancDomain();
  tool["target_link_uri"] = Orthanc::Toolbox::JoinUri(orthancUrl, "education/lti/launch");
  tool["claims"].append("iss");
  tool["claims"].append("sub");
  tool["claims"].append("email");  // Strictly speaking, this is not required but it helps debugging
  tool["description"] = "Create links to medical images stored in Orthanc.";
  tool["messages"] = messages;

  Json::Value registration;
  registration["application_type"] = "web";
  registration["grant_types"].append("client_credentials");
  registration["grant_types"].append("implicit");
  registration["response_types"].append("id_token");
  registration["redirect_uris"].append(Orthanc::Toolbox::JoinUri(orthancUrl, "education/lti/launch"));
  registration["redirect_uris"].append(Orthanc::Toolbox::JoinUri(orthancUrl, "education/lti/deep"));
  registration["initiate_login_uri"] = Orthanc::Toolbox::JoinUri(orthancUrl, "education/lti/oidc");
  registration["client_name"] = "Orthanc for Education";
  registration["jwks_uri"] = Orthanc::Toolbox::JoinUri(orthancUrl, "education/lti/jwks");
  registration["logo_uri"] = Orthanc::Toolbox::JoinUri(orthancUrl, "education/static/img/orthanc-h-negative.png");
  registration["token_endpoint_auth_method"] = "private_key_jwt";
  registration["https://purl.imsglobal.org/spec/lti-tool-configuration"] = tool;
  registration["scope"] = "https://purl.imsglobal.org/spec/lti-nrps/scope/contextmembership.readonly";

  Json::Value response;

  {
    OrthancPlugins::HttpClient client;
    client.SetTimeout(5);  // 5 seconds to avoid freezing
    client.SetMethod(OrthancPluginHttpMethod_Post);
    client.SetUrl(Orthanc::SerializationToolbox::ReadString(platform, "registration_endpoint"));
    client.AddHeader("Authorization", "Bearer " + token);
    client.SetBody(registration.toStyledString());
    OrthancPlugins::HttpHeaders headers;
    client.Execute(headers, response);
  }

  EducationConfiguration::GetInstance().SetLtiClientId(Orthanc::SerializationToolbox::ReadString(response, "client_id"));
  EducationConfiguration::GetInstance().SetLtiPlatformKeysUrlFromRegistration(platformKeysUrl);
  EducationConfiguration::GetInstance().SetLtiPlatformRedirectionUrlFromRegistration(platformRedirectionUrl);

  // Check out Step 4:
  // https://www.imsglobal.org/spec/lti-dr/v1p0#step-4-registration-completed-and-activation
  static const std::string close = ("<html><script type=\"text/javascript\">(window.opener || window.parent)."
                                    "postMessage({subject:'org.imsglobal.lti.close'}, '*')</script></html>");
  OrthancPluginAnswerBuffer(OrthancPlugins::GetGlobalContext(), output, close.c_str(), close.size(),
                            Orthanc::EnumerationToString(Orthanc::MimeType_Html));
}


void RegisterLTIRoutes()
{
  if (platformKeysRegistry_.get() != NULL)
  {
    throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
  }

  LOG(WARNING) << "Enabling LTI 1.3 support";

  platformKeysRegistry_.reset(new PlatformKeysRegistry);
  platformKeysRegistry_->LoadKeys(EducationConfiguration::GetInstance().GetLtiPlatformKeysUrl());

  // Those are safe LTI routes, they can be made public
  RestApiRouter::RegisterPublicGetRoute<ServeDeepJavaScript>("/education/lti/deep.js");
  RestApiRouter::RegisterPublicGetRoute<ServeJwks>("/education/lti/jwks");
  RestApiRouter::RegisterPublicGetRoute<ServeRegister>("/education/lti/register");
  RestApiRouter::RegisterPublicRoute<ServeOidc>("/education/lti/oidc");

  // Those are sensitive routes that must be protected inside the callback
  RestApiRouter::RegisterAuthenticatedRoute<CreateDeepLink>("/education/lti/create-deep-link");
  RestApiRouter::RegisterAuthenticatedRoute<RedirectToViewer>("/education/lti/open-viewer");
  RestApiRouter::RegisterAuthenticatedGetRoute<ListProjectResourcesForLti>("/education/lti/project-resources");

  // Sensitive routes responsible for LTI authentication, they occur
  // after "/education/lti/oidc" and they must be protected using
  // "CheckState()" and "platformKeysRegistry_.VerifyJWT()"
  RestApiRouter::RegisterPublicRoute<ServeDeep>("/education/lti/deep");
  RestApiRouter::RegisterPublicRoute<ServeLaunch>("/education/lti/launch");
}


void ClearLTICookie(OrthancPluginRestOutput* output)
{
  const bool secureCookies = EducationConfiguration::GetInstance().IsSecureCookies();
  HttpToolbox::ClearCookie(output, COOKIE_LTI, CookieSameSite_Lax, secureCookies);
}


AuthenticatedUser* AuthenticateFromLTICookie(const std::list<HttpToolbox::Cookie>& cookies)
{
  for (std::list<HttpToolbox::Cookie>::const_iterator it = cookies.begin(); it != cookies.end(); ++it)
  {
    if (it->GetKey() == COOKIE_LTI)
    {
      try
      {
        return AuthenticatedUser::FromJWT(EducationConfiguration::GetInstance().GetLtiContext(), it->GetValue());
      }
      catch (Orthanc::OrthancException&)
      {
      }
    }
  }

  return NULL;
}