реализация взаимодейстия LMS и external tool по LTI 1.3 с отправкой
This commit is contained in:
parent
8f96e5b3bb
commit
30bc91ee53
|
|
@ -0,0 +1,25 @@
|
||||||
|
package ru.oa2.lti.config;
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.http.HttpHeaders;
|
||||||
|
import org.springframework.http.HttpStatusCode;
|
||||||
|
import org.springframework.web.client.RestClient;
|
||||||
|
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
public class RestClientConfig {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public RestClient getRestClient(RestClient.Builder restClientBuilder) {
|
||||||
|
return restClientBuilder
|
||||||
|
.baseUrl("http://openolat.local")
|
||||||
|
.defaultHeader(HttpHeaders.USER_AGENT, "SpringBootApp")
|
||||||
|
.defaultStatusHandler(HttpStatusCode::isError, (req, res) -> {
|
||||||
|
throw new RuntimeException("API error: " + res.getStatusCode() + " body: " +
|
||||||
|
new String(res.getBody().readAllBytes(), StandardCharsets.UTF_8)); //TODO business exception
|
||||||
|
})
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -4,12 +4,14 @@ import jakarta.servlet.ServletResponse;
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import jakarta.servlet.http.HttpSession;
|
import jakarta.servlet.http.HttpSession;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.stereotype.Controller;
|
import org.springframework.stereotype.Controller;
|
||||||
import org.springframework.ui.Model;
|
import org.springframework.ui.Model;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
|
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
|
||||||
import ru.oa2.lti.ApplicationService;
|
import ru.oa2.lti.ApplicationService;
|
||||||
import ru.oa2.lti.model.LtiLogin;
|
import ru.oa2.lti.model.LtiLogin;
|
||||||
|
import ru.oa2.lti.service.jwt.Payload;
|
||||||
import ru.oa2.lti.service.jwt.TokenService;
|
import ru.oa2.lti.service.jwt.TokenService;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
|
|
@ -26,6 +28,7 @@ public class LoginController {
|
||||||
this.tokenService = tokenService;
|
this.tokenService = tokenService;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ResponseBody
|
||||||
@PostMapping("/login")
|
@PostMapping("/login")
|
||||||
public String login(@RequestParam("client_id") String clientId,
|
public String login(@RequestParam("client_id") String clientId,
|
||||||
@RequestParam("iss") String iss,
|
@RequestParam("iss") String iss,
|
||||||
|
|
@ -49,62 +52,64 @@ public class LoginController {
|
||||||
log.info("BODY: {}", ltiLogin);
|
log.info("BODY: {}", ltiLogin);
|
||||||
|
|
||||||
var payload = service.getPayload(ltiLogin);
|
var payload = service.getPayload(ltiLogin);
|
||||||
var accessToken = tokenService.exchangeForAccessToken(ltiLogin.getClientId(), "http://openolat.local/lti/token");
|
|
||||||
|
|
||||||
// 4. Сохранить access token в сессии
|
// 4. Сохранить access token в сессии
|
||||||
HttpSession session = request.getSession();
|
HttpSession session = request.getSession();
|
||||||
session.setAttribute("lti_access_token", accessToken);
|
session.setAttribute("lti_login", ltiLogin);
|
||||||
|
session.setAttribute("lti_message_hint", Long.valueOf(ltiMessageHint));
|
||||||
session.setAttribute("lti_context_id", payload.getContextId());
|
session.setAttribute("lti_context_id", payload.getContextId());
|
||||||
session.setAttribute("lti_user_id", ltiLogin.getClientId());
|
session.setAttribute("lti_user_id", ltiLogin.getClientId());
|
||||||
|
session.setAttribute("payload", payload);
|
||||||
|
|
||||||
|
if (payload.getContextId() != null) {
|
||||||
|
var body = tokenService.ltiAuth(ltiMessageHint, iss, loginHint);
|
||||||
|
return body.getBody();
|
||||||
|
}
|
||||||
|
|
||||||
|
return "redirect:/tool/lti/select";
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@GetMapping("/task")
|
||||||
|
public String showDockerTas(Model model,
|
||||||
|
HttpServletRequest request) {
|
||||||
|
|
||||||
|
var session = request.getSession();
|
||||||
|
var payload = (Payload) session.getAttribute("payload");
|
||||||
|
|
||||||
|
if (payload != null) {
|
||||||
var data = service.getTask(payload.getContextId());
|
var data = service.getTask(payload.getContextId());
|
||||||
|
|
||||||
redirectAttributes.addFlashAttribute("description", data);
|
if (data == null) {
|
||||||
|
|
||||||
//TODO заполнять из data
|
|
||||||
redirectAttributes.addFlashAttribute("codeTitle", "Dockerfile:");
|
|
||||||
redirectAttributes.addFlashAttribute("inputCode", "FROM maven:3.9.9-eclipse-temurin-21-jammy AS build\n" +
|
|
||||||
"\n" +
|
|
||||||
"COPY . /build\n" +
|
|
||||||
"WORKDIR /build\n" +
|
|
||||||
"\n" +
|
|
||||||
"RUN --mount=type=cache,target=/root/.m2/repository,rw \\\n" +
|
|
||||||
"\tmvn clean package -DskipTests -B\n" +
|
|
||||||
"\n" +
|
|
||||||
"FROM eclipse-temurin:21-jdk AS extract\n" +
|
|
||||||
"\n" +
|
|
||||||
"COPY --from=build /build/target/lti-provider-*.jar app.jar\n" +
|
|
||||||
"RUN java -Djarmode=tools -jar app.jar extract --layers --destination extracted\n" +
|
|
||||||
"\n" +
|
|
||||||
"FROM eclipse-temurin:21-jre\n" +
|
|
||||||
"\n" +
|
|
||||||
"LABEL org.opencontainers.image.title=\"LTI Provider\"\n" +
|
|
||||||
"LABEL org.opencontainers.image.description=\"LTI провайдер для лабораторных по Docker и Kubernetes\"\n" +
|
|
||||||
"LABEL org.opencontainers.image.url=\"TODO\"\n" +
|
|
||||||
"LABEL org.opencontainers.image.source=\"TODO\"\n" +
|
|
||||||
"LABEL org.opencontainers.image.documentation=\"#TODO\"\n" +
|
|
||||||
"\n" +
|
|
||||||
"WORKDIR /opt\n" +
|
|
||||||
"\n" +
|
|
||||||
"COPY --from=extract extracted/dependencies/ ./\n" +
|
|
||||||
"COPY --from=extract extracted/spring-boot-loader/ ./\n" +
|
|
||||||
"COPY --from=extract extracted/snapshot-dependencies/ ./\n" +
|
|
||||||
"COPY --from=extract extracted/application/ ./\n" +
|
|
||||||
"\n" +
|
|
||||||
"ENV TZ=\"Europe/Moscow\"\n" +
|
|
||||||
"ENV JAVA_TOOL_OPTIONS=\"-Xmx1g -Xms1g\"\n" +
|
|
||||||
"\n" +
|
|
||||||
"ENTRYPOINT [\"java\", \"-jar\", \"/opt/app.jar\"]");
|
|
||||||
|
|
||||||
return "redirect:/tool/lti/docker-task";
|
|
||||||
}
|
|
||||||
|
|
||||||
@GetMapping("/docker-task")
|
|
||||||
public String showDockerTas(Model model) {
|
|
||||||
|
|
||||||
if (!model.containsAttribute("description")) {
|
|
||||||
return "redirect:/error";
|
return "redirect:/error";
|
||||||
}
|
}
|
||||||
return "docker-task";
|
|
||||||
|
model.addAttribute("description", data);
|
||||||
|
model.addAttribute("codeTitle", "Dockerfile:");
|
||||||
|
model.addAttribute("inputCode", "FROM ...");
|
||||||
|
}
|
||||||
|
|
||||||
|
return "task";
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/select")
|
||||||
|
public String select(Model model) {
|
||||||
|
return "select";
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/redirect")
|
||||||
|
public String redirect(@RequestParam("id_token") String idToken,
|
||||||
|
@RequestParam("state") String state,
|
||||||
|
HttpServletRequest request) {
|
||||||
|
|
||||||
|
if (log.isDebugEnabled()) {
|
||||||
|
log.debug("/redirect idToken: {}, state: {}", idToken, state);
|
||||||
|
}
|
||||||
|
|
||||||
|
HttpSession session = request.getSession();
|
||||||
|
session.setAttribute("id_token", idToken);
|
||||||
|
session.setAttribute("state", state);
|
||||||
|
|
||||||
|
return "redirect:/tool/lti/task";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,22 @@
|
||||||
package ru.oa2.lti.controller;
|
package ru.oa2.lti.controller;
|
||||||
|
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpSession;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.boot.autoconfigure.web.client.RestClientBuilderConfigurer;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestBody;
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
import org.springframework.web.client.RestClient;
|
||||||
import ru.oa2.lti.ApplicationService;
|
import ru.oa2.lti.ApplicationService;
|
||||||
|
import ru.oa2.lti.model.LtiLogin;
|
||||||
|
import ru.oa2.lti.service.jwt.JwtService;
|
||||||
|
import ru.oa2.lti.service.jwt.TokenService;
|
||||||
|
import ru.oa2.lti.service.results.ResultService;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@RestController
|
@RestController
|
||||||
|
|
@ -13,17 +24,44 @@ import ru.oa2.lti.ApplicationService;
|
||||||
public class ResultController {
|
public class ResultController {
|
||||||
|
|
||||||
final ApplicationService service;
|
final ApplicationService service;
|
||||||
|
final ResultService resultService;
|
||||||
|
final TokenService tokenService;
|
||||||
|
final JwtService jwtService;
|
||||||
|
|
||||||
public ResultController(ApplicationService applicationService) {
|
public ResultController(ApplicationService applicationService,
|
||||||
|
ResultService resultService,
|
||||||
|
TokenService tokenService,
|
||||||
|
JwtService jwtService) {
|
||||||
this.service = applicationService;
|
this.service = applicationService;
|
||||||
|
this.resultService = resultService;
|
||||||
|
this.tokenService = tokenService;
|
||||||
|
this.jwtService = jwtService;
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/docker")
|
@PostMapping("/docker")
|
||||||
public String result(@RequestBody String body) {
|
public ResponseEntity result(@RequestBody String body,
|
||||||
|
HttpServletRequest req) {
|
||||||
|
|
||||||
log.info("RESULT: {}", body);
|
log.info("RESULT: {}", body);
|
||||||
|
|
||||||
|
HttpSession session = req.getSession(false);
|
||||||
|
if (session == null) return ResponseEntity.status(401).build();
|
||||||
|
|
||||||
|
var ltiLogin = (LtiLogin) session.getAttribute("lti_login");
|
||||||
|
var assessToken = tokenService.exchangeForAccessToken(ltiLogin.getClientId(), "http://openolat.local/lti/token");
|
||||||
|
|
||||||
|
String userId = (String) session.getAttribute("lti_user_id");
|
||||||
|
UUID ltiContextId = (UUID) session.getAttribute("lti_context_id");
|
||||||
|
String idToken = (String) session.getAttribute("id_token");
|
||||||
|
|
||||||
|
resultService.setResult(
|
||||||
|
assessToken,
|
||||||
|
jwtService.getTokenPayload(idToken)
|
||||||
|
);
|
||||||
|
|
||||||
//TODO возвращать json
|
//TODO возвращать json
|
||||||
return service.saveResult(body);
|
return ResponseEntity
|
||||||
|
.accepted()
|
||||||
|
.body(service.saveResult(body));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
package ru.oa2.lti.service.jwt;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class IdTokenPayload {
|
||||||
|
|
||||||
|
LocalDateTime iat;
|
||||||
|
LocalDateTime exp;
|
||||||
|
String iss;
|
||||||
|
UUID aud;
|
||||||
|
UUID sub;
|
||||||
|
String messageType;
|
||||||
|
ResourceLink resourceLink;
|
||||||
|
List<String> roles;
|
||||||
|
TokenContext context;
|
||||||
|
NamesRoleService namesRoleService;
|
||||||
|
LaunchPresentation launchPresentation;
|
||||||
|
TokenEndpoint tokenEndpoint;
|
||||||
|
String version;
|
||||||
|
UUID deploymentId;
|
||||||
|
String targetLinkUri;
|
||||||
|
}
|
||||||
|
|
@ -3,4 +3,6 @@ package ru.oa2.lti.service.jwt;
|
||||||
public interface JwtService {
|
public interface JwtService {
|
||||||
|
|
||||||
Payload getPayload(String jwt);
|
Payload getPayload(String jwt);
|
||||||
|
|
||||||
|
IdTokenPayload getTokenPayload(String jwt);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,14 @@
|
||||||
package ru.oa2.lti.service.jwt;
|
package ru.oa2.lti.service.jwt;
|
||||||
|
|
||||||
|
import com.nimbusds.jose.shaded.gson.internal.LinkedTreeMap;
|
||||||
import org.springframework.security.oauth2.jwt.JwtDecoder;
|
import org.springframework.security.oauth2.jwt.JwtDecoder;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.ZoneId;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
|
|
@ -18,14 +24,89 @@ public class JwtServiceImpl implements JwtService {
|
||||||
public Payload getPayload(String token) {
|
public Payload getPayload(String token) {
|
||||||
var jwt = jwtDecoder.decode(token);
|
var jwt = jwtDecoder.decode(token);
|
||||||
|
|
||||||
return Payload.builder()
|
var payload = new Payload();
|
||||||
.deploymentKey(jwt.getClaim("deploymentKey"))
|
for (var key : jwt.getClaims().entrySet()) {
|
||||||
.deploymentId(UUID.fromString(jwt.getClaim("deploymentId")))
|
switch (key.getKey()) {
|
||||||
.contextKey(jwt.getClaim("contextKey"))
|
case "deploymentKey" -> payload.setDeploymentKey((Long) key.getValue());
|
||||||
.contextId(UUID.fromString(jwt.getClaim("contextId")))
|
case "deploymentId" -> payload.setDeploymentId(UUID.fromString((String) key.getValue()));
|
||||||
.courseadmin(jwt.getClaim("courseadmin"))
|
case "contextKey" -> payload.setContextKey((Long) key.getValue());
|
||||||
.coach(jwt.getClaim("coach"))
|
case "contextId" -> payload.setContextId(UUID.fromString((String) key.getValue()));
|
||||||
.participant(jwt.getClaim("participant"))
|
case "courseadmin" -> payload.setCourseadmin((Boolean) key.getValue());
|
||||||
.build();
|
case "coach" -> payload.setCoach((Boolean) key.getValue());
|
||||||
|
case "participant" -> payload.setParticipant((Boolean) key.getValue());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return payload;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public IdTokenPayload getTokenPayload(String idToken) {
|
||||||
|
var jwt = jwtDecoder.decode(idToken);
|
||||||
|
|
||||||
|
var payload = new IdTokenPayload();
|
||||||
|
for (var el : jwt.getClaims().entrySet()) {
|
||||||
|
switch (el.getKey()) {
|
||||||
|
case "iat" -> payload.setIat(LocalDateTime.ofInstant((Instant) el.getValue(), ZoneId.systemDefault()));
|
||||||
|
case "exp" -> payload.setExp(LocalDateTime.ofInstant((Instant) el.getValue(), ZoneId.systemDefault()));
|
||||||
|
case "iss" -> payload.setIss((String) el.getValue());
|
||||||
|
case "aud" -> payload.setAud(UUID.fromString((String) ((ArrayList) el.getValue()).get(0)));
|
||||||
|
case "sub" -> payload.setSub(UUID.fromString((String) el.getValue()));
|
||||||
|
case "https://purl.imsglobal.org/spec/lti/claim/message_type"
|
||||||
|
-> payload.setMessageType((String) el.getValue());
|
||||||
|
case "https://purl.imsglobal.org/spec/lti/claim/resource_link"
|
||||||
|
-> {
|
||||||
|
var m = (LinkedTreeMap) el.getValue();
|
||||||
|
payload.setResourceLink(
|
||||||
|
new ResourceLink(UUID.fromString((String) m.get("id")), (String) m.get("title"))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
case "https://purl.imsglobal.org/spec/lti/claim/roles" ->
|
||||||
|
payload.setRoles((List<String>) el.getValue());
|
||||||
|
case "https://purl.imsglobal.org/spec/lti/claim/context" -> {
|
||||||
|
var tc = (LinkedTreeMap) el.getValue();
|
||||||
|
payload.setContext(
|
||||||
|
new TokenContext(
|
||||||
|
UUID.fromString((String) tc.get("id")),
|
||||||
|
(String) tc.get("label"),
|
||||||
|
(String) tc.get("title"),
|
||||||
|
(List<String>) tc.get("type")
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
case "https://purl.imsglobal.org/spec/lti-nrps/claim/namesroleservice" -> {
|
||||||
|
var nrs = (LinkedTreeMap) el.getValue();
|
||||||
|
payload.setNamesRoleService(
|
||||||
|
new NamesRoleService(
|
||||||
|
(String) nrs.get("context_memberships_url"),
|
||||||
|
(List<String>) nrs.get("service_versions")
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
case "https://purl.imsglobal.org/spec/lti/claim/launch_presentation" -> {
|
||||||
|
payload.setLaunchPresentation(new LaunchPresentation(
|
||||||
|
(String) ((LinkedTreeMap) el.getValue()).get("document_target")
|
||||||
|
));
|
||||||
|
}
|
||||||
|
case "https://purl.imsglobal.org/spec/lti-ags/claim/endpoint" -> {
|
||||||
|
var endpoint = (LinkedTreeMap) el.getValue();
|
||||||
|
payload.setTokenEndpoint(
|
||||||
|
new TokenEndpoint(
|
||||||
|
(List<String>) endpoint.get("scope"),
|
||||||
|
(String) endpoint.get("lineitems"),
|
||||||
|
(String) endpoint.get("lineitem")
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
case "https://purl.imsglobal.org/spec/lti/claim/version" ->
|
||||||
|
payload.setVersion((String) el.getValue());
|
||||||
|
case "https://purl.imsglobal.org/spec/lti/claim/deployment_id" ->
|
||||||
|
payload.setDeploymentId(UUID.fromString((String) el.getValue()));
|
||||||
|
case "https://purl.imsglobal.org/spec/lti/claim/target_link_uri" ->
|
||||||
|
payload.setTargetLinkUri((String) el.getValue());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return payload;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
package ru.oa2.lti.service.jwt;
|
||||||
|
|
||||||
|
/*
|
||||||
|
"https://purl.imsglobal.org/spec/lti/claim/launch_presentation" : {
|
||||||
|
"document_target" : "iframe"
|
||||||
|
},
|
||||||
|
*/
|
||||||
|
public record LaunchPresentation(
|
||||||
|
String documentTarget
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
package ru.oa2.lti.service.jwt;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/*
|
||||||
|
"https://purl.imsglobal.org/spec/lti-nrps/claim/namesroleservice" : {
|
||||||
|
"context_memberships_url" : "http://openolat.local/lti/nrps/b0d12061-2564-47a0-9291-b0d226f1eefe/memberships/",
|
||||||
|
"service_versions" : [ "2.0" ]
|
||||||
|
},
|
||||||
|
*/
|
||||||
|
public record NamesRoleService(
|
||||||
|
String contextMembershipsUrl,
|
||||||
|
List<String>serviceVersions
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
package ru.oa2.lti.service.jwt;
|
package ru.oa2.lti.service.jwt;
|
||||||
|
|
||||||
import lombok.Builder;
|
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
@Builder
|
@NoArgsConstructor
|
||||||
public class Payload {
|
public class Payload {
|
||||||
private Long deploymentKey;
|
private Long deploymentKey;
|
||||||
private UUID deploymentId;
|
private UUID deploymentId;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
package ru.oa2.lti.service.jwt;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public record ResourceLink(
|
||||||
|
UUID id,
|
||||||
|
String title
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
package ru.oa2.lti.service.jwt;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
|
||||||
|
public record TokenContext(
|
||||||
|
UUID id,
|
||||||
|
String label,
|
||||||
|
String title,
|
||||||
|
List<String> type
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
package ru.oa2.lti.service.jwt;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/*
|
||||||
|
"https://purl.imsglobal.org/spec/lti-ags/claim/endpoint" : {
|
||||||
|
"scope" : [ "https://purl.imsglobal.org/spec/lti-ags/scope/lineitem", "https://purl.imsglobal.org/spec/lti-ags/scope/lineitem.readonly", "https://purl.imsglobal.org/spec/lti-ags/scope/result.readonly", "https://purl.imsglobal.org/spec/lti-ags/scope/score" ],
|
||||||
|
"lineitems" : "http://openolat.local/lti/ags/b0d12061-2564-47a0-9291-b0d226f1eefe/context/112999233990970/lineitems/",
|
||||||
|
"lineitem" : "http://openolat.local/lti/ags/b0d12061-2564-47a0-9291-b0d226f1eefe/context/112999233990970/lineitems/112999233990979/lineitem"
|
||||||
|
},
|
||||||
|
*/
|
||||||
|
public record TokenEndpoint(
|
||||||
|
List<String> scope,
|
||||||
|
String lineitems,
|
||||||
|
String lineitem
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
@ -1,17 +1,25 @@
|
||||||
package ru.oa2.lti.service.jwt;
|
package ru.oa2.lti.service.jwt;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.http.*;
|
import org.springframework.http.*;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.util.LinkedMultiValueMap;
|
import org.springframework.util.LinkedMultiValueMap;
|
||||||
import org.springframework.util.MultiValueMap;
|
import org.springframework.util.MultiValueMap;
|
||||||
|
import org.springframework.web.client.RestClient;
|
||||||
import org.springframework.web.client.RestTemplate;
|
import org.springframework.web.client.RestTemplate;
|
||||||
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
@Service
|
@Service
|
||||||
public class TokenService {
|
public class TokenService {
|
||||||
|
|
||||||
private final RestTemplate restTemplate = new RestTemplate();
|
private final RestTemplate restTemplate = new RestTemplate();
|
||||||
|
private final RestClient restClient;
|
||||||
|
|
||||||
|
public TokenService(RestClient restClient) {
|
||||||
|
this.restClient = restClient;
|
||||||
|
}
|
||||||
|
|
||||||
// Вызывается после LTI-запуска
|
// Вызывается после LTI-запуска
|
||||||
public String exchangeForAccessToken(
|
public String exchangeForAccessToken(
|
||||||
|
|
@ -31,7 +39,7 @@ public class TokenService {
|
||||||
body.add("grant_type", "client_credentials");
|
body.add("grant_type", "client_credentials");
|
||||||
body.add("client_assertion_type", "urn:ietf:params:oauth:client-assertion-type:jwt-bearer");
|
body.add("client_assertion_type", "urn:ietf:params:oauth:client-assertion-type:jwt-bearer");
|
||||||
body.add("client_assertion", clientAssertion);
|
body.add("client_assertion", clientAssertion);
|
||||||
body.add("scope", "https://purl.imsglobal.org/spec/lti-ags/scope/lineitem https://purl.imsglobal.org/spec/lti-ags/scope/result");
|
body.add("scope", "https://purl.imsglobal.org/spec/lti-ags/scope/lineitem https://purl.imsglobal.org/spec/lti-ags/scope/scope https://purl.imsglobal.org/spec/lti-ags/claim/endpoint");
|
||||||
|
|
||||||
// Заголовки
|
// Заголовки
|
||||||
HttpHeaders headers = new HttpHeaders();
|
HttpHeaders headers = new HttpHeaders();
|
||||||
|
|
@ -53,4 +61,24 @@ public class TokenService {
|
||||||
throw new RuntimeException("Не удалось получить access token", e);
|
throw new RuntimeException("Не удалось получить access token", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public ResponseEntity<String> ltiAuth(String ltiLoginHint, String iss, String loginHint) {
|
||||||
|
var result = restClient
|
||||||
|
.get()
|
||||||
|
.uri("/lti/auth?" +
|
||||||
|
"state=start" +
|
||||||
|
"&code=code1" +
|
||||||
|
"&iss=" + iss +
|
||||||
|
"&login_hint=" + loginHint +
|
||||||
|
"&redirect_uri=http://openolat.local/tool/lti/redirect" +
|
||||||
|
"<i_message_hint=" + ltiLoginHint)
|
||||||
|
.retrieve();
|
||||||
|
|
||||||
|
ResponseEntity<String> body = result.toEntity(String.class);
|
||||||
|
if (log.isDebugEnabled()) {
|
||||||
|
log.debug("lti/auth RESPONSE: {}", body);
|
||||||
|
}
|
||||||
|
return body;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
package ru.oa2.lti.service.results;
|
||||||
|
|
||||||
|
public record Lineitems(
|
||||||
|
String id,
|
||||||
|
String label,
|
||||||
|
String resourceId
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
package ru.oa2.lti.service.results;
|
||||||
|
|
||||||
|
import ru.oa2.lti.service.jwt.IdTokenPayload;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public interface ResultService {
|
||||||
|
|
||||||
|
void setResult(String accessToken, IdTokenPayload idToken);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,65 @@
|
||||||
|
package ru.oa2.lti.service.results;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.web.client.RestClient;
|
||||||
|
import ru.oa2.lti.service.jwt.IdTokenPayload;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
public class ResultServiceImpl implements ResultService {
|
||||||
|
|
||||||
|
private final RestClient restClient;
|
||||||
|
private final ObjectMapper mapper;
|
||||||
|
|
||||||
|
public ResultServiceImpl(RestClient client) {
|
||||||
|
this.restClient = client;
|
||||||
|
this.mapper = new ObjectMapper();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setResult(String accessToken, IdTokenPayload idToken) {
|
||||||
|
|
||||||
|
// http://openolat.local/lti/ags/b0d12061-2564-47a0-9291-b0d226f1eefe/context/112999233990970/lineitems/112999233990979/lineitem
|
||||||
|
|
||||||
|
|
||||||
|
var resp = restClient.get()
|
||||||
|
.uri("http://openolat.local/lti/ags/b0d12061-2564-47a0-9291-b0d226f1eefe/context/112999233990970/lineitems/112999233990979/lineitem")
|
||||||
|
.header("Authorization", "Bearer " + accessToken)
|
||||||
|
.header("Content-Type", "application/vnd.ims.lis.v1.score+json")
|
||||||
|
.header("Accept", "application/vnd.ims.lis.v1.score+json")
|
||||||
|
.retrieve();
|
||||||
|
|
||||||
|
var respBody = resp.toEntity(String.class).getBody();
|
||||||
|
try {
|
||||||
|
Lineitems lineitems = mapper.readValue(respBody, Lineitems.class);
|
||||||
|
|
||||||
|
String resourceId = "112999233990970"; //TODO
|
||||||
|
String userId = "efc0b988-cfe0-4d00-9466-cf86fcf8f885"; //TODO
|
||||||
|
|
||||||
|
var result = restClient
|
||||||
|
.post()
|
||||||
|
.uri(String.format("/lti/ags/%s/context/%s/lineitems/laba/lineitem/scores/x",
|
||||||
|
idToken.getContext().id(), resourceId))
|
||||||
|
.header("Authorization", "Bearer " + accessToken)
|
||||||
|
.header("Content-Type", "application/vnd.ims.lis.v1.score+json")
|
||||||
|
.header("Accept", "application/vnd.ims.lis.v1.score+json")
|
||||||
|
.body(
|
||||||
|
String.format(
|
||||||
|
"{\"id\":\"%s\", \"userId\": \"%s\", \"scoreMaximum\": 1, \"label\":\"LTI page 1\", \"resourceId\": \"%s\"}",
|
||||||
|
lineitems.id(), userId, lineitems.resourceId()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.retrieve();
|
||||||
|
log.info("RESULT RESP: {}", result.toEntity(String.class));
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error(e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html xmlns:th="http://www.thymeleaf.org">
|
||||||
|
<body>
|
||||||
|
<h1>(.)_(.)</h1>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -125,9 +125,6 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Подключаем FontAwesome для иконок -->
|
|
||||||
<script src="https://kit.fontawesome.com/a076d05399.js" crossorigin="anonymous"></script>
|
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
document.getElementById('runButton').addEventListener('click', function() {
|
document.getElementById('runButton').addEventListener('click', function() {
|
||||||
const codeInput = document.getElementById('codeInput').value;
|
const codeInput = document.getElementById('codeInput').value;
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
package ru.oa2.lti.jwt;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.boot.test.context.SpringBootTest;
|
||||||
|
import ru.oa2.lti.config.JwtConfig;
|
||||||
|
import ru.oa2.lti.service.jwt.JwtService;
|
||||||
|
import ru.oa2.lti.service.jwt.JwtServiceImpl;
|
||||||
|
|
||||||
|
@SpringBootTest(classes = {JwtConfig.class, JwtServiceImpl.class})
|
||||||
|
public class IdTokenTest {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
JwtService jwtService;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void idTokenTest() {
|
||||||
|
//TODO как сделать вечный токен?
|
||||||
|
var token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6IjMyODYzM2M0LWQ0MmMtNDhjZi1hYmJkLTZhYzE4NGNiNDVkOSJ9.eyJpYXQiOjE3NjYwNjk2NDYsImV4cCI6MTc2NjA3MzI0NiwiaXNzIjoiaHR0cHM6Ly9vcGVub2xhdC5sb2NhbCIsImF1ZCI6IjFjYjhiZDg5LTMyYTAtNDM1MC1iZDlmLTExMDQ3NzRjOThiOCIsInN1YiI6ImVmYzBiOTg4LWNmZTAtNGQwMC05NDY2LWNmODZmY2Y4Zjg4NSIsImh0dHBzOi8vcHVybC5pbXNnbG9iYWwub3JnL3NwZWMvbHRpL2NsYWltL21lc3NhZ2VfdHlwZSI6Ikx0aVJlc291cmNlTGlua1JlcXVlc3QiLCJodHRwczovL3B1cmwuaW1zZ2xvYmFsLm9yZy9zcGVjL2x0aS9jbGFpbS9yZXNvdXJjZV9saW5rIjp7ImlkIjoiMzMyMWVjZTItMTQzYS00YzYzLTk2NGQtODQwNmI4MTlkZTk3IiwidGl0bGUiOiJEb2NrZXIifSwiaHR0cHM6Ly9wdXJsLmltc2dsb2JhbC5vcmcvc3BlYy9sdGkvY2xhaW0vcm9sZXMiOlsiaHR0cDovL3B1cmwuaW1zZ2xvYmFsLm9yZy92b2NhYi9saXMvdjIvbWVtYmVyc2hpcCNMZWFybmVyIl0sImh0dHBzOi8vcHVybC5pbXNnbG9iYWwub3JnL3NwZWMvbHRpL2NsYWltL2NvbnRleHQiOnsiaWQiOiJiMGQxMjA2MS0yNTY0LTQ3YTAtOTI5MS1iMGQyMjZmMWVlZmUiLCJsYWJlbCI6IkRvY2tlciIsInRpdGxlIjoiRG9ja2VyIiwidHlwZSI6WyJodHRwOi8vcHVybC5pbXNnbG9iYWwub3JnL3ZvY2FiL2xpcy92Mi9jb3Vyc2UjQ291cnNlU2VjdGlvbiJdfSwiaHR0cHM6Ly9wdXJsLmltc2dsb2JhbC5vcmcvc3BlYy9sdGktbnJwcy9jbGFpbS9uYW1lc3JvbGVzZXJ2aWNlIjp7ImNvbnRleHRfbWVtYmVyc2hpcHNfdXJsIjoiaHR0cDovL29wZW5vbGF0LmxvY2FsL2x0aS9ucnBzL2IwZDEyMDYxLTI1NjQtNDdhMC05MjkxLWIwZDIyNmYxZWVmZS9tZW1iZXJzaGlwcy8iLCJzZXJ2aWNlX3ZlcnNpb25zIjpbIjIuMCJdfSwiaHR0cHM6Ly9wdXJsLmltc2dsb2JhbC5vcmcvc3BlYy9sdGkvY2xhaW0vbGF1bmNoX3ByZXNlbnRhdGlvbiI6eyJkb2N1bWVudF90YXJnZXQiOiJpZnJhbWUifSwiaHR0cHM6Ly9wdXJsLmltc2dsb2JhbC5vcmcvc3BlYy9sdGktYWdzL2NsYWltL2VuZHBvaW50Ijp7InNjb3BlIjpbImh0dHBzOi8vcHVybC5pbXNnbG9iYWwub3JnL3NwZWMvbHRpLWFncy9zY29wZS9saW5laXRlbSIsImh0dHBzOi8vcHVybC5pbXNnbG9iYWwub3JnL3NwZWMvbHRpLWFncy9zY29wZS9saW5laXRlbS5yZWFkb25seSIsImh0dHBzOi8vcHVybC5pbXNnbG9iYWwub3JnL3NwZWMvbHRpLWFncy9zY29wZS9yZXN1bHQucmVhZG9ubHkiLCJodHRwczovL3B1cmwuaW1zZ2xvYmFsLm9yZy9zcGVjL2x0aS1hZ3Mvc2NvcGUvc2NvcmUiXSwibGluZWl0ZW1zIjoiaHR0cDovL29wZW5vbGF0LmxvY2FsL2x0aS9hZ3MvYjBkMTIwNjEtMjU2NC00N2EwLTkyOTEtYjBkMjI2ZjFlZWZlL2NvbnRleHQvMTEyOTk5MjMzOTkwOTcwL2xpbmVpdGVtcy8iLCJsaW5laXRlbSI6Imh0dHA6Ly9vcGVub2xhdC5sb2NhbC9sdGkvYWdzL2IwZDEyMDYxLTI1NjQtNDdhMC05MjkxLWIwZDIyNmYxZWVmZS9jb250ZXh0LzExMjk5OTIzMzk5MDk3MC9saW5laXRlbXMvMTEyOTk5MjMzOTkwOTc5L2xpbmVpdGVtIn0sImh0dHBzOi8vcHVybC5pbXNnbG9iYWwub3JnL3NwZWMvbHRpL2NsYWltL3ZlcnNpb24iOiIxLjMuMCIsImh0dHBzOi8vcHVybC5pbXNnbG9iYWwub3JnL3NwZWMvbHRpL2NsYWltL2RlcGxveW1lbnRfaWQiOiJhOWViZGM3My04MDBmLTRmNmYtOGQ0MS0xYzYyMTViYmUyN2IiLCJodHRwczovL3B1cmwuaW1zZ2xvYmFsLm9yZy9zcGVjL2x0aS9jbGFpbS90YXJnZXRfbGlua191cmkiOiJodHRwczovL29wZW5vbGF0LmxvY2FsIn0.BsB3_7_T53Uoj1YJCOeFP11Twm10wOEVNEadyR_jWp1sM5u9Xnkag_0bBkmQigHpXvXtK2wIwi2VguujYz6t5SoBREa48rCB-qilcs1glVG9vrMPHhwsd5qGbD1y8vtfDwn_LZsi9IfW6vmDsYNUj3zGKupRBCdz46C9cRAXvgWz8Wv-dE2QYqYJmN0S7OBhiSFAXYAIwYLdq07ia4b5ufgD6AQx_sptWHpzmKatlfG8K3bqgCLa6CWH2SgrAmoXxFwimrbel4D94feA9qxnwvN9oa1T2Hs_shQXpn9KpWN0xUEmcpXsaEOG-_xpR26kalVidsqDIGr3wW8XdF8UVA";
|
||||||
|
|
||||||
|
var idTokenPayload = jwtService.getTokenPayload(token);
|
||||||
|
//TODO добавить проверки
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue