WIP: работа с токеном в рамках LTI 1.3
This commit is contained in:
parent
6ef83e9511
commit
0fc9a1f266
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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" +
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue