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.LMSContent;
|
||||||
import ru.oa2.lti.repository.entities.Task;
|
import ru.oa2.lti.repository.entities.Task;
|
||||||
import ru.oa2.lti.service.jwt.JwtService;
|
import ru.oa2.lti.service.jwt.JwtService;
|
||||||
|
import ru.oa2.lti.service.jwt.Payload;
|
||||||
|
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
@Transactional
|
@Transactional
|
||||||
|
|
@ -23,10 +25,13 @@ public class ApplicationService {
|
||||||
this.lmsContentRepository = lmsContentRepository;
|
this.lmsContentRepository = lmsContentRepository;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getTask(LtiLogin ltiLogin) {
|
public Payload getPayload(LtiLogin ltiLogin) {
|
||||||
var payload = jwtService.getPayload(ltiLogin.getLoginHint());
|
return jwtService.getPayload(ltiLogin.getLoginHint());
|
||||||
|
}
|
||||||
|
|
||||||
var content = lmsContentRepository.getLMSContentByContentId(payload.getContextId());
|
public String getTask(UUID contextId) {
|
||||||
|
|
||||||
|
var content = lmsContentRepository.getLMSContentByContentId(contextId);
|
||||||
|
|
||||||
if(content.isPresent()) {
|
if(content.isPresent()) {
|
||||||
LMSContent lmsContent = content.get();
|
LMSContent lmsContent = content.get();
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,8 @@
|
||||||
package ru.oa2.lti.controller;
|
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 lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.stereotype.Controller;
|
import org.springframework.stereotype.Controller;
|
||||||
import org.springframework.ui.Model;
|
import org.springframework.ui.Model;
|
||||||
|
|
@ -7,6 +10,7 @@ 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.TokenService;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Controller
|
@Controller
|
||||||
|
|
@ -14,9 +18,12 @@ import ru.oa2.lti.model.LtiLogin;
|
||||||
public class LoginController {
|
public class LoginController {
|
||||||
|
|
||||||
private final ApplicationService service;
|
private final ApplicationService service;
|
||||||
|
private final TokenService tokenService;
|
||||||
|
|
||||||
public LoginController(ApplicationService applicationService) {
|
public LoginController(ApplicationService applicationService,
|
||||||
|
TokenService tokenService) {
|
||||||
this.service = applicationService;
|
this.service = applicationService;
|
||||||
|
this.tokenService = tokenService;
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/login")
|
@PostMapping("/login")
|
||||||
|
|
@ -26,7 +33,9 @@ public class LoginController {
|
||||||
@RequestParam("lti_deployment_id") String ltiDeploymentId,
|
@RequestParam("lti_deployment_id") String ltiDeploymentId,
|
||||||
@RequestParam("lti_message_hint") String ltiMessageHint,
|
@RequestParam("lti_message_hint") String ltiMessageHint,
|
||||||
@RequestParam("target_link_uri") String targetLinkUri,
|
@RequestParam("target_link_uri") String targetLinkUri,
|
||||||
RedirectAttributes redirectAttributes) {
|
HttpServletRequest request,
|
||||||
|
RedirectAttributes redirectAttributes,
|
||||||
|
ServletResponse servletResponse) {
|
||||||
|
|
||||||
var ltiLogin = LtiLogin.builder()
|
var ltiLogin = LtiLogin.builder()
|
||||||
.clientId(clientId)
|
.clientId(clientId)
|
||||||
|
|
@ -39,8 +48,20 @@ public class LoginController {
|
||||||
|
|
||||||
log.info("BODY: {}", ltiLogin);
|
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);
|
redirectAttributes.addFlashAttribute("description", data);
|
||||||
|
|
||||||
|
//TODO заполнять из data
|
||||||
redirectAttributes.addFlashAttribute("codeTitle", "Dockerfile:");
|
redirectAttributes.addFlashAttribute("codeTitle", "Dockerfile:");
|
||||||
redirectAttributes.addFlashAttribute("inputCode", "FROM maven:3.9.9-eclipse-temurin-21-jammy AS build\n" +
|
redirectAttributes.addFlashAttribute("inputCode", "FROM maven:3.9.9-eclipse-temurin-21-jammy AS build\n" +
|
||||||
"\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