浏览代码

Merge branch 'feature/1st-cut' of gremlin/lti-demo into develop

gremlin 4 年之前
父节点
当前提交
e8eed6fb33
共有 25 个文件被更改,包括 761 次插入0 次删除
  1. 8 0
      .gitignore
  2. 113 0
      pom.xml
  3. 13 0
      src/main/java/riomhaire/lti/JavaLti13ToolApplication.java
  4. 53 0
      src/main/java/riomhaire/lti/adapters/token/JWKBasedJwtToMapAdapter.java
  5. 46 0
      src/main/java/riomhaire/lti/business/actions/LaunchContent.java
  6. 86 0
      src/main/java/riomhaire/lti/business/actions/ProcessLtiMessage.java
  7. 79 0
      src/main/java/riomhaire/lti/business/actions/ProcessOidcRequest.java
  8. 41 0
      src/main/java/riomhaire/lti/business/actions/SelectDeepLinkContent.java
  9. 21 0
      src/main/java/riomhaire/lti/infrastructure/Registry.java
  10. 33 0
      src/main/java/riomhaire/lti/infrastructure/api/tool/LaunchEndpoint.java
  11. 36 0
      src/main/java/riomhaire/lti/infrastructure/api/tool/OIDCEndpoint.java
  12. 19 0
      src/main/java/riomhaire/lti/infrastructure/configuration/DecoupledLogicSetup.java
  13. 19 0
      src/main/java/riomhaire/lti/infrastructure/facades/clientregistration/ConfigBasedClientRegistrationResolver.java
  14. 34 0
      src/main/java/riomhaire/lti/infrastructure/facades/clientregistration/RedisBasedClientRegistrationResolver.java
  15. 23 0
      src/main/java/riomhaire/lti/model/ClientConfiguration.java
  16. 5 0
      src/main/java/riomhaire/lti/model/interfaces/Action.java
  17. 5 0
      src/main/java/riomhaire/lti/model/interfaces/ApplicationRegistry.java
  18. 9 0
      src/main/java/riomhaire/lti/model/interfaces/ClientRegistrationResolver.java
  19. 4 0
      src/main/java/riomhaire/lti/model/interfaces/DecodeException.java
  20. 6 0
      src/main/java/riomhaire/lti/model/interfaces/Decoder.java
  21. 5 0
      src/main/resources/application.properties
  22. 70 0
      src/main/resources/templates/dump-launch.html
  23. 12 0
      src/main/resources/templates/error.html
  24. 8 0
      src/redis/lti-client-registration-fc478730296ffab709218f28a22f0658.json
  25. 13 0
      src/test/java/riomhaire/lti/JavaLti13ToolApplicationTests.java

+ 8 - 0
.gitignore

@@ -12,3 +12,11 @@
 # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
 hs_err_pid*
 
+
+.idea/
+
+target/classes/
+
+*.iml
+
+target/maven-status/maven-compiler-plugin/compile/default-compile/

+ 113 - 0
pom.xml

