diff --git a/pom.xml b/pom.xml index cdbcf49..b5c0ab4 100644 --- a/pom.xml +++ b/pom.xml @@ -11,25 +11,25 @@ org.mitre.dsmiley.httpproxy smiley-http-proxy-servlet - 1.6 + 1.12 org.imsglobal basiclti-util - 1.2.1-SNAPSHOT + 1.2.0 commons-validator commons-validator - 1.4.1 + 1.9.0 javax.servlet - servlet-api - 2.3 + javax.servlet-api + 3.1.0 provided diff --git a/src/main/java/com/pearson/developer/xapi/proxy/AuthFilter.java b/src/main/java/com/pearson/developer/xapi/proxy/AuthFilter.java index 9711e97..d007700 100644 --- a/src/main/java/com/pearson/developer/xapi/proxy/AuthFilter.java +++ b/src/main/java/com/pearson/developer/xapi/proxy/AuthFilter.java @@ -29,6 +29,8 @@ import java.util.Collections; import java.util.Enumeration; import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; import javax.servlet.*; import javax.servlet.http.*; @@ -37,6 +39,7 @@ // Validates auth from Activity Provider before replacing with auth for LRS public class AuthFilter implements Filter { + private static final Logger LOGGER = Logger.getLogger(AuthFilter.class.getName()); private FilterConfig config; public void init(FilterConfig config) throws ServletException { @@ -66,7 +69,7 @@ public void doFilter(ServletRequest request, ServletResponse response, FilterCha } } catch(Exception e) { - // do nothing + LOGGER.log(Level.WARNING, "Authentication failed for request", e); } // proceed to xAPI if session was authorized diff --git a/src/main/java/com/pearson/developer/xapi/proxy/SSOServlet.java b/src/main/java/com/pearson/developer/xapi/proxy/SSOServlet.java index 6a767c2..c3e025d 100644 --- a/src/main/java/com/pearson/developer/xapi/proxy/SSOServlet.java +++ b/src/main/java/com/pearson/developer/xapi/proxy/SSOServlet.java @@ -26,8 +26,15 @@ package com.pearson.developer.xapi.proxy; import java.io.IOException; +import java.net.URI; import java.net.URLDecoder; import java.net.URLEncoder; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; +import java.util.UUID; +import java.util.logging.Level; +import java.util.logging.Logger; import javax.servlet.http.*; @@ -35,15 +42,23 @@ import org.apache.commons.validator.routines.EmailValidator; import org.apache.commons.validator.routines.UrlValidator; -import java.util.UUID; - import org.imsglobal.lti.BasicLTIUtil; import org.imsglobal.lti.launch.LtiVerificationResult; // Performs LTI before generating the Launch Link for provided Activity Provider @SuppressWarnings("serial") public class SSOServlet extends HttpServlet { - + + private static final Logger LOGGER = Logger.getLogger(SSOServlet.class.getName()); + + /** + * Allowed redirect domains. In production, this should be configured + * externally (e.g., via init-param or environment variable). + */ + private static final Set ALLOWED_REDIRECT_DOMAINS = new HashSet<>(Arrays.asList( + // Add trusted activity provider domains here + )); + @Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException { try { @@ -83,7 +98,22 @@ protected void doPost(HttpServletRequest request, HttpServletResponse response) // the parameter is passed double encoded, so decode it once more. activityProvider = URLDecoder.decode(activityProvider,"UTF-8"); - + + // Security: validate redirect URL to prevent open redirect attacks + try { + URI redirectUri = new URI(activityProvider); + String host = redirectUri.getHost(); + if (host == null || (!ALLOWED_REDIRECT_DOMAINS.isEmpty() && !ALLOWED_REDIRECT_DOMAINS.contains(host.toLowerCase()))) { + LOGGER.log(Level.WARNING, "Blocked redirect to untrusted domain: {0}", host); + response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Untrusted redirect target"); + return; + } + } catch (Exception e) { + LOGGER.log(Level.WARNING, "Invalid activity provider URL", e); + response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Invalid redirect URL"); + return; + } + // validate the incoming data is valid try { // userId is expected to be numeric for LearningStudio (TODO - change accordingly) @@ -137,7 +167,8 @@ protected void doPost(HttpServletRequest request, HttpServletResponse response) response.sendRedirect(activityProvider); } - catch(Throwable t) { + catch(Exception e) { + LOGGER.log(Level.SEVERE, "Unexpected error processing SSO request", e); response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR,"Server Error"); } } diff --git a/src/main/java/com/pearson/developer/xapi/proxy/SecurityHeadersFilter.java b/src/main/java/com/pearson/developer/xapi/proxy/SecurityHeadersFilter.java new file mode 100644 index 0000000..f9671c1 --- /dev/null +++ b/src/main/java/com/pearson/developer/xapi/proxy/SecurityHeadersFilter.java @@ -0,0 +1,73 @@ +/* +* Experience API (xAPI) LMS Integration +* +* @category LearningStudio Sample Application - xAPI-LMS-Integration +* @copyright 2015 Pearson Education, Inc. +* @license http://www.apache.org/licenses/LICENSE-2.0 Apache 2.0 +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +package com.pearson.developer.xapi.proxy; + +import javax.servlet.*; +import javax.servlet.http.HttpServletResponse; + +/** + * Adds security-related HTTP response headers to all responses. + * Mitigates common web vulnerabilities including XSS, clickjacking, + * MIME-type sniffing, and content injection attacks. + */ +public class SecurityHeadersFilter implements Filter { + + @Override + public void init(FilterConfig filterConfig) throws ServletException { + // No initialization required + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws java.io.IOException, ServletException { + + HttpServletResponse httpResponse = (HttpServletResponse) response; + + // Prevent clickjacking attacks + httpResponse.setHeader("X-Frame-Options", "DENY"); + + // Enable browser XSS protection + httpResponse.setHeader("X-XSS-Protection", "1; mode=block"); + + // Prevent MIME-type sniffing + httpResponse.setHeader("X-Content-Type-Options", "nosniff"); + + // Content Security Policy + httpResponse.setHeader("Content-Security-Policy", + "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self'; frame-ancestors 'none'"); + + // Referrer policy + httpResponse.setHeader("Referrer-Policy", "strict-origin-when-cross-origin"); + + // Permissions policy + httpResponse.setHeader("Permissions-Policy", "geolocation=(), camera=(), microphone=()"); + + // Cache control for sensitive data + httpResponse.setHeader("Cache-Control", "no-store, no-cache, must-revalidate"); + httpResponse.setHeader("Pragma", "no-cache"); + + chain.doFilter(request, response); + } + + @Override + public void destroy() { + // No cleanup required + } +} diff --git a/src/main/java/com/pearson/developer/xapi/proxy/SessionDatabase.java b/src/main/java/com/pearson/developer/xapi/proxy/SessionDatabase.java index 87e6770..3ec4cee 100644 --- a/src/main/java/com/pearson/developer/xapi/proxy/SessionDatabase.java +++ b/src/main/java/com/pearson/developer/xapi/proxy/SessionDatabase.java @@ -25,7 +25,7 @@ */ package com.pearson.developer.xapi.proxy; -import java.util.HashMap; +import java.util.concurrent.ConcurrentHashMap; // Stores the session info issued to Activity Providers public class SessionDatabase { @@ -33,7 +33,7 @@ public class SessionDatabase { // FOR DEMO PURPOSES ONLY // This should be replaced with a persistent data source // Session needs to expire and be purged - private static final HashMap db = new HashMap(); + private static final ConcurrentHashMap db = new ConcurrentHashMap(); public static void save(String id, String token) { if(id == null || token == null || diff --git a/src/main/webapp/WEB-INF/error/404.html b/src/main/webapp/WEB-INF/error/404.html new file mode 100644 index 0000000..6251107 --- /dev/null +++ b/src/main/webapp/WEB-INF/error/404.html @@ -0,0 +1,23 @@ + + + + + + 404 - Page Not Found + + + +
+

