Browse Source

1st cut

Basic Project structure and oidc initiation and launch decode.
LMS Registration is done via localhost redis instance

Extracted into builder based actions message processing


Added Thymeleaf support to allow pretty print of messages


Cleanup
gremlin 4 years ago
parent
commit
d682229f85
25 changed files with 761 additions and 0 deletions
  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() {
+	}
+
+}