Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -11,25 +11,25 @@
<dependency>
<groupId>org.mitre.dsmiley.httpproxy</groupId>
<artifactId>smiley-http-proxy-servlet</artifactId>
<version>1.6</version>
<version>1.12</version>
</dependency>
<!-- Apache 2.0 - https://github.com/IMSGlobal/basiclti-util-java -->
<dependency>
<groupId>org.imsglobal</groupId>
<artifactId>basiclti-util</artifactId>
<version>1.2.1-SNAPSHOT</version>
<version>1.2.0</version>
</dependency>
<!-- Apache 2.0 - https://github.com/apache/commons-validator/ -->
<dependency>
<groupId>commons-validator</groupId>
<artifactId>commons-validator</artifactId>
<version>1.4.1</version>
<version>1.9.0</version>
</dependency>
<!-- Servlet Spec - for compile only -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
<version>2.3</version>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
<scope>provided</scope>
</dependency>
</dependencies>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.*;
Expand All @@ -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 {
Expand Down Expand Up @@ -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
Expand Down
41 changes: 36 additions & 5 deletions src/main/java/com/pearson/developer/xapi/proxy/SSOServlet.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,24 +26,39 @@
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.*;

import org.apache.commons.codec.binary.Base64;
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<String> ALLOWED_REDIRECT_DOMAINS = new HashSet<>(Arrays.asList(
// Add trusted activity provider domains here
));

@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException {
try {
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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");
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,15 @@
*/
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 {

// FOR DEMO PURPOSES ONLY
// This should be replaced with a persistent data source
// Session needs to expire and be purged
private static final HashMap<String,String> db = new HashMap<String,String>();
private static final ConcurrentHashMap<String,String> db = new ConcurrentHashMap<String,String>();

public static void save(String id, String token) {
if(id == null || token == null ||
Expand Down
23 changes: 23 additions & 0 deletions src/main/webapp/WEB-INF/error/404.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>404 - Page Not Found</title>
<style>
*:focus { outline: 3px solid #005a9c; outline-offset: 2px; }
body { font-family: Arial, Helvetica, sans-serif; margin: 0; padding: 0; color: #1a1a1a; background: #fff; line-height: 1.5; }
main { max-width: 48rem; margin: 4rem auto; padding: 0 1rem; text-align: center; }
h1 { color: #003366; }
a { color: #005a9c; text-decoration: underline; }
a:hover, a:focus { color: #003366; }
</style>
</head>
<body>
<main id="main-content" role="main">
<h1>404 - Page Not Found</h1>
<p>The requested page could not be found.</p>
<p><a href="/">Return to the home page</a></p>
</main>
</body>
</html>
23 changes: 23 additions & 0 deletions src/main/webapp/WEB-INF/error/500.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>500 - Server Error</title>
<style>
*:focus { outline: 3px solid #005a9c; outline-offset: 2px; }
body { font-family: Arial, Helvetica, sans-serif; margin: 0; padding: 0; color: #1a1a1a; background: #fff; line-height: 1.5; }
main { max-width: 48rem; margin: 4rem auto; padding: 0 1rem; text-align: center; }
h1 { color: #003366; }
a { color: #005a9c; text-decoration: underline; }
a:hover, a:focus { color: #003366; }
</style>
</head>
<body>
<main id="main-content" role="main">
<h1>500 - Server Error</h1>
<p>An unexpected error occurred. Please try again later.</p>
<p><a href="/">Return to the home page</a></p>
</main>
</body>
</html>
47 changes: 41 additions & 6 deletions src/main/webapp/WEB-INF/web.xml
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
<!DOCTYPE web-app PUBLIC
"-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"http://java.sun.com/dtd/web-app_2_3.dtd" >
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
version="3.1">

<web-app>
<display-name>xAPI LMS Integration</display-name>

<!-- ensures only xAPI request are sent to the proxy -->
Expand All @@ -19,7 +21,7 @@
<param-value>Basic {lrs-basic-auth}</param-value>
</init-param>
</filter>

<!-- applies endpoint filter to proxy -->
<filter-mapping>
<filter-name>XapiEndpointFilter</filter-name>
Expand Down Expand Up @@ -57,7 +59,7 @@
<param-value>{lti-shared-secret}</param-value>
</init-param>
</servlet>

<!-- all request to /xapi/* are relayed to LRS -->
<servlet-mapping>
<servlet-name>XapiProxyServlet</servlet-name>
Expand All @@ -69,4 +71,37 @@
<url-pattern>/sso</url-pattern>
</servlet-mapping>

<!-- Security: HTTP response headers -->
<filter>
<filter-name>SecurityHeadersFilter</filter-name>
<filter-class>com.pearson.developer.xapi.proxy.SecurityHeadersFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>SecurityHeadersFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>

<!-- Security: session configuration -->
<session-config>
<session-timeout>30</session-timeout>
<cookie-config>
<http-only>true</http-only>
<secure>true</secure>
</cookie-config>
</session-config>

<!-- Security: error pages to prevent information leakage -->
<error-page>
<error-code>404</error-code>
<location>/WEB-INF/error/404.html</location>
</error-page>
<error-page>
<error-code>500</error-code>
<location>/WEB-INF/error/500.html</location>
</error-page>
<error-page>
<exception-type>java.lang.Throwable</exception-type>
<location>/WEB-INF/error/500.html</location>
</error-page>

</web-app>
Loading