404 - Page Not Found

+

The requested page could not be found.

+

Return to the home page

+
+ + diff --git a/src/main/webapp/WEB-INF/error/500.html b/src/main/webapp/WEB-INF/error/500.html new file mode 100644 index 0000000..8d02438 --- /dev/null +++ b/src/main/webapp/WEB-INF/error/500.html @@ -0,0 +1,23 @@ + + + + + + 500 - Server Error + + + +
+

500 - Server Error

+

An unexpected error occurred. Please try again later.

+

Return to the home page

+
+ + diff --git a/src/main/webapp/WEB-INF/web.xml b/src/main/webapp/WEB-INF/web.xml index 6ff9276..d1f96ea 100644 --- a/src/main/webapp/WEB-INF/web.xml +++ b/src/main/webapp/WEB-INF/web.xml @@ -1,8 +1,10 @@ - + + - xAPI LMS Integration @@ -19,7 +21,7 @@ Basic {lrs-basic-auth} - + XapiEndpointFilter @@ -57,7 +59,7 @@ {lti-shared-secret} - + XapiProxyServlet @@ -69,4 +71,37 @@ /sso + + + SecurityHeadersFilter + com.pearson.developer.xapi.proxy.SecurityHeadersFilter + + + SecurityHeadersFilter + /* + + + + + 30 + + true + true + + + + + + 404 + /WEB-INF/error/404.html + + + 500 + /WEB-INF/error/500.html + + + java.lang.Throwable + /WEB-INF/error/500.html + + diff --git a/src/main/webapp/index.jsp b/src/main/webapp/index.jsp index e69de29..152e088 100644 --- a/src/main/webapp/index.jsp +++ b/src/main/webapp/index.jsp @@ -0,0 +1,126 @@ +<%@ page contentType="text/html;charset=UTF-8" language="java" %> + + + + + + xAPI LMS Integration + + + + + +
+

xAPI LMS Integration

+
+ + + +
+

Welcome

+

+ This application provides an xAPI proxy for Learning Management System + integration. It enables LTI-based single sign-on and relays xAPI + statements to a configured Learning Record Store (LRS). +

+

Endpoints

+ + + + + + + + + + + + + + + + + + +
Available xAPI Proxy Endpoints
PathDescription
/ssoLTI Single Sign-On endpoint
/xapi/*xAPI proxy relay to the configured LRS
+
+ + + +