Skip to content

Commit

Permalink
SAK-40488 Initial commit of LTI 1.3 Launch (sakaiproject#5905)
Browse files Browse the repository at this point in the history
* SAK-40488 - Hard-coded LTI 1.3 launch works

Next up - not hard coded :)

* SAK-40488 - No longer using hard-coded data

* SAK-40488 - Reveal the public key for simple consumers
  • Loading branch information
csev authored Aug 23, 2018
1 parent 2779b0d commit 9a12ddb
Show file tree
Hide file tree
Showing 7 changed files with 226 additions and 18 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ public interface LTIService extends LTISubstitutionsFilter {
"lti13_client_id:text:hide=insert:label=bl_lti13_client_id:maxlength=1024:role=admin",
"lti13_tool_public:textarea:label=bl_lti13_tool_public:maxlength=1M:role=admin",
"lti13_tool_private:textarea:hide=insert:label=bl_lti13_tool_private:maxlength=1M:role=admin",
"lti13_platform_public:textarea:hidden=true:label=bl_lti13_platform_public:maxlength=1M:role=admin",
"lti13_platform_public:textarea:hide=insert:label=bl_lti13_platform_public:maxlength=1M:role=admin",
"lti13_platform_private:textarea:hidden=true:label=bl_lti13_platform_private:maxlength=1M:role=admin",
"lti13_settings:textarea:hidden=true:maxlength=1M:role=admin",

Expand Down Expand Up @@ -292,6 +292,14 @@ public interface LTIService extends LTISubstitutionsFilter {
String LTI_SITE_ATTRIBUTION_PROPERTY_NAME = "basiclti.tool.site.attribution.name";
String LTI_SITE_ATTRIBUTION_PROPERTY_NAME_DEFAULT = "content.attribution";

// LTI 1.3
String LTI13 = "lti13";
String LTI13_CLIENT_ID = "lti13_client_id";
String LTI13_TOOL_PUBLIC = "lti13_tool_public";
String LTI13_TOOL_PRIVATE = "lti13_tool_private";
String LTI13_PLATFORM_PUBLIC = "lti13_platform_public";
String LTI13_PLATFORM_PRIVATE = "lti13_platform_private";

// For Instructors, this model is filtered down dynamically based on
// Tool settings

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,24 @@
import org.sakaiproject.lti.api.LTIService;
import org.sakaiproject.lti2.SakaiLTI2Config;

import org.tsugi.lti13.LTI13Util;
import org.tsugi.lti13.LTI13JwtUtil;
import org.tsugi.lti13.LTI13JacksonUtil;

import org.tsugi.lti13.objects.LaunchJWT;
import org.tsugi.lti13.objects.ResourceLink;
import org.tsugi.lti13.objects.Context;
import org.tsugi.lti13.objects.ToolPlatform;
import org.tsugi.lti13.objects.LaunchLIS;
import org.tsugi.lti13.objects.BasicOutcome;
import org.tsugi.lti13.objects.Endpoint;

import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;

import java.security.Key;

import org.sakaiproject.tool.api.Session;
import org.sakaiproject.tool.cover.SessionManager;
import org.sakaiproject.tool.cover.ToolManager;
Expand Down Expand Up @@ -809,9 +827,11 @@ public static String[] postLaunchHTML(Map<String, Object> content, Map<String,Ob
proxyBinding = ltiService.getProxyBindingDao(toolKey,context);

Long toolVersion = getLongNull(tool.get(LTIService.LTI_VERSION));
Long longLTI13 = getLongNull(tool.get(LTIService.LTI13));
boolean isLTI1 = toolVersion == null || (!toolVersion.equals(LTIService.LTI_VERSION_2));
boolean isLTI2 = ! isLTI1; // In case there is an LTI 3
log.debug("toolVersion={} isLTI1={}", toolVersion, isLTI1);
boolean isLTI13 = longLTI13.equals(1L); // In case there is an LTI 3
log.debug("toolVersion={} isLTI1={} isLTI13={}", toolVersion, isLTI1, isLTI13);

// If we are doing LTI2, We will need a ToolProxyBinding
ToolProxyBinding toolProxyBinding = null;
Expand Down Expand Up @@ -1039,6 +1059,9 @@ public static String[] postLaunchHTML(Map<String, Object> content, Map<String,Ob
}

log.debug("LAUNCH TYPE {}", (isLTI1 ? "LTI 1" : "LTI 2"));
if ( isLTI13 ) {
return postLaunchJWT(toolProps, ltiProps, tool, rb);
}
return postLaunchHTML(toolProps, ltiProps, rb);
}

Expand Down Expand Up @@ -1496,6 +1519,131 @@ public static String[] postLaunchHTML(Properties toolProps, Properties ltiProps,
return retval;
}

public static String[] postLaunchJWT(Properties toolProps, Properties ltiProps, Map<String,Object> tool, ResourceLoader rb)
{

String launch_url = toolProps.getProperty("secure_launch_url");
if ( launch_url == null ) launch_url = toolProps.getProperty("launch_url");
if ( launch_url == null ) return postError("<p>" + getRB(rb, "error.missing" ,"Not configured")+"</p>");

String org_guid = ServerConfigurationService.getString("basiclti.consumer_instance_guid",
ServerConfigurationService.getString("serverName", null));
String org_desc = ServerConfigurationService.getString("basiclti.consumer_instance_description",null);
String org_url = ServerConfigurationService.getString("basiclti.consumer_instance_url",
ServerConfigurationService.getString("serverUrl", null));

String client_id = (String) tool.get(LTIService.LTI13_CLIENT_ID);
String tool_public = (String) tool.get(LTIService.LTI13_TOOL_PUBLIC);
String tool_private = (String) tool.get(LTIService.LTI13_TOOL_PRIVATE);
String platform_public = (String) tool.get(LTIService.LTI13_PLATFORM_PUBLIC);
String platform_private = (String) tool.get(LTIService.LTI13_PLATFORM_PRIVATE);
System.out.println("platform_public="+platform_public);

if ( platform_private == null ){
return postError("<p>" + getRB(rb, "error.no.platform.private.key", "Missing Platform Private Key.")+"</p>");
}

/*
context_id: mercury
context_label: mercury site
context_title: mercury site
context_type: Group
ext_ims_lis_basic_outcome_url: http://localhost:8080/imsblis/service/
ext_ims_lis_memberships_id: c1007fb6345a87cd651785422a2925114d0707fad32c66edb6bfefbf2165819a:::admin:::content:3
ext_ims_lis_memberships_url: http://localhost:8080/imsblis/service/
ext_ims_lti_tool_setting_id: c1007fb6345a87cd651785422a2925114d0707fad32c66edb6bfefbf2165819a:::admin:::content:3
ext_ims_lti_tool_setting_url: http://localhost:8080/imsblis/service/
ext_lms: sakai-19-SNAPSHOT
ext_sakai_academic_session: OTHER
ext_sakai_launch_presentation_css_url_list: http://localhost:8080/library/skin/tool_base.css,http://localhost:8080/library/skin/morpheus-default/tool.css?version=49b21ca5
ext_sakai_role: maintain
ext_sakai_server: http://localhost:8080
ext_sakai_serverid: MacBook-Pro-92.local
launch_presentation_css_url: http://localhost:8080/library/skin/tool_base.css
launch_presentation_locale: en_US
launch_presentation_return_url: http://localhost:8080/imsblis/service/return-url/site/mercury
lis_course_offering_sourcedid: mercury
lis_course_section_sourcedid: mercury
lis_outcome_service_url: http://localhost:8080/imsblis/service/
lis_person_name_family: Administrator
lis_person_name_full: Sakai Administrator
lis_person_name_given: Sakai
lis_person_sourcedid: admin
lti_version: LTI-1p0
resource_link_description: Tsugi Breakout
resource_link_id: content:3
resource_link_title: Tsugi Breakout
roles: Instructor,Administrator,urn:lti:instrole:ims/lis/Administrator,urn:lti:sysrole:ims/lis/Administrator
user_id: admin
*/
// Lets make a JWT from the LTI 1.x data
LaunchJWT lj = new LaunchJWT();
lj.launch_presentation.css_url = ltiProps.getProperty("launch_presentation_css_url");
lj.locale = ltiProps.getProperty("launch_presentation_locale");
lj.launch_presentation.return_url = ltiProps.getProperty("launch_presentation_return_url");
lj.issuer = "https://www.sakaiproject.org/";
lj.audience = client_id;
lj.deployment_id = org_guid;
lj.subject = ltiProps.getProperty("user_id");
lj.name = ltiProps.getProperty("lis_person_name_full");
lj.nonce = new Long(System.currentTimeMillis()) + "_42";
lj.email = ltiProps.getProperty("lis_person_contact_email_primary");
lj.issued = new Long(System.currentTimeMillis() / 1000L);
lj.expires = lj.issued + 600L;
lj.roles.add(LaunchJWT.ROLE_INSTRUCTOR);

lj.resource_link = new ResourceLink();
lj.resource_link.id = ltiProps.getProperty("resource_link_id");
lj.resource_link.title = ltiProps.getProperty("resource_link_title");
lj.resource_link.description = ltiProps.getProperty("resource_link_description");

lj.context = new Context();
lj.context.id = ltiProps.getProperty("context_id");
lj.context.label = ltiProps.getProperty("context_label");
lj.context.title = ltiProps.getProperty("context_title");
lj.context.type.add(Context.COURSE_OFFERING);

// XXX
lj.tool_platform = new ToolPlatform();
lj.tool_platform.name = "Sakai";
lj.tool_platform.version = ltiProps.getProperty("tool_consumer_info_version");
lj.tool_platform.product_family_code = ltiProps.getProperty("tool_consumer_info_product_family_code");
lj.tool_platform.url = org_url;
lj.tool_platform.description = org_desc;

LaunchLIS lis = new LaunchLIS();
lis.person_sourcedid = ltiProps.getProperty("lis_person_sourcedid");
lis.course_offering_sourcedid = ltiProps.getProperty("lis_course_offering_sourcedid");
lis.course_section_sourcedid = ltiProps.getProperty("lis_course_section_sourcedid");
lj.lis = lis;

BasicOutcome outcome = new BasicOutcome();
outcome.lis_result_sourcedid = ltiProps.getProperty("lis_result_sourcedid");
outcome.lis_outcome_service_url = ltiProps.getProperty("lis_outcome_service_url");
lj.basicoutcome = outcome;

String ljs = LTI13JacksonUtil.toString(lj);
System.out.println("ljs"); System.out.println(ljs);

Key privateKey = LTI13Util.string2PrivateKey(platform_private);
Key publicKey = LTI13Util.string2PublicKey(platform_public);

String jws = Jwts.builder().setPayload(ljs).signWith(privateKey).compact();
// System.out.println("jws"); System.out.println(jws);
// String subject = Jwts.parser().setSigningKey(publicKey).parseClaimsJws(jws).getBody().getSubject();
// System.out.println("subject="+subject);


String html = "<form action=\""+launch_url+"\" method=\"POST\">\n"+
" <input type=\"hidden\" name=\"id_token\" value=\""+BasicLTIUtil.htmlspecialchars(jws)+"\" />\n" +
" <input type=\"submit\" value=\"Go!\" />\n" +
"</form>\n";

String [] retval = { html, launch_url };
return retval;
}

public static String getSourceDID(User user, Placement placement, Properties config)
{
String placementSecret = toNull(getCorrectProperty(config,"placementsecret", placement));
Expand Down
32 changes: 32 additions & 0 deletions basiclti/basiclti-impl/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,38 @@
<artifactId>poi</artifactId>
<version>${sakai.poi.version}</version>
</dependency>
<dependency>
<groupId>com.googlecode.json-simple</groupId>
<artifactId>json-simple</artifactId>
<version>1.1</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.10.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.10.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.10.5</version>
</dependency>
</dependencies>

<build>
Expand Down
6 changes: 6 additions & 0 deletions basiclti/basiclti-tool/src/webapp/vm/lti_tool_view.vm
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,12 @@ $(document).ready( function() {
setMainFrameHeight('sakai-basiclti-admin-iframe');
});
$('#lti13_tool_private').hide();

$('#lti13_tool_public').css('font-family', '"Courier New", monospace');
$('#lti13_tool_public').css('white-space', 'pre');

$('#lti13_platform_public').css('font-family', '"Courier New", monospace');
$('#lti13_platform_public').css('white-space', 'pre');
});
</script>
#end
Expand Down
41 changes: 25 additions & 16 deletions basiclti/tsugi-util/src/java/org/tsugi/lti13/LTI13Util.java
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ public static KeyPair generateKeyPair()
KeyPair kp = keyGen.genKeyPair();
return kp;
} catch(Exception e) {
return null;
throw new RuntimeException(e);
}
}

