From 0fc9a1f266e189e28bdb466bdfad383b80f6c4f5 Mon Sep 17 00:00:00 2001 From: Anton Dzyk Date: Sun, 14 Dec 2025 19:42:46 +0300 Subject: [PATCH] =?UTF-8?q?WIP:=20=D1=80=D0=B0=D0=B1=D0=BE=D1=82=D0=B0=20?= =?UTF-8?q?=D1=81=20=D1=82=D0=BE=D0=BA=D0=B5=D0=BD=D0=BE=D0=BC=20=D0=B2=20?= =?UTF-8?q?=D1=80=D0=B0=D0=BC=D0=BA=D0=B0=D1=85=20LTI=201.3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/ru/oa2/lti/ApplicationService.java | 11 +++- .../oa2/lti/controller/LoginController.java | 27 +++++++- .../service/jwt/JwtAssertionGenerator.java | 61 +++++++++++++++++++ .../ru/oa2/lti/service/jwt/TokenService.java | 56 +++++++++++++++++ src/main/java/ru/oa2/lti/utils/Keys.java | 12 ++++ 5 files changed, 161 insertions(+), 6 deletions(-) create mode 100644 src/main/java/ru/oa2/lti/service/jwt/JwtAssertionGenerator.java create mode 100644 src/main/java/ru/oa2/lti/service/jwt/TokenService.java create mode 100644 src/main/java/ru/oa2/lti/utils/Keys.java diff --git a/src/main/java/ru/oa2/lti/ApplicationService.java b/src/main/java/ru/oa2/lti/ApplicationService.java index 854f52d..2bfcfef 100644 --- a/src/main/java/ru/oa2/lti/ApplicationService.java +++ b/src/main/java/ru/oa2/lti/ApplicationService.java @@ -7,8 +7,10 @@ import ru.oa2.lti.repository.LMSContentRepository; import ru.oa2.lti.repository.entities.LMSContent; import ru.oa2.lti.repository.entities.Task; import ru.oa2.lti.service.jwt.JwtService; +import ru.oa2.lti.service.jwt.Payload; import java.util.Collection; +import java.util.UUID; @Service @Transactional @@ -23,10 +25,13 @@ public class ApplicationService { this.lmsContentRepository = lmsContentRepository; } - public String getTask(LtiLogin ltiLogin) { - var payload = jwtService.getPayload(ltiLogin.getLoginHint()); + public Payload getPayload(LtiLogin ltiLogin) { + return jwtService.getPayload(ltiLogin.getLoginHint()); + } - var content = lmsContentRepository.getLMSContentByContentId(payload.getContextId()); + public String getTask(UUID contextId) { + + var content = lmsContentRepository.getLMSContentByContentId(contextId); if(content.isPresent()) { LMSContent lmsContent = content.get(); diff --git a/src/main/java/ru/oa2/lti/controller/LoginController.java b/src/main/java/ru/oa2/lti/controller/LoginController.java index 82428f0..e254a9c 100644 --- a/src/main/java/ru/oa2/lti/controller/LoginController.java +++ b/src/main/java/ru/oa2/lti/controller/LoginController.java @@ -1,5 +1,8 @@ package ru.oa2.lti.controller; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpSession; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; @@ -7,6 +10,7 @@ import org.springframework.web.bind.annotation.*; import org.springframework.web.servlet.mvc.support.RedirectAttributes; import ru.oa2.lti.ApplicationService; import ru.oa2.lti.model.LtiLogin; +import ru.oa2.lti.service.jwt.TokenService; @Slf4j @Controller @@ -14,9 +18,12 @@ import ru.oa2.lti.model.LtiLogin; public class LoginController { private final ApplicationService service; + private final TokenService tokenService; - public LoginController(ApplicationService applicationService) { + public LoginController(ApplicationService applicationService, + TokenService tokenService) { this.service = applicationService; + this.tokenService = tokenService; } @PostMapping("/login") @@ -26,7 +33,9 @@ public class LoginController { @RequestParam("lti_deployment_id") String ltiDeploymentId, @RequestParam("lti_message_hint") String ltiMessageHint, @RequestParam("target_link_uri") String targetLinkUri, - RedirectAttributes redirectAttributes) { + HttpServletRequest request, + RedirectAttributes redirectAttributes, + ServletResponse servletResponse) { var ltiLogin = LtiLogin.builder() .clientId(clientId) @@ -39,8 +48,20 @@ public class LoginController { log.info("BODY: {}", ltiLogin); - var data = service.getTask(ltiLogin); + var payload = service.getPayload(ltiLogin); + var accessToken = tokenService.exchangeForAccessToken(ltiLogin.getClientId(), "http://openolat.local/lti/token"); + + // 4. Сохранить access token в сессии + HttpSession session = request.getSession(); + session.setAttribute("lti_access_token", accessToken); + session.setAttribute("lti_context_id", payload.getContextId()); + session.setAttribute("lti_user_id", ltiLogin.getClientId()); + + var data = service.getTask(payload.getContextId()); + redirectAttributes.addFlashAttribute("description", data); + + //TODO заполнять из data redirectAttributes.addFlashAttribute("codeTitle", "Dockerfile:"); redirectAttributes.addFlashAttribute("inputCode", "FROM maven:3.9.9-eclipse-temurin-21-jammy AS build\n" + "\n" + diff --git a/src/main/java/ru/oa2/lti/service/jwt/JwtAssertionGenerator.java b/src/main/java/ru/oa2/lti/service/jwt/JwtAssertionGenerator.java new file mode 100644 index 0000000..e5d388e --- /dev/null +++ b/src/main/java/ru/oa2/lti/service/jwt/JwtAssertionGenerator.java @@ -0,0 +1,61 @@ +package ru.oa2.lti.service.jwt; + +import com.nimbusds.jose.JWSAlgorithm; +import com.nimbusds.jose.JWSHeader; +import com.nimbusds.jose.crypto.RSASSASigner; +import com.nimbusds.jwt.JWTClaimsSet; +import com.nimbusds.jwt.SignedJWT; +import ru.oa2.lti.utils.Keys; + +import java.security.KeyFactory; +import java.security.interfaces.RSAPrivateKey; +import java.security.spec.PKCS8EncodedKeySpec; +import java.util.Base64; +import java.util.Date; +import java.util.UUID; + +public class JwtAssertionGenerator { + + + private static RSAPrivateKey loadPrivateKey() throws Exception { + String bodyKeyPEM = Keys.readFile("/home/toxa/study/siil/code/lti-provider/keys/private.pem"); + + String privateKeyPEM = bodyKeyPEM + .replace("-----BEGIN PRIVATE KEY-----", "") + .replace("-----END PRIVATE KEY-----", "") + .replaceAll("\\s", ""); + + byte[] decoded = Base64.getDecoder().decode(privateKeyPEM); + PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(decoded); + KeyFactory kf = KeyFactory.getInstance("RSA"); + return (RSAPrivateKey) kf.generatePrivate(spec); + } + + public static String generateClientAssertion( + String clientId, + String tokenEndpoint, + String keyId) throws Exception { + + RSAPrivateKey privateKey = loadPrivateKey(); + + // Заголовок + JWSHeader header = new JWSHeader.Builder(JWSAlgorithm.RS256) + .keyID(keyId) + .build(); + + // Полезная нагрузка + JWTClaimsSet claimsSet = new JWTClaimsSet.Builder() + .issuer(clientId) + .subject(clientId) + .audience(tokenEndpoint) + .jwtID(UUID.randomUUID().toString()) // уникален на каждый запрос! + .issueTime(new Date()) + .expirationTime(new Date(System.currentTimeMillis() + 300_000)) // +5 минут + .build(); + + SignedJWT signedJWT = new SignedJWT(header, claimsSet); + signedJWT.sign(new RSASSASigner(privateKey)); + + return signedJWT.serialize(); + } +} \ No newline at end of file diff --git a/src/main/java/ru/oa2/lti/service/jwt/TokenService.java b/src/main/java/ru/oa2/lti/service/jwt/TokenService.java new file mode 100644 index 0000000..a23991c --- /dev/null +++ b/src/main/java/ru/oa2/lti/service/jwt/TokenService.java @@ -0,0 +1,56 @@ +package ru.oa2.lti.service.jwt; + +import org.springframework.http.*; +import org.springframework.stereotype.Service; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.client.RestTemplate; + +import java.util.Map; + +@Service +public class TokenService { + + private final RestTemplate restTemplate = new RestTemplate(); + + // Вызывается после LTI-запуска + public String exchangeForAccessToken( + String clientId, + String tokenEndpoint) { + + try { + // Генерируем client_assertion + String clientAssertion = JwtAssertionGenerator.generateClientAssertion( + clientId, + tokenEndpoint, + "my-key-id-1" // должен совпадать с тем, что в JWKS + ); + + // Формируем тело запроса + MultiValueMap body = new LinkedMultiValueMap<>(); + body.add("grant_type", "client_credentials"); + body.add("client_assertion_type", "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"); + 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"); + + // Заголовки + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); + + HttpEntity> request = new HttpEntity<>(body, headers); + + // Отправляем запрос к OpenOLAT + ResponseEntity response = restTemplate.postForEntity(tokenEndpoint, request, Map.class); + + if (response.getStatusCode() == HttpStatus.OK) { + Map responseBody = response.getBody(); + return (String) responseBody.get("access_token"); + } else { + throw new RuntimeException("Ошибка получения токена: " + response.getStatusCode()); + } + + } catch (Exception e) { + throw new RuntimeException("Не удалось получить access token", e); + } + } +} \ No newline at end of file diff --git a/src/main/java/ru/oa2/lti/utils/Keys.java b/src/main/java/ru/oa2/lti/utils/Keys.java new file mode 100644 index 0000000..a474372 --- /dev/null +++ b/src/main/java/ru/oa2/lti/utils/Keys.java @@ -0,0 +1,12 @@ +package ru.oa2.lti.utils; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; + +public class Keys { + + public static String readFile(String filename) throws IOException { + return Files.readString(Paths.get(filename)); + } +}