WIP: работа с токеном в рамках LTI 1.3

This commit is contained in:
Anton Dzyk 2025-12-14 19:42:46 +03:00
parent 6ef83e9511
commit 0fc9a1f266
5 changed files with 161 additions and 6 deletions

View File

@ -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();

View File

@ -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" +

View File

@ -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();
}
}

View File

@ -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<String, String> 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<MultiValueMap<String, String>> request = new HttpEntity<>(body, headers);
// Отправляем запрос к OpenOLAT
ResponseEntity<Map> response = restTemplate.postForEntity(tokenEndpoint, request, Map.class);
if (response.getStatusCode() == HttpStatus.OK) {
Map<String, Object> responseBody = response.getBody();
return (String) responseBody.get("access_token");
} else {
throw new RuntimeException("Ошибка получения токена: " + response.getStatusCode());
}
} catch (Exception e) {
throw new RuntimeException("Не удалось получить access token", e);
}
}
}

View File

@ -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));
}
}