@@ -0,0 +1,113 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
+	<modelVersion>4.0.0</modelVersion>
+	<parent>
+		<groupId>org.springframework.boot</groupId>
+		<artifactId>spring-boot-starter-parent</artifactId>
+		<version>2.4.3</version>
+		<relativePath/> <!-- lookup parent from repository -->
+	</parent>
+	<groupId>lti</groupId>
+	<artifactId>tool-13</artifactId>
+	<version>0.0.1-SNAPSHOT</version>
+	<name>java-lti13-tool</name>
+	<description>Demo project for Spring Boot</description>
+	<properties>
+		<java.version>14</java.version>
+	</properties>
+	<dependencies>
+
+		<dependency>
+			<groupId>com.auth0</groupId>
+			<artifactId>java-jwt</artifactId>
+			<version>3.10.0</version>
+		</dependency>
+		<dependency>
+			<groupId>org.bitbucket.b_c</groupId>
+			<artifactId>jose4j</artifactId>
+			<version>0.7.0</version>
+		</dependency>
+		<dependency>
+			<groupId>org.springframework.boot</groupId>
+			<artifactId>spring-boot-starter-web</artifactId>
+		</dependency>
+		<!-- https://mvnrepository.com/artifact/org.thymeleaf/thymeleaf-spring5 -->
+		<dependency>
+			<groupId>org.thymeleaf</groupId>
+			<artifactId>thymeleaf-spring5</artifactId>
+			<version>3.0.12.RELEASE</version>
+		</dependency>
+
+		<dependency>
+			<groupId>org.springframework.boot</groupId>
+			<artifactId>spring-boot-starter-thymeleaf</artifactId>
+		</dependency>
+		<dependency>
+			<groupId>org.springframework.boot</groupId>
+			<artifactId>spring-boot-configuration-processor</artifactId>
+			<optional>true</optional>
+		</dependency>
+		<dependency>
+			<groupId>org.springframework.boot</groupId>
+			<artifactId>spring-boot-starter-test</artifactId>
+			<scope>test</scope>
+			<exclusions>
+				<exclusion>
+					<groupId>org.junit.vintage</groupId>
+					<artifactId>junit-vintage-engine</artifactId>
+				</exclusion>
+			</exclusions>
+		</dependency>
+
+		<dependency>
+			<groupId>org.springframework.boot</groupId>
+			<artifactId>spring-boot-starter-actuator</artifactId>
+		</dependency>
+		<dependency>
+			<groupId>org.springframework.boot</groupId>
+			<artifactId>spring-boot-starter-cache</artifactId>
+		</dependency>
+		<dependency>
+			<groupId>org.springframework.boot</groupId>
+			<artifactId>spring-boot-starter-data-redis</artifactId>
+		</dependency>
+		<dependency>
+			<groupId>org.springframework.boot</groupId>
+			<artifactId>spring-boot-starter-integration</artifactId>
+		</dependency>
+		<dependency>
+			<groupId>org.springframework.integration</groupId>
+			<artifactId>spring-integration-redis</artifactId>
+		</dependency>
+		<dependency>
+			<groupId>org.projectlombok</groupId>
+			<artifactId>lombok</artifactId>
+			<optional>true</optional>
+		</dependency>
+
+		<dependency>
+			<groupId>org.springframework.integration</groupId>
+			<artifactId>spring-integration-test</artifactId>
+			<scope>test</scope>
+		</dependency>
+	</dependencies>
+
+	<build>
+		<plugins>
+			<plugin>
+				<groupId>org.springframework.boot</groupId>
+				<artifactId>spring-boot-maven-plugin</artifactId>
+				<configuration>
+					<excludes>
+						<exclude>
+							<groupId>org.projectlombok</groupId>
+							<artifactId>lombok</artifactId>
+						</exclude>
+					</excludes>
+				</configuration>
+			</plugin>
+		</plugins>
+	</build>
+
+</project>

+ 13 - 0
src/main/java/riomhaire/lti/JavaLti13ToolApplication.java

@@ -0,0 +1,13 @@
+package riomhaire.lti;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication
+public class JavaLti13ToolApplication {
+
+	public static void main(String[] args) {
+		SpringApplication.run(JavaLti13ToolApplication.class, args);
+	}
+
+}

+ 53 - 0
src/main/java/riomhaire/lti/adapters/token/JWKBasedJwtToMapAdapter.java

@@ -0,0 +1,53 @@
+package riomhaire.lti.adapters.token;
+
+import com.auth0.jwt.JWT;
+import com.auth0.jwt.interfaces.DecodedJWT;
+import lombok.Builder;
+import lombok.extern.slf4j.Slf4j;
+import org.jose4j.jwk.HttpsJwks;
+import org.jose4j.jwt.JwtClaims;
+import org.jose4j.jwt.consumer.InvalidJwtException;
+import org.jose4j.jwt.consumer.JwtConsumer;
+import org.jose4j.jwt.consumer.JwtConsumerBuilder;
+import org.jose4j.keys.resolvers.HttpsJwksVerificationKeyResolver;
+import riomhaire.lti.model.interfaces.DecodeException;
+import riomhaire.lti.model.interfaces.Decoder;
+
+import java.util.Map;
+
+@Slf4j
+@Builder
+public class JWKBasedJwtToMapAdapter implements Decoder<Map<String, Object>,String> {
+
+    protected String jwksUrl;
+    protected boolean skipVerification;
+
+
+    public Map<String, Object> decode(String token) throws DecodeException {
+        DecodedJWT jwt = JWT.decode(token);
+        JwtClaims verifiedClaims;
+
+
+        String clientId = jwt.getClaim("aud").asString();
+
+
+        try {
+            // OK look up jwks and verify
+            log.info("Using key:"+jwt.getKeyId()+" to search: "+jwksUrl);
+            HttpsJwks httpsJkws = new HttpsJwks(jwksUrl);
+            HttpsJwksVerificationKeyResolver httpsJwksKeyResolver = new HttpsJwksVerificationKeyResolver(httpsJkws);
+            JwtConsumer jwtConsumer = new JwtConsumerBuilder()
+                    .setVerificationKeyResolver(httpsJwksKeyResolver)
+                    .setExpectedAudience(clientId)
+                    .build();
+            if( skipVerification )  { // Dont verify
+                jwtConsumer.setSkipVerificationKeyResolutionOnNone(true);
+            }
+            verifiedClaims = jwtConsumer.processToClaims(token);
+        } catch ( InvalidJwtException e) {
+            throw new DecodeException();
+        }
+        // OK were here ... so valid
+        return verifiedClaims.getClaimsMap();
+    }
+}

