Пример #1
0
        /// <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));
        }
Пример #2
0
 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
     });
 }
Пример #3
0
        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);
        }
Пример #4
0
        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));
        }
Пример #5
0
        /// <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));
        }
Пример #6
0
        /// <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());
        }
Пример #7
0
        /// <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));
        }
Пример #8
0
        /// <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));
        }