Expand All @@ -112,7 +112,7 @@ public static String getPublicEncoded(KeyPair kp)
Base64.Encoder encoder = Base64.getEncoder();

String publicRSA = "-----BEGIN RSA PUBLIC KEY-----\n" +
encoder.encodeToString(encodeArray) +
breakKeyIntoLines(encoder.encodeToString(encodeArray)) +
"\n-----END RSA PUBLIC KEY-----\n";
return publicRSA;
}
Expand All @@ -124,40 +124,49 @@ public static String getPrivateEncoded(KeyPair kp)

String privateRSA = "-----BEGIN RSA PRIVATE KEY-----\n" +
breakKeyIntoLines(encoder.encodeToString(encodeArray)) +
"-----END RSA PRIVATE KEY-----\n";
"\n-----END RSA PRIVATE KEY-----\n";
return privateRSA;
}

public static String breakKeyIntoLines(String inp)
public static String breakKeyIntoLines(String rawkey)
{
int len=65;
StringBuffer ret = new StringBuffer();

for(int i=0; i<inp.length(); i+=len ) {
String trimmed = rawkey.trim();
for(int i=0; i<trimmed.length(); i+=len ) {
int end=i+len;
if ( end > inp.length() ) end = inp.length();
ret.append(inp.substring(i, end));
ret.append("\n");
if ( ret.length() > 0 ) ret.append("\n");
if ( end > trimmed.length() ) end = trimmed.length();
ret.append(trimmed.substring(i, end));
}
return ret.toString();
}

public static Key string2PrivateKey(String keyString)
throws NoSuchAlgorithmException, InvalidKeySpecException
{
KeyFactory kf = KeyFactory.getInstance("RSA");
try {
KeyFactory kf = KeyFactory.getInstance("RSA");

PKCS8EncodedKeySpec keySpecPKCS8 = new PKCS8EncodedKeySpec(Base64.getDecoder().decode(keyString.getBytes()));
return (Key) kf.generatePrivate(keySpecPKCS8);
keyString = stripPKCS8(keyString);
PKCS8EncodedKeySpec keySpecPKCS8 = new PKCS8EncodedKeySpec(Base64.getDecoder().decode(keyString.getBytes()));
return (Key) kf.generatePrivate(keySpecPKCS8);
} catch(Exception e) {
throw new RuntimeException(e);
}
}

public static Key string2PublicKey(String keyString)
throws NoSuchAlgorithmException, InvalidKeySpecException
{
KeyFactory kf = KeyFactory.getInstance("RSA");
try {
KeyFactory kf = KeyFactory.getInstance("RSA");

X509EncodedKeySpec keySpecX509 = new X509EncodedKeySpec(Base64.getDecoder().decode(keyString));
return (Key) kf.generatePublic(keySpecX509);
keyString = stripPKCS8(keyString);
X509EncodedKeySpec keySpecX509 = new X509EncodedKeySpec(Base64.getDecoder().decode(keyString));
return (Key) kf.generatePublic(keySpecX509);
} catch(Exception e) {
throw new RuntimeException(e);
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ public class LaunchJWT extends BaseJWT {
public String email;
@JsonProperty("name")
public String name;
@JsonProperty("locale")
public String locale;

@JsonProperty("https://purl.imsglobal.org/spec/lti/claim/roles")
public List<String> roles = new ArrayList<String>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,7 @@ public class LaunchPresentation {
@JsonProperty("return_url")
public String return_url;

@JsonProperty("css_url")
public String css_url;

}

0 comments on commit 9a12ddb

Please sign in to comment.