+ 46 - 0
src/main/java/riomhaire/lti/business/actions/LaunchContent.java

@@ -0,0 +1,46 @@
+package riomhaire.lti.business.actions;
+
+import lombok.Builder;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.servlet.ModelAndView;
+import riomhaire.lti.infrastructure.Registry;
+import riomhaire.lti.model.interfaces.Action;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.function.Function;
+
+@Slf4j
+@Builder
+/**
+ * LaunchContent is called after we have decoded the ltu1.3 token an ascertained that the message type is 'LtiResourceLinkRequest'
+ * This simple example just displays whats in the message after simplifies the main keys
+ */
+public class LaunchContent implements Action<ModelAndView> {
+    Registry registry;
+    Map<String, String> queryParams;
+    Map<String, Object> metadata;
+    Map<String, Object> claims;
+
+
+    @Override
+    public ModelAndView execute() {
+        final String prefix = "https://purl.imsglobal.org/spec/lti/claim/";
+        var simplifiedClaims = new HashMap<String,Object>();
+        var mav = new ModelAndView("dump-launch");
+//        var mav = new ModelAndView("redirect:https://www.riomhaire.com");
+
+        // Strip "https://purl.imsglobal.org/spec/lti/claim/" from keys
+        Function<String,String> simplifier = s -> {
+                if (s != null && s.startsWith(prefix)){
+                    return s.substring(prefix.length());
+                }
+                return s;
+        };
+        claims.forEach((s, o) -> simplifiedClaims.put(simplifier.apply(s),o));
+        mav.addObject("claims",simplifiedClaims);
+        mav.addObject("metadata",metadata);
+
+        return mav;
+    }
+}

+ 86 - 0
src/main/java/riomhaire/lti/business/actions/ProcessLtiMessage.java

@@ -0,0 +1,86 @@
+package riomhaire.lti.business.actions;
+
+import com.auth0.jwt.JWT;
+import lombok.Builder;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.servlet.ModelAndView;
+import riomhaire.lti.adapters.token.JWKBasedJwtToMapAdapter;
+import riomhaire.lti.infrastructure.Registry;
+import riomhaire.lti.model.interfaces.Action;
+import riomhaire.lti.model.interfaces.DecodeException;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * This is the router for lti type messages - lti launch and create deep link .. this action will delegate to the
+ * appropriate sub handler
+ */
+@Slf4j
+@Builder
+public class ProcessLtiMessage implements Action<ModelAndView> {
+    public static final String CLAIM_MESSAGE_TYPE = "https://purl.imsglobal.org/spec/lti/claim/message_type";
+    public static final String LTI_RESOURCE_LINK_REQUEST = "LtiResourceLinkRequest";
+    public static final String LTI_DEEP_LINKING_REQUEST = "LtiDeepLinkingRequest";
+    Registry registry;
+    Map<String, String> queryParams;
+    Map<String, Object> metadata;
+
+
+    @Override
+    public ModelAndView execute() {
+        var idToken = queryParams.get("id_token");
+        var jwtToken = queryParams.get("JWT");
+        // Crack open so we can find the config
+        String token = (idToken != null) ? idToken : jwtToken;
+        var jwt = JWT.decode(token);
+        // Lookup client id and issuer
+        var issuer = jwt.getClaim("iss").asString();
+        var clientId = jwt.getClaim("aud").asString();
+
+        var toolRegistration = registry.clientRegistrationResolver().lookupClient(issuer, clientId);
+        var claims = new HashMap<>();
+
+        if (toolRegistration.isPresent()) {
+            // Validate JWT to verify its by who they say they are
+            var clientConfiguration = toolRegistration.get();
+            var adapter = JWKBasedJwtToMapAdapter.builder()
+                    .jwksUrl(clientConfiguration.getJwksUrl())
+                    .skipVerification(clientConfiguration.isSkipVerification())
+                    .build();
+            try {
+                claims.putAll(adapter.decode(token));
+            } catch (DecodeException e) {
+                // OK not valid
+                claims.put("error", "cannot verify token because of: " + e.toString());
+            }
+        } else {
+            claims.put("error", "cannot find client for " + issuer + "  client-id " + clientId);
+        }
+//
+        // OK based off of the message delegate to the right sub-action
+        var messageType = String.valueOf(claims.get(CLAIM_MESSAGE_TYPE));
+        return switch (messageType) {
+            case LTI_RESOURCE_LINK_REQUEST:
+                //noinspection unchecked
+                yield LaunchContent.builder()
+                        .registry(registry)
+                        .metadata(metadata)
+                        .queryParams(queryParams)
+                        .claims((Map) claims)
+                        .build()
+                        .execute();
+            case LTI_DEEP_LINKING_REQUEST:
+                //noinspection unchecked
+                yield SelectDeepLinkContent.builder()
+                        .registry(registry)
+                        .metadata(metadata)
+                        .queryParams(queryParams)
+                        .claims((Map) claims)
+                        .build()
+                        .execute();
+            default:
+                throw new IllegalStateException("Unexpected value: " + messageType);
+        };
+    }
+}

