/// <summary> /// Handler for creating a line item. /// </summary> /// <returns>The result.</returns> public async Task <IActionResult> OnPostDeleteLineItemAsync([FromForm(Name = "id_token")] string idToken, string lineItemUrl) { if (idToken.IsMissing()) { Error = $"{nameof(idToken)} is missing."; return(Page()); } var handler = new JwtSecurityTokenHandler(); var jwt = handler.ReadJwtToken(idToken); LtiRequest = new LtiResourceLinkRequest(jwt.Payload); var tokenResponse = await _accessTokenService.GetAccessTokenAsync( LtiRequest.Iss, Constants.LtiScopes.Ags.LineItem); // The IMS reference implementation returns "Created" with success. if (tokenResponse.IsError && tokenResponse.Error != "Created") { Error = tokenResponse.Error; return(Page()); } var httpClient = _httpClientFactory.CreateClient(); httpClient.SetBearerToken(tokenResponse.AccessToken); httpClient.DefaultRequestHeaders.Accept .Add(new MediaTypeWithQualityHeaderValue(Constants.MediaTypes.LineItem)); try { using (var response = await httpClient.DeleteAsync(lineItemUrl)) { if (!response.IsSuccessStatusCode) { Error = response.ReasonPhrase; return(Page()); } } } catch (Exception e) { Error = e.Message; return(Page()); } return(Relaunch( LtiRequest.Iss, LtiRequest.UserId, LtiRequest.ResourceLink.Id, LtiRequest.Context.Id)); }
private Assignment ConvertRequestToAssignment(LtiResourceLinkRequest ltiRequest) { return(new Assignment { ContextId = ltiRequest.Context.Id, ResourceLinkId = ltiRequest.ResourceLink.Id, Name = ltiRequest.ResourceLink.Title, CourseName = ltiRequest.Context.Title, LtiVersion = LtiVersionClass.GetLtiVersion(ltiRequest.Version), ContextMembershipsUrl = ltiRequest.NamesRoleService.ContextMembershipUrl }); }
public void CreateValidResourceLinkRequestFromScratch() { var request = new LtiResourceLinkRequest { DeploymentId = "12345", TargetLinkUri = "https://www.example.edu", ResourceLink = new ResourceLinkClaimValueType { Id = "12345" }, UserId = "12345", Lti11LegacyUserId = "12345", Roles = new[] { Role.ContextInstructor, Role.InstitutionInstructor } }; Assert.True(request.TryGetValue("https://purl.imsglobal.org/spec/lti/claim/message_type", out var messageType)); Assert.Equal("LtiResourceLinkRequest", messageType); Assert.True(request.TryGetValue("https://purl.imsglobal.org/spec/lti/claim/version", out var version)); Assert.Equal("1.3.0", version); Assert.True(request.TryGetValue("https://purl.imsglobal.org/spec/lti/claim/deployment_id", out var deploymentId)); Assert.Equal("12345", deploymentId); Assert.True(request.TryGetValue("https://purl.imsglobal.org/spec/lti/claim/target_link_uri", out var targetLinkUri)); Assert.Equal("https://www.example.edu", targetLinkUri); Assert.True(request.TryGetValue("https://purl.imsglobal.org/spec/lti/claim/resource_link", out var resourceLinkJson)); var resourceLink = (JObject)resourceLinkJson; Assert.True(resourceLink.TryGetValue("id", out var id)); Assert.Equal("12345", id); Assert.True(request.TryGetValue("sub", out var sub)); Assert.Equal("12345", sub); Assert.True(request.TryGetValue("https://purl.imsglobal.org/spec/lti/claim/lti11_legacy_user_id", out var legacyUserId)); Assert.Equal("12345", legacyUserId); Assert.True(request.TryGetValue("https://purl.imsglobal.org/spec/lti/claim/roles", out var rolesJson)); var roles = ((JArray)rolesJson).ToObject <string[]>(); Assert.Contains("http://purl.imsglobal.org/vocab/lis/v2/membership#Instructor", roles); Assert.Contains("http://purl.imsglobal.org/vocab/lis/v2/institution/person#Instructor", roles); }
public async Task <IActionResult> LtiAdvantageLaunch( [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "lti-advantage-launch/{platformId}")] HttpRequest req, [LtiAdvantage] ILtiResourceLinkRequestClient ltiRequestClient, [LtiAdvantage] INrpsClient nrpsClient, [Platform(PlatformId = "{platformId}")] Platform platform, [Assignment] IAsyncCollector <Assignment> assignmentsCollector, [DurableClient] IDurableEntityClient entityClient, string platformId) { LtiResourceLinkRequest ltiResourceLinkRequest = null; try { ltiResourceLinkRequest = await ltiRequestClient.GetLtiResourceLinkRequest(platform.JwkSetUrl, platform.ClientId, platform.Issuer); } catch (Exception e) { _logger.LogError($"Could not validate request.\n{e}"); } if (ltiResourceLinkRequest == null) { return(new BadRequestErrorMessageResult("Could not validate request.")); } string nonce = ltiResourceLinkRequest.Nonce; string state = req.Form["state"].ToString(); bool isNonceValid = await ValidateNonce(nonce, state, entityClient); if (!isNonceValid) { return(new BadRequestErrorMessageResult("Could not validate nonce.")); } Assignment assignment = ConvertRequestToAssignment(ltiResourceLinkRequest); assignment.PlatformId = platformId; _logger.LogTrace($"Parsed Assignment '{assignment.Name}'."); await assignmentsCollector.AddAsync(assignment); await assignmentsCollector.FlushAsync(); if (string.IsNullOrEmpty(assignment.Id)) { return(new InternalServerErrorResult()); } string asStudentParam = ""; if (ltiResourceLinkRequest.Roles.Contains(Role.ContextLearner) || ltiResourceLinkRequest.Roles.Contains(Role.InstitutionLearner)) { Member launchingMember = await nrpsClient.GetById(platform.ClientId, platform.AccessTokenUrl, ltiResourceLinkRequest.NamesRoleService.ContextMembershipUrl, ltiResourceLinkRequest.UserId); if (launchingMember != null && (launchingMember.Roles.Contains(Role.ContextInstructor) || launchingMember.Roles.Contains(Role.InstitutionInstructor))) { asStudentParam = "?asStudent"; } } var urlWithParams = $"{RedirectUrl}/{assignment.Id}{asStudentParam}"; _logger.LogInformation($"Redirect to {urlWithParams}"); return(new RedirectResult(urlWithParams)); }
/// <summary> /// Returns the LTI claims for an LtiResourceLinkRequest. /// </summary> /// <param name="resourceLink">The resource link.</param> /// <param name="gradebookColumn">The gradebool column for this resource link.</param> /// <param name="person">The person being authorized.</param> /// <param name="course">The course (can be null).</param> /// <param name="platform">The platform.</param> /// <returns></returns> private List <Claim> GetResourceLinkRequestClaims( ResourceLink resourceLink, GradebookColumn gradebookColumn, Person person, Course course, Platform platform) { var httpRequest = _httpContextAccessor.HttpContext.Request; var request = new LtiResourceLinkRequest { DeploymentId = resourceLink.Tool.DeploymentId, FamilyName = person.LastName, GivenName = person.FirstName, LaunchPresentation = new LaunchPresentationClaimValueType { DocumentTarget = DocumentTarget.Window, Locale = CultureInfo.CurrentUICulture.Name, ReturnUrl = $"{httpRequest.Scheme}://{httpRequest.Host}" }, Lis = new LisClaimValueType { PersonSourcedId = person.SisId, CourseSectionSourcedId = course?.SisId }, Lti11LegacyUserId = person.Id.ToString(), Platform = new PlatformClaimValueType { ContactEmail = platform.ContactEmail, Description = platform.Description, Guid = platform.Id.ToString(), Name = platform.Name, ProductFamilyCode = platform.ProductFamilyCode, Url = platform.Url, Version = platform.Version }, ResourceLink = new ResourceLinkClaimValueType { Id = resourceLink.Id.ToString(), Title = resourceLink.Title, Description = resourceLink.Description }, Roles = PeopleModel.ParsePersonRoles(person.Roles), TargetLinkUri = resourceLink.Tool.LaunchUrl }; // Add the context if the launch is from a course. if (course == null) { // Remove context roles request.Roles = request.Roles.Where(r => !r.ToString().StartsWith("Context")).ToArray(); } else { request.Context = new ContextClaimValueType { Id = course.Id.ToString(), Title = course.Name, Type = new[] { ContextType.CourseSection } }; request.AssignmentGradeServices = new AssignmentGradeServicesClaimValueType { Scope = new List <string> { Constants.LtiScopes.Ags.LineItem }, LineItemUrl = gradebookColumn == null ? null : _linkGenerator.GetUriByRouteValues(Constants.ServiceEndpoints.Ags.LineItemService, new { contextId = course.Id, lineItemId = gradebookColumn.Id }, httpRequest.Scheme, httpRequest.Host), LineItemsUrl = _linkGenerator.GetUriByRouteValues(Constants.ServiceEndpoints.Ags.LineItemsService, new { contextId = course.Id }, httpRequest.Scheme, httpRequest.Host) }; request.NamesRoleService = new NamesRoleServiceClaimValueType { ContextMembershipUrl = _linkGenerator.GetUriByRouteValues(Constants.ServiceEndpoints.Nrps.MembershipService, new { contextId = course.Id }, httpRequest.Scheme, httpRequest.Host) }; } // Collect custom properties if (!resourceLink.Tool.CustomProperties.TryConvertToDictionary(out var custom)) { custom = new Dictionary <string, string>(); } if (resourceLink.CustomProperties.TryConvertToDictionary(out var linkDictionary)) { foreach (var property in linkDictionary) { if (custom.ContainsKey(property.Key)) { custom[property.Key] = property.Value; } else { custom.Add(property.Key, property.Value); } } } // Prepare for custom property substitutions var substitutions = new CustomPropertySubstitutions { LtiUser = new LtiUser { Username = person.Username } }; request.Custom = substitutions.ReplaceCustomPropertyValues(custom); return(new List <Claim>(request.Claims)); }
/// <summary> /// Handle the LTI POST request from the Authorization Server. /// </summary> /// <returns></returns> public async Task <IActionResult> OnPostAsync( string platformId, [FromForm(Name = "id_token")] string idToken, [FromForm(Name = "scope")] string scope = null, [FromForm(Name = "state")] string state = null, [FromForm(Name = "session_state")] string sessionState = null) { // Authenticate the request starting at step 5 in the OpenId Implicit Flow // See https://www.imsglobal.org/spec/security/v1p0/#platform-originating-messages // See https://openid.net/specs/openid-connect-core-1_0.html#ImplicitFlowSteps // The Platform MUST send the id_token via the OAuth 2 Form Post // See https://www.imsglobal.org/spec/security/v1p0/#successful-authentication // See http://openid.net/specs/oauth-v2-form-post-response-mode-1_0.html if (string.IsNullOrEmpty(idToken)) { Error = "id_token is missing or empty"; return(Page()); } var handler = new JwtSecurityTokenHandler(); if (!handler.CanReadToken(idToken)) { Error = "Cannot read id_token"; return(Page()); } var jwt = handler.ReadJwtToken(idToken); JwtHeader = jwt.Header; var messageType = jwt.Claims.SingleOrDefault(c => c.Type == Constants.LtiClaims.MessageType)?.Value; if (messageType.IsMissing()) { Error = $"{Constants.LtiClaims.MessageType} claim is missing."; return(Page()); } // Authentication Response Validation // See https://www.imsglobal.org/spec/security/v1p0/#authentication-response-validation // The ID Token MUST contain a nonce Claim. var nonce = jwt.Claims.SingleOrDefault(c => c.Type == "nonce")?.Value; if (string.IsNullOrEmpty(nonce)) { Error = "Nonce is missing from request."; return(Page()); } // If the launch was initiated with a 3rd party login, then there will be a state // entry for the nonce. var memorizedState = _stateContext.GetState(nonce); if (memorizedState == null) { Error = "Invalid nonce. Possible request replay."; return(Page()); } // The state should be echoed back by the AS without modification if (memorizedState.Value != state) { Error = "Invalid state."; return(Page()); } // Look for the platform with platformId in the redirect URI var platform = await _context.GetPlatformByPlatformId(platformId); if (platform == null) { Error = "Unknown platform."; return(Page()); } // Using the JwtSecurityTokenHandler.ValidateToken method, validate four things: // // 1. The Issuer Identifier for the Platform MUST exactly match the value of the iss // (Issuer) Claim (therefore the Tool MUST previously have been made aware of this // identifier. // 2. The Tool MUST Validate the signature of the ID Token according to JSON Web Signature // RFC 7515, Section 5; using the Public Key for the Platform which collected offline. // 3. The Tool MUST validate that the aud (audience) Claim contains its client_id value // registered as an audience with the Issuer identified by the iss (Issuer) Claim. The // aud (audience) Claim MAY contain an array with more than one element. The Tool MUST // reject the ID Token if it does not list the client_id as a valid audience, or if it // contains additional audiences not trusted by the Tool. // 4. The current time MUST be before the time represented by the exp Claim; RSAParameters rsaParameters; try { var httpClient = _httpClientFactory.CreateClient(); var keySetJson = await httpClient.GetStringAsync(platform.JwkSetUrl); var keySet = JsonConvert.DeserializeObject <JsonWebKeySet>(keySetJson); var key = keySet.Keys.SingleOrDefault(k => k.Kid == jwt.Header.Kid); if (key == null) { Error = "No matching key found."; return(Page()); } rsaParameters = new RSAParameters { Modulus = Base64UrlEncoder.DecodeBytes(key.N), Exponent = Base64UrlEncoder.DecodeBytes(key.E) }; } catch (Exception e) { Error = e.Message; return(Page()); } var validationParameters = new TokenValidationParameters { ValidateTokenReplay = true, ValidateAudience = true, ValidateIssuer = true, RequireSignedTokens = true, ValidateIssuerSigningKey = true, ValidAudience = Request.GetUri().GetLeftPart(UriPartial.Authority), ValidIssuer = platform.Issuer, IssuerSigningKey = new RsaSecurityKey(rsaParameters), ValidateLifetime = true, ClockSkew = TimeSpan.FromMinutes(5.0) }; try { handler.ValidateToken(idToken, validationParameters, out _); } catch (Exception e) { Error = e.Message; return(Page()); } if (messageType == Constants.Lti.LtiDeepLinkingRequestMessageType) { return(Post("/Catalog", new { idToken })); } IdToken = idToken; LtiRequest = new LtiResourceLinkRequest(jwt.Payload); if (LtiRequest.ResourceLink.Id.Contains("YT=")) { return(Post("/ResourcePresenters/YoutubePresenter", new { LtiRequest = JsonConvert.SerializeObject(LtiRequest) })); } else if (LtiRequest.ResourceLink.Id.Contains("VI=")) { return(Post("/ResourcePresenters/VimeoPresenter", new { LtiRequest = JsonConvert.SerializeObject(LtiRequest) })); } else if (LtiRequest.Custom.ContainsKey("videoId")) { var video = _context.Videos.FirstOrDefault(v => v.Id == int.Parse(LtiRequest.Custom["videoId"])); if (video != null && video.VideoType == VideoType.Youtube) { return(Post("/ResourcePresenters/YoutubePresenter", new { LtiRequest = JsonConvert.SerializeObject(LtiRequest) })); } else if (video != null && video.VideoType == VideoType.Vimeo) { return(Post("/ResourcePresenters/VimeoPresenter", new { LtiRequest = JsonConvert.SerializeObject(LtiRequest) })); } } return(Page()); }
/// <summary> /// Handler for posting a score. /// </summary> /// <returns>The posted score.</returns> public async Task <IActionResult> OnPostPostScoreAsync([FromForm(Name = "id_token")] string idToken, string lineItemUrl) { if (idToken.IsMissing()) { Error = $"{nameof(idToken)} is missing."; return(Page()); } if (lineItemUrl.IsMissing()) { Error = $"{nameof(lineItemUrl)} is missing."; return(Page()); } var handler = new JwtSecurityTokenHandler(); var jwt = handler.ReadJwtToken(idToken); LtiRequest = new LtiResourceLinkRequest(jwt.Payload); var tokenResponse = await _accessTokenService.GetAccessTokenAsync( Request.GetUri().GetLeftPart(UriPartial.Authority), LtiRequest.Iss, Constants.LtiScopes.Ags.Score); // The IMS reference implementation returns "Created" with success. if (tokenResponse.IsError && tokenResponse.Error != "Created") { Error = tokenResponse.Error; return(Page()); } var httpClient = _httpClientFactory.CreateClient(); httpClient.SetBearerToken(tokenResponse.AccessToken); httpClient.DefaultRequestHeaders.Accept .Add(new MediaTypeWithQualityHeaderValue(Constants.MediaTypes.Score)); try { var score = new Score { ActivityProgress = ActivityProgress.Completed, GradingProgress = GradingProgess.FullyGraded, ScoreGiven = new Random().NextDouble() * 100, ScoreMaximum = 100, TimeStamp = DateTime.UtcNow, UserId = LtiRequest.UserId }; if (score.ScoreGiven > 75) { score.Comment = "Good job!"; } else if (score.ScoreGiven > 50) { score.Comment = "Not bad!"; } using (var response = await httpClient.PostAsync( lineItemUrl.EnsureTrailingSlash() + "scores", new StringContent(JsonConvert.SerializeObject(score), Encoding.UTF8, Constants.MediaTypes.Score))) { if (!response.IsSuccessStatusCode) { Error = response.ReasonPhrase; return(Page()); } } } catch (Exception e) { Error = e.Message; return(Page()); } return(Relaunch( LtiRequest.Iss, LtiRequest.UserId, LtiRequest.ResourceLink.Id, LtiRequest.Context.Id)); }
/// <summary> /// Handler for creating a line item. /// </summary> /// <returns>The result.</returns> public async Task <IActionResult> OnPostCreateLineItemAsync([FromForm(Name = "id_token")] string idToken) { if (idToken.IsMissing()) { Error = $"{nameof(idToken)} is missing."; return(Page()); } var handler = new JwtSecurityTokenHandler(); var jwt = handler.ReadJwtToken(idToken); LtiRequest = new LtiResourceLinkRequest(jwt.Payload); var tokenResponse = await _accessTokenService.GetAccessTokenAsync( Request.GetUri().GetLeftPart(UriPartial.Authority), LtiRequest.Iss, Constants.LtiScopes.Ags.LineItem); // The IMS reference implementation returns "Created" with success. if (tokenResponse.IsError && tokenResponse.Error != "Created") { Error = tokenResponse.Error; return(Page()); } var httpClient = _httpClientFactory.CreateClient(); httpClient.SetBearerToken(tokenResponse.AccessToken); httpClient.DefaultRequestHeaders.Accept .Add(new MediaTypeWithQualityHeaderValue(Constants.MediaTypes.LineItem)); try { var lineItem = new LineItem { EndDateTime = DateTime.UtcNow.AddMonths(3), Label = LtiRequest.ResourceLink.Title, ResourceLinkId = LtiRequest.ResourceLink.Id, ResourceId = Guid.NewGuid().ToString(), ScoreMaximum = 100, StartDateTime = DateTime.UtcNow }; using (var response = await httpClient.PostAsync( LtiRequest.AssignmentGradeServices.LineItemsUrl, new StringContent(JsonConvert.SerializeObject(lineItem), Encoding.UTF8, Constants.MediaTypes.LineItem))) { if (!response.IsSuccessStatusCode) { Error = response.ReasonPhrase; return(Page()); } } } catch (Exception e) { Error = e.Message; return(Page()); } return(Relaunch( LtiRequest.Iss, LtiRequest.UserId, LtiRequest.ResourceLink.Id, LtiRequest.Context.Id)); }