-
Notifications
You must be signed in to change notification settings - Fork 22
LTI: поддержка протокола LTI 1.3 для интеграции с внешними инструментами. #655
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
KirillBorisovich
wants to merge
33
commits into
InteIIigeNET:master
Choose a base branch
from
KirillBorisovich:feat/lti-development
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 23 commits
Commits
Show all changes
33 commits
Select commit
Hold shift + click to select a range
e67db88
fix: made it possible to run on macOS
KirillBorisovich e96c761
feat: made a controller for OIDC
KirillBorisovich fac5635
feat: added a repository for storing the connection between HomeworkT…
KirillBorisovich e0ce973
feat: добавил возможность выбора LtiTool при создании курса
KirillBorisovich 6d1c5c3
Merge branch 'InteIIigeNET:master' into master
KirillBorisovich 6dda568
feat: почти доделал deeplinking
KirillBorisovich aed3452
feat: made front-end support for deeplinking
KirillBorisovich a74f3f0
feat: migrated the database
KirillBorisovich 47455bc
feat: debugged the deeplinking implementation and added it to the cor…
KirillBorisovich f02fd06
fix: fixed a bug with the presence of ltiLaunchUrl in the server resp…
KirillBorisovich a21edf9
refactor: deleted unnecessary folders
KirillBorisovich 1a92b69
feat: did deeplinking
KirillBorisovich a83f718
feat: made it possible to take into account the maximum score when im…
KirillBorisovich 548e1e3
feat: support test launches through LTI. Final grading is not included.
KirillBorisovich 22b513d
feat: added functionality for LTI tools to report scores
KirillBorisovich 8db5f7d
refactor: deleted MockToolController
KirillBorisovich 5233e52
fix: enabled notifications
KirillBorisovich 55c2f76
fix and refactor: fixed a bug in the signature verification in LtiDee…
KirillBorisovich 7ec4f6a
feat: added a pop-up window to warn you before you start testing
KirillBorisovich 16a57bc
fix: added a parameter required for the protocol
KirillBorisovich 4075fde
refactor: added appsettings.json
KirillBorisovich d13ff7b
Merge branch master
KirillBorisovich 03ee496
refactor: updated appsettings.json and deleted MockToolController
KirillBorisovich 7d2c0fb
feat: added LtiMockTool
KirillBorisovich cb113d1
refactor: made almost all the necessary edits
KirillBorisovich cb091b0
refactor: implement ForeignKey relationship for HomeworkTaskLtiLaunch…
KirillBorisovich efe2b8f
refactor: update appsettings.json
KirillBorisovich b9efaf0
refactor: returned the appsettings.json
KirillBorisovich b6a268b
refactor: improved the structure in LtiKeyServices and HomeworkContro…
KirillBorisovich 525c1e6
refactor: changed the EventBus credits, combined the sending and eval…
KirillBorisovich b59a18a
fix: Improved the LTI tool response parser and mitigated XSS vulnerab…
KirillBorisovich 740c562
refactor: moved the logic of filling tasks with LTI information to Ta…
KirillBorisovich 2f3c1d6
fix: fixed an inaccuracy in the protocol implementation
KirillBorisovich File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -362,3 +362,4 @@ StyleCop.Cache | |
| swagger-codegen | ||
| hwproj.front/static_dist/ | ||
| hwproj.front/dist/ | ||
| .DS_Store | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
57 changes: 57 additions & 0 deletions
57
HwProj.APIGateway/HwProj.APIGateway.API/Lti/Controllers/JwksController.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,57 @@ | ||
| using System.Security.Cryptography; | ||
| using HwProj.APIGateway.API.Lti.Models; | ||
| using Microsoft.AspNetCore.Authorization; | ||
| using Microsoft.AspNetCore.Mvc; | ||
| using Microsoft.Extensions.Options; | ||
| using Microsoft.IdentityModel.Tokens; | ||
|
|
||
| namespace HwProj.APIGateway.API.Lti.Controllers; | ||
|
|
||
| [Route("api/lti")] | ||
| [ApiController] | ||
| public class JwksController(IOptions<LtiPlatformConfig> options) : ControllerBase | ||
| { | ||
| private readonly LtiPlatformConfig _config = options.Value; | ||
|
|
||
| [HttpGet("jwks")] | ||
| [AllowAnonymous] | ||
| public IActionResult GetJwks() | ||
| { | ||
| var keyConfig = _config.SigningKey; | ||
|
|
||
| if (string.IsNullOrEmpty(keyConfig?.PrivateKeyPem)) | ||
| { | ||
| return StatusCode(500, "Signing key is not configured."); | ||
| } | ||
|
|
||
| using var rsa = RSA.Create(); | ||
| try | ||
| { | ||
| rsa.ImportFromPem(keyConfig.PrivateKeyPem); | ||
| } | ||
| catch (CryptographicException) | ||
| { | ||
| return StatusCode(500, "Invalid Private Key format in configuration."); | ||
| } | ||
|
|
||
| var publicParams = rsa.ExportParameters(false); | ||
|
|
||
| var jwks = new | ||
| { | ||
| keys = new[] | ||
| { | ||
| new | ||
| { | ||
| kty = "RSA", | ||
| e = Base64UrlEncoder.Encode(publicParams.Exponent), | ||
| n = Base64UrlEncoder.Encode(publicParams.Modulus), | ||
| kid = keyConfig.KeyId, | ||
| alg = "RS256", | ||
| use = "sig" | ||
| } | ||
| } | ||
| }; | ||
|
|
||
| return Ok(jwks); | ||
| } | ||
| } |
98 changes: 98 additions & 0 deletions
98
HwProj.APIGateway/HwProj.APIGateway.API/Lti/Controllers/LtiAccessTokenController.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,98 @@ | ||
| using System; | ||
| using System.IdentityModel.Tokens.Jwt; | ||
| using System.Threading.Tasks; | ||
| using HwProj.APIGateway.API.Lti.Models; | ||
| using HwProj.APIGateway.API.Lti.Services; | ||
| using HwProj.APIGateway.API.LTI.Services; | ||
| using Microsoft.AspNetCore.Authorization; | ||
| using Microsoft.AspNetCore.Http; | ||
| using Microsoft.AspNetCore.Mvc; | ||
| using Microsoft.Extensions.Options; | ||
| using Microsoft.IdentityModel.Tokens; | ||
|
|
||
| namespace HwProj.APIGateway.API.Lti.Controllers; | ||
|
|
||
| [Route("api/lti")] | ||
| [ApiController] | ||
| public class LtiAccessTokenController( | ||
| IOptions<LtiPlatformConfig> options, | ||
| ILtiToolService toolService, | ||
| ILtiKeyService ltiKeyService, | ||
| ILtiTokenService tokenService | ||
| ) : ControllerBase | ||
| { | ||
| [HttpPost("token")] | ||
| [AllowAnonymous] | ||
| public async Task<IActionResult> GetTokenAsync([FromForm] IFormCollection form) | ||
| { | ||
| if (!form.TryGetValue("grant_type", out var grantType) || grantType != "client_credentials") | ||
| { | ||
| return BadRequest(new { error = "unsupported_grant_type", error_description = "Only 'client_credentials' is supported." }); | ||
| } | ||
|
|
||
| if (!form.TryGetValue("client_assertion_type", out var assertionType) || | ||
| assertionType != "urn:ietf:params:oauth:client-assertion-type:jwt-bearer") | ||
| { | ||
| return BadRequest(new { error = "invalid_request", error_description = "Invalid client_assertion_type." }); | ||
| } | ||
|
|
||
| if (!form.TryGetValue("client_assertion", out var clientAssertion)) | ||
| { | ||
| return BadRequest(new { error = "invalid_request", error_description = "Missing client_assertion." }); | ||
| } | ||
|
|
||
| var handler = new JwtSecurityTokenHandler(); | ||
| if (!handler.CanReadToken(clientAssertion)) | ||
| { | ||
| return BadRequest(new { error = "invalid_client", error_description = "Invalid JWT structure." }); | ||
| } | ||
|
|
||
| var unverifiedToken = handler.ReadJwtToken(clientAssertion); | ||
|
|
||
| var clientId = unverifiedToken.Subject; | ||
|
|
||
| var tool = await toolService.GetByClientIdAsync(clientId); | ||
| if (tool == null) | ||
| { | ||
| return Unauthorized(new { error = "invalid_client", error_description = $"Unknown clientId: {clientId}" }); | ||
| } | ||
|
|
||
| var signingKeys = await ltiKeyService.GetKeysAsync(tool.JwksEndpoint); | ||
|
|
||
| try | ||
| { | ||
| var tokenEndpointUrl = options.Value.AccessTokenUrl; | ||
|
|
||
| handler.ValidateToken(clientAssertion, new TokenValidationParameters | ||
| { | ||
| ValidateIssuer = true, | ||
| ValidIssuer = unverifiedToken.Issuer, | ||
|
|
||
| ValidateAudience = true, | ||
| ValidAudience = tokenEndpointUrl, | ||
|
|
||
| ValidateLifetime = true, | ||
| ClockSkew = TimeSpan.FromMinutes(5), | ||
|
|
||
| ValidateIssuerSigningKey = true, | ||
| IssuerSigningKeys = signingKeys | ||
| }, out _); | ||
| } | ||
| catch (Exception ex) | ||
| { | ||
| return Unauthorized(new { error = "invalid_client", error_description = $"Token validation failed: {ex.Message}" }); | ||
| } | ||
|
|
||
| const string scope = "https://purl.imsglobal.org/spec/lti-ags/scope/score"; | ||
|
|
||
| var accessToken = tokenService.GenerateAccessTokenForLti(tool.ClientId, scope); | ||
|
|
||
| return Ok(new | ||
| { | ||
| access_token = accessToken, | ||
| token_type = "Bearer", | ||
| expires_in = 3600, | ||
| scope | ||
| }); | ||
| } | ||
| } |
99 changes: 99 additions & 0 deletions
99
HwProj.APIGateway/HwProj.APIGateway.API/Lti/Controllers/LtiAssignmentsGradesControllers.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,99 @@ | ||
| using System; | ||
| using System.Collections.Generic; | ||
| using System.Security.Claims; | ||
| using System.Threading.Tasks; | ||
| using HwProj.APIGateway.API.Lti.Services; | ||
| using HwProj.CoursesService.Client; | ||
| using HwProj.Models.SolutionsService; | ||
| using HwProj.SolutionsService.Client; | ||
| using Microsoft.AspNetCore.Authorization; | ||
| using Microsoft.AspNetCore.Mvc; | ||
| using LtiAdvantage.AssignmentGradeServices; | ||
|
|
||
| namespace HwProj.APIGateway.API.Lti.Controllers; | ||
|
|
||
| [Route("api/lti")] | ||
| [ApiController] | ||
| [Authorize(AuthenticationSchemes = "LtiScheme")] | ||
| public class LtiAssignmentsGradesControllers( | ||
| ICoursesServiceClient coursesServiceClient, | ||
| ISolutionsServiceClient solutionsClient, | ||
| ILtiToolService toolService) | ||
| : ControllerBase | ||
| { | ||
| [HttpPost("lineItem/{taskId}/scores")] | ||
| [Consumes("application/json", "application/vnd.ims.lis.v1.score+json")] | ||
| public async Task<IActionResult> UpdateTaskScore(long taskId, [FromBody] Score score) | ||
| { | ||
| var scopeClaim = User.FindFirst("scope")?.Value; | ||
| if (string.IsNullOrEmpty(scopeClaim) || !scopeClaim.Contains("https://purl.imsglobal.org/spec/lti-ags/scope/score")) | ||
| { | ||
| return Forbid(); | ||
| } | ||
|
|
||
| var toolClientId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value | ||
| ?? User.FindFirst("sub")?.Value; | ||
|
|
||
| if (string.IsNullOrEmpty(toolClientId)) | ||
| { | ||
| return Unauthorized("Unknown tool client id."); | ||
| } | ||
|
|
||
| var tool = await toolService.GetByClientIdAsync(toolClientId); | ||
| if (tool == null) | ||
| { | ||
| return BadRequest("Tool not found."); | ||
| } | ||
|
|
||
| var course = await coursesServiceClient.GetCourseByTaskForLti(taskId, score.UserId); | ||
| if (course == null) | ||
| { | ||
| return BadRequest("The task does not belong to any course."); | ||
| } | ||
|
|
||
| if (course.LtiToolId != tool.Id) | ||
| { | ||
| return BadRequest("This tool does not apply to this course."); | ||
| } | ||
|
|
||
| if (score.ScoreGiven < 0 || score.ScoreGiven > score.ScoreMaximum) | ||
| { | ||
| return BadRequest("ScoreGiven must be between 0 and ScoreMaximum."); | ||
| } | ||
|
|
||
| try | ||
| { | ||
| await this.SetTaskGrade(taskId, score); | ||
| return Ok(new { message = "Score updated successfully" }); | ||
| } | ||
| catch (KeyNotFoundException ex) | ||
| { | ||
| return NotFound(ex.Message); | ||
| } | ||
| catch (Exception ex) | ||
| { | ||
| return StatusCode(500, "Internal Server Error"); | ||
| } | ||
| } | ||
|
|
||
| private async Task SetTaskGrade(long taskId, Score score) | ||
| { | ||
| var postSolutionModel = new PostSolutionModel | ||
| { | ||
| StudentId = score.UserId, | ||
| LecturerComment = score.Comment, | ||
| Rating = (int)Math.Round(score.ScoreGiven) | ||
| }; | ||
|
|
||
|
|
||
| var solutionId = await solutionsClient.PostSolutionForLti(taskId, postSolutionModel); | ||
|
|
||
| var rate = new RateSolutionModel | ||
| { | ||
| Rating = (int)Math.Round(score.ScoreGiven), | ||
| LecturerComment = score.Comment | ||
| }; | ||
|
|
||
| await solutionsClient.RateSolutionForLti(solutionId, rate); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Можно объединить в один запрос/метод |
||
| } | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Давай добавим оценку в комментарий