+ 79 - 0
src/main/java/riomhaire/lti/business/actions/ProcessOidcRequest.java

@@ -0,0 +1,79 @@
+package riomhaire.lti.business.actions;
+
+import lombok.Builder;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.servlet.ModelAndView;
+import org.springframework.web.servlet.view.RedirectView;
+import org.springframework.web.servlet.view.json.MappingJackson2JsonView;
+import riomhaire.lti.infrastructure.Registry;
+import riomhaire.lti.model.interfaces.Action;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.UUID;
+
+@Slf4j
+@Builder
+/***
+ *  ProcessOidcRequest handles the oidc initiation step of the oidc flow ...
+ *  you should add some checks and generate state value you can check on in later calls.
+ *
+ *  It uses a client registry (in this implementation based on a json object in redis) to locate where
+ *  the authenticate endpoint is locate.
+ */
+public class ProcessOidcRequest implements Action<ModelAndView> {
+    Registry registry;
+    Map<String, String> queryParams;
+    Map<String, Object> metadata;
+
+    /**
+     * Executte
+     * @return
+     */
+    public ModelAndView execute() {
+        final ModelAndView mav;
+        String iss = queryParams.get("iss");
+        String clientId = queryParams.get("client_id");
+        var clientRegistration = registry.clientRegistrationResolver().lookupClient(iss, clientId);
+        var claims = new HashMap<String,Object>();
+
+        if (clientRegistration.isPresent()) {
+            // Build Location
+            var location = new StringBuffer();
+            var clientConfiguration = clientRegistration.get();
+            var loginHint = queryParams.get("login_hint");
+            var ltiMessageHint = queryParams.get("lti_message_hint");
+            var targetLinkUri = queryParams.get("target_link_uri");
+
+
+            location.append(clientConfiguration.getAuthenticateUrl());
+            location.append("?");
+            location.append("scope=openid&response_type=id_token&client_id=");
+            location.append(clientConfiguration.getClientId());
+            location.append("&login_hint=");
+            location.append(loginHint);
+
+            // This should be used to map to some backend thing we can verify ... jwt etc
+            location.append("&state=");
+            final String state = UUID.randomUUID().toString();
+            location.append(state);
+
+            // This should be used to map to some backend thing we can verify ... as once only
+            location.append("&response_mode=form_post&prompt=none&nonce=");
+            location.append(UUID.randomUUID().toString());
+            location.append("&lti_message_hint=");
+            location.append(ltiMessageHint);
+            location.append("&redirect_uri=");
+            location.append(targetLinkUri);
+
+            log.info("OIDC Redirecting to "+location.toString());
+            mav = new ModelAndView(new RedirectView(location.toString()));
+
+        } else {
+            log.error("unknown issuer ["+iss+"] client ["+clientId+"]");
+            mav = new ModelAndView(new MappingJackson2JsonView());
+            mav.addObject("bean", claims);
+        }
+        return mav;
+    }
+}

+ 41 - 0
src/main/java/riomhaire/lti/business/actions/SelectDeepLinkContent.java

@@ -0,0 +1,41 @@
+package riomhaire.lti.business.actions;
+
+import lombok.Builder;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.servlet.ModelAndView;
+import riomhaire.lti.infrastructure.Registry;
+import riomhaire.lti.model.interfaces.Action;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.function.Function;
+
+@Slf4j
+@Builder
+public class SelectDeepLinkContent implements Action<ModelAndView> {
+    Registry registry;
+    Map<String, String> queryParams;
+    Map<String, Object> metadata;
+    Map<String, Object> claims;
+
+    @Override
+    public ModelAndView execute() {
+        final String prefix = "https://purl.imsglobal.org/spec/lti/claim/";
+        var simplifiedClaims = new HashMap<String,Object>();
+        var mav = new ModelAndView("dump-launch");
+//        var mav = new ModelAndView("redirect:https://www.riomhaire.com");
+
+        // Strip "https://purl.imsglobal.org/spec/lti/claim/" from keys
+        Function<String,String> simplifier = s -> {
+            if (s != null  && s.startsWith(prefix)){
+                return s.substring(prefix.length());
+            }
+            return s;
+        };
+        claims.forEach((s, o) -> simplifiedClaims.put(simplifier.apply(s),o));
+        mav.addObject("claims",simplifiedClaims);
+        mav.addObject("metadata",metadata);
+
+        return mav;
+    }
+}

+ 21 - 0
src/main/java/riomhaire/lti/infrastructure/Registry.java

@@ -0,0 +1,21 @@
+package riomhaire.lti.infrastructure;
+
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.stereotype.Service;
+import riomhaire.lti.model.interfaces.*;
+
+
+@Service
+public class Registry implements ApplicationRegistry {
+
+  @Autowired
+  @Qualifier("redisBasedClientRegistrationResolver")
+  ClientRegistrationResolver clientRegistrationResolver;
+
+  @Override
+  public ClientRegistrationResolver clientRegistrationResolver() {
+    return clientRegistrationResolver;
+  }
+}

+ 33 - 0
src/main/java/riomhaire/lti/infrastructure/api/tool/LaunchEndpoint.java

@@ -0,0 +1,33 @@
+package riomhaire.lti.infrastructure.api.tool;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.RequestHeader;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.servlet.ModelAndView;
+import riomhaire.lti.business.actions.ProcessLtiMessage;
+import riomhaire.lti.infrastructure.Registry;
+
+import java.util.Map;
+
+@RestController
+@Slf4j
+public class LaunchEndpoint {
+
+    @Autowired
+    Registry registry;
+
+    @RequestMapping(path = "/tool")
+    public ModelAndView tool(@RequestParam Map<String,String> queryParams, @RequestHeader Map<String,Object> headers) {
+
+        // Take Either of the possible tokens
+        return ProcessLtiMessage.builder()
+                .registry(registry)
+                .queryParams(queryParams)
+                .metadata(headers)
+                .build()
+                .execute();
+    }
+}

+ 36 - 0
src/main/java/riomhaire/lti/infrastructure/api/tool/OIDCEndpoint.java

@@ -0,0 +1,36 @@
+package riomhaire.lti.infrastructure.api.tool;
+
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.RequestHeader;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.servlet.ModelAndView;
+import riomhaire.lti.business.actions.ProcessOidcRequest;
+import riomhaire.lti.infrastructure.Registry;
+
+import java.util.HashMap;
+import java.util.Map;
+
+@RestController
+@Slf4j
+public class OIDCEndpoint {
+
+    @Autowired
+    Registry registry;
+
+    @RequestMapping(path = "/oidc")
+    public ModelAndView oidc(@RequestParam Map<String, String> queryParams, @RequestHeader HashMap<String, Object> headers) {
+
+        return ProcessOidcRequest.builder()
+                .registry(registry)
+                .queryParams(queryParams)
+                .metadata(headers)
+                .build()
+                .execute();
+    }
+
+
+}

+ 19 - 0
src/main/java/riomhaire/lti/infrastructure/configuration/DecoupledLogicSetup.java

@@ -0,0 +1,19 @@
+package riomhaire.lti.infrastructure.configuration;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.thymeleaf.spring5.templateresolver.SpringResourceTemplateResolver;
+
+@Slf4j
+@Configuration
+public class DecoupledLogicSetup {
+    @Bean
+    public SpringResourceTemplateResolver templateResolver() {
+        final SpringResourceTemplateResolver templateResolver = new SpringResourceTemplateResolver();
+        templateResolver.setCacheable(false);
+        templateResolver.setPrefix("classpath:/templates/");
+        templateResolver.setSuffix(".html");
+        return templateResolver;
+    }
+}

+ 19 - 0
src/main/java/riomhaire/lti/infrastructure/facades/clientregistration/ConfigBasedClientRegistrationResolver.java

@@ -0,0 +1,19 @@
+package riomhaire.lti.infrastructure.facades.clientregistration;
+
+
+
+import org.springframework.stereotype.Service;
+import riomhaire.lti.model.ClientConfiguration;
+import riomhaire.lti.model.interfaces.ClientRegistrationResolver;
+
+import java.util.Optional;
+
+
+@Service
+public class ConfigBasedClientRegistrationResolver implements ClientRegistrationResolver {
+
+    @Override
+    public Optional<ClientConfiguration> lookupClient(String issuer, String clientId) {
+        return Optional.empty();
+    }
+}

+ 34 - 0
src/main/java/riomhaire/lti/infrastructure/facades/clientregistration/RedisBasedClientRegistrationResolver.java

@@ -0,0 +1,34 @@
+package riomhaire.lti.infrastructure.facades.clientregistration;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import lombok.SneakyThrows;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.stereotype.Service;
+import org.springframework.util.DigestUtils;
+import riomhaire.lti.model.ClientConfiguration;
+import riomhaire.lti.model.interfaces.ClientRegistrationResolver;
+
+import java.util.Optional;
+
+@Service
+public class RedisBasedClientRegistrationResolver implements ClientRegistrationResolver {
+    @Autowired
+    private RedisTemplate<String, String> template;
+
+    final ObjectMapper objectMapper = new ObjectMapper();
+
+    @SneakyThrows
+    @Override
+    public Optional<ClientConfiguration> lookupClient(String issuer, String clientId) {
+        var hash =  DigestUtils.md5DigestAsHex((issuer+clientId).getBytes());
+        var key  = String.format("lti-client-registration-%s",hash);
+        var blob = template.opsForValue().get(key);
+
+        if( blob != null) {
+          ClientConfiguration client = objectMapper.readValue(blob,ClientConfiguration.class);
+          return Optional.of(client);
+        }
+        return Optional.empty();
+    }
+}

+ 23 - 0
src/main/java/riomhaire/lti/model/ClientConfiguration.java

@@ -0,0 +1,23 @@
+package riomhaire.lti.model;
+
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import lombok.Data;
+
+import java.util.List;
+
+@Data
+@JsonInclude(JsonInclude.Include.NON_NULL)
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class ClientConfiguration {
+    String id;            // Hash of issuer and clientid
+    String name;          // Name of the tool installation
+    String issuer;        // WHo issued the request - usually the LMS
+    String clientId;      // Tool Client ID
+    String jwksUrl;       // Where the public key for the issuer can be found
+    String authenticateUrl;  // The call back authentication url
+    boolean skipVerification;          // If true then skip token verification
+    List<String> deployments; // List of specific deployments of this client
+
+}

+ 5 - 0
src/main/java/riomhaire/lti/model/interfaces/Action.java

@@ -0,0 +1,5 @@
+package riomhaire.lti.model.interfaces;
+
+public interface Action<T> {
+    T execute();
+}

+ 5 - 0
src/main/java/riomhaire/lti/model/interfaces/ApplicationRegistry.java

@@ -0,0 +1,5 @@
+package riomhaire.lti.model.interfaces;
+
+public interface ApplicationRegistry {
+  ClientRegistrationResolver clientRegistrationResolver();
+}

+ 9 - 0
src/main/java/riomhaire/lti/model/interfaces/ClientRegistrationResolver.java

@@ -0,0 +1,9 @@
+package riomhaire.lti.model.interfaces;
+
+import riomhaire.lti.model.ClientConfiguration;
+
+import java.util.Optional;
+
+public interface ClientRegistrationResolver {
+    Optional<ClientConfiguration> lookupClient(String issuer, String clientId);
+}

+ 4 - 0
src/main/java/riomhaire/lti/model/interfaces/DecodeException.java

@@ -0,0 +1,4 @@
+package riomhaire.lti.model.interfaces;
+
+public class DecodeException extends Exception{
+}

+ 6 - 0
src/main/java/riomhaire/lti/model/interfaces/Decoder.java

@@ -0,0 +1,6 @@
+package riomhaire.lti.model.interfaces;
+
+@FunctionalInterface
+public interface Decoder<T,P> {
+    T decode(P token) throws DecodeException;
+}

+ 5 - 0
src/main/resources/application.properties

@@ -0,0 +1,5 @@
+spring.thymeleaf.cache=false
+spring.thymeleaf.suffix=.html
+#spring.thymeleaf.prefix=classpath:/templates/
+
+      

+ 70 - 0
src/main/resources/templates/dump-launch.html

@@ -0,0 +1,70 @@
+<!DOCTYPE HTML>
+<html xmlns:th="https://www.thymeleaf.org">
+<head>
+    <meta charset="utf-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=yes">
+    <title>Spring Boot Thymeleaf Hello World Example</title>
+    <style>
+        th {
+            min-width: 200px;
+            text-align: right;
+            background-color: #394b58;
+            color: white;
+            font-family: Serif;
+            padding-left: 10px;
+            padding-right: 10px;
+            border-radius: 5px;
+            text-overflow: ellipsis;
+            white-space: nowrap;
+            overflow: hidden;
+            vertical-align: top;
+        }
+        td {
+            text-align: left;
+            background-color: #d1edfa;
+            color: #394b58;
+            font-family: Serif;
+            padding-left: 10px;
+            padding-right: 10px;
+            border-radius: 5px;
+            text-overflow: ellipsis;
+            white-space: nowrap;
+            overflow: hidden;
+        }
+
+    </style>
+</head>
+
+<body>
+<h3>Meta Data</h3>
+<table>
+    <tr th:each="metadata : ${metadata}">
+        <th th:text="${metadata.key}">keyvalue</th>
+        <td th:text="${metadata.value}">num</td>
+    </tr>
+</table>
+
+<h3>Launch Claims</h3>
+<table title="Launch Claims">
+    <tr th:each="claim : ${claims}">
+        <th th:text="${claim.key}"></th>
+        <td th:if="${claim.value.class.name == 'java.util.ArrayList'}" >
+            <div th:each="row : ${claim.value}">
+                <div th:text="${row}"/>
+            </div>
+        </td>
+        <td th:if="${claim.value.class.name == 'org.jose4j.json.JsonUtil$DupeKeyDisallowingLinkedHashMap'}" >
+            <table>
+                <tr th:each="objmap : ${claim.value}">
+                    <th th:text="${objmap.key}"></th>
+                    <td th:text="${objmap.value}"></td>
+
+                </tr>
+            </table>
+        </td>
+        <td th:if="${claim.value.class.name == 'java.lang.String'}" th:text="${claim.value}">num</td>
+        <td th:if="${claim.value.class.name == 'java.lang.Long'}" th:text="${claim.value}">num</td>
+    </tr>
+</table>
+</body>
+</html>

+ 12 - 0
src/main/resources/templates/error.html

@@ -0,0 +1,12 @@
+<!DOCTYPE HTML>
+<html xmlns:th="https://www.thymeleaf.org">
+<head>
+    <meta charset="utf-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=yes">
+    <title>Opps - we have an error</title>
+</head>
+
+<body>
+Hmm
+</body>
+</html>

+ 8 - 0
src/redis/lti-client-registration-fc478730296ffab709218f28a22f0658.json

@@ -0,0 +1,8 @@
+{
+  "id": "368940666--705430222",
+  "name": "Canvas",
+  "clientId": "32990000000000155",
+  "issuer": "https://canvas.instructure.com",
+  "jwksUrl": "https://hmh.instructure.com/api/lti/security/jwks",
+  "authenticateUrl": "https://hmh.instructure.com/api/lti/authorize"
+}

+ 13 - 0
src/test/java/riomhaire/lti/JavaLti13ToolApplicationTests.java

@@ -0,0 +1,13 @@
+package riomhaire.lti;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.boot.test.context.SpringBootTest;
+
+@SpringBootTest
+class JavaLti13ToolApplicationTests {
+
+	@Test
+	void contextLoads() {
+	}
+
+}