playerAffiliations = Arrays.asList(playerAffiliation.split(",[ ]*"));
-
- boolean foundAdmin = false;
- boolean foundPlayer = false;
-
- for (String affiliation : adminAffiliations) {
- if (affiliations.contains(affiliation)) {
- foundAdmin = true;
- }
- }
-
- if (!foundAdmin) {
- for (String affiliation : playerAffiliations) {
- if (affiliations.contains(affiliation)) {
- foundPlayer = true;
- }
- }
- }
-
- if (foundAdmin) {
- userRole = "admin";
- ssoValid = true;
- } else if (foundPlayer) {
- userRole = "player";
- ssoValid = true;
- } else {
- ssoValid = false;
-
- errorMessage +=
- "Authorization failed. Please ensure that you are a member of one of the"
- + " following groups: ";
-
- for (String affiliation : adminAffiliations) {
- errorMessage += affiliation + ", ";
- }
-
- for (String affiliation : playerAffiliations) {
- errorMessage += affiliation + ", ";
- }
- }
-
- log.debug("userRole = " + userRole);
- }
- } else {
- String errorMsg =
- "SAML unpack properties file '" + unpackFileName + "' not found in the classpath";
- log.error(errorMsg);
- throw new RuntimeException(errorMsg);
- }
- } catch (IOException e) {
- String errorMsg =
- "SAML unpack properties file '" + unpackFileName + "' cannot be loaded";
-
- log.error(errorMsg);
- throw new RuntimeException(errorMsg);
- }
-
- if (ssoValid) {
-
- if (ssoName == null || userName == null || userRole == null) {
- String errorMsg = "Unknown error occurred when unpacking SAML properties";
-
- log.error(errorMsg);
- throw new RuntimeException(errorMsg);
- }
-
- log.debug("Saml userdata loaded, calling authUserSSO");
-
- String user[] = Getter.authUserSSO(ApplicationRoot, null, userName, ssoName, userRole);
-
- if (user != null && !user[0].isEmpty()) {
-
- // Kill Session and Create a new one with user logged in
- log.debug("Creating new session for " + user[2] + " " + user[1]);
- ses.invalidate();
- ses = request.getSession(true);
- ses.setAttribute("userStamp", user[0]);
- ses.setAttribute("userName", user[1]);
- ses.setAttribute("userRole", user[2]);
- ses.setAttribute("lang", language);
- log.debug("userClassId = " + user[4]);
-
- ses.setAttribute("userClass", user[4]);
-
- if (user[5].equalsIgnoreCase("true")) {
- log.debug("Temporary Username Detected, user will be prompted to change");
- ses.setAttribute("ChangeUsername", "true");
- }
-
- log.debug("Setting CSRF cookie");
- Cookie token = new Cookie("token", Hash.randomString());
- if (request.getRequestURL().toString().startsWith("https")) // If Requested over HTTPs
- {
- token.setSecure(true);
- }
-
- // We must set the path because the ACS servlet is in a subdir...
- token.setPath("/");
- response.addCookie(token);
-
- mustRedirect = true;
+ log.debug("User authenticated, unpacking SAML attributes...");
+ SsoUserMapper.Result ssoUser =
+ SsoUserMapper.map(profile.get().getAttributes(), loadSsoProperties());
+ String errorMessage = "";
+ boolean loggedIn = false;
+
+ if (ssoUser.isValid()) {
+ log.debug("ssoName = " + ssoUser.ssoName);
+ log.debug("userName = " + ssoUser.userName);
+ log.debug("userRole = " + ssoUser.userRole);
+ log.debug("Saml userdata loaded, calling authUserSSO");
+
+ String user[] =
+ Getter.authUserSSO(
+ ApplicationRoot, null, ssoUser.userName, ssoUser.ssoName, ssoUser.userRole);
+
+ if (user != null && !user[0].isEmpty()) {
+
+ // Kill Session and Create a new one with user logged in
+ log.debug("Creating new session for " + user[2] + " " + user[1]);
+ ses.invalidate();
+ ses = request.getSession(true);
+ ses.setAttribute("userStamp", user[0]);
+ ses.setAttribute("userName", user[1]);
+ ses.setAttribute("userRole", user[2]);
+ ses.setAttribute("lang", language);
+ // Kept for single logout, which needs the NameID and session index
+ ses.setAttribute(SamlSso.PROFILE_SESSION_ATTRIBUTE, profile.get());
+ // Lets an IdP-initiated logout end this session
+ SamlSso.registerLoginSession(ses, profile.get());
+ log.debug("userClassId = " + user[4]);
+
+ ses.setAttribute("userClass", user[4]);
+
+ if (user[5].equalsIgnoreCase("true")) {
+ log.debug("Temporary Username Detected, user will be prompted to change");
+ ses.setAttribute("ChangeUsername", "true");
+ }
- // Removing user from kick list. If they were on it before, their suspension
- // must have ended if their authentication Succeeded
- UserKicker.removeFromKicklist(user[1]);
- }
+ log.debug("Setting CSRF cookie");
+ Cookie token = new Cookie("token", Hash.randomString());
+ if (request.getRequestURL().toString().startsWith("https")) // If Requested over HTTPs
+ {
+ token.setSecure(true);
+ }
- if (mustRedirect) {
- response.sendRedirect("../index.jsp");
- } else {
- ssoValid = false;
- }
- }
+ // We must set the path because the ACS servlet is in a subdir...
+ token.setPath("/");
+ response.addCookie(token);
- if (!ssoValid) {
- log.debug("Could not authenticate");
+ // Removing user from kick list. If they were on it before, their suspension
+ // must have ended if their authentication Succeeded
+ UserKicker.removeFromKicklist(user[1]);
+ loggedIn = true;
+ }
+ } else {
+ log.debug("SSO attributes rejected: " + ssoUser.error);
+ errorMessage = ssoUser.error;
+ }
- errorMessage += "SSO login failed.";
- ses.setAttribute("loginFailed", errorMessage);
- // Lagging Response
- try {
- Thread.sleep(2000);
- } catch (InterruptedException ex) {
- Thread.currentThread().interrupt();
- }
- response.sendRedirect("../login.jsp");
- }
- }
+ if (loggedIn) {
+ response.sendRedirect("../index.jsp");
+ } else {
+ log.debug("Could not authenticate");
+ ses.setAttribute("loginFailed", errorMessage + "SSO login failed.");
+ // Lagging Response
+ try {
+ Thread.sleep(2000);
+ } catch (InterruptedException ex) {
+ Thread.currentThread().interrupt();
}
+ response.sendRedirect("../login.jsp");
}
log.debug("**** End servlets.ACS ***");
@@ -306,4 +161,25 @@ public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
response.sendRedirect("../index.jsp");
}
+
+ private Properties loadSsoProperties() {
+ String unpackFileName = "sso.properties";
+ log.debug("Loading saml unpack properties file");
+ try (InputStream inputStream =
+ getClass().getClassLoader().getResourceAsStream(unpackFileName)) {
+ if (inputStream == null) {
+ String errorMsg =
+ "SAML unpack properties file '" + unpackFileName + "' not found in the classpath";
+ log.error(errorMsg);
+ throw new RuntimeException(errorMsg);
+ }
+ Properties prop = new Properties();
+ prop.load(inputStream);
+ return prop;
+ } catch (IOException e) {
+ String errorMsg = "SAML unpack properties file '" + unpackFileName + "' cannot be loaded";
+ log.error(errorMsg);
+ throw new RuntimeException(errorMsg);
+ }
+ }
}
diff --git a/src/main/java/servlets/ChangePassword.java b/src/main/java/servlets/ChangePassword.java
index aede6289a..e4b1b232f 100644
--- a/src/main/java/servlets/ChangePassword.java
+++ b/src/main/java/servlets/ChangePassword.java
@@ -2,14 +2,14 @@
import dbProcs.Getter;
import dbProcs.Setter;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.Cookie;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
import java.io.IOException;
import java.math.BigInteger;
-import javax.servlet.ServletException;
-import javax.servlet.http.Cookie;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpSession;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import utils.ShepherdLogManager;
diff --git a/src/main/java/servlets/ChangeUsername.java b/src/main/java/servlets/ChangeUsername.java
index 4e310ce3f..7b64bd96c 100644
--- a/src/main/java/servlets/ChangeUsername.java
+++ b/src/main/java/servlets/ChangeUsername.java
@@ -1,13 +1,13 @@
package servlets;
import dbProcs.Setter;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.Cookie;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
import java.io.IOException;
-import javax.servlet.ServletException;
-import javax.servlet.http.Cookie;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpSession;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.owasp.encoder.Encode;
diff --git a/src/main/java/servlets/GetJsonScore.java b/src/main/java/servlets/GetJsonScore.java
index 158b763ae..5d07ee290 100644
--- a/src/main/java/servlets/GetJsonScore.java
+++ b/src/main/java/servlets/GetJsonScore.java
@@ -1,14 +1,14 @@
package servlets;
import dbProcs.Getter;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.Cookie;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
import java.io.IOException;
import java.io.PrintWriter;
-import javax.servlet.ServletException;
-import javax.servlet.http.Cookie;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpSession;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import servlets.admin.moduleManagement.GetFeedback;
diff --git a/src/main/java/servlets/Login.java b/src/main/java/servlets/Login.java
index cfa533154..2048d02dd 100644
--- a/src/main/java/servlets/Login.java
+++ b/src/main/java/servlets/Login.java
@@ -1,13 +1,13 @@
package servlets;
import dbProcs.Getter;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.Cookie;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
import java.io.IOException;
-import javax.servlet.ServletException;
-import javax.servlet.http.Cookie;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpSession;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import utils.Hash;
diff --git a/src/main/java/servlets/Logout.java b/src/main/java/servlets/Logout.java
index 0da068f72..7604a62c9 100644
--- a/src/main/java/servlets/Logout.java
+++ b/src/main/java/servlets/Logout.java
@@ -1,18 +1,17 @@
package servlets;
-import com.onelogin.saml2.Auth;
-import com.onelogin.saml2.exception.Error;
-import com.onelogin.saml2.exception.SettingsException;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.Cookie;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
import java.io.IOException;
-import javax.servlet.ServletException;
-import javax.servlet.http.Cookie;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpSession;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
+import org.pac4j.saml.profile.SAML2Profile;
import utils.LoginMethod;
+import utils.SamlSso;
import utils.ShepherdLogManager;
import utils.Validate;
@@ -62,6 +61,10 @@ public void doGet(HttpServletRequest request, HttpServletResponse response)
Object tokenParmeter = request.getParameter("csrfToken");
if (Validate.validateTokens(tokenCookie, tokenParmeter)) {
+ // Read before the session is invalidated: single logout needs the NameID and
+ // session index captured at login
+ Object samlProfile = ses.getAttribute(SamlSso.PROFILE_SESSION_ATTRIBUTE);
+
// Remove Everything
ses.removeAttribute("userStamp");
ses.removeAttribute("userName");
@@ -77,51 +80,12 @@ public void doGet(HttpServletRequest request, HttpServletResponse response)
response.addCookie(emptyCookie);
log.debug("User Logged Out");
- if (LoginMethod.isSaml()) {
-
- Auth auth;
- try {
- auth = new Auth(request, response);
- } catch (SettingsException e) {
- throw new RuntimeException("SAML not configured: " + e.toString());
- } catch (Error e) {
- throw new RuntimeException("SAML error : " + e.toString());
- }
-
- String nameId = null;
- if (ses.getAttribute("nameId") != null) {
- nameId = ses.getAttribute("nameId").toString();
- }
- String nameIdFormat = null;
- if (ses.getAttribute("nameIdFormat") != null) {
- nameIdFormat = ses.getAttribute("nameIdFormat").toString();
- }
- String nameidNameQualifier = null;
- if (ses.getAttribute("nameidNameQualifier") != null) {
- nameIdFormat = ses.getAttribute("nameidNameQualifier").toString();
+ if (LoginMethod.isSaml() && samlProfile instanceof SAML2Profile) {
+ if (!SamlSso.logout(request, response, (SAML2Profile) samlProfile)) {
+ log.debug("No IdP single logout URL configured, ending local session only");
+ response.sendRedirect("login.jsp");
}
- String nameidSPNameQualifier = null;
- if (ses.getAttribute("nameidSPNameQualifier") != null) {
- nameidSPNameQualifier = ses.getAttribute("nameidSPNameQualifier").toString();
- }
- String sessionIndex = null;
- if (ses.getAttribute("sessionIndex") != null) {
- sessionIndex = ses.getAttribute("sessionIndex").toString();
- }
- try {
- auth.logout(
- null,
- nameId,
- sessionIndex,
- nameIdFormat,
- nameidNameQualifier,
- nameidSPNameQualifier);
- } catch (SettingsException e) {
- throw new RuntimeException("SAML settings error : " + e.toString());
- }
-
} else {
-
response.sendRedirect("login.jsp");
}
diff --git a/src/main/java/servlets/MobileLogin.java b/src/main/java/servlets/MobileLogin.java
index 9c8e794b6..4a3d55ee3 100644
--- a/src/main/java/servlets/MobileLogin.java
+++ b/src/main/java/servlets/MobileLogin.java
@@ -1,14 +1,14 @@
package servlets;
import dbProcs.Getter;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.Cookie;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
import java.io.IOException;
import java.io.PrintWriter;
-import javax.servlet.ServletException;
-import javax.servlet.http.Cookie;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpSession;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.json.JSONObject;
diff --git a/src/main/java/servlets/Register.java b/src/main/java/servlets/Register.java
index 0cbbcf323..faad0d0f1 100644
--- a/src/main/java/servlets/Register.java
+++ b/src/main/java/servlets/Register.java
@@ -2,14 +2,14 @@
import dbProcs.Getter;
import dbProcs.Setter;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
import java.io.IOException;
import java.io.PrintWriter;
import java.sql.SQLException;
-import javax.servlet.ServletException;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpSession;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import utils.OpenRegistration;
diff --git a/src/main/java/servlets/SLS.java b/src/main/java/servlets/SLS.java
index 996f52ed6..601396eae 100644
--- a/src/main/java/servlets/SLS.java
+++ b/src/main/java/servlets/SLS.java
@@ -1,22 +1,16 @@
package servlets;
-import com.onelogin.saml2.Auth;
-import com.onelogin.saml2.exception.Error;
-import com.onelogin.saml2.exception.SettingsException;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
import java.io.IOException;
-import java.util.List;
-import javax.servlet.ServletException;
-import javax.servlet.http.Cookie;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpSession;
-import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import utils.LoginMethod;
+import utils.SamlSso;
import utils.ShepherdLogManager;
-import utils.Validate;
/**
* Control class for the SSO logout operation
@@ -41,78 +35,61 @@ public class SLS extends HttpServlet {
private static final long serialVersionUID = -5824919455464886874L;
private static final Logger log = LogManager.getLogger(SLS.class);
+ /** IdP logout messages arrive here over the HTTP-Redirect binding */
+ @Override
+ public void doGet(HttpServletRequest request, HttpServletResponse response)
+ throws ServletException, IOException {
+ handleRequest(request, response);
+ }
+
+ /** IdP logout messages arrive here over the HTTP-POST binding */
+ @Override
+ public void doPost(HttpServletRequest request, HttpServletResponse response)
+ throws ServletException, IOException {
+ handleRequest(request, response);
+ }
+
/**
- * Initiated in index.jsp. Invalidates session and Security Shepherd tokens are removed. The user
- * is logged out.
+ * Single logout service. Handles the IdP's LogoutResponse to an SP-initiated logout (see Logout),
+ * and IdP-initiated LogoutRequests, answering them with a LogoutResponse. The SAML message is
+ * validated (including its IdP signature when want_messages_signed is set), so no CSRF token is
+ * needed: the request originates from the IdP, not from a Security Shepherd page.
*
- * @param csrfToken
+ * Sessions are ended by SamlSso for the login an IdP LogoutRequest names, not by whichever
+ * session the delivering browser happens to have. A LogoutResponse ends nothing: Logout already
+ * ended the local session before sending the user to the IdP. An invalid message gets a 400,
+ * never a redirect to login.jsp (which would end the browser's session).
*/
public void handleRequest(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
+ if (!LoginMethod.isSaml()) {
+ // SSO is off (sso.enabled in sso.properties): this endpoint must not authenticate anyone,
+ // even if a SAML configuration is present
+ response.sendError(HttpServletResponse.SC_NOT_FOUND);
+ return;
+ }
// Setting IpAddress To Log and taking header for original IP if forwarded from
// proxy
ShepherdLogManager.setRequestIp(request.getRemoteAddr(), request.getHeader("X-Forwarded-For"));
log.debug("**** servlets.SLS ***");
response.setCharacterEncoding("UTF-8");
request.setCharacterEncoding("UTF-8");
- HttpSession ses = request.getSession(true);
- if (Validate.validateSession(ses)) {
- ShepherdLogManager.setRequestIp(
- request.getRemoteAddr(),
- request.getHeader("X-Forwarded-For"),
- ses.getAttribute("userName").toString());
- log.debug("Current User: " + ses.getAttribute("userName").toString());
- Cookie tokenCookie = Validate.getToken(request.getCookies());
- Object tokenParmeter = request.getParameter("csrfToken");
-
- if (LoginMethod.isSaml()) {
-
- Auth auth;
- try {
- auth = new Auth(request, response);
- } catch (SettingsException e) {
- throw new RuntimeException("SAML not configured: " + e.toString());
- } catch (Error e) {
- throw new RuntimeException("SAML error : " + e.toString());
- }
-
- try {
- auth.processSLO();
- } catch (Exception e) {
- throw new RuntimeException("SAML error when processing response: " + e.toString());
- }
-
- List errors = auth.getErrors();
- if (errors.isEmpty()) {
- log.debug("SSO Logout completed");
- } else {
- log.debug("Error when performing SSO Logout: " + StringUtils.join(errors, ", "));
- }
+ SamlSso.LogoutMessage message = SamlSso.processLogoutMessage(request, response);
+ log.debug("SAML logout message: " + message);
- if (Validate.validateTokens(tokenCookie, tokenParmeter)) {
- // Remove Everything
- ses.removeAttribute("userStamp");
- ses.removeAttribute("userName");
- ses.removeAttribute("userRole");
- // Invalid Session on server
- ses.invalidate();
- ses = request.getSession(true);
- // Remove cookie
- Cookie emptyCookie = new Cookie("token", "");
- emptyCookie.setPath("/");
- response.addCookie(emptyCookie);
- log.debug("User Logged Out");
- response.sendRedirect("../login.jsp");
- } else {
- log.error("CSRF Attack Detected");
- response.sendRedirect("../index.jsp");
- }
- }
- } else {
- log.error("SLS Function Called with no valid session");
- response.sendRedirect("../login.jsp");
+ if (message == SamlSso.LogoutMessage.INVALID) {
+ // No redirect: login.jsp invalidates the current session, so redirecting there would let
+ // any page log a user out by sending their browser here with a bogus message
+ response.sendError(HttpServletResponse.SC_BAD_REQUEST);
+ } else if (message == SamlSso.LogoutMessage.LOGOUT_RESPONSE) {
+ // Logout already ended this browser's session; if it still has one (e.g. the response was
+ // delivered to a different browser), don't end it via login.jsp
+ HttpSession ses = request.getSession(false);
+ boolean loggedIn = ses != null && ses.getAttribute("userRole") != null;
+ response.sendRedirect(loggedIn ? "../index.jsp" : "../login.jsp");
}
+ // For an IdP LogoutRequest the LogoutResponse back to the IdP has already been written
log.debug("*** END SLS ***");
}
}
diff --git a/src/main/java/servlets/SSOLogin.java b/src/main/java/servlets/SSOLogin.java
index be2229ea1..232658650 100644
--- a/src/main/java/servlets/SSOLogin.java
+++ b/src/main/java/servlets/SSOLogin.java
@@ -1,15 +1,14 @@
package servlets;
-import com.onelogin.saml2.Auth;
-import com.onelogin.saml2.exception.Error;
-import com.onelogin.saml2.exception.SettingsException;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
-import javax.servlet.ServletException;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
+import utils.LoginMethod;
+import utils.SamlSso;
import utils.ShepherdLogManager;
/**
@@ -44,16 +43,20 @@ public void doPost(HttpServletRequest request, HttpServletResponse response)
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
+ if (!LoginMethod.isSaml()) {
+ // SSO is off (sso.enabled in sso.properties): this endpoint must not authenticate anyone,
+ // even if a SAML configuration is present
+ response.sendError(HttpServletResponse.SC_NOT_FOUND);
+ return;
+ }
ShepherdLogManager.setRequestIp(request.getRemoteAddr(), request.getHeader("X-Forwarded-For"));
log.debug("**** servlets.SSOLogin ***");
request.setCharacterEncoding("UTF-8");
- Auth auth;
try {
- auth = new Auth(request, response);
- auth.login();
- } catch (SettingsException | Error e) {
+ SamlSso.login(request, response);
+ } catch (RuntimeException e) {
log.error("Caught exception when initializing SSO: " + e.toString());
- throw new RuntimeException(e);
+ throw e;
}
log.debug("**** End servlets.SSOLogin ***");
diff --git a/src/main/java/servlets/SSOMetadata.java b/src/main/java/servlets/SSOMetadata.java
index c8eb312c8..4db1a786f 100644
--- a/src/main/java/servlets/SSOMetadata.java
+++ b/src/main/java/servlets/SSOMetadata.java
@@ -1,16 +1,14 @@
package servlets;
-import com.onelogin.saml2.Auth;
-import com.onelogin.saml2.settings.Saml2Settings;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
-import java.io.PrintWriter;
-import java.util.List;
-import javax.servlet.ServletException;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
+import utils.LoginMethod;
+import utils.SamlSso;
import utils.ShepherdLogManager;
/**
@@ -45,29 +43,18 @@ public void doPost(HttpServletRequest request, HttpServletResponse response)
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
+ if (!LoginMethod.isSaml()) {
+ // SSO is off (sso.enabled in sso.properties): this endpoint must not authenticate anyone,
+ // even if a SAML configuration is present
+ response.sendError(HttpServletResponse.SC_NOT_FOUND);
+ return;
+ }
ShepherdLogManager.setRequestIp(request.getRemoteAddr(), request.getHeader("X-Forwarded-For"));
try {
- Auth auth = new Auth();
-
- Saml2Settings settings = auth.getSettings();
- settings.setSPValidationOnly(true);
- String metadata = settings.getSPMetadata();
- List errors = Saml2Settings.validateMetadata(metadata);
-
- PrintWriter out = response.getWriter();
-
- if (errors.isEmpty()) {
- response.setContentType("application/xml; charset=UTF-8");
-
- out.println(metadata);
- } else {
- response.setContentType("text/html; charset=UTF-8");
-
- for (String error : errors) {
- out.println("" + error + "
");
- }
- }
+ String metadata = SamlSso.getServiceProviderMetadata();
+ response.setContentType("application/xml; charset=UTF-8");
+ response.getWriter().println(metadata);
} catch (Exception e) {
log.error("Caught exception when initializing SSO: " + e.toString());
throw new RuntimeException(e);
diff --git a/src/main/java/servlets/Setup.java b/src/main/java/servlets/Setup.java
index bb08235f7..8b53fd653 100644
--- a/src/main/java/servlets/Setup.java
+++ b/src/main/java/servlets/Setup.java
@@ -5,6 +5,10 @@
import dbProcs.Database;
import dbProcs.MongoDatabase;
import dbProcs.Setter;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
@@ -26,10 +30,6 @@
import java.util.Properties;
import java.util.ResourceBundle;
import java.util.UUID;
-import javax.servlet.ServletException;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
import org.apache.commons.io.FileUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
diff --git a/src/main/java/servlets/SetupFilter.java b/src/main/java/servlets/SetupFilter.java
index e30421005..6eca5e496 100644
--- a/src/main/java/servlets/SetupFilter.java
+++ b/src/main/java/servlets/SetupFilter.java
@@ -1,14 +1,14 @@
package servlets;
+import jakarta.servlet.Filter;
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.FilterConfig;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.ServletRequest;
+import jakarta.servlet.ServletResponse;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
-import javax.servlet.Filter;
-import javax.servlet.FilterChain;
-import javax.servlet.FilterConfig;
-import javax.servlet.ServletException;
-import javax.servlet.ServletRequest;
-import javax.servlet.ServletResponse;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
import org.apache.commons.io.FilenameUtils;
public class SetupFilter implements Filter {
diff --git a/src/main/java/servlets/admin/config/ChangeCoreDatabase.java b/src/main/java/servlets/admin/config/ChangeCoreDatabase.java
index 32ac6fae5..8c8d5fe34 100644
--- a/src/main/java/servlets/admin/config/ChangeCoreDatabase.java
+++ b/src/main/java/servlets/admin/config/ChangeCoreDatabase.java
@@ -1,14 +1,14 @@
package servlets.admin.config;
import dbProcs.Setter;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.Cookie;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
import java.io.IOException;
import java.io.PrintWriter;
-import javax.servlet.ServletException;
-import javax.servlet.http.Cookie;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpSession;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.owasp.encoder.Encode;
diff --git a/src/main/java/servlets/admin/config/DisableCheats.java b/src/main/java/servlets/admin/config/DisableCheats.java
index 5a9a709fc..6e271f5d1 100644
--- a/src/main/java/servlets/admin/config/DisableCheats.java
+++ b/src/main/java/servlets/admin/config/DisableCheats.java
@@ -1,13 +1,13 @@
package servlets.admin.config;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.Cookie;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
import java.io.IOException;
import java.io.PrintWriter;
-import javax.servlet.ServletException;
-import javax.servlet.http.Cookie;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpSession;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import utils.CheatSheetStatus;
diff --git a/src/main/java/servlets/admin/config/DisableFeedback.java b/src/main/java/servlets/admin/config/DisableFeedback.java
index db8d26f1c..5e373417e 100644
--- a/src/main/java/servlets/admin/config/DisableFeedback.java
+++ b/src/main/java/servlets/admin/config/DisableFeedback.java
@@ -1,13 +1,13 @@
package servlets.admin.config;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.Cookie;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
import java.io.IOException;
import java.io.PrintWriter;
-import javax.servlet.ServletException;
-import javax.servlet.http.Cookie;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpSession;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import utils.FeedbackStatus;
diff --git a/src/main/java/servlets/admin/config/DisableScoreboard.java b/src/main/java/servlets/admin/config/DisableScoreboard.java
index 96d59cde1..cec9a3d99 100644
--- a/src/main/java/servlets/admin/config/DisableScoreboard.java
+++ b/src/main/java/servlets/admin/config/DisableScoreboard.java
@@ -1,13 +1,13 @@
package servlets.admin.config;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.Cookie;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
import java.io.IOException;
import java.io.PrintWriter;
-import javax.servlet.ServletException;
-import javax.servlet.http.Cookie;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpSession;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import utils.ScoreboardStatus;
diff --git a/src/main/java/servlets/admin/config/EnableCheats.java b/src/main/java/servlets/admin/config/EnableCheats.java
index 563101758..4a4c7d46c 100644
--- a/src/main/java/servlets/admin/config/EnableCheats.java
+++ b/src/main/java/servlets/admin/config/EnableCheats.java
@@ -1,13 +1,13 @@
package servlets.admin.config;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.Cookie;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
import java.io.IOException;
import java.io.PrintWriter;
-import javax.servlet.ServletException;
-import javax.servlet.http.Cookie;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpSession;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import utils.CheatSheetStatus;
diff --git a/src/main/java/servlets/admin/config/EnableFeedback.java b/src/main/java/servlets/admin/config/EnableFeedback.java
index 6503a0124..7bb44ea19 100644
--- a/src/main/java/servlets/admin/config/EnableFeedback.java
+++ b/src/main/java/servlets/admin/config/EnableFeedback.java
@@ -1,13 +1,13 @@
package servlets.admin.config;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.Cookie;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
import java.io.IOException;
import java.io.PrintWriter;
-import javax.servlet.ServletException;
-import javax.servlet.http.Cookie;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpSession;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import utils.FeedbackStatus;
diff --git a/src/main/java/servlets/admin/config/EnableScoreboard.java b/src/main/java/servlets/admin/config/EnableScoreboard.java
index c1f34db06..5d960caff 100644
--- a/src/main/java/servlets/admin/config/EnableScoreboard.java
+++ b/src/main/java/servlets/admin/config/EnableScoreboard.java
@@ -1,14 +1,14 @@
package servlets.admin.config;
import dbProcs.Getter;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.Cookie;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
import java.io.IOException;
import java.io.PrintWriter;
-import javax.servlet.ServletException;
-import javax.servlet.http.Cookie;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpSession;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.owasp.encoder.Encode;
diff --git a/src/main/java/servlets/admin/config/SetCountdown.java b/src/main/java/servlets/admin/config/SetCountdown.java
index ea18228a0..cacccb5f0 100644
--- a/src/main/java/servlets/admin/config/SetCountdown.java
+++ b/src/main/java/servlets/admin/config/SetCountdown.java
@@ -1,14 +1,14 @@
package servlets.admin.config;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.Cookie;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
import java.io.IOException;
import java.io.PrintWriter;
import java.time.LocalDateTime;
-import javax.servlet.ServletException;
-import javax.servlet.http.Cookie;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpSession;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import utils.CountdownHandler;
diff --git a/src/main/java/servlets/admin/config/SetCtfMode.java b/src/main/java/servlets/admin/config/SetCtfMode.java
index 8d1652f38..71bda8cda 100644
--- a/src/main/java/servlets/admin/config/SetCtfMode.java
+++ b/src/main/java/servlets/admin/config/SetCtfMode.java
@@ -1,13 +1,13 @@
package servlets.admin.config;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.Cookie;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
import java.io.IOException;
import java.io.PrintWriter;
-import javax.servlet.ServletException;
-import javax.servlet.http.Cookie;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpSession;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import utils.ModulePlan;
diff --git a/src/main/java/servlets/admin/config/SetOpenFloorMode.java b/src/main/java/servlets/admin/config/SetOpenFloorMode.java
index ba988ef31..d4b147285 100644
--- a/src/main/java/servlets/admin/config/SetOpenFloorMode.java
+++ b/src/main/java/servlets/admin/config/SetOpenFloorMode.java
@@ -1,13 +1,13 @@
package servlets.admin.config;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.Cookie;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
import java.io.IOException;
import java.io.PrintWriter;
-import javax.servlet.ServletException;
-import javax.servlet.http.Cookie;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpSession;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import utils.ModulePlan;
diff --git a/src/main/java/servlets/admin/config/SetTournamentMode.java b/src/main/java/servlets/admin/config/SetTournamentMode.java
index 7dc613232..fe39298a7 100644
--- a/src/main/java/servlets/admin/config/SetTournamentMode.java
+++ b/src/main/java/servlets/admin/config/SetTournamentMode.java
@@ -1,13 +1,13 @@
package servlets.admin.config;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.Cookie;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
import java.io.IOException;
import java.io.PrintWriter;
-import javax.servlet.ServletException;
-import javax.servlet.http.Cookie;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpSession;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import utils.ModulePlan;
diff --git a/src/main/java/servlets/admin/config/ToggleRegistration.java b/src/main/java/servlets/admin/config/ToggleRegistration.java
index b080e9e72..38c938b60 100644
--- a/src/main/java/servlets/admin/config/ToggleRegistration.java
+++ b/src/main/java/servlets/admin/config/ToggleRegistration.java
@@ -1,13 +1,13 @@
package servlets.admin.config;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.Cookie;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
import java.io.IOException;
import java.io.PrintWriter;
-import javax.servlet.ServletException;
-import javax.servlet.http.Cookie;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpSession;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import utils.OpenRegistration;
diff --git a/src/main/java/servlets/admin/moduleManagement/CloseAllModules.java b/src/main/java/servlets/admin/moduleManagement/CloseAllModules.java
index 1bfd04b37..6e6934916 100644
--- a/src/main/java/servlets/admin/moduleManagement/CloseAllModules.java
+++ b/src/main/java/servlets/admin/moduleManagement/CloseAllModules.java
@@ -1,14 +1,14 @@
package servlets.admin.moduleManagement;
import dbProcs.Setter;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.Cookie;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
import java.io.IOException;
import java.io.PrintWriter;
-import javax.servlet.ServletException;
-import javax.servlet.http.Cookie;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpSession;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import utils.ShepherdLogManager;
diff --git a/src/main/java/servlets/admin/moduleManagement/EnableModuleBlock.java b/src/main/java/servlets/admin/moduleManagement/EnableModuleBlock.java
index 2cb6c30c4..82ca153eb 100644
--- a/src/main/java/servlets/admin/moduleManagement/EnableModuleBlock.java
+++ b/src/main/java/servlets/admin/moduleManagement/EnableModuleBlock.java
@@ -1,14 +1,14 @@
package servlets.admin.moduleManagement;
import dbProcs.Getter;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.Cookie;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
import java.io.IOException;
import java.io.PrintWriter;
-import javax.servlet.ServletException;
-import javax.servlet.http.Cookie;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpSession;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import utils.ModuleBlock;
diff --git a/src/main/java/servlets/admin/moduleManagement/GetFeedback.java b/src/main/java/servlets/admin/moduleManagement/GetFeedback.java
index f8a6981a1..ec168d4a8 100644
--- a/src/main/java/servlets/admin/moduleManagement/GetFeedback.java
+++ b/src/main/java/servlets/admin/moduleManagement/GetFeedback.java
@@ -1,14 +1,14 @@
package servlets.admin.moduleManagement;
import dbProcs.Getter;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.Cookie;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
import java.io.IOException;
import java.io.PrintWriter;
-import javax.servlet.ServletException;
-import javax.servlet.http.Cookie;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpSession;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import utils.ShepherdLogManager;
diff --git a/src/main/java/servlets/admin/moduleManagement/GetJsonProgress.java b/src/main/java/servlets/admin/moduleManagement/GetJsonProgress.java
index acaf82412..9c900c54d 100644
--- a/src/main/java/servlets/admin/moduleManagement/GetJsonProgress.java
+++ b/src/main/java/servlets/admin/moduleManagement/GetJsonProgress.java
@@ -1,14 +1,14 @@
package servlets.admin.moduleManagement;
import dbProcs.Getter;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.Cookie;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
import java.io.IOException;
import java.io.PrintWriter;
-import javax.servlet.ServletException;
-import javax.servlet.http.Cookie;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpSession;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import utils.ShepherdLogManager;
diff --git a/src/main/java/servlets/admin/moduleManagement/GetProgress.java b/src/main/java/servlets/admin/moduleManagement/GetProgress.java
index b84793f28..9c3e6a11f 100644
--- a/src/main/java/servlets/admin/moduleManagement/GetProgress.java
+++ b/src/main/java/servlets/admin/moduleManagement/GetProgress.java
@@ -1,14 +1,14 @@
package servlets.admin.moduleManagement;
import dbProcs.Getter;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.Cookie;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
import java.io.IOException;
import java.io.PrintWriter;
-import javax.servlet.ServletException;
-import javax.servlet.http.Cookie;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpSession;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import utils.ShepherdLogManager;
diff --git a/src/main/java/servlets/admin/moduleManagement/OpenAllModules.java b/src/main/java/servlets/admin/moduleManagement/OpenAllModules.java
index b9457366e..820aff14e 100644
--- a/src/main/java/servlets/admin/moduleManagement/OpenAllModules.java
+++ b/src/main/java/servlets/admin/moduleManagement/OpenAllModules.java
@@ -1,14 +1,14 @@
package servlets.admin.moduleManagement;
import dbProcs.Setter;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.Cookie;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
import java.io.IOException;
import java.io.PrintWriter;
-import javax.servlet.ServletException;
-import javax.servlet.http.Cookie;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpSession;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import servlets.module.challenge.XxeChallenge1;
diff --git a/src/main/java/servlets/admin/moduleManagement/OpenMobileModules.java b/src/main/java/servlets/admin/moduleManagement/OpenMobileModules.java
index cae111c23..3bd01c3bd 100644
--- a/src/main/java/servlets/admin/moduleManagement/OpenMobileModules.java
+++ b/src/main/java/servlets/admin/moduleManagement/OpenMobileModules.java
@@ -1,14 +1,14 @@
package servlets.admin.moduleManagement;
import dbProcs.Setter;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.Cookie;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
import java.io.IOException;
import java.io.PrintWriter;
-import javax.servlet.ServletException;
-import javax.servlet.http.Cookie;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpSession;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import utils.ShepherdLogManager;
diff --git a/src/main/java/servlets/admin/moduleManagement/OpenOrCloseByCategory.java b/src/main/java/servlets/admin/moduleManagement/OpenOrCloseByCategory.java
index e905b00ed..17e1b9e32 100644
--- a/src/main/java/servlets/admin/moduleManagement/OpenOrCloseByCategory.java
+++ b/src/main/java/servlets/admin/moduleManagement/OpenOrCloseByCategory.java
@@ -1,14 +1,14 @@
package servlets.admin.moduleManagement;
import dbProcs.Setter;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.Cookie;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
import java.io.IOException;
import java.io.PrintWriter;
-import javax.servlet.ServletException;
-import javax.servlet.http.Cookie;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpSession;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.owasp.encoder.Encode;
diff --git a/src/main/java/servlets/admin/moduleManagement/OpenWebModules.java b/src/main/java/servlets/admin/moduleManagement/OpenWebModules.java
index 1c49400cd..ed6c2f047 100644
--- a/src/main/java/servlets/admin/moduleManagement/OpenWebModules.java
+++ b/src/main/java/servlets/admin/moduleManagement/OpenWebModules.java
@@ -1,14 +1,14 @@
package servlets.admin.moduleManagement;
import dbProcs.Setter;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.Cookie;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
import java.io.IOException;
import java.io.PrintWriter;
-import javax.servlet.ServletException;
-import javax.servlet.http.Cookie;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpSession;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import servlets.module.challenge.XxeChallenge1;
diff --git a/src/main/java/servlets/admin/moduleManagement/RemoveModuleLock.java b/src/main/java/servlets/admin/moduleManagement/RemoveModuleLock.java
index ce0353ea7..29854a11f 100644
--- a/src/main/java/servlets/admin/moduleManagement/RemoveModuleLock.java
+++ b/src/main/java/servlets/admin/moduleManagement/RemoveModuleLock.java
@@ -1,13 +1,13 @@
package servlets.admin.moduleManagement;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.Cookie;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
import java.io.IOException;
import java.io.PrintWriter;
-import javax.servlet.ServletException;
-import javax.servlet.http.Cookie;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpSession;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import utils.ModuleBlock;
diff --git a/src/main/java/servlets/admin/moduleManagement/SetModuleStatus.java b/src/main/java/servlets/admin/moduleManagement/SetModuleStatus.java
index ae9fd2eb9..e168338fe 100644
--- a/src/main/java/servlets/admin/moduleManagement/SetModuleStatus.java
+++ b/src/main/java/servlets/admin/moduleManagement/SetModuleStatus.java
@@ -1,14 +1,14 @@
package servlets.admin.moduleManagement;
import dbProcs.Setter;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.Cookie;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
import java.io.IOException;
import java.io.PrintWriter;
-import javax.servlet.ServletException;
-import javax.servlet.http.Cookie;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpSession;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import utils.ShepherdLogManager;
diff --git a/src/main/java/servlets/admin/userManagement/AddPlayer.java b/src/main/java/servlets/admin/userManagement/AddPlayer.java
index de391e1ad..d204d5d4d 100644
--- a/src/main/java/servlets/admin/userManagement/AddPlayer.java
+++ b/src/main/java/servlets/admin/userManagement/AddPlayer.java
@@ -2,14 +2,14 @@
import dbProcs.Getter;
import dbProcs.Setter;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.Cookie;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
import java.io.IOException;
import java.io.PrintWriter;
-import javax.servlet.ServletException;
-import javax.servlet.http.Cookie;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpSession;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.owasp.encoder.Encode;
diff --git a/src/main/java/servlets/admin/userManagement/AssignPlayers.java b/src/main/java/servlets/admin/userManagement/AssignPlayers.java
index 88389e336..2232056b1 100644
--- a/src/main/java/servlets/admin/userManagement/AssignPlayers.java
+++ b/src/main/java/servlets/admin/userManagement/AssignPlayers.java
@@ -2,14 +2,14 @@
import dbProcs.Getter;
import dbProcs.Setter;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.Cookie;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
import java.io.IOException;
import java.io.PrintWriter;
-import javax.servlet.ServletException;
-import javax.servlet.http.Cookie;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpSession;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.owasp.encoder.Encode;
diff --git a/src/main/java/servlets/admin/userManagement/ChangeUserPassword.java b/src/main/java/servlets/admin/userManagement/ChangeUserPassword.java
index 18aaa6615..dd191e104 100644
--- a/src/main/java/servlets/admin/userManagement/ChangeUserPassword.java
+++ b/src/main/java/servlets/admin/userManagement/ChangeUserPassword.java
@@ -2,14 +2,14 @@
import dbProcs.Getter;
import dbProcs.Setter;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.Cookie;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
import java.io.IOException;
import java.io.PrintWriter;
-import javax.servlet.ServletException;
-import javax.servlet.http.Cookie;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpSession;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.owasp.encoder.Encode;
diff --git a/src/main/java/servlets/admin/userManagement/CreateClass.java b/src/main/java/servlets/admin/userManagement/CreateClass.java
index 44205f7ca..8189d4c22 100644
--- a/src/main/java/servlets/admin/userManagement/CreateClass.java
+++ b/src/main/java/servlets/admin/userManagement/CreateClass.java
@@ -1,14 +1,14 @@
package servlets.admin.userManagement;
import dbProcs.Setter;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.Cookie;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
import java.io.IOException;
import java.io.PrintWriter;
-import javax.servlet.ServletException;
-import javax.servlet.http.Cookie;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpSession;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.owasp.encoder.Encode;
diff --git a/src/main/java/servlets/admin/userManagement/CreateNewAdmin.java b/src/main/java/servlets/admin/userManagement/CreateNewAdmin.java
index 805e84849..d7083c49a 100644
--- a/src/main/java/servlets/admin/userManagement/CreateNewAdmin.java
+++ b/src/main/java/servlets/admin/userManagement/CreateNewAdmin.java
@@ -1,14 +1,14 @@
package servlets.admin.userManagement;
import dbProcs.Setter;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.Cookie;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
import java.io.IOException;
import java.io.PrintWriter;
-import javax.servlet.ServletException;
-import javax.servlet.http.Cookie;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpSession;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.owasp.encoder.Encode;
diff --git a/src/main/java/servlets/admin/userManagement/DeletePlayers.java b/src/main/java/servlets/admin/userManagement/DeletePlayers.java
index 20eb82dfc..2733d649f 100644
--- a/src/main/java/servlets/admin/userManagement/DeletePlayers.java
+++ b/src/main/java/servlets/admin/userManagement/DeletePlayers.java
@@ -2,14 +2,14 @@
import dbProcs.Getter;
import dbProcs.Setter;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.Cookie;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
import java.io.IOException;
import java.io.PrintWriter;
-import javax.servlet.ServletException;
-import javax.servlet.http.Cookie;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpSession;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.owasp.encoder.Encode;
diff --git a/src/main/java/servlets/admin/userManagement/DowngradeAdmin.java b/src/main/java/servlets/admin/userManagement/DowngradeAdmin.java
index f3223b215..137515d86 100644
--- a/src/main/java/servlets/admin/userManagement/DowngradeAdmin.java
+++ b/src/main/java/servlets/admin/userManagement/DowngradeAdmin.java
@@ -2,14 +2,14 @@
import dbProcs.Getter;
import dbProcs.Setter;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.Cookie;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
import java.io.IOException;
import java.io.PrintWriter;
-import javax.servlet.ServletException;
-import javax.servlet.http.Cookie;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpSession;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.owasp.encoder.Encode;
diff --git a/src/main/java/servlets/admin/userManagement/GetPlayersByClass.java b/src/main/java/servlets/admin/userManagement/GetPlayersByClass.java
index 3e7ea2b8d..121819c38 100644
--- a/src/main/java/servlets/admin/userManagement/GetPlayersByClass.java
+++ b/src/main/java/servlets/admin/userManagement/GetPlayersByClass.java
@@ -1,16 +1,16 @@
package servlets.admin.userManagement;
import dbProcs.Getter;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.Cookie;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
import java.io.IOException;
import java.io.PrintWriter;
import java.sql.ResultSet;
import java.sql.SQLException;
-import javax.servlet.ServletException;
-import javax.servlet.http.Cookie;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpSession;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.owasp.encoder.Encode;
diff --git a/src/main/java/servlets/admin/userManagement/GiveTakePoints.java b/src/main/java/servlets/admin/userManagement/GiveTakePoints.java
index 7cf8056d9..045c741c4 100644
--- a/src/main/java/servlets/admin/userManagement/GiveTakePoints.java
+++ b/src/main/java/servlets/admin/userManagement/GiveTakePoints.java
@@ -2,14 +2,14 @@
import dbProcs.Getter;
import dbProcs.Setter;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.Cookie;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
import java.io.IOException;
import java.io.PrintWriter;
-import javax.servlet.ServletException;
-import javax.servlet.http.Cookie;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpSession;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.owasp.encoder.Encode;
diff --git a/src/main/java/servlets/admin/userManagement/SetDefaultClass.java b/src/main/java/servlets/admin/userManagement/SetDefaultClass.java
index 03c709ec1..06a89047f 100644
--- a/src/main/java/servlets/admin/userManagement/SetDefaultClass.java
+++ b/src/main/java/servlets/admin/userManagement/SetDefaultClass.java
@@ -1,14 +1,14 @@
package servlets.admin.userManagement;
import dbProcs.Getter;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.Cookie;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
import java.io.IOException;
import java.io.PrintWriter;
-import javax.servlet.ServletException;
-import javax.servlet.http.Cookie;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpSession;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.owasp.encoder.Encode;
diff --git a/src/main/java/servlets/admin/userManagement/SuspendUser.java b/src/main/java/servlets/admin/userManagement/SuspendUser.java
index 927392858..854aaad4a 100644
--- a/src/main/java/servlets/admin/userManagement/SuspendUser.java
+++ b/src/main/java/servlets/admin/userManagement/SuspendUser.java
@@ -2,14 +2,14 @@
import dbProcs.Getter;
import dbProcs.Setter;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.Cookie;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
import java.io.IOException;
import java.io.PrintWriter;
-import javax.servlet.ServletException;
-import javax.servlet.http.Cookie;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpSession;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.owasp.encoder.Encode;
diff --git a/src/main/java/servlets/admin/userManagement/UnSuspendUser.java b/src/main/java/servlets/admin/userManagement/UnSuspendUser.java
index f28df6edc..ef9ff6e6b 100644
--- a/src/main/java/servlets/admin/userManagement/UnSuspendUser.java
+++ b/src/main/java/servlets/admin/userManagement/UnSuspendUser.java
@@ -2,14 +2,14 @@
import dbProcs.Getter;
import dbProcs.Setter;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.Cookie;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
import java.io.IOException;
import java.io.PrintWriter;
-import javax.servlet.ServletException;
-import javax.servlet.http.Cookie;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpSession;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.owasp.encoder.Encode;
diff --git a/src/main/java/servlets/admin/userManagement/UpgradePlayer.java b/src/main/java/servlets/admin/userManagement/UpgradePlayer.java
index 3c08b835d..2457b45c9 100644
--- a/src/main/java/servlets/admin/userManagement/UpgradePlayer.java
+++ b/src/main/java/servlets/admin/userManagement/UpgradePlayer.java
@@ -2,14 +2,14 @@
import dbProcs.Getter;
import dbProcs.Setter;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.Cookie;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
import java.io.IOException;
import java.io.PrintWriter;
-import javax.servlet.ServletException;
-import javax.servlet.http.Cookie;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpSession;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.owasp.encoder.Encode;
diff --git a/src/main/java/servlets/api/Cheats.java b/src/main/java/servlets/api/Cheats.java
index 95be4c3d9..6d4f06c65 100644
--- a/src/main/java/servlets/api/Cheats.java
+++ b/src/main/java/servlets/api/Cheats.java
@@ -1,13 +1,13 @@
package servlets.api;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.annotation.WebServlet;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
import java.io.IOException;
import java.io.PrintWriter;
-import javax.servlet.ServletException;
-import javax.servlet.annotation.WebServlet;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpSession;
import utils.CheatSheetStatus;
@WebServlet("/api/cheats")
diff --git a/src/main/java/servlets/api/Levels.java b/src/main/java/servlets/api/Levels.java
index 3e7c4d951..8bd9c5fdd 100644
--- a/src/main/java/servlets/api/Levels.java
+++ b/src/main/java/servlets/api/Levels.java
@@ -1,15 +1,15 @@
package servlets.api;
import dbProcs.Getter;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.annotation.WebServlet;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Locale;
-import javax.servlet.ServletException;
-import javax.servlet.annotation.WebServlet;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpSession;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.json.JSONArray;
diff --git a/src/main/java/servlets/api/Scoreboard.java b/src/main/java/servlets/api/Scoreboard.java
index fbac01fc0..ff1bd85d3 100644
--- a/src/main/java/servlets/api/Scoreboard.java
+++ b/src/main/java/servlets/api/Scoreboard.java
@@ -1,13 +1,13 @@
package servlets.api;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.annotation.WebServlet;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
import java.io.IOException;
import java.io.PrintWriter;
-import javax.servlet.ServletException;
-import javax.servlet.annotation.WebServlet;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpSession;
import utils.ScoreboardStatus;
@WebServlet("/api/scoreboard")
diff --git a/src/main/java/servlets/module/FeedbackSubmit.java b/src/main/java/servlets/module/FeedbackSubmit.java
index 60d563384..3babb0b18 100644
--- a/src/main/java/servlets/module/FeedbackSubmit.java
+++ b/src/main/java/servlets/module/FeedbackSubmit.java
@@ -2,14 +2,14 @@
import dbProcs.Getter;
import dbProcs.Setter;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.Cookie;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
import java.io.IOException;
import java.io.PrintWriter;
-import javax.servlet.ServletException;
-import javax.servlet.http.Cookie;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpSession;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.jsoup.parser.Parser;
diff --git a/src/main/java/servlets/module/GetCheat.java b/src/main/java/servlets/module/GetCheat.java
index fa401e795..6bf427369 100644
--- a/src/main/java/servlets/module/GetCheat.java
+++ b/src/main/java/servlets/module/GetCheat.java
@@ -1,15 +1,15 @@
package servlets.module;
import dbProcs.Getter;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.Cookie;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Locale;
-import javax.servlet.ServletException;
-import javax.servlet.http.Cookie;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpSession;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.owasp.encoder.Encode;
diff --git a/src/main/java/servlets/module/GetModule.java b/src/main/java/servlets/module/GetModule.java
index af47fb6dc..93e218f1f 100644
--- a/src/main/java/servlets/module/GetModule.java
+++ b/src/main/java/servlets/module/GetModule.java
@@ -1,14 +1,14 @@
package servlets.module;
import dbProcs.Getter;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.Cookie;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
import java.io.IOException;
import java.io.PrintWriter;
-import javax.servlet.ServletException;
-import javax.servlet.http.Cookie;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpSession;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.owasp.encoder.Encode;
diff --git a/src/main/java/servlets/module/ModuleServletTemplate.java b/src/main/java/servlets/module/ModuleServletTemplate.java
index d4242a323..d3987bffb 100644
--- a/src/main/java/servlets/module/ModuleServletTemplate.java
+++ b/src/main/java/servlets/module/ModuleServletTemplate.java
@@ -1,6 +1,11 @@
package servlets.module;
import dbProcs.Database;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
import java.io.IOException;
import java.io.PrintWriter;
import java.sql.Connection;
@@ -9,11 +14,6 @@
import java.sql.Statement;
import java.util.Locale;
import java.util.ResourceBundle;
-import javax.servlet.ServletException;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpSession;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.owasp.encoder.Encode;
diff --git a/src/main/java/servlets/module/RefreshMenu.java b/src/main/java/servlets/module/RefreshMenu.java
index 67947d806..cf543dc34 100644
--- a/src/main/java/servlets/module/RefreshMenu.java
+++ b/src/main/java/servlets/module/RefreshMenu.java
@@ -1,17 +1,17 @@
package servlets.module;
import dbProcs.Getter;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.Cookie;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
import java.io.IOException;
import java.io.PrintWriter;
import java.sql.SQLException;
import java.util.Locale;
import java.util.ResourceBundle;
-import javax.servlet.ServletException;
-import javax.servlet.http.Cookie;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpSession;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.owasp.encoder.Encode;
diff --git a/src/main/java/servlets/module/SolutionSubmit.java b/src/main/java/servlets/module/SolutionSubmit.java
index 8d8aa346e..a8222de67 100644
--- a/src/main/java/servlets/module/SolutionSubmit.java
+++ b/src/main/java/servlets/module/SolutionSubmit.java
@@ -2,16 +2,16 @@
import dbProcs.Getter;
import dbProcs.Setter;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.Cookie;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Locale;
import java.util.ResourceBundle;
-import javax.servlet.ServletException;
-import javax.servlet.http.Cookie;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpSession;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.owasp.encoder.Encode;
diff --git a/src/main/java/servlets/module/challenge/BrokenCrypto3.java b/src/main/java/servlets/module/challenge/BrokenCrypto3.java
index 0f8f50dcc..f853afa98 100644
--- a/src/main/java/servlets/module/challenge/BrokenCrypto3.java
+++ b/src/main/java/servlets/module/challenge/BrokenCrypto3.java
@@ -1,14 +1,14 @@
package servlets.module.challenge;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Locale;
import java.util.ResourceBundle;
-import javax.servlet.ServletException;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpSession;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.owasp.encoder.Encode;
diff --git a/src/main/java/servlets/module/challenge/BrokenCrypto4.java b/src/main/java/servlets/module/challenge/BrokenCrypto4.java
index 017031089..37adb11fb 100644
--- a/src/main/java/servlets/module/challenge/BrokenCrypto4.java
+++ b/src/main/java/servlets/module/challenge/BrokenCrypto4.java
@@ -2,6 +2,11 @@
import dbProcs.Database;
import dbProcs.Getter;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
import java.io.IOException;
import java.io.PrintWriter;
import java.sql.Connection;
@@ -9,11 +14,6 @@
import java.sql.ResultSet;
import java.util.Locale;
import java.util.ResourceBundle;
-import javax.servlet.ServletException;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpSession;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import utils.Hash;
diff --git a/src/main/java/servlets/module/challenge/BrokenCryptoHomeMade.java b/src/main/java/servlets/module/challenge/BrokenCryptoHomeMade.java
index b6afa0c96..a645d0016 100644
--- a/src/main/java/servlets/module/challenge/BrokenCryptoHomeMade.java
+++ b/src/main/java/servlets/module/challenge/BrokenCryptoHomeMade.java
@@ -1,6 +1,12 @@
package servlets.module.challenge;
import dbProcs.Getter;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.Cookie;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
import java.io.IOException;
import java.io.PrintWriter;
import java.nio.charset.Charset;
@@ -13,12 +19,6 @@
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
-import javax.servlet.ServletException;
-import javax.servlet.http.Cookie;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpSession;
import org.apache.commons.codec.binary.Base64;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
diff --git a/src/main/java/servlets/module/challenge/CsrfChallengeFive.java b/src/main/java/servlets/module/challenge/CsrfChallengeFive.java
index a37cc267d..ebb3bc324 100644
--- a/src/main/java/servlets/module/challenge/CsrfChallengeFive.java
+++ b/src/main/java/servlets/module/challenge/CsrfChallengeFive.java
@@ -2,16 +2,16 @@
import dbProcs.Getter;
import dbProcs.Setter;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.Cookie;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Locale;
import java.util.ResourceBundle;
-import javax.servlet.ServletException;
-import javax.servlet.http.Cookie;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpSession;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import utils.ShepherdLogManager;
diff --git a/src/main/java/servlets/module/challenge/CsrfChallengeFour.java b/src/main/java/servlets/module/challenge/CsrfChallengeFour.java
index 87ca8a4e7..2331bb213 100644
--- a/src/main/java/servlets/module/challenge/CsrfChallengeFour.java
+++ b/src/main/java/servlets/module/challenge/CsrfChallengeFour.java
@@ -2,16 +2,16 @@
import dbProcs.Getter;
import dbProcs.Setter;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.Cookie;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Locale;
import java.util.ResourceBundle;
-import javax.servlet.ServletException;
-import javax.servlet.http.Cookie;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpSession;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import utils.ShepherdLogManager;
diff --git a/src/main/java/servlets/module/challenge/CsrfChallengeJSON.java b/src/main/java/servlets/module/challenge/CsrfChallengeJSON.java
index 8cb659b8e..817c586bc 100644
--- a/src/main/java/servlets/module/challenge/CsrfChallengeJSON.java
+++ b/src/main/java/servlets/module/challenge/CsrfChallengeJSON.java
@@ -2,16 +2,16 @@
import dbProcs.Getter;
import dbProcs.Setter;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.Cookie;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Locale;
import java.util.ResourceBundle;
-import javax.servlet.ServletException;
-import javax.servlet.http.Cookie;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpSession;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import utils.ShepherdLogManager;
diff --git a/src/main/java/servlets/module/challenge/CsrfChallengeOne.java b/src/main/java/servlets/module/challenge/CsrfChallengeOne.java
index 4173f6949..862b22ab1 100644
--- a/src/main/java/servlets/module/challenge/CsrfChallengeOne.java
+++ b/src/main/java/servlets/module/challenge/CsrfChallengeOne.java
@@ -2,16 +2,16 @@
import dbProcs.Getter;
import dbProcs.Setter;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.Cookie;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Locale;
import java.util.ResourceBundle;
-import javax.servlet.ServletException;
-import javax.servlet.http.Cookie;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpSession;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import utils.ShepherdLogManager;
diff --git a/src/main/java/servlets/module/challenge/CsrfChallengeSeven.java b/src/main/java/servlets/module/challenge/CsrfChallengeSeven.java
index 73450b083..efa030723 100644
--- a/src/main/java/servlets/module/challenge/CsrfChallengeSeven.java
+++ b/src/main/java/servlets/module/challenge/CsrfChallengeSeven.java
@@ -2,16 +2,16 @@
import dbProcs.Getter;
import dbProcs.Setter;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.Cookie;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Locale;
import java.util.ResourceBundle;
-import javax.servlet.ServletException;
-import javax.servlet.http.Cookie;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpSession;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import utils.ShepherdLogManager;
diff --git a/src/main/java/servlets/module/challenge/CsrfChallengeSevenGetToken.java b/src/main/java/servlets/module/challenge/CsrfChallengeSevenGetToken.java
index 74c288a69..c604375df 100644
--- a/src/main/java/servlets/module/challenge/CsrfChallengeSevenGetToken.java
+++ b/src/main/java/servlets/module/challenge/CsrfChallengeSevenGetToken.java
@@ -1,6 +1,11 @@
package servlets.module.challenge;
import dbProcs.Database;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
import java.io.IOException;
import java.io.PrintWriter;
import java.sql.Connection;
@@ -8,11 +13,6 @@
import java.sql.ResultSet;
import java.util.Locale;
import java.util.ResourceBundle;
-import javax.servlet.ServletException;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpSession;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.owasp.encoder.Encode;
diff --git a/src/main/java/servlets/module/challenge/CsrfChallengeSix.java b/src/main/java/servlets/module/challenge/CsrfChallengeSix.java
index 43bc936cd..37665c47b 100644
--- a/src/main/java/servlets/module/challenge/CsrfChallengeSix.java
+++ b/src/main/java/servlets/module/challenge/CsrfChallengeSix.java
@@ -2,16 +2,16 @@
import dbProcs.Getter;
import dbProcs.Setter;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.Cookie;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Locale;
import java.util.ResourceBundle;
-import javax.servlet.ServletException;
-import javax.servlet.http.Cookie;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpSession;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import utils.ShepherdLogManager;
diff --git a/src/main/java/servlets/module/challenge/CsrfChallengeSixGetToken.java b/src/main/java/servlets/module/challenge/CsrfChallengeSixGetToken.java
index cb61a3d85..aafaa5ac7 100644
--- a/src/main/java/servlets/module/challenge/CsrfChallengeSixGetToken.java
+++ b/src/main/java/servlets/module/challenge/CsrfChallengeSixGetToken.java
@@ -1,6 +1,11 @@
package servlets.module.challenge;
import dbProcs.Database;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
import java.io.IOException;
import java.io.PrintWriter;
import java.sql.Connection;
@@ -8,11 +13,6 @@
import java.sql.ResultSet;
import java.util.Locale;
import java.util.ResourceBundle;
-import javax.servlet.ServletException;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpSession;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.owasp.encoder.Encode;
diff --git a/src/main/java/servlets/module/challenge/CsrfChallengeTargetFive.java b/src/main/java/servlets/module/challenge/CsrfChallengeTargetFive.java
index 3a0627115..3e332df84 100644
--- a/src/main/java/servlets/module/challenge/CsrfChallengeTargetFive.java
+++ b/src/main/java/servlets/module/challenge/CsrfChallengeTargetFive.java
@@ -2,16 +2,16 @@
import dbProcs.Getter;
import dbProcs.Setter;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Locale;
import java.util.Random;
import java.util.ResourceBundle;
-import javax.servlet.ServletException;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpSession;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import utils.ShepherdLogManager;
diff --git a/src/main/java/servlets/module/challenge/CsrfChallengeTargetFour.java b/src/main/java/servlets/module/challenge/CsrfChallengeTargetFour.java
index c090403dd..ff85dae70 100644
--- a/src/main/java/servlets/module/challenge/CsrfChallengeTargetFour.java
+++ b/src/main/java/servlets/module/challenge/CsrfChallengeTargetFour.java
@@ -3,6 +3,11 @@
import dbProcs.Database;
import dbProcs.Getter;
import dbProcs.Setter;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
import java.io.IOException;
import java.io.PrintWriter;
import java.sql.Connection;
@@ -11,11 +16,6 @@
import java.sql.SQLException;
import java.util.Locale;
import java.util.ResourceBundle;
-import javax.servlet.ServletException;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpSession;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import utils.Hash;
diff --git a/src/main/java/servlets/module/challenge/CsrfChallengeTargetJSON.java b/src/main/java/servlets/module/challenge/CsrfChallengeTargetJSON.java
index d68a2817a..0ccb1ba8b 100644
--- a/src/main/java/servlets/module/challenge/CsrfChallengeTargetJSON.java
+++ b/src/main/java/servlets/module/challenge/CsrfChallengeTargetJSON.java
@@ -2,16 +2,16 @@
import dbProcs.Getter;
import dbProcs.Setter;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Locale;
import java.util.ResourceBundle;
import java.util.Scanner;
-import javax.servlet.ServletException;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpSession;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.json.JSONObject;
diff --git a/src/main/java/servlets/module/challenge/CsrfChallengeTargetOne.java b/src/main/java/servlets/module/challenge/CsrfChallengeTargetOne.java
index b076906bb..779849d2b 100644
--- a/src/main/java/servlets/module/challenge/CsrfChallengeTargetOne.java
+++ b/src/main/java/servlets/module/challenge/CsrfChallengeTargetOne.java
@@ -2,15 +2,15 @@
import dbProcs.Getter;
import dbProcs.Setter;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Locale;
import java.util.ResourceBundle;
-import javax.servlet.ServletException;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpSession;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import utils.ShepherdLogManager;
diff --git a/src/main/java/servlets/module/challenge/CsrfChallengeTargetSeven.java b/src/main/java/servlets/module/challenge/CsrfChallengeTargetSeven.java
index 179abc11f..c185093b7 100644
--- a/src/main/java/servlets/module/challenge/CsrfChallengeTargetSeven.java
+++ b/src/main/java/servlets/module/challenge/CsrfChallengeTargetSeven.java
@@ -2,15 +2,15 @@
import dbProcs.Getter;
import dbProcs.Setter;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Locale;
import java.util.ResourceBundle;
-import javax.servlet.ServletException;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpSession;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import utils.Hash;
diff --git a/src/main/java/servlets/module/challenge/CsrfChallengeTargetSix.java b/src/main/java/servlets/module/challenge/CsrfChallengeTargetSix.java
index 684fd90b2..5487cc3cf 100644
--- a/src/main/java/servlets/module/challenge/CsrfChallengeTargetSix.java
+++ b/src/main/java/servlets/module/challenge/CsrfChallengeTargetSix.java
@@ -2,16 +2,16 @@
import dbProcs.Getter;
import dbProcs.Setter;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Locale;
import java.util.Random;
import java.util.ResourceBundle;
-import javax.servlet.ServletException;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpSession;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import utils.ShepherdLogManager;
diff --git a/src/main/java/servlets/module/challenge/CsrfChallengeTargetThree.java b/src/main/java/servlets/module/challenge/CsrfChallengeTargetThree.java
index 550c978c4..26694c2ba 100644
--- a/src/main/java/servlets/module/challenge/CsrfChallengeTargetThree.java
+++ b/src/main/java/servlets/module/challenge/CsrfChallengeTargetThree.java
@@ -2,15 +2,15 @@
import dbProcs.Getter;
import dbProcs.Setter;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Locale;
import java.util.ResourceBundle;
-import javax.servlet.ServletException;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpSession;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import utils.ShepherdLogManager;
diff --git a/src/main/java/servlets/module/challenge/CsrfChallengeTargetTwo.java b/src/main/java/servlets/module/challenge/CsrfChallengeTargetTwo.java
index bba136e80..b078a696b 100644
--- a/src/main/java/servlets/module/challenge/CsrfChallengeTargetTwo.java
+++ b/src/main/java/servlets/module/challenge/CsrfChallengeTargetTwo.java
@@ -2,15 +2,15 @@
import dbProcs.Getter;
import dbProcs.Setter;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Locale;
import java.util.ResourceBundle;
-import javax.servlet.ServletException;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpSession;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import utils.ShepherdLogManager;
diff --git a/src/main/java/servlets/module/challenge/CsrfChallengeThree.java b/src/main/java/servlets/module/challenge/CsrfChallengeThree.java
index 448f4c290..8d69f487b 100644
--- a/src/main/java/servlets/module/challenge/CsrfChallengeThree.java
+++ b/src/main/java/servlets/module/challenge/CsrfChallengeThree.java
@@ -2,16 +2,16 @@
import dbProcs.Getter;
import dbProcs.Setter;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.Cookie;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Locale;
import java.util.ResourceBundle;
-import javax.servlet.ServletException;
-import javax.servlet.http.Cookie;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpSession;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import utils.ShepherdLogManager;
diff --git a/src/main/java/servlets/module/challenge/CsrfChallengeTwo.java b/src/main/java/servlets/module/challenge/CsrfChallengeTwo.java
index bebec19ba..3a58229a8 100644
--- a/src/main/java/servlets/module/challenge/CsrfChallengeTwo.java
+++ b/src/main/java/servlets/module/challenge/CsrfChallengeTwo.java
@@ -2,16 +2,16 @@
import dbProcs.Getter;
import dbProcs.Setter;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.Cookie;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Locale;
import java.util.ResourceBundle;
-import javax.servlet.ServletException;
-import javax.servlet.http.Cookie;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpSession;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import utils.ShepherdLogManager;
diff --git a/src/main/java/servlets/module/challenge/DirectObject1.java b/src/main/java/servlets/module/challenge/DirectObject1.java
index 3f5381e58..becb2fc9e 100644
--- a/src/main/java/servlets/module/challenge/DirectObject1.java
+++ b/src/main/java/servlets/module/challenge/DirectObject1.java
@@ -1,6 +1,11 @@
package servlets.module.challenge;
import dbProcs.Database;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
import java.io.IOException;
import java.io.PrintWriter;
import java.sql.Connection;
@@ -8,11 +13,6 @@
import java.sql.ResultSet;
import java.util.Locale;
import java.util.ResourceBundle;
-import javax.servlet.ServletException;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpSession;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.owasp.encoder.Encode;
diff --git a/src/main/java/servlets/module/challenge/DirectObject2.java b/src/main/java/servlets/module/challenge/DirectObject2.java
index b404b738c..fc6d23569 100644
--- a/src/main/java/servlets/module/challenge/DirectObject2.java
+++ b/src/main/java/servlets/module/challenge/DirectObject2.java
@@ -1,6 +1,11 @@
package servlets.module.challenge;
import dbProcs.Database;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
import java.io.IOException;
import java.io.PrintWriter;
import java.sql.Connection;
@@ -8,11 +13,6 @@
import java.sql.ResultSet;
import java.util.Locale;
import java.util.ResourceBundle;
-import javax.servlet.ServletException;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpSession;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.owasp.encoder.Encode;
diff --git a/src/main/java/servlets/module/challenge/DirectObjectBankCurrentBalance.java b/src/main/java/servlets/module/challenge/DirectObjectBankCurrentBalance.java
index eb4438da4..e347461e2 100644
--- a/src/main/java/servlets/module/challenge/DirectObjectBankCurrentBalance.java
+++ b/src/main/java/servlets/module/challenge/DirectObjectBankCurrentBalance.java
@@ -1,15 +1,15 @@
package servlets.module.challenge;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
import java.io.IOException;
import java.io.PrintWriter;
import java.sql.SQLException;
import java.util.Locale;
import java.util.ResourceBundle;
-import javax.servlet.ServletException;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpSession;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import utils.ShepherdLogManager;
diff --git a/src/main/java/servlets/module/challenge/DirectObjectBankLogin.java b/src/main/java/servlets/module/challenge/DirectObjectBankLogin.java
index 2757754a3..35106aae8 100644
--- a/src/main/java/servlets/module/challenge/DirectObjectBankLogin.java
+++ b/src/main/java/servlets/module/challenge/DirectObjectBankLogin.java
@@ -1,6 +1,11 @@
package servlets.module.challenge;
import dbProcs.Database;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
import java.io.IOException;
import java.io.PrintWriter;
import java.sql.CallableStatement;
@@ -9,11 +14,6 @@
import java.sql.SQLException;
import java.util.Locale;
import java.util.ResourceBundle;
-import javax.servlet.ServletException;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpSession;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.owasp.encoder.Encode;
diff --git a/src/main/java/servlets/module/challenge/DirectObjectBankLogout.java b/src/main/java/servlets/module/challenge/DirectObjectBankLogout.java
index 577f99ae4..df7a896db 100644
--- a/src/main/java/servlets/module/challenge/DirectObjectBankLogout.java
+++ b/src/main/java/servlets/module/challenge/DirectObjectBankLogout.java
@@ -1,14 +1,14 @@
package servlets.module.challenge;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Locale;
import java.util.ResourceBundle;
-import javax.servlet.ServletException;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpSession;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import utils.ShepherdLogManager;
diff --git a/src/main/java/servlets/module/challenge/DirectObjectBankRegistration.java b/src/main/java/servlets/module/challenge/DirectObjectBankRegistration.java
index 9eb9d6c54..5cb01d791 100644
--- a/src/main/java/servlets/module/challenge/DirectObjectBankRegistration.java
+++ b/src/main/java/servlets/module/challenge/DirectObjectBankRegistration.java
@@ -1,6 +1,11 @@
package servlets.module.challenge;
import dbProcs.Database;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
import java.io.IOException;
import java.io.PrintWriter;
import java.sql.CallableStatement;
@@ -8,11 +13,6 @@
import java.sql.SQLException;
import java.util.Locale;
import java.util.ResourceBundle;
-import javax.servlet.ServletException;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpSession;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import utils.ShepherdLogManager;
diff --git a/src/main/java/servlets/module/challenge/DirectObjectBankTransfer.java b/src/main/java/servlets/module/challenge/DirectObjectBankTransfer.java
index 2ceaafaed..f399de6ba 100644
--- a/src/main/java/servlets/module/challenge/DirectObjectBankTransfer.java
+++ b/src/main/java/servlets/module/challenge/DirectObjectBankTransfer.java
@@ -1,6 +1,11 @@
package servlets.module.challenge;
import dbProcs.Database;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
import java.io.IOException;
import java.io.PrintWriter;
import java.sql.CallableStatement;
@@ -8,11 +13,6 @@
import java.sql.SQLException;
import java.util.Locale;
import java.util.ResourceBundle;
-import javax.servlet.ServletException;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpSession;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import utils.ShepherdLogManager;
diff --git a/src/main/java/servlets/module/challenge/NoSqlInjection1.java b/src/main/java/servlets/module/challenge/NoSqlInjection1.java
index ce1aa9b28..e9d25b59d 100644
--- a/src/main/java/servlets/module/challenge/NoSqlInjection1.java
+++ b/src/main/java/servlets/module/challenge/NoSqlInjection1.java
@@ -14,15 +14,15 @@
import com.mongodb.MongoSocketException;
import com.mongodb.MongoTimeoutException;
import dbProcs.MongoDatabase;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Locale;
import java.util.ResourceBundle;
-import javax.servlet.ServletException;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpSession;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.owasp.encoder.Encode;
diff --git a/src/main/java/servlets/module/challenge/PoorValidation1.java b/src/main/java/servlets/module/challenge/PoorValidation1.java
index d222a0ca0..3d9393b4d 100644
--- a/src/main/java/servlets/module/challenge/PoorValidation1.java
+++ b/src/main/java/servlets/module/challenge/PoorValidation1.java
@@ -1,14 +1,14 @@
package servlets.module.challenge;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Locale;
import java.util.ResourceBundle;
-import javax.servlet.ServletException;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpSession;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import utils.Hash;
diff --git a/src/main/java/servlets/module/challenge/PoorValidation2.java b/src/main/java/servlets/module/challenge/PoorValidation2.java
index 818084430..40bac0f86 100644
--- a/src/main/java/servlets/module/challenge/PoorValidation2.java
+++ b/src/main/java/servlets/module/challenge/PoorValidation2.java
@@ -1,14 +1,14 @@
package servlets.module.challenge;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Locale;
import java.util.ResourceBundle;
-import javax.servlet.ServletException;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpSession;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import utils.Hash;
diff --git a/src/main/java/servlets/module/challenge/SecurityMisconfigStealTokens.java b/src/main/java/servlets/module/challenge/SecurityMisconfigStealTokens.java
index cb6e7a37b..79800a7ed 100644
--- a/src/main/java/servlets/module/challenge/SecurityMisconfigStealTokens.java
+++ b/src/main/java/servlets/module/challenge/SecurityMisconfigStealTokens.java
@@ -1,6 +1,12 @@
package servlets.module.challenge;
import dbProcs.Database;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.Cookie;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
import java.io.IOException;
import java.io.PrintWriter;
import java.sql.CallableStatement;
@@ -9,12 +15,6 @@
import java.sql.SQLException;
import java.util.Locale;
import java.util.ResourceBundle;
-import javax.servlet.ServletException;
-import javax.servlet.http.Cookie;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpSession;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import utils.Hash;
diff --git a/src/main/java/servlets/module/challenge/SessionManagement1.java b/src/main/java/servlets/module/challenge/SessionManagement1.java
index 104ee88c0..b64275e80 100644
--- a/src/main/java/servlets/module/challenge/SessionManagement1.java
+++ b/src/main/java/servlets/module/challenge/SessionManagement1.java
@@ -1,15 +1,15 @@
package servlets.module.challenge;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.Cookie;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Locale;
import java.util.ResourceBundle;
-import javax.servlet.ServletException;
-import javax.servlet.http.Cookie;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpSession;
import org.apache.commons.codec.binary.Base64;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
diff --git a/src/main/java/servlets/module/challenge/SessionManagement2.java b/src/main/java/servlets/module/challenge/SessionManagement2.java
index a120f56d5..e527dcb0c 100644
--- a/src/main/java/servlets/module/challenge/SessionManagement2.java
+++ b/src/main/java/servlets/module/challenge/SessionManagement2.java
@@ -2,6 +2,11 @@
import dbProcs.Database;
import dbProcs.Getter;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
import java.io.IOException;
import java.io.PrintWriter;
import java.sql.Connection;
@@ -9,11 +14,6 @@
import java.sql.ResultSet;
import java.util.Locale;
import java.util.ResourceBundle;
-import javax.servlet.ServletException;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpSession;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.owasp.encoder.Encode;
diff --git a/src/main/java/servlets/module/challenge/SessionManagement2ChangePassword.java b/src/main/java/servlets/module/challenge/SessionManagement2ChangePassword.java
index fd47b6c45..2657b61ef 100644
--- a/src/main/java/servlets/module/challenge/SessionManagement2ChangePassword.java
+++ b/src/main/java/servlets/module/challenge/SessionManagement2ChangePassword.java
@@ -1,6 +1,11 @@
package servlets.module.challenge;
import dbProcs.Database;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
import java.io.IOException;
import java.io.PrintWriter;
import java.sql.Connection;
@@ -8,11 +13,6 @@
import java.sql.SQLException;
import java.util.Locale;
import java.util.ResourceBundle;
-import javax.servlet.ServletException;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpSession;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.owasp.encoder.Encode;
diff --git a/src/main/java/servlets/module/challenge/SessionManagement3.java b/src/main/java/servlets/module/challenge/SessionManagement3.java
index 9cc0f38be..e2a5cd6bf 100644
--- a/src/main/java/servlets/module/challenge/SessionManagement3.java
+++ b/src/main/java/servlets/module/challenge/SessionManagement3.java
@@ -1,6 +1,11 @@
package servlets.module.challenge;
import dbProcs.Database;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
import java.io.IOException;
import java.io.PrintWriter;
import java.sql.Connection;
@@ -8,11 +13,6 @@
import java.sql.ResultSet;
import java.util.Locale;
import java.util.ResourceBundle;
-import javax.servlet.ServletException;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpSession;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.owasp.encoder.Encode;
diff --git a/src/main/java/servlets/module/challenge/SessionManagement3ChangePassword.java b/src/main/java/servlets/module/challenge/SessionManagement3ChangePassword.java
index 32fdb81e0..690000527 100644
--- a/src/main/java/servlets/module/challenge/SessionManagement3ChangePassword.java
+++ b/src/main/java/servlets/module/challenge/SessionManagement3ChangePassword.java
@@ -1,6 +1,12 @@
package servlets.module.challenge;
import dbProcs.Database;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.Cookie;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.UnsupportedEncodingException;
@@ -8,12 +14,6 @@
import java.sql.PreparedStatement;
import java.util.Locale;
import java.util.ResourceBundle;
-import javax.servlet.ServletException;
-import javax.servlet.http.Cookie;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpSession;
import org.apache.commons.codec.binary.Base64;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
diff --git a/src/main/java/servlets/module/challenge/SessionManagement4.java b/src/main/java/servlets/module/challenge/SessionManagement4.java
index 065d4b06b..af4128972 100644
--- a/src/main/java/servlets/module/challenge/SessionManagement4.java
+++ b/src/main/java/servlets/module/challenge/SessionManagement4.java
@@ -1,15 +1,15 @@
package servlets.module.challenge;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.Cookie;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Locale;
import java.util.ResourceBundle;
-import javax.servlet.ServletException;
-import javax.servlet.http.Cookie;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpSession;
import org.apache.commons.codec.binary.Base64;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
diff --git a/src/main/java/servlets/module/challenge/SessionManagement5.java b/src/main/java/servlets/module/challenge/SessionManagement5.java
index 0de15b90e..8fe4e9e4b 100644
--- a/src/main/java/servlets/module/challenge/SessionManagement5.java
+++ b/src/main/java/servlets/module/challenge/SessionManagement5.java
@@ -1,6 +1,11 @@
package servlets.module.challenge;
import dbProcs.Database;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
import java.io.IOException;
import java.io.PrintWriter;
import java.sql.Connection;
@@ -8,11 +13,6 @@
import java.sql.ResultSet;
import java.util.Locale;
import java.util.ResourceBundle;
-import javax.servlet.ServletException;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpSession;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.owasp.encoder.Encode;
diff --git a/src/main/java/servlets/module/challenge/SessionManagement5ChangePassword.java b/src/main/java/servlets/module/challenge/SessionManagement5ChangePassword.java
index a48ddc69e..f3b77e7fe 100644
--- a/src/main/java/servlets/module/challenge/SessionManagement5ChangePassword.java
+++ b/src/main/java/servlets/module/challenge/SessionManagement5ChangePassword.java
@@ -1,6 +1,11 @@
package servlets.module.challenge;
import dbProcs.Database;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.UnsupportedEncodingException;
@@ -11,11 +16,6 @@
import java.util.Date;
import java.util.Locale;
import java.util.ResourceBundle;
-import javax.servlet.ServletException;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpSession;
import org.apache.commons.codec.binary.Base64;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
diff --git a/src/main/java/servlets/module/challenge/SessionManagement5SetToken.java b/src/main/java/servlets/module/challenge/SessionManagement5SetToken.java
index 7080e18ca..0a35e4718 100644
--- a/src/main/java/servlets/module/challenge/SessionManagement5SetToken.java
+++ b/src/main/java/servlets/module/challenge/SessionManagement5SetToken.java
@@ -1,6 +1,11 @@
package servlets.module.challenge;
import dbProcs.Database;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
import java.io.IOException;
import java.io.PrintWriter;
import java.sql.Connection;
@@ -8,11 +13,6 @@
import java.sql.ResultSet;
import java.util.Locale;
import java.util.ResourceBundle;
-import javax.servlet.ServletException;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpSession;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.owasp.encoder.Encode;
diff --git a/src/main/java/servlets/module/challenge/SessionManagement6.java b/src/main/java/servlets/module/challenge/SessionManagement6.java
index d351d8c3f..b2f64c6ad 100644
--- a/src/main/java/servlets/module/challenge/SessionManagement6.java
+++ b/src/main/java/servlets/module/challenge/SessionManagement6.java
@@ -2,6 +2,12 @@
import dbProcs.Database;
import dbProcs.Getter;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.Cookie;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
import java.io.IOException;
import java.io.PrintWriter;
import java.sql.Connection;
@@ -9,12 +15,6 @@
import java.sql.ResultSet;
import java.util.Locale;
import java.util.ResourceBundle;
-import javax.servlet.ServletException;
-import javax.servlet.http.Cookie;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpSession;
import org.apache.commons.codec.binary.Base64;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
diff --git a/src/main/java/servlets/module/challenge/SessionManagement6SecretQuestion.java b/src/main/java/servlets/module/challenge/SessionManagement6SecretQuestion.java
index 5bb2c5732..404c3ffa6 100644
--- a/src/main/java/servlets/module/challenge/SessionManagement6SecretQuestion.java
+++ b/src/main/java/servlets/module/challenge/SessionManagement6SecretQuestion.java
@@ -2,6 +2,12 @@
import dbProcs.Database;
import dbProcs.Getter;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.Cookie;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
import java.io.IOException;
import java.io.PrintWriter;
import java.sql.Connection;
@@ -10,12 +16,6 @@
import java.sql.SQLException;
import java.util.Locale;
import java.util.ResourceBundle;
-import javax.servlet.ServletException;
-import javax.servlet.http.Cookie;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpSession;
import org.apache.commons.codec.binary.Base64;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
diff --git a/src/main/java/servlets/module/challenge/SessionManagement7.java b/src/main/java/servlets/module/challenge/SessionManagement7.java
index 190d859e2..9f0d66c83 100644
--- a/src/main/java/servlets/module/challenge/SessionManagement7.java
+++ b/src/main/java/servlets/module/challenge/SessionManagement7.java
@@ -2,6 +2,12 @@
import dbProcs.Database;
import dbProcs.Getter;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.Cookie;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
import java.io.IOException;
import java.io.PrintWriter;
import java.sql.Connection;
@@ -9,12 +15,6 @@
import java.sql.ResultSet;
import java.util.Locale;
import java.util.ResourceBundle;
-import javax.servlet.ServletException;
-import javax.servlet.http.Cookie;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpSession;
import org.apache.commons.codec.binary.Base64;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
diff --git a/src/main/java/servlets/module/challenge/SessionManagement7SecretQuestion.java b/src/main/java/servlets/module/challenge/SessionManagement7SecretQuestion.java
index 817b06335..a00da001c 100644
--- a/src/main/java/servlets/module/challenge/SessionManagement7SecretQuestion.java
+++ b/src/main/java/servlets/module/challenge/SessionManagement7SecretQuestion.java
@@ -2,6 +2,12 @@
import dbProcs.Database;
import dbProcs.Getter;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.Cookie;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
import java.io.IOException;
import java.io.PrintWriter;
import java.sql.Connection;
@@ -10,12 +16,6 @@
import java.sql.SQLException;
import java.util.Locale;
import java.util.ResourceBundle;
-import javax.servlet.ServletException;
-import javax.servlet.http.Cookie;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpSession;
import org.apache.commons.codec.binary.Base64;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
diff --git a/src/main/java/servlets/module/challenge/SessionManagement8.java b/src/main/java/servlets/module/challenge/SessionManagement8.java
index a4266d025..34b9729af 100644
--- a/src/main/java/servlets/module/challenge/SessionManagement8.java
+++ b/src/main/java/servlets/module/challenge/SessionManagement8.java
@@ -1,16 +1,16 @@
package servlets.module.challenge;
import dbProcs.Getter;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.Cookie;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Locale;
import java.util.ResourceBundle;
-import javax.servlet.ServletException;
-import javax.servlet.http.Cookie;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpSession;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import utils.Hash;
diff --git a/src/main/java/servlets/module/challenge/SqlInjection1.java b/src/main/java/servlets/module/challenge/SqlInjection1.java
index dbae687fd..7b4c9b2e2 100644
--- a/src/main/java/servlets/module/challenge/SqlInjection1.java
+++ b/src/main/java/servlets/module/challenge/SqlInjection1.java
@@ -1,6 +1,11 @@
package servlets.module.challenge;
import dbProcs.Database;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
import java.io.IOException;
import java.io.PrintWriter;
import java.sql.Connection;
@@ -9,11 +14,6 @@
import java.sql.Statement;
import java.util.Locale;
import java.util.ResourceBundle;
-import javax.servlet.ServletException;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpSession;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.owasp.encoder.Encode;
diff --git a/src/main/java/servlets/module/challenge/SqlInjection3.java b/src/main/java/servlets/module/challenge/SqlInjection3.java
index 7e7304c8d..f0e7e4cb6 100644
--- a/src/main/java/servlets/module/challenge/SqlInjection3.java
+++ b/src/main/java/servlets/module/challenge/SqlInjection3.java
@@ -1,6 +1,11 @@
package servlets.module.challenge;
import dbProcs.Database;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
import java.io.IOException;
import java.io.PrintWriter;
import java.sql.Connection;
@@ -9,11 +14,6 @@
import java.sql.Statement;
import java.util.Locale;
import java.util.ResourceBundle;
-import javax.servlet.ServletException;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpSession;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.owasp.encoder.Encode;
diff --git a/src/main/java/servlets/module/challenge/SqlInjection4.java b/src/main/java/servlets/module/challenge/SqlInjection4.java
index 23f43a228..5ab0d2ee2 100644
--- a/src/main/java/servlets/module/challenge/SqlInjection4.java
+++ b/src/main/java/servlets/module/challenge/SqlInjection4.java
@@ -1,6 +1,11 @@
package servlets.module.challenge;
import dbProcs.Database;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
import java.io.IOException;
import java.io.PrintWriter;
import java.sql.Connection;
@@ -9,11 +14,6 @@
import java.sql.Statement;
import java.util.Locale;
import java.util.ResourceBundle;
-import javax.servlet.ServletException;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpSession;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.owasp.encoder.Encode;
diff --git a/src/main/java/servlets/module/challenge/SqlInjection5.java b/src/main/java/servlets/module/challenge/SqlInjection5.java
index f07e53cbc..6d85a2703 100644
--- a/src/main/java/servlets/module/challenge/SqlInjection5.java
+++ b/src/main/java/servlets/module/challenge/SqlInjection5.java
@@ -1,6 +1,11 @@
package servlets.module.challenge;
import dbProcs.Database;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
import java.io.IOException;
import java.io.PrintWriter;
import java.sql.Connection;
@@ -8,11 +13,6 @@
import java.sql.ResultSet;
import java.util.Locale;
import java.util.ResourceBundle;
-import javax.servlet.ServletException;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpSession;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.owasp.encoder.Encode;
diff --git a/src/main/java/servlets/module/challenge/SqlInjection5CouponCheck.java b/src/main/java/servlets/module/challenge/SqlInjection5CouponCheck.java
index 1a8ae486a..c65201a7f 100644
--- a/src/main/java/servlets/module/challenge/SqlInjection5CouponCheck.java
+++ b/src/main/java/servlets/module/challenge/SqlInjection5CouponCheck.java
@@ -1,6 +1,11 @@
package servlets.module.challenge;
import dbProcs.Database;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
import java.io.IOException;
import java.io.PrintWriter;
import java.sql.Connection;
@@ -8,11 +13,6 @@
import java.sql.ResultSet;
import java.util.Locale;
import java.util.ResourceBundle;
-import javax.servlet.ServletException;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpSession;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.owasp.encoder.Encode;
diff --git a/src/main/java/servlets/module/challenge/SqlInjection5VipCheck.java b/src/main/java/servlets/module/challenge/SqlInjection5VipCheck.java
index 65a5a0672..d747d3cca 100644
--- a/src/main/java/servlets/module/challenge/SqlInjection5VipCheck.java
+++ b/src/main/java/servlets/module/challenge/SqlInjection5VipCheck.java
@@ -1,6 +1,11 @@
package servlets.module.challenge;
import dbProcs.Database;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
import java.io.IOException;
import java.io.PrintWriter;
import java.sql.Connection;
@@ -8,11 +13,6 @@
import java.sql.ResultSet;
import java.util.Locale;
import java.util.ResourceBundle;
-import javax.servlet.ServletException;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpSession;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.owasp.encoder.Encode;
diff --git a/src/main/java/servlets/module/challenge/SqlInjection6.java b/src/main/java/servlets/module/challenge/SqlInjection6.java
index cfc7f8516..595933866 100644
--- a/src/main/java/servlets/module/challenge/SqlInjection6.java
+++ b/src/main/java/servlets/module/challenge/SqlInjection6.java
@@ -1,6 +1,11 @@
package servlets.module.challenge;
import dbProcs.Database;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
import java.io.IOException;
import java.io.PrintWriter;
import java.sql.Connection;
@@ -8,11 +13,6 @@
import java.sql.ResultSet;
import java.util.Locale;
import java.util.ResourceBundle;
-import javax.servlet.ServletException;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpSession;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.owasp.encoder.Encode;
diff --git a/src/main/java/servlets/module/challenge/SqlInjection7.java b/src/main/java/servlets/module/challenge/SqlInjection7.java
index 5842e8d4a..4b610daf9 100644
--- a/src/main/java/servlets/module/challenge/SqlInjection7.java
+++ b/src/main/java/servlets/module/challenge/SqlInjection7.java
@@ -2,6 +2,11 @@
import dbProcs.Database;
import dbProcs.Getter;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
import java.io.IOException;
import java.io.PrintWriter;
import java.sql.Connection;
@@ -9,11 +14,6 @@
import java.sql.ResultSet;
import java.util.Locale;
import java.util.ResourceBundle;
-import javax.servlet.ServletException;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpSession;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.owasp.encoder.Encode;
diff --git a/src/main/java/servlets/module/challenge/SqlInjectionEmail.java b/src/main/java/servlets/module/challenge/SqlInjectionEmail.java
index 8c0108047..05d198360 100644
--- a/src/main/java/servlets/module/challenge/SqlInjectionEmail.java
+++ b/src/main/java/servlets/module/challenge/SqlInjectionEmail.java
@@ -1,6 +1,11 @@
package servlets.module.challenge;
import dbProcs.Database;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
import java.io.IOException;
import java.io.PrintWriter;
import java.sql.Connection;
@@ -9,11 +14,6 @@
import java.sql.Statement;
import java.util.Locale;
import java.util.ResourceBundle;
-import javax.servlet.ServletException;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpSession;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.owasp.encoder.Encode;
diff --git a/src/main/java/servlets/module/challenge/SqlInjectionEscaping.java b/src/main/java/servlets/module/challenge/SqlInjectionEscaping.java
index 79f888fc0..8f5d614a9 100644
--- a/src/main/java/servlets/module/challenge/SqlInjectionEscaping.java
+++ b/src/main/java/servlets/module/challenge/SqlInjectionEscaping.java
@@ -1,6 +1,11 @@
package servlets.module.challenge;
import dbProcs.Database;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
import java.io.IOException;
import java.io.PrintWriter;
import java.sql.Connection;
@@ -9,11 +14,6 @@
import java.sql.Statement;
import java.util.Locale;
import java.util.ResourceBundle;
-import javax.servlet.ServletException;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpSession;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.owasp.encoder.Encode;
diff --git a/src/main/java/servlets/module/challenge/SqlInjectionStoredProcedure.java b/src/main/java/servlets/module/challenge/SqlInjectionStoredProcedure.java
index 9e7890770..cfb141074 100644
--- a/src/main/java/servlets/module/challenge/SqlInjectionStoredProcedure.java
+++ b/src/main/java/servlets/module/challenge/SqlInjectionStoredProcedure.java
@@ -1,6 +1,11 @@
package servlets.module.challenge;
import dbProcs.Database;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
import java.io.IOException;
import java.io.PrintWriter;
import java.sql.Connection;
@@ -9,11 +14,6 @@
import java.sql.Statement;
import java.util.Locale;
import java.util.ResourceBundle;
-import javax.servlet.ServletException;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpSession;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.owasp.encoder.Encode;
diff --git a/src/main/java/servlets/module/challenge/UrlAccess1.java b/src/main/java/servlets/module/challenge/UrlAccess1.java
index ac32f03ec..fc0bea77e 100644
--- a/src/main/java/servlets/module/challenge/UrlAccess1.java
+++ b/src/main/java/servlets/module/challenge/UrlAccess1.java
@@ -1,14 +1,14 @@
package servlets.module.challenge;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Locale;
import java.util.ResourceBundle;
-import javax.servlet.ServletException;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpSession;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.owasp.encoder.Encode;
diff --git a/src/main/java/servlets/module/challenge/UrlAccess1Admin.java b/src/main/java/servlets/module/challenge/UrlAccess1Admin.java
index 7ef36b0d5..3f14e92d2 100644
--- a/src/main/java/servlets/module/challenge/UrlAccess1Admin.java
+++ b/src/main/java/servlets/module/challenge/UrlAccess1Admin.java
@@ -1,14 +1,14 @@
package servlets.module.challenge;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Locale;
import java.util.ResourceBundle;
-import javax.servlet.ServletException;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpSession;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.owasp.encoder.Encode;
diff --git a/src/main/java/servlets/module/challenge/UrlAccess2.java b/src/main/java/servlets/module/challenge/UrlAccess2.java
index 4ef27f57e..0d2eff414 100644
--- a/src/main/java/servlets/module/challenge/UrlAccess2.java
+++ b/src/main/java/servlets/module/challenge/UrlAccess2.java
@@ -1,14 +1,14 @@
package servlets.module.challenge;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Locale;
import java.util.ResourceBundle;
-import javax.servlet.ServletException;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpSession;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import utils.ShepherdLogManager;
diff --git a/src/main/java/servlets/module/challenge/UrlAccess2Admin.java b/src/main/java/servlets/module/challenge/UrlAccess2Admin.java
index 0614491a5..ed3d6cb70 100644
--- a/src/main/java/servlets/module/challenge/UrlAccess2Admin.java
+++ b/src/main/java/servlets/module/challenge/UrlAccess2Admin.java
@@ -1,14 +1,14 @@
package servlets.module.challenge;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Locale;
import java.util.ResourceBundle;
-import javax.servlet.ServletException;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpSession;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.owasp.encoder.Encode;
diff --git a/src/main/java/servlets/module/challenge/UrlAccess3.java b/src/main/java/servlets/module/challenge/UrlAccess3.java
index 0c13de19d..4110e18c4 100644
--- a/src/main/java/servlets/module/challenge/UrlAccess3.java
+++ b/src/main/java/servlets/module/challenge/UrlAccess3.java
@@ -1,16 +1,16 @@
package servlets.module.challenge;
import dbProcs.Getter;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.Cookie;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Locale;
import java.util.ResourceBundle;
-import javax.servlet.ServletException;
-import javax.servlet.http.Cookie;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpSession;
import org.apache.commons.codec.binary.Base64;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
diff --git a/src/main/java/servlets/module/challenge/UrlAccess3UserList.java b/src/main/java/servlets/module/challenge/UrlAccess3UserList.java
index 1a0e0fcee..11983000b 100644
--- a/src/main/java/servlets/module/challenge/UrlAccess3UserList.java
+++ b/src/main/java/servlets/module/challenge/UrlAccess3UserList.java
@@ -1,6 +1,12 @@
package servlets.module.challenge;
import dbProcs.Database;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.Cookie;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
import java.io.IOException;
import java.io.PrintWriter;
import java.sql.Connection;
@@ -8,12 +14,6 @@
import java.sql.ResultSet;
import java.util.Locale;
import java.util.ResourceBundle;
-import javax.servlet.ServletException;
-import javax.servlet.http.Cookie;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpSession;
import org.apache.commons.codec.binary.Base64;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
diff --git a/src/main/java/servlets/module/challenge/XssChallengeFive.java b/src/main/java/servlets/module/challenge/XssChallengeFive.java
index 6b3d89aaa..f451ad394 100644
--- a/src/main/java/servlets/module/challenge/XssChallengeFive.java
+++ b/src/main/java/servlets/module/challenge/XssChallengeFive.java
@@ -1,16 +1,16 @@
package servlets.module.challenge;
import dbProcs.Getter;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.Cookie;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Locale;
import java.util.ResourceBundle;
-import javax.servlet.ServletException;
-import javax.servlet.http.Cookie;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpSession;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import utils.FindXSS;
diff --git a/src/main/java/servlets/module/challenge/XssChallengeFour.java b/src/main/java/servlets/module/challenge/XssChallengeFour.java
index 11beb2531..93d5693c1 100644
--- a/src/main/java/servlets/module/challenge/XssChallengeFour.java
+++ b/src/main/java/servlets/module/challenge/XssChallengeFour.java
@@ -1,16 +1,16 @@
package servlets.module.challenge;
import dbProcs.Getter;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.Cookie;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Locale;
import java.util.ResourceBundle;
-import javax.servlet.ServletException;
-import javax.servlet.http.Cookie;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpSession;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import utils.FindXSS;
diff --git a/src/main/java/servlets/module/challenge/XssChallengeOne.java b/src/main/java/servlets/module/challenge/XssChallengeOne.java
index f3930d562..5b220f6d1 100644
--- a/src/main/java/servlets/module/challenge/XssChallengeOne.java
+++ b/src/main/java/servlets/module/challenge/XssChallengeOne.java
@@ -1,16 +1,16 @@
package servlets.module.challenge;
import dbProcs.Getter;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.Cookie;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Locale;
import java.util.ResourceBundle;
-import javax.servlet.ServletException;
-import javax.servlet.http.Cookie;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpSession;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import utils.FindXSS;
diff --git a/src/main/java/servlets/module/challenge/XssChallengeSix.java b/src/main/java/servlets/module/challenge/XssChallengeSix.java
index 679a80fe0..9eb74e1f4 100644
--- a/src/main/java/servlets/module/challenge/XssChallengeSix.java
+++ b/src/main/java/servlets/module/challenge/XssChallengeSix.java
@@ -1,16 +1,16 @@
package servlets.module.challenge;
import dbProcs.Getter;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.Cookie;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Locale;
import java.util.ResourceBundle;
-import javax.servlet.ServletException;
-import javax.servlet.http.Cookie;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpSession;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import utils.FindXSS;
diff --git a/src/main/java/servlets/module/challenge/XssChallengeThree.java b/src/main/java/servlets/module/challenge/XssChallengeThree.java
index 7df78c920..01a3830e1 100644
--- a/src/main/java/servlets/module/challenge/XssChallengeThree.java
+++ b/src/main/java/servlets/module/challenge/XssChallengeThree.java
@@ -1,16 +1,16 @@
package servlets.module.challenge;
import dbProcs.Getter;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.Cookie;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Locale;
import java.util.ResourceBundle;
-import javax.servlet.ServletException;
-import javax.servlet.http.Cookie;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpSession;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import utils.FindXSS;
diff --git a/src/main/java/servlets/module/challenge/XssChallengeTwo.java b/src/main/java/servlets/module/challenge/XssChallengeTwo.java
index 6ac70af59..dce49b042 100644
--- a/src/main/java/servlets/module/challenge/XssChallengeTwo.java
+++ b/src/main/java/servlets/module/challenge/XssChallengeTwo.java
@@ -1,16 +1,16 @@
package servlets.module.challenge;
import dbProcs.Getter;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.Cookie;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Locale;
import java.util.ResourceBundle;
-import javax.servlet.ServletException;
-import javax.servlet.http.Cookie;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpSession;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import utils.FindXSS;
diff --git a/src/main/java/servlets/module/challenge/XxeChallenge1.java b/src/main/java/servlets/module/challenge/XxeChallenge1.java
index 0f2b83b74..39c3d29cc 100644
--- a/src/main/java/servlets/module/challenge/XxeChallenge1.java
+++ b/src/main/java/servlets/module/challenge/XxeChallenge1.java
@@ -2,6 +2,12 @@
import dbProcs.FileInputProperties;
import dbProcs.Getter;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.Cookie;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
@@ -11,12 +17,6 @@
import java.nio.charset.StandardCharsets;
import java.util.Locale;
import java.util.ResourceBundle;
-import javax.servlet.ServletException;
-import javax.servlet.http.Cookie;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpSession;
import org.apache.commons.io.FileUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
diff --git a/src/main/java/servlets/module/challenge/XxeChallenge1OldWebService.java b/src/main/java/servlets/module/challenge/XxeChallenge1OldWebService.java
index 868563583..ef35f2fd3 100644
--- a/src/main/java/servlets/module/challenge/XxeChallenge1OldWebService.java
+++ b/src/main/java/servlets/module/challenge/XxeChallenge1OldWebService.java
@@ -1,11 +1,15 @@
package servlets.module.challenge;
import dbProcs.Getter;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.Cookie;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
import java.io.*;
import java.util.Locale;
import java.util.ResourceBundle;
-import javax.servlet.ServletException;
-import javax.servlet.http.*;
import javax.xml.parsers.DocumentBuilder;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
diff --git a/src/main/java/servlets/module/lesson/CsrfLesson.java b/src/main/java/servlets/module/lesson/CsrfLesson.java
index 94fc8d004..28d87cbc9 100644
--- a/src/main/java/servlets/module/lesson/CsrfLesson.java
+++ b/src/main/java/servlets/module/lesson/CsrfLesson.java
@@ -1,16 +1,16 @@
package servlets.module.lesson;
import dbProcs.Getter;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.Cookie;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Locale;
import java.util.ResourceBundle;
-import javax.servlet.ServletException;
-import javax.servlet.http.Cookie;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpSession;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.owasp.encoder.Encode;
diff --git a/src/main/java/servlets/module/lesson/CsrfLessonTarget.java b/src/main/java/servlets/module/lesson/CsrfLessonTarget.java
index 3bcdd31fd..9a9dd6cd5 100644
--- a/src/main/java/servlets/module/lesson/CsrfLessonTarget.java
+++ b/src/main/java/servlets/module/lesson/CsrfLessonTarget.java
@@ -1,14 +1,14 @@
package servlets.module.lesson;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Locale;
import java.util.ResourceBundle;
-import javax.servlet.ServletException;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpSession;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import utils.ShepherdLogManager;
diff --git a/src/main/java/servlets/module/lesson/DirectObjectLesson.java b/src/main/java/servlets/module/lesson/DirectObjectLesson.java
index c63fd1a4b..1302086d1 100644
--- a/src/main/java/servlets/module/lesson/DirectObjectLesson.java
+++ b/src/main/java/servlets/module/lesson/DirectObjectLesson.java
@@ -1,14 +1,14 @@
package servlets.module.lesson;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Locale;
import java.util.ResourceBundle;
-import javax.servlet.ServletException;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpSession;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.owasp.encoder.Encode;
diff --git a/src/main/java/servlets/module/lesson/PoorValidationLesson.java b/src/main/java/servlets/module/lesson/PoorValidationLesson.java
index 4406fe339..7a2d3e65c 100644
--- a/src/main/java/servlets/module/lesson/PoorValidationLesson.java
+++ b/src/main/java/servlets/module/lesson/PoorValidationLesson.java
@@ -1,14 +1,14 @@
package servlets.module.lesson;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Locale;
import java.util.ResourceBundle;
-import javax.servlet.ServletException;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpSession;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import utils.Hash;
diff --git a/src/main/java/servlets/module/lesson/Redirect.java b/src/main/java/servlets/module/lesson/Redirect.java
index 18f5ada9f..30900938d 100644
--- a/src/main/java/servlets/module/lesson/Redirect.java
+++ b/src/main/java/servlets/module/lesson/Redirect.java
@@ -1,14 +1,14 @@
package servlets.module.lesson;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Locale;
import java.util.ResourceBundle;
-import javax.servlet.ServletException;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpSession;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import utils.ShepherdLogManager;
diff --git a/src/main/java/servlets/module/lesson/RedirectLessonTarget.java b/src/main/java/servlets/module/lesson/RedirectLessonTarget.java
index 9c7e290d5..475063c4a 100644
--- a/src/main/java/servlets/module/lesson/RedirectLessonTarget.java
+++ b/src/main/java/servlets/module/lesson/RedirectLessonTarget.java
@@ -1,14 +1,14 @@
package servlets.module.lesson;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Locale;
import java.util.ResourceBundle;
-import javax.servlet.ServletException;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpSession;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import utils.ShepherdLogManager;
diff --git a/src/main/java/servlets/module/lesson/SecurityMisconfigLesson.java b/src/main/java/servlets/module/lesson/SecurityMisconfigLesson.java
index 4f400c93f..e01805007 100644
--- a/src/main/java/servlets/module/lesson/SecurityMisconfigLesson.java
+++ b/src/main/java/servlets/module/lesson/SecurityMisconfigLesson.java
@@ -1,14 +1,14 @@
package servlets.module.lesson;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Locale;
import java.util.ResourceBundle;
-import javax.servlet.ServletException;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpSession;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.owasp.encoder.Encode;
diff --git a/src/main/java/servlets/module/lesson/SessionManagementLesson.java b/src/main/java/servlets/module/lesson/SessionManagementLesson.java
index d4c77e105..dcf779236 100644
--- a/src/main/java/servlets/module/lesson/SessionManagementLesson.java
+++ b/src/main/java/servlets/module/lesson/SessionManagementLesson.java
@@ -1,15 +1,15 @@
package servlets.module.lesson;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.Cookie;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Locale;
import java.util.ResourceBundle;
-import javax.servlet.ServletException;
-import javax.servlet.http.Cookie;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpSession;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import utils.Hash;
diff --git a/src/main/java/servlets/module/lesson/SqlInjectionLesson.java b/src/main/java/servlets/module/lesson/SqlInjectionLesson.java
index 1849f65c9..845e1a475 100644
--- a/src/main/java/servlets/module/lesson/SqlInjectionLesson.java
+++ b/src/main/java/servlets/module/lesson/SqlInjectionLesson.java
@@ -1,6 +1,11 @@
package servlets.module.lesson;
import dbProcs.Database;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
import java.io.IOException;
import java.io.PrintWriter;
import java.sql.Connection;
@@ -9,11 +14,6 @@
import java.sql.Statement;
import java.util.Locale;
import java.util.ResourceBundle;
-import javax.servlet.ServletException;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpSession;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.owasp.encoder.Encode;
diff --git a/src/main/java/servlets/module/lesson/UnvalidatedForwardsLesson.java b/src/main/java/servlets/module/lesson/UnvalidatedForwardsLesson.java
index 27f0d0f46..4f878d074 100644
--- a/src/main/java/servlets/module/lesson/UnvalidatedForwardsLesson.java
+++ b/src/main/java/servlets/module/lesson/UnvalidatedForwardsLesson.java
@@ -1,18 +1,18 @@
package servlets.module.lesson;
import dbProcs.Getter;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.Cookie;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
import java.io.IOException;
import java.io.PrintWriter;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Locale;
import java.util.ResourceBundle;
-import javax.servlet.ServletException;
-import javax.servlet.http.Cookie;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpSession;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.owasp.encoder.Encode;
diff --git a/src/main/java/servlets/module/lesson/XssLesson.java b/src/main/java/servlets/module/lesson/XssLesson.java
index 3ade358f8..3cc0beada 100644
--- a/src/main/java/servlets/module/lesson/XssLesson.java
+++ b/src/main/java/servlets/module/lesson/XssLesson.java
@@ -1,16 +1,16 @@
package servlets.module.lesson;
import dbProcs.Getter;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.Cookie;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Locale;
import java.util.ResourceBundle;
-import javax.servlet.ServletException;
-import javax.servlet.http.Cookie;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpSession;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import utils.FindXSS;
diff --git a/src/main/java/servlets/module/lesson/XxeLesson.java b/src/main/java/servlets/module/lesson/XxeLesson.java
index 72e6faa4b..6fdcd8d16 100644
--- a/src/main/java/servlets/module/lesson/XxeLesson.java
+++ b/src/main/java/servlets/module/lesson/XxeLesson.java
@@ -2,18 +2,18 @@
import dbProcs.FileInputProperties;
import dbProcs.Getter;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.Cookie;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.util.Locale;
import java.util.ResourceBundle;
-import javax.servlet.ServletException;
-import javax.servlet.http.Cookie;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpSession;
import javax.xml.parsers.DocumentBuilder;
import org.apache.commons.io.FileUtils;
import org.apache.logging.log4j.LogManager;
diff --git a/src/main/java/utils/LoginMethod.java b/src/main/java/utils/LoginMethod.java
index 833df8dfb..9ef1a3965 100644
--- a/src/main/java/utils/LoginMethod.java
+++ b/src/main/java/utils/LoginMethod.java
@@ -73,6 +73,12 @@ public static boolean isSaml() {
}
}
+ /** Overrides sso.properties; for tests. Null goes back to reading the file. */
+ static void setSaml(Boolean saml) {
+ isSet = saml != null;
+ isSaml = saml != null && saml;
+ }
+
public static Boolean isLogin() {
return (!isSaml());
}
diff --git a/src/main/java/utils/SamlRequestStore.java b/src/main/java/utils/SamlRequestStore.java
new file mode 100644
index 000000000..002f27080
--- /dev/null
+++ b/src/main/java/utils/SamlRequestStore.java
@@ -0,0 +1,195 @@
+package utils;
+
+import jakarta.servlet.http.Cookie;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import java.net.URI;
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.Base64;
+import java.util.Comparator;
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.ConcurrentHashMap;
+import org.opensaml.core.xml.XMLObject;
+import org.pac4j.core.context.WebContext;
+import org.pac4j.core.context.session.SessionStore;
+import org.pac4j.jee.context.JEEContext;
+import org.pac4j.saml.store.SAMLMessageStore;
+import org.pac4j.saml.store.SAMLMessageStoreFactory;
+
+/**
+ * Remembers the SAML requests this SP sends, each bound to the browser that started it, so pac4j
+ * only accepts a Response whose InResponseTo names a request started in the same browser. This
+ * stops login CSRF: a valid response for one account can't be replayed through another browser's
+ * assertion consumer service.
+ * Each request gets its own random token, in its own cookie (named {@value #COOKIE} plus a hash of
+ * the request ID), so logins started at once in several tabs can't overwrite each other's binding.
+ * The cookie is HttpOnly, Secure and SameSite=None so it survives the IdP's cross-site POST back,
+ * scoped to the SAML endpoint directory (the ACS URL's directory, e.g. /saml/), lives as long as
+ * its request and is deleted once the login completes. The session cookie's policy is left alone.
+ *
+ * This file is part of the Security Shepherd Project.
+ *
+ * The Security Shepherd project is free software: you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License as published by the Free Software Foundation, either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ *
The Security Shepherd project is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
+ * PURPOSE. See the GNU General Public License for more details.
+ *
+ *
You should have received a copy of the GNU General Public License along with the Security
+ * Shepherd project. If not, see .
+ */
+public class SamlRequestStore implements SAMLMessageStoreFactory {
+
+ /** Prefix of the binding cookies' names */
+ static final String COOKIE = "shepherd_saml_request";
+
+ /** How long a started login may take at the IdP */
+ static final Duration LIFETIME = Duration.ofMinutes(10);
+
+ /** Upper bound on pending requests; the oldest are dropped beyond it */
+ static int maxPending = 20000;
+
+ private static final SecureRandom random = new SecureRandom();
+
+ private static final class Pending {
+ final String token;
+ final XMLObject message;
+ final Instant expires;
+
+ Pending(String token, XMLObject message, Instant expires) {
+ this.token = token;
+ this.message = message;
+ this.expires = expires;
+ }
+ }
+
+ private final Map pending = new ConcurrentHashMap<>();
+ private final String cookiePath;
+
+ /**
+ * @param assertionConsumerServiceUrl The ACS URL, whose directory (including any context prefix)
+ * scopes the binding cookie, so both the login start and the ACS receive it
+ */
+ public SamlRequestStore(String assertionConsumerServiceUrl) {
+ String path = URI.create(assertionConsumerServiceUrl).getPath();
+ this.cookiePath =
+ path == null || path.lastIndexOf('/') < 0
+ ? "/"
+ : path.substring(0, path.lastIndexOf('/') + 1);
+ }
+
+ /** The binding cookie's Path */
+ String cookiePath() {
+ return cookiePath;
+ }
+
+ @Override
+ public SAMLMessageStore getMessageStore(WebContext context, SessionStore sessionStore) {
+ JEEContext jee = (JEEContext) context;
+ return new BoundStore(jee.getNativeRequest(), jee.getNativeResponse());
+ }
+
+ /** The name of the cookie binding a request to its browser */
+ static String cookieName(String requestId) {
+ try {
+ byte[] digest =
+ MessageDigest.getInstance("SHA-256").digest(requestId.getBytes(StandardCharsets.UTF_8));
+ return COOKIE
+ + "_"
+ + Base64.getUrlEncoder().withoutPadding().encodeToString(digest).substring(0, 22);
+ } catch (NoSuchAlgorithmException e) {
+ throw new IllegalStateException("SHA-256 is unavailable", e);
+ }
+ }
+
+ /** Forgets a request once its response has been accepted, so each request is used once */
+ void forget(String requestId) {
+ if (requestId != null) {
+ pending.remove(requestId);
+ }
+ }
+
+ /** As {@link #forget(String)}, also deleting the request's binding cookie from the browser */
+ void complete(String requestId, HttpServletResponse response) {
+ forget(requestId);
+ if (requestId != null) {
+ response.addCookie(bindingCookie(requestId, "", 0));
+ }
+ }
+
+ private Cookie bindingCookie(String requestId, String token, int maxAge) {
+ Cookie cookie = new Cookie(cookieName(requestId), token);
+ cookie.setPath(cookiePath);
+ cookie.setHttpOnly(true);
+ cookie.setSecure(true);
+ cookie.setMaxAge(maxAge);
+ cookie.setAttribute("SameSite", "None");
+ return cookie;
+ }
+
+ private final class BoundStore implements SAMLMessageStore {
+ private final HttpServletRequest request;
+ private final HttpServletResponse response;
+
+ BoundStore(HttpServletRequest request, HttpServletResponse response) {
+ this.request = request;
+ this.response = response;
+ }
+
+ @Override
+ public void set(String id, XMLObject message) {
+ Instant now = Instant.now();
+ pending.values().removeIf(p -> p.expires.isBefore(now));
+ while (pending.size() >= maxPending) {
+ pending.entrySet().stream()
+ .min(Comparator.comparing(e -> e.getValue().expires))
+ .ifPresent(oldest -> pending.remove(oldest.getKey()));
+ }
+ byte[] bytes = new byte[32];
+ random.nextBytes(bytes);
+ String token = Base64.getUrlEncoder().withoutPadding().encodeToString(bytes);
+ response.addCookie(bindingCookie(id, token, (int) LIFETIME.getSeconds()));
+ pending.put(id, new Pending(token, message, now.plus(LIFETIME)));
+ }
+
+ @Override
+ public Optional get(String id) {
+ Pending entry = id == null ? null : pending.get(id);
+ String token = id == null ? null : cookieToken(cookieName(id));
+ if (entry == null || token == null || entry.expires.isBefore(Instant.now())) {
+ return Optional.empty();
+ }
+ boolean sameBrowser =
+ MessageDigest.isEqual(
+ entry.token.getBytes(StandardCharsets.US_ASCII),
+ token.getBytes(StandardCharsets.US_ASCII));
+ return sameBrowser ? Optional.of(entry.message) : Optional.empty();
+ }
+
+ @Override
+ public void remove(String id) {
+ forget(id);
+ }
+
+ private String cookieToken(String name) {
+ Cookie[] cookies = request.getCookies();
+ if (cookies == null) {
+ return null;
+ }
+ for (Cookie cookie : cookies) {
+ if (name.equals(cookie.getName()) && !cookie.getValue().isEmpty()) {
+ return cookie.getValue();
+ }
+ }
+ return null;
+ }
+ }
+}
diff --git a/src/main/java/utils/SamlSessionRegistry.java b/src/main/java/utils/SamlSessionRegistry.java
new file mode 100644
index 000000000..1c84ee00b
--- /dev/null
+++ b/src/main/java/utils/SamlSessionRegistry.java
@@ -0,0 +1,134 @@
+package utils;
+
+import jakarta.servlet.http.HttpSession;
+import jakarta.servlet.http.HttpSessionBindingEvent;
+import jakarta.servlet.http.HttpSessionBindingListener;
+import java.io.Serializable;
+import java.util.Map;
+import java.util.UUID;
+import java.util.concurrent.ConcurrentHashMap;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+/**
+ * Tracks which HTTP sessions were created by which SAML login, so an IdP-initiated logout can end
+ * exactly the sessions it names (by NameID and SessionIndex), whichever browser delivers the logout
+ * message, and whether or not it carries a session cookie.
+ * Entries are removed when their session ends (logout, invalidation or timeout). The registry is
+ * held in memory, so sessions restored after a server restart are not tracked.
+ * This file is part of the Security Shepherd Project.
+ *
+ * The Security Shepherd project is free software: you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License as published by the Free Software Foundation, either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ *
The Security Shepherd project is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
+ * PURPOSE. See the GNU General Public License for more details.
+ *
+ *
You should have received a copy of the GNU General Public License along with the Security
+ * Shepherd project. If not, see .
+ */
+public class SamlSessionRegistry {
+
+ private static final Logger log = LogManager.getLogger(SamlSessionRegistry.class);
+
+ static final String REGISTRATION_ATTRIBUTE = "samlSessionRegistration";
+
+ private static final Map sessions = new ConcurrentHashMap<>();
+
+ private static class Entry {
+ final HttpSession session;
+ final String nameId;
+ final String sessionIndex;
+
+ Entry(HttpSession session, String nameId, String sessionIndex) {
+ this.session = session;
+ this.nameId = nameId;
+ this.sessionIndex = sessionIndex;
+ }
+ }
+
+ /** Session attribute that removes the registry entry when its session ends */
+ private static class Registration implements HttpSessionBindingListener, Serializable {
+ private static final long serialVersionUID = 1L;
+ private final String key;
+
+ Registration(String key) {
+ this.key = key;
+ }
+
+ @Override
+ public void valueUnbound(HttpSessionBindingEvent event) {
+ sessions.remove(key);
+ }
+ }
+
+ /**
+ * Records that a session belongs to a SAML login.
+ *
+ * @param session The post-login session
+ * @param nameId The complete NameID identity from the assertion (see
+ * SamlSso.registerLoginSession)
+ * @param sessionIndex The SessionIndex from the assertion, if any
+ */
+ public static void register(HttpSession session, String nameId, String sessionIndex) {
+ String key = UUID.randomUUID().toString();
+ sessions.put(key, new Entry(session, nameId, sessionIndex));
+ session.setAttribute(REGISTRATION_ATTRIBUTE, new Registration(key));
+ }
+
+ /**
+ * Invalidates the sessions belonging to a SAML login.
+ *
+ * @param nameId The complete NameID identity named in the logout request
+ * @param sessionIndex The SessionIndex named in the logout request, or null to end every session
+ * for the NameID
+ * @return How many sessions were invalidated
+ */
+ public static int invalidate(String nameId, String sessionIndex) {
+ if (nameId == null) {
+ return 0;
+ }
+ int invalidated = 0;
+ for (Map.Entry item : sessions.entrySet()) {
+ Entry entry = item.getValue();
+ if (!nameId.equals(entry.nameId)
+ || (sessionIndex != null && !sessionIndex.equals(entry.sessionIndex))) {
+ continue;
+ }
+ sessions.remove(item.getKey());
+ try {
+ entry.session.invalidate();
+ invalidated++;
+ } catch (IllegalStateException alreadyInvalidated) {
+ log.debug("Session for SAML logout was already invalidated");
+ }
+ }
+ return invalidated;
+ }
+
+ /**
+ * Whether a session is tracked for this login.
+ *
+ * @param nameId The complete NameID identity
+ * @param sessionIndex The SessionIndex
+ * @return true if a live session was registered for exactly this NameID and SessionIndex
+ */
+ public static boolean isRegistered(String nameId, String sessionIndex) {
+ if (nameId == null || sessionIndex == null) {
+ return false;
+ }
+ for (Entry entry : sessions.values()) {
+ if (nameId.equals(entry.nameId) && sessionIndex.equals(entry.sessionIndex)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /** Number of tracked sessions; for tests */
+ static int size() {
+ return sessions.size();
+ }
+}
diff --git a/src/main/java/utils/SamlSso.java b/src/main/java/utils/SamlSso.java
new file mode 100644
index 000000000..824d550e4
--- /dev/null
+++ b/src/main/java/utils/SamlSso.java
@@ -0,0 +1,1071 @@
+package utils;
+
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.math.BigInteger;
+import java.net.URLDecoder;
+import java.nio.charset.StandardCharsets;
+import java.security.GeneralSecurityException;
+import java.security.KeyFactory;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.KeyStore;
+import java.security.PrivateKey;
+import java.security.Signature;
+import java.security.cert.Certificate;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.PKCS8EncodedKeySpec;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.Base64;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Properties;
+import java.util.Set;
+import java.util.UUID;
+import java.util.concurrent.ConcurrentHashMap;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.bouncycastle.asn1.DERNull;
+import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers;
+import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
+import org.bouncycastle.asn1.pkcs.RSAPrivateKey;
+import org.bouncycastle.asn1.x500.X500Name;
+import org.bouncycastle.asn1.x509.AlgorithmIdentifier;
+import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
+import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;
+import org.bouncycastle.operator.OperatorCreationException;
+import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
+import org.opensaml.saml.saml2.core.LogoutRequest;
+import org.opensaml.saml.saml2.core.NameID;
+import org.opensaml.saml.saml2.core.NameIDType;
+import org.opensaml.saml.saml2.core.Response;
+import org.opensaml.saml.saml2.core.SessionIndex;
+import org.opensaml.saml.saml2.core.SubjectConfirmationData;
+import org.opensaml.saml.saml2.metadata.SPSSODescriptor;
+import org.opensaml.xmlsec.encryption.support.DecryptionException;
+import org.owasp.encoder.Encode;
+import org.pac4j.core.context.CallContext;
+import org.pac4j.core.credentials.Credentials;
+import org.pac4j.core.exception.http.HttpAction;
+import org.pac4j.core.http.callback.NoParameterCallbackUrlResolver;
+import org.pac4j.core.keystore.generation.KeystoreGenerator;
+import org.pac4j.core.profile.UserProfile;
+import org.pac4j.jee.context.JEEContext;
+import org.pac4j.jee.context.session.JEESessionStore;
+import org.pac4j.jee.http.adapter.JEEHttpActionAdapter;
+import org.pac4j.saml.client.SAML2Client;
+import org.pac4j.saml.config.SAML2Configuration;
+import org.pac4j.saml.context.SAML2MessageContext;
+import org.pac4j.saml.credentials.SAML2Credentials;
+import org.pac4j.saml.exceptions.SAMLAuthnContextClassRefException;
+import org.pac4j.saml.metadata.SAML2FileSystemMetadataGenerator;
+import org.pac4j.saml.profile.SAML2Profile;
+import org.pac4j.saml.sso.impl.SAML2AuthnResponseValidator;
+import org.springframework.core.io.ByteArrayResource;
+
+/**
+ * SAML 2.0 single sign-on, backed by pac4j-saml.
+ * The service provider and identity provider are configured from onelogin.saml.properties on the
+ * classpath, using the same keys the previous java-saml implementation read, so existing
+ * deployments keep their configuration.
+ * This file is part of the Security Shepherd Project.
+ *
+ * The Security Shepherd project is free software: you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License as published by the Free Software Foundation, either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ *
The Security Shepherd project is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
+ * PURPOSE. See the GNU General Public License for more details.
+ *
+ *
You should have received a copy of the GNU General Public License along with the Security
+ * Shepherd project. If not, see .
+ */
+public class SamlSso {
+
+ private static final Logger log = LogManager.getLogger(SamlSso.class);
+
+ /** Session attribute holding the authenticated user's SAML profile, used for single logout */
+ public static final String PROFILE_SESSION_ATTRIBUTE = "samlProfile";
+
+ static final String PROPERTIES_FILE = "onelogin.saml.properties";
+
+ static final String SP_ENTITY_ID = "onelogin.saml2.sp.entityid";
+ static final String SP_ACS_URL = "onelogin.saml2.sp.assertion_consumer_service.url";
+ static final String SP_ACS_BINDING = "onelogin.saml2.sp.assertion_consumer_service.binding";
+ static final String SP_SLS_URL = "onelogin.saml2.sp.single_logout_service.url";
+ static final String SP_NAMEID_FORMAT = "onelogin.saml2.sp.nameidformat";
+ static final String SP_CERT = "onelogin.saml2.sp.x509cert";
+ static final String SP_KEY = "onelogin.saml2.sp.privatekey";
+ static final String IDP_ENTITY_ID = "onelogin.saml2.idp.entityid";
+ static final String IDP_SSO_URL = "onelogin.saml2.idp.single_sign_on_service.url";
+ static final String IDP_SSO_BINDING = "onelogin.saml2.idp.single_sign_on_service.binding";
+ static final String IDP_SLO_URL = "onelogin.saml2.idp.single_logout_service.url";
+ static final String IDP_SLO_RESPONSE_URL =
+ "onelogin.saml2.idp.single_logout_service.response.url";
+ static final String IDP_SLO_BINDING = "onelogin.saml2.idp.single_logout_service.binding";
+ static final String IDP_CERT = "onelogin.saml2.idp.x509cert";
+ static final String IDP_CERT_FINGERPRINT = "onelogin.saml2.idp.certfingerprint";
+ static final String STRICT = "onelogin.saml2.strict";
+ static final String AUTHN_REQUEST_SIGNED = "onelogin.saml2.security.authnrequest_signed";
+ static final String LOGOUT_REQUEST_SIGNED = "onelogin.saml2.security.logoutrequest_signed";
+ static final String WANT_MESSAGES_SIGNED = "onelogin.saml2.security.want_messages_signed";
+ static final String WANT_ASSERTIONS_SIGNED = "onelogin.saml2.security.want_assertions_signed";
+ static final String SIGN_METADATA = "onelogin.saml2.security.sign_metadata";
+ static final String REQUESTED_AUTHN_CONTEXT = "onelogin.saml2.security.requested_authncontext";
+ static final String REQUESTED_AUTHN_CONTEXT_COMPARISON =
+ "onelogin.saml2.security.requested_authncontextcomparison";
+
+ /** The key as misspelled in the pre-pac4j sample file, still accepted */
+ static final String LEGACY_REQUESTED_AUTHN_CONTEXT_COMPARISON =
+ "onelogin.saml2.security.onelogin.saml2.security.requested_authncontextcomparison";
+
+ static final String SIGNATURE_ALGORITHM = "onelogin.saml2.security.signature_algorithm";
+
+ private static final String HTTP_REDIRECT_BINDING =
+ "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect";
+ private static final String HTTP_POST_BINDING = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST";
+ private static final String SP_KEY_ALIAS = "shepherd-sp";
+
+ /**
+ * HTTP-Redirect binding signature algorithms accepted on logout messages (no SHA-1). XML-DSig
+ * ECDSA signatures are the raw r || s concatenation (IEEE P1363), not ASN.1 DER.
+ */
+ private static final Map REDIRECT_SIGNATURE_ALGORITHMS = new HashMap<>();
+
+ static {
+ REDIRECT_SIGNATURE_ALGORITHMS.put(
+ "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256", "SHA256withRSA");
+ REDIRECT_SIGNATURE_ALGORITHMS.put(
+ "http://www.w3.org/2001/04/xmldsig-more#rsa-sha384", "SHA384withRSA");
+ REDIRECT_SIGNATURE_ALGORITHMS.put(
+ "http://www.w3.org/2001/04/xmldsig-more#rsa-sha512", "SHA512withRSA");
+ REDIRECT_SIGNATURE_ALGORITHMS.put(
+ "http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha256", "SHA256withECDSAinP1363Format");
+ REDIRECT_SIGNATURE_ALGORITHMS.put(
+ "http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha384", "SHA384withECDSAinP1363Format");
+ REDIRECT_SIGNATURE_ALGORITHMS.put(
+ "http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha512", "SHA512withECDSAinP1363Format");
+ }
+
+ private static final Set REDIRECT_SIGNED_PARAMETERS =
+ Set.of("SAMLRequest", "SAMLResponse", "RelayState", "SigAlg", "Signature");
+
+ /** How long after its IssueInstant an IdP LogoutRequest is accepted (plus clock skew) */
+ static final Duration LOGOUT_REQUEST_LIFETIME = Duration.ofMinutes(5);
+
+ /** Allowed clock difference between the IdP and this server */
+ static final Duration CLOCK_SKEW = Duration.ofMinutes(5);
+
+ /** Upper bound on remembered LogoutRequest IDs; beyond it new requests are refused */
+ static int maxRememberedLogoutRequests = 10000;
+
+ /**
+ * IDs of LogoutRequests already acted on, with when they can be forgotten. An entry outlives the
+ * window in which its request would be accepted, so a replay is always caught here or by the age
+ * check.
+ */
+ private static final Map seenLogoutRequests = new ConcurrentHashMap<>();
+
+ /** The configured SAML client and the settings SamlSso enforces itself */
+ static final class Sso {
+ final SAML2Client client;
+ final X509Certificate idpCertificate;
+
+ /** want_messages_signed: logout messages must carry a valid IdP signature */
+ final boolean requireSignedLogout;
+
+ /**
+ * This SP's single logout endpoint, which IdP LogoutRequests must be addressed to; null when
+ * single logout isn't configured
+ */
+ final String singleLogoutUrl;
+
+ /** Defaults for omitted NameQualifier / SPNameQualifier (SAML Core 8.3.7) */
+ final String idpEntityId;
+
+ final String spEntityId;
+
+ /** Binds each sent AuthnRequest to the browser that started the login */
+ final SamlRequestStore requestStore;
+
+ Sso(
+ SAML2Client client,
+ X509Certificate idpCertificate,
+ boolean requireSignedLogout,
+ String singleLogoutUrl,
+ String idpEntityId,
+ String spEntityId,
+ SamlRequestStore requestStore) {
+ this.client = client;
+ this.idpCertificate = idpCertificate;
+ this.requireSignedLogout = requireSignedLogout;
+ this.singleLogoutUrl = singleLogoutUrl;
+ this.idpEntityId = idpEntityId;
+ this.spEntityId = spEntityId;
+ this.requestStore = requestStore;
+ }
+ }
+
+ private static volatile Sso sso;
+
+ private static Sso get() {
+ Sso result = sso;
+ if (result == null) {
+ synchronized (SamlSso.class) {
+ result = sso;
+ if (result == null) {
+ result = build(loadProperties());
+ sso = result;
+ }
+ }
+ }
+ return result;
+ }
+
+ /** Replaces the configuration built from onelogin.saml.properties; for tests. */
+ static void set(Sso replacement) {
+ sso = replacement;
+ }
+
+ /**
+ * Returns the SAML client, building it from onelogin.saml.properties on first use.
+ *
+ * @return The initialised SAML client
+ */
+ public static SAML2Client getClient() {
+ return get().client;
+ }
+
+ /**
+ * Sends the browser to the identity provider with a SAML authentication request.
+ *
+ * @param request The incoming request
+ * @param response The response the redirect (or auto-submitting form) is written to
+ */
+ public static void login(HttpServletRequest request, HttpServletResponse response) {
+ CallContext ctx = callContext(request, response);
+ HttpAction action =
+ getClient()
+ .getRedirectionAction(ctx)
+ .orElseThrow(() -> new IllegalStateException("No SAML redirection action"));
+ JEEHttpActionAdapter.INSTANCE.adapt(action, ctx.webContext());
+ }
+
+ /**
+ * Validates the SAML response posted to the assertion consumer service.
+ *
+ * @param request The ACS request carrying the SAML response
+ * @param response The ACS response
+ * @return The authenticated user's profile, or empty if the response did not authenticate a user
+ */
+ public static Optional authenticate(
+ HttpServletRequest request, HttpServletResponse response) {
+ Sso config = get();
+ SAML2Client saml = config.client;
+ CallContext ctx = callContext(request, response);
+ try {
+ Optional extracted = saml.getCredentials(ctx);
+ if (extracted.isEmpty()) {
+ return Optional.empty();
+ }
+ // Only answers to a login started in this browser are accepted (login CSRF): an unsolicited
+ // (IdP-initiated) Response has no InResponseTo to bind it to a browser, so it is refused;
+ // otherwise pac4j checks InResponseTo against this browser's pending requests
+ String inResponseTo = inResponseTo(extracted.get());
+ if (inResponseTo == null) {
+ log.error("SAML response rejected: unsolicited (no InResponseTo)");
+ return Optional.empty();
+ }
+ Optional credentials = saml.validateCredentials(ctx, extracted.get());
+ if (credentials.isEmpty()) {
+ return Optional.empty();
+ }
+ config.requestStore.complete(inResponseTo, response);
+ Optional profile = saml.getUserProfile(ctx, credentials.get());
+ return profile.filter(SAML2Profile.class::isInstance).map(SAML2Profile.class::cast);
+ } catch (RuntimeException e) {
+ // Signature, issuer, audience, destination and time checks fail with exceptions
+ log.error("SAML response rejected: " + e.getMessage());
+ log.debug("SAML response rejected", e);
+ return Optional.empty();
+ }
+ }
+
+ private static String inResponseTo(Credentials credentials) {
+ if (!(credentials instanceof SAML2Credentials)) {
+ return null;
+ }
+ Object message = ((SAML2Credentials) credentials).getContext().getMessageContext().getMessage();
+ return message instanceof Response ? ((Response) message).getInResponseTo() : null;
+ }
+
+ /**
+ * Starts SP-initiated single logout for the given profile.
+ *
+ * @param request The logout request
+ * @param response The response the redirect (or auto-submitting form) is written to
+ * @param profile The SAML profile captured at login
+ * @return true if the browser was sent to the identity provider, false if no logout could be
+ * started (no IdP logout URL configured) and the caller must respond itself
+ */
+ public static boolean logout(
+ HttpServletRequest request, HttpServletResponse response, SAML2Profile profile) {
+ CallContext ctx = callContext(request, response);
+ Optional action =
+ getClient().getLogoutAction(ctx, profile, null).map(HttpAction.class::cast);
+ if (action.isEmpty()) {
+ return false;
+ }
+ JEEHttpActionAdapter.INSTANCE.adapt(action.get(), ctx.webContext());
+ return true;
+ }
+
+ /**
+ * Records a post-login session against the SAML login that created it, so an IdP-initiated logout
+ * naming that login can end it.
+ *
+ * @param session The post-login session
+ * @param profile The profile from the validated assertion
+ */
+ public static void registerLoginSession(HttpSession session, SAML2Profile profile) {
+ SamlSessionRegistry.register(
+ session,
+ nameIdKey(
+ get(),
+ profile.getId(),
+ profile.getSamlNameIdFormat(),
+ profile.getSamlNameIdNameQualifier(),
+ profile.getSamlNameIdSpNameQualifier()),
+ profile.getSessionIndex());
+ }
+
+ /** The complete identity of a NameID, as {@link SamlSessionRegistry} matches it */
+ static String nameIdKey(
+ String value, String format, String nameQualifier, String spNameQualifier) {
+ return nameIdKey(get(), value, format, nameQualifier, spNameQualifier);
+ }
+
+ /**
+ * Two NameIDs identify the same principal only if value, Format, NameQualifier and
+ * SPNameQualifier all match. Omitted fields take their SAML defaults: unspecified format, and the
+ * IdP / SP entity IDs as qualifiers (SAML Core 8.3.7).
+ */
+ private static String nameIdKey(
+ Sso config, String value, String format, String nameQualifier, String spNameQualifier) {
+ if (value == null) {
+ return null;
+ }
+ return String.join(
+ "\u0000",
+ value,
+ format == null || format.isEmpty() ? NameIDType.UNSPECIFIED : format,
+ nameQualifier == null || nameQualifier.isEmpty() ? config.idpEntityId : nameQualifier,
+ spNameQualifier == null || spNameQualifier.isEmpty() ? config.spEntityId : spNameQualifier);
+ }
+
+ /** Outcome of a message received on the single logout service */
+ public enum LogoutMessage {
+ /**
+ * The IdP asked to end a login: the matching sessions were invalidated and a LogoutResponse has
+ * been sent back to the IdP
+ */
+ IDP_LOGOUT_REQUEST,
+ /** The IdP confirmed an SP-initiated logout */
+ LOGOUT_RESPONSE,
+ /** The message was missing, unsigned when a signature is required, or failed validation */
+ INVALID
+ }
+
+ /**
+ * Validates a LogoutRequest or LogoutResponse received on the single logout service.
+ *
+ * When want_messages_signed is set, the message must be signed by the configured IdP: either
+ * an XML signature (HTTP-POST binding) or a query-string signature (HTTP-Redirect binding).
+ *
+ *
For an IdP-initiated LogoutRequest, the sessions created by the login it names (NameID and
+ * SessionIndex) are invalidated, whichever browser delivered the request, and the LogoutResponse
+ * is written to the response. A LogoutResponse ends nothing itself: the local session was already
+ * ended when the SP-initiated logout started.
+ *
+ * @param request The SLS request
+ * @param response The SLS response
+ * @return What was received
+ */
+ public static LogoutMessage processLogoutMessage(
+ HttpServletRequest request, HttpServletResponse response) {
+ Sso config = get();
+ SAML2Client saml = config.client;
+ CallContext ctx = callContext(request, response);
+ try {
+ Optional credentials =
+ saml.getCredentials(ctx).flatMap(c -> saml.validateCredentials(ctx, c));
+ if (credentials.isEmpty() || !(credentials.get() instanceof SAML2Credentials)) {
+ return LogoutMessage.INVALID;
+ }
+ SAML2MessageContext messageContext = ((SAML2Credentials) credentials.get()).getContext();
+ // pac4j validates an XML signature when one is present, but never requires one and does not
+ // check HTTP-Redirect query-string signatures
+ boolean signed =
+ messageContext.getSAMLPeerEntityContext().isAuthenticated()
+ || hasValidRedirectSignature(request, config.idpCertificate);
+ if (config.requireSignedLogout && !signed) {
+ log.error("SAML logout message rejected: no valid IdP signature");
+ return LogoutMessage.INVALID;
+ }
+
+ Object message = messageContext.getMessageContext().getMessage();
+ if (!(message instanceof LogoutRequest)) {
+ return LogoutMessage.LOGOUT_RESPONSE;
+ }
+ LogoutRequest logoutRequest = (LogoutRequest) message;
+ // pac4j checks none of these for LogoutRequests
+ String rejection = checkLogoutRequest(logoutRequest, signed, config.singleLogoutUrl);
+ if (rejection != null) {
+ log.error("SAML LogoutRequest rejected: " + rejection);
+ return LogoutMessage.INVALID;
+ }
+ NameID nameId = logoutRequest.getNameID();
+ if (nameId == null && logoutRequest.getEncryptedID() != null) {
+ nameId = (NameID) saml.getDecrypter().decrypt(logoutRequest.getEncryptedID());
+ }
+ String nameIdKey =
+ nameId == null
+ ? null
+ : nameIdKey(
+ config,
+ nameId.getValue(),
+ nameId.getFormat(),
+ nameId.getNameQualifier(),
+ nameId.getSPNameQualifier());
+ List sessionIndexes = new ArrayList<>();
+ for (SessionIndex index : logoutRequest.getSessionIndexes()) {
+ if (index != null && index.getValue() != null) {
+ sessionIndexes.add(index.getValue());
+ }
+ }
+ // Only a request that will end something takes a slot in the replay cache: a signed request,
+ // or an unsigned one naming a login we track. Otherwise unauthenticated no-op requests could
+ // fill the cache and lock out genuine signed logouts. (A NameID alone is guessable, so only
+ // a signed request may end all of a user's sessions.)
+ boolean acts =
+ signed
+ || sessionIndexes.stream()
+ .anyMatch(index -> SamlSessionRegistry.isRegistered(nameIdKey, index));
+ int ended = 0;
+ if (acts) {
+ String replay = recordLogoutRequest(logoutRequest);
+ if (replay != null) {
+ log.error("SAML LogoutRequest rejected: " + replay);
+ return LogoutMessage.INVALID;
+ }
+ if (!sessionIndexes.isEmpty()) {
+ for (String sessionIndex : sessionIndexes) {
+ ended += SamlSessionRegistry.invalidate(nameIdKey, sessionIndex);
+ }
+ } else {
+ ended = SamlSessionRegistry.invalidate(nameIdKey, null);
+ }
+ } else {
+ log.error("Unsigned SAML LogoutRequest names no tracked login; no sessions ended");
+ }
+ log.debug("IdP-initiated SAML logout ended " + ended + " session(s)");
+ HttpAction action = saml.processLogout(ctx, credentials.get());
+ JEEHttpActionAdapter.INSTANCE.adapt(action, ctx.webContext());
+ return LogoutMessage.IDP_LOGOUT_REQUEST;
+ } catch (RuntimeException | DecryptionException e) {
+ log.error("SAML logout message rejected: " + e.getMessage());
+ log.debug("SAML logout message rejected", e);
+ return LogoutMessage.INVALID;
+ }
+ }
+
+ /**
+ * Checks an IdP LogoutRequest's addressing and freshness before it may end sessions. A signed
+ * request must name this SP's logout endpoint as its Destination (SAML Bindings 3.4.5.2 /
+ * 3.5.5.2); any Destination given must match. Replay is checked by {@link #recordLogoutRequest},
+ * only for requests that will act.
+ *
+ * @return why the request is rejected, or null if it may be acted on
+ */
+ static String checkLogoutRequest(LogoutRequest request, boolean signed, String singleLogoutUrl) {
+ if (singleLogoutUrl == null) {
+ return "no single logout endpoint is configured (" + SP_SLS_URL + ")";
+ }
+ String destination = request.getDestination();
+ if (destination == null ? signed : !destination.equals(singleLogoutUrl)) {
+ return "Destination " + destination + " is not " + singleLogoutUrl;
+ }
+ Instant now = Instant.now();
+ Instant issued = request.getIssueInstant();
+ if (issued == null
+ || issued.isAfter(now.plus(CLOCK_SKEW))
+ || issued.isBefore(now.minus(LOGOUT_REQUEST_LIFETIME).minus(CLOCK_SKEW))) {
+ return "IssueInstant " + issued + " is outside the accepted window";
+ }
+ Instant notOnOrAfter = request.getNotOnOrAfter();
+ if (notOnOrAfter != null && !now.minus(CLOCK_SKEW).isBefore(notOnOrAfter)) {
+ return "expired at " + notOnOrAfter;
+ }
+ if (request.getID() == null) {
+ return "no ID";
+ }
+ return null;
+ }
+
+ /**
+ * Records a LogoutRequest's ID (per issuer) before it ends sessions, rejecting a replay. Fails
+ * closed when the cache is full.
+ *
+ * @return why the request is rejected, or null if it was recorded
+ */
+ static String recordLogoutRequest(LogoutRequest request) {
+ Instant now = Instant.now();
+ String issuer = request.getIssuer() == null ? "" : request.getIssuer().getValue();
+ seenLogoutRequests.values().removeIf(forgetAt -> forgetAt.isBefore(now));
+ if (seenLogoutRequests.size() >= maxRememberedLogoutRequests) {
+ return "too many recent logout requests to check for replay";
+ }
+ Instant forgetAt = now.plus(LOGOUT_REQUEST_LIFETIME).plus(CLOCK_SKEW.multipliedBy(2));
+ if (seenLogoutRequests.putIfAbsent(issuer + "|" + request.getID(), forgetAt) != null) {
+ return "replay of " + request.getID();
+ }
+ return null;
+ }
+
+ /** Empties the replay cache; for tests. */
+ static void clearRememberedLogoutRequests() {
+ seenLogoutRequests.clear();
+ }
+
+ /**
+ * Verifies an HTTP-Redirect binding signature (SAML Bindings 3.4.4.1): the signature covers
+ * SAMLRequest/SAMLResponse, RelayState and SigAlg exactly as they appear, URL-encoded, in the
+ * query string.
+ *
+ * The signed values must be the ones the servlet container (and so pac4j) reads: only a GET is
+ * accepted, each signed parameter must appear exactly once under its literal name (no
+ * percent-encoded aliases, no duplicates, nothing extra in a request body), and its decoded value
+ * must equal the container's.
+ */
+ static boolean hasValidRedirectSignature(HttpServletRequest request, X509Certificate idpCert) {
+ String query = request.getQueryString();
+ if (!"GET".equalsIgnoreCase(request.getMethod()) || query == null || idpCert == null) {
+ return false;
+ }
+ Map raw = new HashMap<>();
+ for (String pair : query.split("&")) {
+ if (pair.isEmpty()) {
+ continue;
+ }
+ int eq = pair.indexOf('=');
+ String rawName = eq < 0 ? pair : pair.substring(0, eq);
+ String name;
+ try {
+ name = URLDecoder.decode(rawName, StandardCharsets.UTF_8);
+ } catch (IllegalArgumentException e) {
+ return false;
+ }
+ if (!REDIRECT_SIGNED_PARAMETERS.contains(name)) {
+ continue;
+ }
+ if (!name.equals(rawName)
+ || raw.putIfAbsent(name, eq < 0 ? "" : pair.substring(eq + 1)) != null) {
+ log.error("SAML redirect query has an encoded or repeated " + name + " parameter");
+ return false;
+ }
+ }
+ if (raw.containsKey("SAMLRequest") == raw.containsKey("SAMLResponse")) {
+ return false;
+ }
+ for (Map.Entry param : raw.entrySet()) {
+ String[] values = request.getParameterValues(param.getKey());
+ String decoded;
+ try {
+ decoded = URLDecoder.decode(param.getValue(), StandardCharsets.UTF_8);
+ } catch (IllegalArgumentException e) {
+ return false;
+ }
+ if (values == null || values.length != 1 || !decoded.equals(values[0])) {
+ log.error("SAML redirect parameter " + param.getKey() + " does not match the signed query");
+ return false;
+ }
+ }
+ String messageParam = raw.containsKey("SAMLRequest") ? "SAMLRequest" : "SAMLResponse";
+ String sigAlg = raw.get("SigAlg");
+ String signature = raw.get("Signature");
+ if (sigAlg == null || signature == null) {
+ return false;
+ }
+ String algorithm =
+ REDIRECT_SIGNATURE_ALGORITHMS.get(URLDecoder.decode(sigAlg, StandardCharsets.UTF_8));
+ if (algorithm == null) {
+ log.error("Unsupported SAML redirect signature algorithm: " + sigAlg);
+ return false;
+ }
+ StringBuilder signedContent = new StringBuilder();
+ signedContent.append(messageParam).append('=').append(raw.get(messageParam));
+ if (raw.containsKey("RelayState")) {
+ signedContent.append("&RelayState=").append(raw.get("RelayState"));
+ }
+ signedContent.append("&SigAlg=").append(sigAlg);
+ try {
+ Signature verifier = Signature.getInstance(algorithm);
+ verifier.initVerify(idpCert.getPublicKey());
+ verifier.update(signedContent.toString().getBytes(StandardCharsets.UTF_8));
+ return verifier.verify(
+ Base64.getDecoder().decode(URLDecoder.decode(signature, StandardCharsets.UTF_8)));
+ } catch (GeneralSecurityException | IllegalArgumentException e) {
+ log.debug("SAML redirect signature verification failed", e);
+ return false;
+ }
+ }
+
+ /**
+ * Returns this service provider's SAML metadata.
+ *
+ * @return The SP metadata XML
+ */
+ public static String getServiceProviderMetadata() {
+ return getClient().getServiceProviderMetadataResolver().getMetadata();
+ }
+
+ static CallContext callContext(HttpServletRequest request, HttpServletResponse response) {
+ return new CallContext(new JEEContext(request, response), new JEESessionStore());
+ }
+
+ static Properties loadProperties() {
+ try (InputStream in = SamlSso.class.getClassLoader().getResourceAsStream(PROPERTIES_FILE)) {
+ if (in == null) {
+ throw new IllegalStateException(
+ "SAML properties file '" + PROPERTIES_FILE + "' not found in the classpath");
+ }
+ Properties props = new Properties();
+ props.load(in);
+ return props;
+ } catch (IOException e) {
+ throw new IllegalStateException(
+ "SAML properties file '" + PROPERTIES_FILE + "' cannot be loaded", e);
+ }
+ }
+
+ /**
+ * Builds and initialises the SAML client from java-saml style properties.
+ *
+ * @param props onelogin.saml.properties contents
+ * @return The initialised client and the settings SamlSso enforces itself
+ */
+ static Sso build(Properties props) {
+ if (!flag(props, STRICT, true)) {
+ log.warn(STRICT + "=false is ignored: SAML messages are always validated strictly");
+ }
+
+ SAML2Configuration config = new SAML2Configuration();
+ config.setServiceProviderEntityId(required(props, SP_ENTITY_ID));
+ config.setIdentityProviderEntityId(required(props, IDP_ENTITY_ID));
+ config.setSingleSignOutServiceUrl(optional(props, SP_SLS_URL));
+ config.setResponseBindingType(optional(props, SP_ACS_BINDING, HTTP_POST_BINDING));
+ config.setAuthnRequestBindingType(optional(props, IDP_SSO_BINDING, HTTP_REDIRECT_BINDING));
+ String sloBinding = optional(props, IDP_SLO_BINDING, HTTP_REDIRECT_BINDING);
+ config.setSpLogoutRequestBindingType(sloBinding);
+ config.setSpLogoutResponseBindingType(sloBinding);
+ config.setNameIdPolicyFormat(optional(props, SP_NAMEID_FORMAT));
+ // Send NameQualifier / SPNameQualifier back in LogoutRequests, so IdPs that issue qualified
+ // NameIDs can match the logout to the login
+ config.setUseNameQualifier(true);
+
+ config.setAuthnRequestSigned(flag(props, AUTHN_REQUEST_SIGNED, false));
+ config.setSpLogoutRequestSigned(flag(props, LOGOUT_REQUEST_SIGNED, false));
+ config.setWantsResponsesSigned(flag(props, WANT_MESSAGES_SIGNED, false));
+ config.setWantsAssertionsSigned(flag(props, WANT_ASSERTIONS_SIGNED, false));
+ config.setSignMetadata(flag(props, SIGN_METADATA, false));
+
+ String authnContext = optional(props, REQUESTED_AUTHN_CONTEXT);
+ if (authnContext != null) {
+ config.setAuthnContextClassRefs(splitList(authnContext));
+ // pac4j drops RequestedAuthnContext entirely when no comparison is set
+ String comparison =
+ optional(
+ props,
+ REQUESTED_AUTHN_CONTEXT_COMPARISON,
+ optional(props, LEGACY_REQUESTED_AUTHN_CONTEXT_COMPARISON, "exact"));
+ config.setComparisonType(comparison);
+ }
+ String signatureAlgorithm = optional(props, SIGNATURE_ALGORITHM);
+ if (signatureAlgorithm != null) {
+ config.setSignatureAlgorithms(List.of(signatureAlgorithm));
+ }
+
+ configureKeystore(config, spCredentials(props));
+ config.setMetadataGenerator(
+ new SupportedBindingsMetadataGenerator(
+ optional(props, SP_SLS_URL) != null, optional(props, SP_CERT) != null));
+ config.setIdentityProviderMetadataResource(
+ new ByteArrayResource(
+ buildIdentityProviderMetadata(props).getBytes(StandardCharsets.UTF_8),
+ "identity provider metadata from " + PROPERTIES_FILE));
+
+ // pac4j's default store keeps nothing, so a Response's InResponseTo was never checked
+ SamlRequestStore requestStore = new SamlRequestStore(required(props, SP_ACS_URL));
+ config.setSamlMessageStoreFactory(requestStore);
+
+ SAML2Client saml = new AlternativeAuthnContextClient(config);
+ // The ACS URL is registered with the IdP as-is, so don't append ?client_name=...
+ saml.setCallbackUrlResolver(new NoParameterCallbackUrlResolver());
+ saml.setCallbackUrl(required(props, SP_ACS_URL));
+ saml.init();
+ X509Certificate idpCertificate;
+ try {
+ idpCertificate = parseCertificate(required(props, IDP_CERT));
+ } catch (GeneralSecurityException | IllegalArgumentException e) {
+ throw new IllegalStateException("Cannot load " + IDP_CERT + ": " + e.getMessage(), e);
+ }
+ // Null when no SLS URL is configured: no logout endpoint is advertised, and IdP
+ // LogoutRequests are refused
+ String singleLogoutUrl = optional(props, SP_SLS_URL);
+ return new Sso(
+ saml,
+ idpCertificate,
+ flag(props, WANT_MESSAGES_SIGNED, false),
+ singleLogoutUrl,
+ required(props, IDP_ENTITY_ID),
+ required(props, SP_ENTITY_ID),
+ requestStore);
+ }
+
+ /**
+ * pac4j requires every requested AuthnContextClassRef to appear in the response, but SAML treats
+ * the requested classes as alternatives (SAML Core 3.3.2.2.1). With "exact" comparison, at least
+ * one returned class must be one of those requested. With minimum / better / maximum the IdP
+ * judges relative strength, which an SP can't check in general (a stronger class may be one that
+ * wasn't listed), so the returned class is accepted, as java-saml did.
+ */
+ private static final class AlternativeAuthnContextClient extends SAML2Client {
+ AlternativeAuthnContextClient(SAML2Configuration configuration) {
+ super(configuration);
+ }
+
+ @Override
+ protected void initSAMLResponseValidator(boolean forceReinit) {
+ if (authnResponseValidator == null || forceReinit) {
+ String comparison = configuration.getComparisonType();
+ authnResponseValidator =
+ new SAML2AuthnResponseValidator(
+ signatureTrustEngineProvider,
+ decrypter,
+ replayCache,
+ configuration,
+ findSessionLogoutHandler()) {
+ @Override
+ protected void validateAuthnContextClassRefs(
+ SAML2MessageContext context, List provided) {
+ List requested = configuration.getAuthnContextClassRefs();
+ if (requested.isEmpty()
+ || (comparison != null && !"exact".equalsIgnoreCase(comparison))) {
+ return;
+ }
+ if (provided.stream().noneMatch(requested::contains)) {
+ throw new SAMLAuthnContextClassRefException(
+ "None of the requested authentication context classes "
+ + requested
+ + " is in the IdP's authentication statements "
+ + provided);
+ }
+ }
+
+ /**
+ * pac4j binds a response to this browser's request only through the Response's
+ * InResponseTo, which isn't signed unless the whole message is. The bearer
+ * confirmation sits inside the signed assertion and must name the same request (SAML
+ * Profiles 4.1.4.3), so an assertion can't be moved into an envelope naming another
+ * request. Only SP-initiated login is supported, so it must name one. This runs on
+ * decrypted assertions too.
+ */
+ @Override
+ protected boolean isValidBearerSubjectConfirmationData(
+ SubjectConfirmationData data, SAML2MessageContext context) {
+ if (!super.isValidBearerSubjectConfirmationData(data, context)) {
+ return false;
+ }
+ Object message = context.getMessageContext().getMessage();
+ String requestId =
+ message instanceof Response ? ((Response) message).getInResponseTo() : null;
+ if (requestId == null || !requestId.equals(data.getInResponseTo())) {
+ log.error(
+ "SAML response rejected: the assertion's bearer confirmation doesn't answer"
+ + " the request named by the response");
+ return false;
+ }
+ return true;
+ }
+ };
+ authnResponseValidator.setAcceptedSkew(configuration.getAcceptedSkew());
+ }
+ }
+ }
+
+ /**
+ * pac4j advertises HTTP-POST, HTTP-POST-SimpleSign, HTTP-Redirect and SOAP single logout
+ * endpoints. SamlSso can only verify signatures for HTTP-POST (XML signature) and HTTP-Redirect
+ * (query-string signature), so advertise just those. With no SLS URL configured pac4j would
+ * advertise the ACS URL, which doesn't process logout messages, so advertise none. Without a
+ * configured SP certificate the key is temporary, so publish no KeyDescriptor. The descriptor is
+ * signed afterwards.
+ */
+ private static final class SupportedBindingsMetadataGenerator
+ extends SAML2FileSystemMetadataGenerator {
+ private final boolean advertiseSingleLogout;
+ private final boolean publishKeys;
+
+ SupportedBindingsMetadataGenerator(boolean advertiseSingleLogout, boolean publishKeys) {
+ super(null);
+ this.advertiseSingleLogout = advertiseSingleLogout;
+ this.publishKeys = publishKeys;
+ }
+
+ @Override
+ protected SPSSODescriptor buildSPSSODescriptor() {
+ SPSSODescriptor descriptor = super.buildSPSSODescriptor();
+ descriptor
+ .getSingleLogoutServices()
+ .removeIf(
+ service ->
+ !advertiseSingleLogout
+ || (!HTTP_POST_BINDING.equals(service.getBinding())
+ && !HTTP_REDIRECT_BINDING.equals(service.getBinding())));
+ if (!publishKeys) {
+ // A temporary key changes on every restart, so an IdP must not sign-verify or encrypt to it
+ descriptor.getKeyDescriptors().clear();
+ }
+ return descriptor;
+ }
+ }
+
+ /**
+ * The SP's signing/decryption key and certificate. Like java-saml, both settings may be left out
+ * when the SP signs nothing (authnrequest_signed, logoutrequest_signed and sign_metadata all
+ * false): pac4j still needs a key pair, so a temporary one is generated in memory. Encrypted
+ * assertions then can't be used, since the key changes on every restart.
+ */
+ private static KeyStore.PrivateKeyEntry spCredentials(Properties props) {
+ String certPem = optional(props, SP_CERT);
+ String keyPem = optional(props, SP_KEY);
+ try {
+ if (certPem != null && keyPem != null) {
+ return new KeyStore.PrivateKeyEntry(
+ parsePrivateKey(keyPem), new Certificate[] {parseCertificate(certPem)});
+ }
+ if (certPem != null || keyPem != null) {
+ throw new IllegalStateException("Set both " + SP_CERT + " and " + SP_KEY + ", or neither");
+ }
+ List signingSettings = new ArrayList<>();
+ for (String setting : List.of(AUTHN_REQUEST_SIGNED, LOGOUT_REQUEST_SIGNED, SIGN_METADATA)) {
+ if (flag(props, setting, false)) {
+ signingSettings.add(setting);
+ }
+ }
+ if (!signingSettings.isEmpty()) {
+ throw new IllegalStateException(
+ "Missing SAML setting "
+ + SP_CERT
+ + " / "
+ + SP_KEY
+ + ", required by "
+ + String.join(", ", signingSettings));
+ }
+ log.info(
+ "No SP certificate configured: using a temporary in-memory key pair. SP signing and"
+ + " encrypted assertions are unavailable.");
+ return temporaryCredentials();
+ } catch (GeneralSecurityException | IOException | OperatorCreationException e) {
+ throw new IllegalStateException(
+ "Cannot load " + SP_CERT + " / " + SP_KEY + ": " + e.getMessage(), e);
+ }
+ }
+
+ private static KeyStore.PrivateKeyEntry temporaryCredentials()
+ throws GeneralSecurityException, OperatorCreationException {
+ KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA");
+ generator.initialize(2048);
+ KeyPair keys = generator.generateKeyPair();
+ X500Name name = new X500Name("CN=Security Shepherd SAML SP (temporary)");
+ Instant now = Instant.now();
+ X509Certificate cert =
+ new JcaX509CertificateConverter()
+ .getCertificate(
+ new JcaX509v3CertificateBuilder(
+ name,
+ BigInteger.valueOf(now.toEpochMilli()),
+ Date.from(now.minus(Duration.ofDays(1))),
+ Date.from(now.plus(Duration.ofDays(3650))),
+ name,
+ keys.getPublic())
+ .build(new JcaContentSignerBuilder("SHA256withRSA").build(keys.getPrivate())));
+ return new KeyStore.PrivateKeyEntry(keys.getPrivate(), new Certificate[] {cert});
+ }
+
+ /**
+ * Loads the SP's private key and certificate into an in-memory keystore, so pac4j neither reads
+ * nor generates a keystore file.
+ */
+ private static void configureKeystore(
+ SAML2Configuration config, KeyStore.PrivateKeyEntry credentials) {
+ String password = UUID.randomUUID().toString();
+ byte[] keystore;
+ try {
+ KeyStore ks = KeyStore.getInstance("PKCS12");
+ ks.load(null, null);
+ ks.setKeyEntry(
+ SP_KEY_ALIAS,
+ credentials.getPrivateKey(),
+ password.toCharArray(),
+ credentials.getCertificateChain());
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ ks.store(out, password.toCharArray());
+ keystore = out.toByteArray();
+ } catch (GeneralSecurityException | IOException e) {
+ throw new IllegalStateException("Cannot build the SP keystore: " + e.getMessage(), e);
+ }
+
+ config.getKeystore().setKeystoreResource(new ByteArrayResource(keystore, "SP keystore"));
+ config.getKeystore().setKeyStoreType("PKCS12");
+ config.getKeystore().setKeyStoreAlias(SP_KEY_ALIAS);
+ config.getKeystore().setKeystorePassword(password);
+ config.getKeystore().setPrivateKeyPassword(password);
+ config
+ .getKeystore()
+ .setKeystoreGenerator(
+ new KeystoreGenerator() {
+ @Override
+ public void generate() {}
+
+ @Override
+ public boolean shouldGenerate() {
+ return false;
+ }
+
+ @Override
+ public InputStream retrieve() {
+ return new ByteArrayInputStream(keystore);
+ }
+ });
+ }
+
+ /**
+ * pac4j reads the IdP from SAML metadata, while onelogin.saml.properties describes it with
+ * individual keys. Build the equivalent metadata document from those keys.
+ */
+ static String buildIdentityProviderMetadata(Properties props) {
+ String cert = optional(props, IDP_CERT);
+ if (cert == null) {
+ String reason =
+ optional(props, IDP_CERT_FINGERPRINT) != null
+ ? " (" + IDP_CERT_FINGERPRINT + " alone is not supported; set the full certificate)"
+ : "";
+ throw new IllegalStateException("Missing SAML setting " + IDP_CERT + reason);
+ }
+ StringBuilder xml = new StringBuilder();
+ xml.append("")
+ .append("")
+ .append("")
+ .append(pemBody(cert))
+ .append("");
+ String sloUrl = optional(props, IDP_SLO_URL);
+ if (sloUrl != null) {
+ xml.append("");
+ }
+ xml.append("")
+ .append("");
+ return xml.toString();
+ }
+
+ static X509Certificate parseCertificate(String pem) throws GeneralSecurityException {
+ byte[] der = Base64.getDecoder().decode(pemBody(pem));
+ return (X509Certificate)
+ CertificateFactory.getInstance("X.509").generateCertificate(new ByteArrayInputStream(der));
+ }
+
+ /** Accepts PKCS#8 ("BEGIN PRIVATE KEY") or PKCS#1 ("BEGIN RSA PRIVATE KEY") RSA keys. */
+ static PrivateKey parsePrivateKey(String pem) throws GeneralSecurityException, IOException {
+ byte[] der = Base64.getDecoder().decode(pemBody(pem));
+ KeyFactory rsa = KeyFactory.getInstance("RSA");
+ try {
+ return rsa.generatePrivate(new PKCS8EncodedKeySpec(der));
+ } catch (InvalidKeySpecException notPkcs8) {
+ PrivateKeyInfo pkcs8 =
+ new PrivateKeyInfo(
+ new AlgorithmIdentifier(PKCSObjectIdentifiers.rsaEncryption, DERNull.INSTANCE),
+ RSAPrivateKey.getInstance(der));
+ return rsa.generatePrivate(new PKCS8EncodedKeySpec(pkcs8.getEncoded()));
+ }
+ }
+
+ /** Strips PEM armour and whitespace, leaving the base64 body. */
+ static String pemBody(String pem) {
+ return pem.replaceAll("-----(BEGIN|END)[^-]*-----", "").replaceAll("\\s", "");
+ }
+
+ private static List splitList(String value) {
+ List items = new ArrayList<>();
+ for (String item : value.split(",")) {
+ if (!item.trim().isEmpty()) {
+ items.add(item.trim());
+ }
+ }
+ return items;
+ }
+
+ private static String required(Properties props, String key) {
+ String value = optional(props, key);
+ if (value == null) {
+ throw new IllegalStateException("Missing SAML setting " + key + " in " + PROPERTIES_FILE);
+ }
+ return value;
+ }
+
+ private static String optional(Properties props, String key) {
+ String value = props.getProperty(key);
+ return value == null || value.trim().isEmpty() ? null : value.trim();
+ }
+
+ private static String optional(Properties props, String key, String defaultValue) {
+ String value = optional(props, key);
+ return value == null ? defaultValue : value;
+ }
+
+ private static boolean flag(Properties props, String key, boolean defaultValue) {
+ String value = optional(props, key);
+ return value == null ? defaultValue : Boolean.parseBoolean(value);
+ }
+}
diff --git a/src/main/java/utils/SsoUserMapper.java b/src/main/java/utils/SsoUserMapper.java
new file mode 100644
index 000000000..11d80d113
--- /dev/null
+++ b/src/main/java/utils/SsoUserMapper.java
@@ -0,0 +1,132 @@
+package utils;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+
+/**
+ * Maps the attributes released by the SAML identity provider to a Security Shepherd user, using the
+ * attribute names and affiliations configured in sso.properties.
+ * This file is part of the Security Shepherd Project.
+ *
+ * The Security Shepherd project is free software: you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License as published by the Free Software Foundation, either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ *
The Security Shepherd project is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
+ * PURPOSE. See the GNU General Public License for more details.
+ *
+ *
You should have received a copy of the GNU General Public License along with the Security
+ * Shepherd project. If not, see .
+ */
+public class SsoUserMapper {
+
+ /** Result of mapping: either a user or an error message for the login page */
+ public static class Result {
+ public final String ssoName;
+ public final String userName;
+ public final String userRole;
+ public final String error;
+
+ private Result(String ssoName, String userName, String userRole, String error) {
+ this.ssoName = ssoName;
+ this.userName = userName;
+ this.userRole = userRole;
+ this.error = error;
+ }
+
+ public boolean isValid() {
+ return error == null;
+ }
+ }
+
+ /**
+ * Maps IdP attributes to a user.
+ *
+ * @param attributes Attributes from the SAML assertion. Values may be single objects or
+ * collections.
+ * @param ssoProperties sso.properties contents
+ * @return The mapped user, or an error if a required attribute is missing or the user has none of
+ * the configured affiliations
+ */
+ public static Result map(Map attributes, Properties ssoProperties) {
+ if (attributes == null || attributes.isEmpty()) {
+ return error("You don't have any attributes");
+ }
+ String ssoName = first(attributes, ssoProperties.getProperty("sso.saml.ssoName"));
+ String userName = first(attributes, ssoProperties.getProperty("sso.saml.userName"));
+ if (ssoName == null || userName == null) {
+ return error("Required SSO attributes are missing");
+ }
+
+ List affiliations =
+ values(attributes, ssoProperties.getProperty("sso.saml.affiliation"));
+ List adminAffiliations =
+ splitList(ssoProperties.getProperty("sso.saml.adminAffiliation"));
+ List playerAffiliations =
+ splitList(ssoProperties.getProperty("sso.saml.playerAffiliation"));
+
+ if (!Collections.disjoint(affiliations, adminAffiliations)) {
+ return new Result(ssoName, userName, "admin", null);
+ }
+ if (!Collections.disjoint(affiliations, playerAffiliations)) {
+ return new Result(ssoName, userName, "player", null);
+ }
+
+ StringBuilder message =
+ new StringBuilder(
+ "Authorization failed. Please ensure that you are a member of one of the"
+ + " following groups: ");
+ for (String affiliation : adminAffiliations) {
+ message.append(affiliation).append(", ");
+ }
+ for (String affiliation : playerAffiliations) {
+ message.append(affiliation).append(", ");
+ }
+ return error(message.toString());
+ }
+
+ private static Result error(String message) {
+ return new Result(null, null, null, message);
+ }
+
+ private static String first(Map attributes, String key) {
+ List values = values(attributes, key);
+ return values.isEmpty() ? null : values.get(0);
+ }
+
+ private static List values(Map attributes, String key) {
+ List values = new ArrayList<>();
+ Object value = key == null ? null : attributes.get(key);
+ if (value instanceof Collection) {
+ for (Object item : (Collection>) value) {
+ addIfNotBlank(values, item);
+ }
+ } else {
+ addIfNotBlank(values, value);
+ }
+ return values;
+ }
+
+ /**
+ * Blank values count as missing: a blank ssoName would let different principals share one
+ * account, since accounts are looked up by it.
+ */
+ private static void addIfNotBlank(List values, Object value) {
+ if (value != null && !value.toString().trim().isEmpty()) {
+ values.add(value.toString());
+ }
+ }
+
+ private static List splitList(String value) {
+ if (value == null || value.trim().isEmpty()) {
+ return Collections.emptyList();
+ }
+ return Arrays.asList(value.trim().split("\\s*,\\s*"));
+ }
+}
diff --git a/src/main/java/utils/Validate.java b/src/main/java/utils/Validate.java
index 29450396b..12f7c0b72 100644
--- a/src/main/java/utils/Validate.java
+++ b/src/main/java/utils/Validate.java
@@ -1,10 +1,10 @@
package utils;
+import jakarta.mail.internet.AddressException;
+import jakarta.mail.internet.InternetAddress;
+import jakarta.servlet.http.Cookie;
+import jakarta.servlet.http.HttpSession;
import java.math.BigInteger;
-import javax.mail.internet.AddressException;
-import javax.mail.internet.InternetAddress;
-import javax.servlet.http.Cookie;
-import javax.servlet.http.HttpSession;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
diff --git a/src/main/resources/onelogin.saml.properties.sample b/src/main/resources/onelogin.saml.properties.sample
index 8d571eee9..d7e417526 100644
--- a/src/main/resources/onelogin.saml.properties.sample
+++ b/src/main/resources/onelogin.saml.properties.sample
@@ -1,30 +1,49 @@
-# If 'strict' is True, then the Java Toolkit will reject unsigned
-# or unencrypted messages if it expects them signed or encrypted
-# Also will reject the messages if not strictly follow the SAML
-onelogin.saml2.strict = true
+# SAML SSO settings, read by utils.SamlSso (pac4j-saml). The key names are
+# kept from the java-saml toolkit Security Shepherd used before, so existing
+# files keep working. Copy to onelogin.saml.properties and enable SSO with
+# sso.enabled=true in sso.properties.
+#
+# Not supported with pac4j, and ignored:
+# onelogin.saml2.strict = false (messages are always validated strictly)
+# onelogin.saml2.debug
+# onelogin.saml2.security.nameid_encrypted, logoutresponse_signed,
+# want_assertions_encrypted, want_nameid_encrypted, want_xml_validation
+# (encrypted assertions and NameIDs are still decrypted when received)
+# onelogin.saml2.idp.certfingerprint alone is not supported: set
+# onelogin.saml2.idp.x509cert to the IdP's full signing certificate.
+#
+# Only SP-initiated login is supported (users start at /saml/dologin).
+# IdP-initiated (unsolicited) responses are refused: they can't be tied to the
+# browser that is logging in, which would allow login CSRF. Each login is bound
+# to the browser by a cookie of its own, shepherd_saml_request_ (HttpOnly,
+# Secure, SameSite=None, path of the ACS URL's directory, e.g. /saml/), deleted
+# once the login completes, so the ACS must be served over HTTPS. Logins started
+# in several tabs at once all work.
+# The assertion's bearer SubjectConfirmationData must carry the AuthnRequest's
+# ID in InResponseTo, as the SAML profile requires (standard IdPs do this), so
+# the binding holds even when the Response itself is unsigned.
-# Enable debug mode (to print errors)
-onelogin.saml2.debug = false
+onelogin.saml2.strict = true
# Service Provider Data that we are deploying
#
# Identifier of the SP entity (must be a URI)
-onelogin.saml2.sp.entityid = http://localhost:8080/owaspSecurityShepherd/ssometadata.xml
+onelogin.saml2.sp.entityid = https://localhost/saml/metadata.xml
# Specifies info about where and how the message MUST be
# returned to the requester, in this case our SP.
# URL Location where the from the IdP will be returned
-onelogin.saml2.sp.assertion_consumer_service.url = http://localhost:8080/owaspSecurityShepherd/acs
+onelogin.saml2.sp.assertion_consumer_service.url = https://localhost/saml/acs
# SAML protocol binding to be used when returning the
-# message. Onelogin Toolkit supports for this endpoint the
-# HTTP-POST binding only
+# message. HTTP-POST is the usual choice.
onelogin.saml2.sp.assertion_consumer_service.binding = urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST
# Specifies info about where and how the message MUST be
-# returned to the requester, in this case our SP.
-onelogin.saml2.sp.single_logout_service.url = http://localhost:8080/owaspSecurityShepherd/sls
+# returned to the requester, in this case our SP. Leave empty to disable
+# IdP-initiated single logout: no logout endpoint is then advertised.
+onelogin.saml2.sp.single_logout_service.url = https://localhost/saml/sls
# SAML protocol binding to be used when returning the or sending the
# message. Onelogin Toolkit supports for this endpoint the
@@ -36,13 +55,14 @@ onelogin.saml2.sp.single_logout_service.binding = urn:oasis:names:tc:SAML:2.0:bi
# Take a look on lib/Saml2/Constants.php to see the NameIdFormat supported
onelogin.saml2.sp.nameidformat = urn:oasis:names:tc:SAML:2.0:nameid-format:persistent
-# Usually x509cert and privateKey of the SP are provided by files placed at
-# the certs folder. But we can also provide them with the following parameters
+# The SP's certificate and private key. Both may be left empty if the SP signs
+# nothing (authnrequest_signed, logoutrequest_signed and sign_metadata all false):
+# a temporary in-memory key is then used, and encrypted assertions can't be used.
onelogin.saml2.sp.x509cert =
-# Requires Format PKCS#8 BEGIN PRIVATE KEY
-# If you have PKCS#1 BEGIN RSA PRIVATE KEY convert it by openssl pkcs8 -topk8 -inform pem -nocrypt -in sp.rsa_key -outform pem -out sp.pem
+# RSA private key, PKCS#8 (BEGIN PRIVATE KEY) or PKCS#1 (BEGIN RSA PRIVATE KEY).
+# The PEM header/footer and line breaks are optional.
onelogin.saml2.sp.privatekey =
# Identity Provider Data that we want connect with our SP
@@ -74,27 +94,13 @@ onelogin.saml2.idp.single_logout_service.response.url =
# HTTP-Redirect binding only
onelogin.saml2.idp.single_logout_service.binding = urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect
-# Public x509 certificate of the IdP
+# Public x509 certificate of the IdP (required)
onelogin.saml2.idp.x509cert =
-# Instead of use the whole x509cert you can use a fingerprint
-# (openssl x509 -noout -fingerprint -in "idp.crt" to generate it,
-# or add for example the -sha256 , -sha384 or -sha512 parameter)
-#
-# If a fingerprint is provided, then the certFingerprintAlgorithm is required in order to
-# let the toolkit know which Algorithm was used. Possible values: sha1, sha256, sha384 or sha512
-# 'sha1' is the default value.
-onelogin.saml2.idp.certfingerprint =
-onelogin.saml2.idp.certfingerprint_algorithm = sha256
-
# Security settings
#
-# Indicates that the nameID of the sent by this SP
-# will be encrypted.
-onelogin.saml2.security.nameid_encrypted = true
-
# Indicates whether the messages sent by this SP
# will be signed. [The Metadata of the SP will offer this info]
onelogin.saml2.security.authnrequest_signed = true
@@ -103,10 +109,6 @@ onelogin.saml2.security.authnrequest_signed = true
# will be signed.
onelogin.saml2.security.logoutrequest_signed = true
-# Indicates whether the messages sent by this SP
-# will be signed.
-onelogin.saml2.security.logoutresponse_signed = true
-
# Indicates a requirement for the , and
# elements received by this SP to be signed.
onelogin.saml2.security.want_messages_signed = true
@@ -118,23 +120,13 @@ onelogin.saml2.security.want_assertions_signed = true
# Right now supported null (in order to not sign) or true (sign using SP private key)
onelogin.saml2.security.sign_metadata = true
-# Indicates a requirement for the Assertions received by this SP to be encrypted
-onelogin.saml2.security.want_assertions_encrypted = true
-
-# Indicates a requirement for the NameID received by this SP to be encrypted
-onelogin.saml2.security.want_nameid_encrypted = true
-
# Authentication context.
# Set Empty and no AuthContext will be sent in the AuthNRequest
# You can set multiple values (comma separated them)
onelogin.saml2.security.requested_authncontext =
# Allows the authn comparison parameter to be set, defaults to 'exact'
-onelogin.saml2.security.onelogin.saml2.security.requested_authncontextcomparison = exact
-
-# Indicates if the SP will validate all received xmls.
-# (In order to validate the xml, 'strict' and 'wantXMLValidation' must be true).
-onelogin.saml2.security.want_xml_validation = true
+onelogin.saml2.security.requested_authncontextcomparison = exact
# Algorithm that the toolkit will use on signing process. Options:
# 'http://www.w3.org/2000/09/xmldsig#rsa-sha1'
diff --git a/src/main/resources/sso.properties.sample b/src/main/resources/sso.properties.sample
index e7c075fb5..97c0dca8d 100644
--- a/src/main/resources/sso.properties.sample
+++ b/src/main/resources/sso.properties.sample
@@ -1,2 +1,16 @@
+# Copy to sso.properties to configure SAML single sign-on.
+# SAML endpoint settings live in onelogin.saml.properties.
+
+# Set to true to replace the username/password login with SAML SSO
+sso.enabled=false
+
+# Names of the SAML attributes holding the user's unique SSO id and display name
sso.saml.ssoName=
sso.saml.userName=
+
+# SAML attribute listing the user's groups/affiliations, and the values that
+# make a user an admin or a player (comma separated). Users with none of these
+# affiliations are refused.
+sso.saml.affiliation=
+sso.saml.adminAffiliation=
+sso.saml.playerAffiliation=
diff --git a/src/main/webapp/WEB-INF/web.xml b/src/main/webapp/WEB-INF/web.xml
index d26286ad4..da70aff23 100644
--- a/src/main/webapp/WEB-INF/web.xml
+++ b/src/main/webapp/WEB-INF/web.xml
@@ -1,5 +1,5 @@
-
+
Security Shepherd
diff --git a/src/main/webapp/translation.jsp b/src/main/webapp/translation.jsp
index ea7528a7a..b0c589918 100644
--- a/src/main/webapp/translation.jsp
+++ b/src/main/webapp/translation.jsp
@@ -1,6 +1,6 @@
<%@page import="java.util.Locale"%>
-<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
-<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt"%>
+<%@ taglib prefix="c" uri="jakarta.tags.core"%>
+<%@ taglib prefix="fmt" uri="jakarta.tags.fmt"%>
> attributes = new LinkedHashMap<>();
+ attributes.put("uid", Collections.singletonList("jdoe"));
+ attributes.put("displayName", Collections.singletonList("Jane Doe"));
+ attributes.put("groups", Arrays.asList("staff"));
+ String samlResponse = saml.signedResponse("user-123", attributes, saml.idpKeys, saml.idpCert);
+ MockHttpServletRequest request = new MockHttpServletRequest("POST", "/saml/acs");
+ request.setScheme("https");
+ request.setSecure(true);
+ request.setServerName("shepherd.test");
+ request.setServerPort(443);
+ request.addParameter("SAMLResponse", samlResponse);
+ request.setCookies(saml.browserCookie());
+ MockHttpSession session = new MockHttpSession();
+ request.setSession(session);
+ MockHttpServletResponse response = new MockHttpServletResponse();
+
+ new ACS().doPost(request, response);
+
+ assertEquals(404, response.getStatus());
+ assertNull(session.getAttribute("userName"));
+ }
+
+ @Test
+ public void ssoLogin_IsNotFound() throws Exception {
+ MockHttpServletResponse response = new MockHttpServletResponse();
+ new SSOLogin().doGet(new MockHttpServletRequest("GET", "/saml/dologin"), response);
+ assertEquals(404, response.getStatus());
+ }
+
+ @Test
+ public void sls_IsNotFound() throws Exception {
+ MockHttpServletResponse response = new MockHttpServletResponse();
+ new SLS().doGet(new MockHttpServletRequest("GET", "/saml/sls"), response);
+ assertEquals(404, response.getStatus());
+ }
+
+ @Test
+ public void ssoMetadata_IsNotFound() throws Exception {
+ MockHttpServletResponse response = new MockHttpServletResponse();
+ new SSOMetadata().doGet(new MockHttpServletRequest("GET", "/saml/metadata.xml"), response);
+ assertEquals(404, response.getStatus());
+ }
+}
diff --git a/src/test/java/testUtils/TestProperties.java b/src/test/java/testUtils/TestProperties.java
index 81be0119f..999fc48bd 100644
--- a/src/test/java/testUtils/TestProperties.java
+++ b/src/test/java/testUtils/TestProperties.java
@@ -9,6 +9,7 @@
import dbProcs.MongoDatabase;
import dbProcs.Setter;
import io.github.cdimascio.dotenv.Dotenv;
+import jakarta.servlet.ServletException;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
@@ -22,7 +23,6 @@
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Properties;
-import javax.servlet.ServletException;
import org.apache.commons.io.FileUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
diff --git a/src/test/java/utils/SamlLogoutTest.java b/src/test/java/utils/SamlLogoutTest.java
new file mode 100644
index 000000000..224cbf86d
--- /dev/null
+++ b/src/test/java/utils/SamlLogoutTest.java
@@ -0,0 +1,481 @@
+package utils;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.startsWith;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.security.KeyPair;
+import java.security.cert.X509Certificate;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.Arrays;
+import java.util.Properties;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.opensaml.saml.saml2.core.LogoutRequest;
+import org.opensaml.saml.saml2.core.NameIDType;
+import org.springframework.mock.web.MockHttpServletRequest;
+import org.springframework.mock.web.MockHttpServletResponse;
+import org.springframework.mock.web.MockHttpSession;
+
+/** Single logout service: which logout messages are trusted, and which sessions they end */
+public class SamlLogoutTest {
+
+ private static SamlTestSupport saml;
+ private static KeyPair attackerKeys;
+ private static X509Certificate attackerCert;
+
+ private MockHttpSession loggedIn;
+ private MockHttpSession otherUser;
+
+ @BeforeAll
+ public static void generateKeys() throws Exception {
+ saml = new SamlTestSupport();
+ attackerKeys = SamlTestSupport.newKeyPair();
+ attackerCert = SamlTestSupport.selfSigned(attackerKeys, "idp.test");
+ }
+
+ /** Installs a client that requires signed logout messages, as the sample configuration does */
+ @BeforeEach
+ public void setUp() throws Exception {
+ SamlSso.clearRememberedLogoutRequests();
+ SamlTestSupport.install(signedLogoutProperties(true));
+ loggedIn = new MockHttpSession();
+ register(loggedIn, "user-123", "_session-1");
+ otherUser = new MockHttpSession();
+ register(otherUser, "user-456", "_session-2");
+ }
+
+ @AfterEach
+ public void tearDown() {
+ for (MockHttpSession session : new MockHttpSession[] {loggedIn, otherUser}) {
+ if (!session.isInvalid()) {
+ session.invalidate();
+ }
+ }
+ SamlTestSupport.uninstallClient();
+ SamlSso.maxRememberedLogoutRequests = 10000;
+ SamlSso.clearRememberedLogoutRequests();
+ }
+
+ /** Registers a session for a persistent NameID with default qualifiers, as ACS would */
+ private static void register(MockHttpSession session, String nameId, String sessionIndex) {
+ SamlSessionRegistry.register(
+ session, SamlSso.nameIdKey(nameId, NameIDType.PERSISTENT, null, null), sessionIndex);
+ }
+
+ private static Properties signedLogoutProperties(boolean wantMessagesSigned) throws Exception {
+ Properties props = saml.properties();
+ props.setProperty(SamlSso.WANT_MESSAGES_SIGNED, Boolean.toString(wantMessagesSigned));
+ return props;
+ }
+
+ private static SamlSso.LogoutMessage process(MockHttpServletRequest request) {
+ return SamlSso.processLogoutMessage(request, new MockHttpServletResponse());
+ }
+
+ private String logoutResponseXml() throws Exception {
+ return SamlTestSupport.toXml(saml.logoutResponse(), null, null);
+ }
+
+ // LogoutResponse (end of an SP-initiated logout)
+
+ @Test
+ public void redirectLogoutResponse_SignedByIdp_IsAccepted() throws Exception {
+ String query = SamlTestSupport.redirectQuery("SAMLResponse", logoutResponseXml(), saml.idpKeys);
+
+ assertEquals(SamlSso.LogoutMessage.LOGOUT_RESPONSE, process(SamlTestSupport.slsGet(query)));
+ }
+
+ @Test
+ public void redirectLogoutResponse_Unsigned_IsRejected() throws Exception {
+ String query = SamlTestSupport.redirectQuery("SAMLResponse", logoutResponseXml(), null);
+
+ assertEquals(SamlSso.LogoutMessage.INVALID, process(SamlTestSupport.slsGet(query)));
+ }
+
+ @Test
+ public void redirectLogoutResponse_SignedByUnknownKey_IsRejected() throws Exception {
+ String query = SamlTestSupport.redirectQuery("SAMLResponse", logoutResponseXml(), attackerKeys);
+
+ assertEquals(SamlSso.LogoutMessage.INVALID, process(SamlTestSupport.slsGet(query)));
+ }
+
+ @Test
+ public void redirectLogoutResponse_MessageSwappedAfterSigning_IsRejected() throws Exception {
+ String signed =
+ SamlTestSupport.redirectQuery("SAMLResponse", logoutResponseXml(), saml.idpKeys);
+ String otherMessage = SamlTestSupport.redirectQuery("SAMLResponse", logoutResponseXml(), null);
+ String tampered = otherMessage + signed.substring(signed.indexOf("&SigAlg="));
+
+ assertEquals(SamlSso.LogoutMessage.INVALID, process(SamlTestSupport.slsGet(tampered)));
+ }
+
+ @Test
+ public void postLogoutResponse_XmlSignedByIdp_IsAccepted() throws Exception {
+ String xml = SamlTestSupport.toXml(saml.logoutResponse(), saml.idpKeys, saml.idpCert);
+
+ assertEquals(
+ SamlSso.LogoutMessage.LOGOUT_RESPONSE,
+ process(SamlTestSupport.slsPost("SAMLResponse", xml)));
+ }
+
+ @Test
+ public void postLogoutResponse_Unsigned_IsRejected() throws Exception {
+ assertEquals(
+ SamlSso.LogoutMessage.INVALID,
+ process(SamlTestSupport.slsPost("SAMLResponse", logoutResponseXml())));
+ }
+
+ @Test
+ public void postLogoutResponse_XmlSignedByUnknownKey_IsRejected() throws Exception {
+ String xml = SamlTestSupport.toXml(saml.logoutResponse(), attackerKeys, attackerCert);
+
+ assertEquals(
+ SamlSso.LogoutMessage.INVALID, process(SamlTestSupport.slsPost("SAMLResponse", xml)));
+ }
+
+ @Test
+ public void logoutResponse_EndsNoSession() throws Exception {
+ String query = SamlTestSupport.redirectQuery("SAMLResponse", logoutResponseXml(), saml.idpKeys);
+ MockHttpServletRequest request = SamlTestSupport.slsGet(query);
+ request.setSession(loggedIn);
+
+ process(request);
+
+ assertFalse(loggedIn.isInvalid());
+ assertFalse(otherUser.isInvalid());
+ }
+
+ @Test
+ public void unsignedLogoutResponse_AcceptedWhenSignaturesNotRequired() throws Exception {
+ SamlTestSupport.install(signedLogoutProperties(false));
+ String query = SamlTestSupport.redirectQuery("SAMLResponse", logoutResponseXml(), null);
+
+ assertEquals(SamlSso.LogoutMessage.LOGOUT_RESPONSE, process(SamlTestSupport.slsGet(query)));
+ }
+
+ // IdP-initiated LogoutRequest
+
+ private String logoutRequestQuery(String nameId, String sessionIndex, KeyPair keys)
+ throws Exception {
+ String xml = SamlTestSupport.toXml(saml.logoutRequest(nameId, sessionIndex), null, null);
+ return SamlTestSupport.redirectQuery("SAMLRequest", xml, keys);
+ }
+
+ @Test
+ public void idpLogoutRequest_EndsOnlyTheNamedSession_RegardlessOfBrowserSession()
+ throws Exception {
+ MockHttpServletRequest request =
+ SamlTestSupport.slsGet(logoutRequestQuery("user-123", "_session-1", saml.idpKeys));
+ // Delivered through a different user's browser
+ request.setSession(otherUser);
+ MockHttpServletResponse response = new MockHttpServletResponse();
+
+ SamlSso.LogoutMessage result = SamlSso.processLogoutMessage(request, response);
+
+ assertEquals(SamlSso.LogoutMessage.IDP_LOGOUT_REQUEST, result);
+ assertTrue(loggedIn.isInvalid());
+ assertFalse(otherUser.isInvalid());
+ assertThat(response.getRedirectedUrl(), startsWith(SamlTestSupport.IDP_SLO_URL + "?"));
+ assertThat(response.getRedirectedUrl(), containsString("SAMLResponse="));
+ }
+
+ @Test
+ public void idpLogoutRequest_WithoutSessionCookie_StillEndsTheNamedSession() throws Exception {
+ MockHttpServletRequest request =
+ SamlTestSupport.slsGet(logoutRequestQuery("user-123", "_session-1", saml.idpKeys));
+
+ assertEquals(SamlSso.LogoutMessage.IDP_LOGOUT_REQUEST, process(request));
+ assertTrue(loggedIn.isInvalid());
+ }
+
+ @Test
+ public void idpLogoutRequest_DifferentSessionIndex_EndsNothing() throws Exception {
+ MockHttpServletRequest request =
+ SamlTestSupport.slsGet(logoutRequestQuery("user-123", "_other-login", saml.idpKeys));
+ request.setSession(loggedIn);
+
+ process(request);
+
+ assertFalse(loggedIn.isInvalid());
+ }
+
+ @Test
+ public void idpLogoutRequest_Unsigned_IsRejectedAndEndsNothing() throws Exception {
+ MockHttpServletRequest request =
+ SamlTestSupport.slsGet(logoutRequestQuery("user-123", "_session-1", null));
+
+ assertEquals(SamlSso.LogoutMessage.INVALID, process(request));
+ assertFalse(loggedIn.isInvalid());
+ }
+
+ @Test
+ public void idpLogoutRequest_SignedByUnknownKey_IsRejectedAndEndsNothing() throws Exception {
+ MockHttpServletRequest request =
+ SamlTestSupport.slsGet(logoutRequestQuery("user-123", "_session-1", attackerKeys));
+
+ assertEquals(SamlSso.LogoutMessage.INVALID, process(request));
+ assertFalse(loggedIn.isInvalid());
+ }
+
+ @Test
+ public void postIdpLogoutRequest_XmlSignedByIdp_EndsTheNamedSession() throws Exception {
+ String xml =
+ SamlTestSupport.toXml(
+ saml.logoutRequest("user-123", "_session-1"), saml.idpKeys, saml.idpCert);
+
+ assertEquals(
+ SamlSso.LogoutMessage.IDP_LOGOUT_REQUEST,
+ process(SamlTestSupport.slsPost("SAMLRequest", xml)));
+ assertTrue(loggedIn.isInvalid());
+ assertFalse(otherUser.isInvalid());
+ }
+
+ @Test
+ public void unsignedIdpLogoutRequestWithoutSessionIndex_EndsNothing() throws Exception {
+ // Even when signatures are not required, a bare NameID is too guessable to end sessions
+ SamlTestSupport.install(signedLogoutProperties(false));
+ MockHttpServletRequest request =
+ SamlTestSupport.slsGet(logoutRequestQuery("user-123", null, null));
+
+ process(request);
+
+ assertFalse(loggedIn.isInvalid());
+ }
+
+ @Test
+ public void idpLogoutRequest_EndsEverySessionIndexItNames() throws Exception {
+ MockHttpSession secondLogin = new MockHttpSession();
+ register(secondLogin, "user-123", "_session-3");
+ String xml =
+ SamlTestSupport.toXml(
+ saml.logoutRequest("user-123", Arrays.asList("_session-1", "_session-3")), null, null);
+ String query = SamlTestSupport.redirectQuery("SAMLRequest", xml, saml.idpKeys);
+
+ assertEquals(SamlSso.LogoutMessage.IDP_LOGOUT_REQUEST, process(SamlTestSupport.slsGet(query)));
+ assertTrue(loggedIn.isInvalid());
+ assertTrue(secondLogin.isInvalid());
+ assertFalse(otherUser.isInvalid());
+ }
+
+ /**
+ * A captured signed query with an unsigned message prepended under a percent-encoded name: the
+ * container decodes the alias to SAMLRequest and serves it first, so it must not borrow the
+ * signature of the real SAMLRequest.
+ */
+ @Test
+ public void unsignedMessageUnderEncodedAlias_DoesNotBorrowSignature() throws Exception {
+ String signed = logoutRequestQuery("user-456", "_session-2", saml.idpKeys);
+ String unsigned = logoutRequestQuery("user-123", null, null);
+ String smuggled = "%53AMLRequest" + unsigned.substring("SAMLRequest".length()) + "&" + signed;
+
+ assertEquals(SamlSso.LogoutMessage.INVALID, process(SamlTestSupport.slsGet(smuggled)));
+ assertFalse(loggedIn.isInvalid());
+ assertFalse(otherUser.isInvalid());
+ }
+
+ @Test
+ public void repeatedSignedParameter_IsRejected() throws Exception {
+ String signed = logoutRequestQuery("user-456", "_session-2", saml.idpKeys);
+ String unsigned = logoutRequestQuery("user-123", null, null);
+
+ assertEquals(
+ SamlSso.LogoutMessage.INVALID, process(SamlTestSupport.slsGet(unsigned + "&" + signed)));
+ assertFalse(loggedIn.isInvalid());
+ }
+
+ @Test
+ public void redirectSignatureOnPostRequest_IsNotAccepted() throws Exception {
+ String signed = logoutRequestQuery("user-456", "_session-2", saml.idpKeys);
+ String unsignedXml =
+ SamlTestSupport.toXml(saml.logoutRequest("user-123", (String) null), null, null);
+ MockHttpServletRequest request = SamlTestSupport.slsPost("SAMLRequest", unsignedXml);
+ request.setQueryString(signed);
+
+ assertEquals(SamlSso.LogoutMessage.INVALID, process(request));
+ assertFalse(loggedIn.isInvalid());
+ }
+
+ private static String signedRedirect(LogoutRequest request) throws Exception {
+ return SamlTestSupport.redirectQuery(
+ "SAMLRequest", SamlTestSupport.toXml(request, null, null), saml.idpKeys);
+ }
+
+ @Test
+ public void signedLogoutRequest_ForAnotherSp_EndsNothing() throws Exception {
+ LogoutRequest request = saml.logoutRequest("user-123", "_session-1");
+ request.setDestination("https://other-sp.test/saml/sls");
+
+ assertEquals(
+ SamlSso.LogoutMessage.INVALID, process(SamlTestSupport.slsGet(signedRedirect(request))));
+ assertFalse(loggedIn.isInvalid());
+ }
+
+ @Test
+ public void signedLogoutRequest_WithoutDestination_EndsNothing() throws Exception {
+ LogoutRequest request = saml.logoutRequest("user-123", "_session-1");
+ request.setDestination(null);
+
+ assertEquals(
+ SamlSso.LogoutMessage.INVALID, process(SamlTestSupport.slsGet(signedRedirect(request))));
+ assertFalse(loggedIn.isInvalid());
+ }
+
+ @Test
+ public void staleLogoutRequest_EndsNothing() throws Exception {
+ LogoutRequest request = saml.logoutRequest("user-123", "_session-1");
+ request.setIssueInstant(Instant.now().minus(Duration.ofMinutes(20)));
+
+ assertEquals(
+ SamlSso.LogoutMessage.INVALID, process(SamlTestSupport.slsGet(signedRedirect(request))));
+ assertFalse(loggedIn.isInvalid());
+ }
+
+ @Test
+ public void futureDatedLogoutRequest_EndsNothing() throws Exception {
+ LogoutRequest request = saml.logoutRequest("user-123", "_session-1");
+ request.setIssueInstant(Instant.now().plus(Duration.ofMinutes(20)));
+
+ assertEquals(
+ SamlSso.LogoutMessage.INVALID, process(SamlTestSupport.slsGet(signedRedirect(request))));
+ assertFalse(loggedIn.isInvalid());
+ }
+
+ @Test
+ public void expiredLogoutRequest_EndsNothing() throws Exception {
+ LogoutRequest request = saml.logoutRequest("user-123", "_session-1");
+ request.setNotOnOrAfter(Instant.now().minus(Duration.ofMinutes(10)));
+
+ assertEquals(
+ SamlSso.LogoutMessage.INVALID, process(SamlTestSupport.slsGet(signedRedirect(request))));
+ assertFalse(loggedIn.isInvalid());
+ }
+
+ @Test
+ public void replayedLogoutRequest_DoesNotEndALaterLogin() throws Exception {
+ String query = signedRedirect(saml.logoutRequest("user-123", (String) null));
+ assertEquals(SamlSso.LogoutMessage.IDP_LOGOUT_REQUEST, process(SamlTestSupport.slsGet(query)));
+ assertTrue(loggedIn.isInvalid());
+ // The user logs in again
+ MockHttpSession newLogin = new MockHttpSession();
+ register(newLogin, "user-123", "_session-new");
+
+ assertEquals(SamlSso.LogoutMessage.INVALID, process(SamlTestSupport.slsGet(query)));
+ assertFalse(newLogin.isInvalid());
+ newLogin.invalidate();
+ }
+
+ /** XML-DSig ecdsa-sha256 signatures are raw r || s, not the DER that SHA256withECDSA expects */
+ @Test
+ public void ecdsaSignedRedirectLogoutResponse_FromEcIdp_IsAccepted() throws Exception {
+ KeyPair ecKeys = SamlTestSupport.newEcKeyPair();
+ Properties props = signedLogoutProperties(true);
+ props.setProperty(
+ SamlSso.IDP_CERT,
+ SamlTestSupport.certificatePem(SamlTestSupport.selfSigned(ecKeys, "idp.test")));
+ SamlTestSupport.install(props);
+ String query =
+ SamlTestSupport.redirectQuery(
+ "SAMLResponse",
+ logoutResponseXml(),
+ ecKeys,
+ "http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha256",
+ "SHA256withECDSAinP1363Format");
+
+ assertEquals(SamlSso.LogoutMessage.LOGOUT_RESPONSE, process(SamlTestSupport.slsGet(query)));
+ }
+
+ @Test
+ public void nameIdOnlyLogout_DoesNotEndSameValueWithDifferentFormatOrQualifier()
+ throws Exception {
+ MockHttpSession emailFormat = new MockHttpSession();
+ SamlSessionRegistry.register(
+ emailFormat, SamlSso.nameIdKey("user-123", NameIDType.EMAIL, null, null), "_e");
+ MockHttpSession otherIdp = new MockHttpSession();
+ SamlSessionRegistry.register(
+ otherIdp,
+ SamlSso.nameIdKey("user-123", NameIDType.PERSISTENT, "https://other-idp.test", null),
+ "_o");
+
+ assertEquals(
+ SamlSso.LogoutMessage.IDP_LOGOUT_REQUEST,
+ process(
+ SamlTestSupport.slsGet(signedRedirect(saml.logoutRequest("user-123", (String) null)))));
+
+ assertTrue(loggedIn.isInvalid());
+ assertFalse(emailFormat.isInvalid());
+ assertFalse(otherIdp.isInvalid());
+ emailFormat.invalidate();
+ otherIdp.invalidate();
+ }
+
+ @Test
+ public void explicitDefaultQualifiers_MatchOmittedOnes() throws Exception {
+ LogoutRequest request = saml.logoutRequest("user-123", "_session-1");
+ request.getNameID().setNameQualifier(SamlTestSupport.IDP_ENTITY_ID);
+ request.getNameID().setSPNameQualifier(SamlTestSupport.SP_ENTITY_ID);
+
+ assertEquals(
+ SamlSso.LogoutMessage.IDP_LOGOUT_REQUEST,
+ process(SamlTestSupport.slsGet(signedRedirect(request))));
+ assertTrue(loggedIn.isInvalid());
+ }
+
+ @Test
+ public void withoutSlsUrl_IdpLogoutRequestIsRefusedAndEndsNothing() throws Exception {
+ Properties props = signedLogoutProperties(true);
+ props.remove(SamlSso.SP_SLS_URL);
+ SamlTestSupport.install(props);
+ LogoutRequest request = saml.logoutRequest("user-123", "_session-1");
+ request.setDestination(SamlTestSupport.ACS_URL);
+
+ assertEquals(
+ SamlSso.LogoutMessage.INVALID, process(SamlTestSupport.slsGet(signedRedirect(request))));
+ assertFalse(loggedIn.isInvalid());
+ }
+
+ /**
+ * Unsigned requests that end nothing must not take replay-cache slots; otherwise flooding them
+ * would make genuine signed logouts be refused as "too many".
+ */
+ @Test
+ public void floodOfUnsignedNoOpLogouts_DoesNotBlockSignedLogout() throws Exception {
+ SamlSso.maxRememberedLogoutRequests = 5;
+ SamlTestSupport.install(signedLogoutProperties(false));
+ for (int i = 0; i < 10; i++) {
+ process(SamlTestSupport.slsGet(logoutRequestQuery("user-" + i, null, null)));
+ }
+
+ MockHttpServletRequest signed =
+ SamlTestSupport.slsGet(logoutRequestQuery("user-123", "_session-1", saml.idpKeys));
+
+ assertEquals(SamlSso.LogoutMessage.IDP_LOGOUT_REQUEST, process(signed));
+ assertTrue(loggedIn.isInvalid());
+ }
+
+ @Test
+ public void unsignedLogoutNamingATrackedLogin_StillEndsIt() throws Exception {
+ SamlTestSupport.install(signedLogoutProperties(false));
+
+ assertEquals(
+ SamlSso.LogoutMessage.IDP_LOGOUT_REQUEST,
+ process(SamlTestSupport.slsGet(logoutRequestQuery("user-123", "_session-1", null))));
+ assertTrue(loggedIn.isInvalid());
+ assertFalse(otherUser.isInvalid());
+ }
+
+ @Test
+ public void endedSession_IsRemovedFromRegistry() {
+ int before = SamlSessionRegistry.size();
+
+ loggedIn.invalidate();
+
+ assertEquals(before - 1, SamlSessionRegistry.size());
+ }
+}
diff --git a/src/test/java/utils/SamlSsoTest.java b/src/test/java/utils/SamlSsoTest.java
new file mode 100644
index 000000000..79c1de579
--- /dev/null
+++ b/src/test/java/utils/SamlSsoTest.java
@@ -0,0 +1,614 @@
+package utils;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.startsWith;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import jakarta.servlet.http.Cookie;
+import java.security.KeyPair;
+import java.security.PrivateKey;
+import java.security.cert.X509Certificate;
+import java.time.Duration;
+import java.util.Arrays;
+import java.util.Base64;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Properties;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.opensaml.saml.saml2.core.AuthnContext;
+import org.pac4j.saml.profile.SAML2Profile;
+import org.springframework.mock.web.MockHttpServletRequest;
+import org.springframework.mock.web.MockHttpServletResponse;
+
+public class SamlSsoTest {
+
+ private static SamlTestSupport saml;
+
+ @BeforeAll
+ public static void generateKeys() throws Exception {
+ saml = new SamlTestSupport();
+ }
+
+ @BeforeEach
+ public void installClient() throws Exception {
+ saml.installClient();
+ }
+
+ @AfterEach
+ public void uninstallClient() {
+ SamlTestSupport.uninstallClient();
+ }
+
+ private static Map> studentAttributes() {
+ Map> attributes = new LinkedHashMap<>();
+ attributes.put("uid", Collections.singletonList("jdoe"));
+ attributes.put("displayName", Collections.singletonList("Jane Doe"));
+ attributes.put("groups", Arrays.asList("students", "chess-club"));
+ return attributes;
+ }
+
+ private static MockHttpServletRequest acsPost(String samlResponse) {
+ MockHttpServletRequest request = new MockHttpServletRequest("POST", "/saml/acs");
+ request.setScheme("https");
+ request.setSecure(true);
+ request.setServerName("shepherd.test");
+ request.setServerPort(443);
+ request.addParameter("SAMLResponse", samlResponse);
+ // Delivered by the browser that started the login, with the cookies it sends to the ACS
+ Cookie[] cookies = SamlTestSupport.cookiesFor("/saml/acs");
+ if (cookies.length > 0) {
+ request.setCookies(cookies);
+ }
+ return request;
+ }
+
+ @Test
+ public void parsePrivateKey_AcceptsPkcs8AndPkcs1() throws Exception {
+ KeyPair keys = SamlTestSupport.newKeyPair();
+
+ PrivateKey fromPkcs8 = SamlSso.parsePrivateKey(SamlTestSupport.pkcs8Pem(keys));
+ PrivateKey fromPkcs1 = SamlSso.parsePrivateKey(SamlTestSupport.pkcs1Pem(keys));
+
+ assertEquals(keys.getPrivate(), fromPkcs8);
+ assertEquals(keys.getPrivate(), fromPkcs1);
+ }
+
+ @Test
+ public void parseCertificate_AcceptsBareBase64() throws Exception {
+ String bare = Base64.getEncoder().encodeToString(saml.idpCert.getEncoded());
+
+ X509Certificate cert = SamlSso.parseCertificate(bare);
+
+ assertEquals(saml.idpCert, cert);
+ }
+
+ @Test
+ public void buildClient_MissingIdpCertificate_NamesTheSetting() throws Exception {
+ Properties props = saml.properties();
+ props.remove(SamlSso.IDP_CERT);
+ props.setProperty(SamlSso.IDP_CERT_FINGERPRINT, "AB:CD");
+
+ IllegalStateException e = assertThrows(IllegalStateException.class, () -> SamlSso.build(props));
+
+ assertThat(e.getMessage(), containsString(SamlSso.IDP_CERT));
+ assertThat(e.getMessage(), containsString("not supported"));
+ }
+
+ @Test
+ public void buildClient_MissingAcsUrl_NamesTheSetting() throws Exception {
+ Properties props = saml.properties();
+ props.setProperty(SamlSso.SP_ACS_URL, " ");
+
+ IllegalStateException e = assertThrows(IllegalStateException.class, () -> SamlSso.build(props));
+
+ assertThat(e.getMessage(), containsString(SamlSso.SP_ACS_URL));
+ }
+
+ @Test
+ public void login_RedirectsToIdpWithAuthnRequest() throws Exception {
+ MockHttpServletRequest request = new MockHttpServletRequest("GET", "/saml/dologin");
+ MockHttpServletResponse response = new MockHttpServletResponse();
+
+ SamlSso.login(request, response);
+
+ assertEquals(302, response.getStatus());
+ String location = response.getRedirectedUrl();
+ assertThat(location, startsWith(SamlTestSupport.IDP_SSO_URL + "?"));
+ assertThat(location, containsString("Signature="));
+ String authnRequest = SamlTestSupport.decodeRedirectRequest(location);
+ assertThat(authnRequest, containsString("AuthnRequest"));
+ assertThat(authnRequest, containsString(SamlTestSupport.ACS_URL));
+ assertThat(authnRequest, containsString(SamlTestSupport.SP_ENTITY_ID));
+ }
+
+ @Test
+ public void getServiceProviderMetadata_DescribesConfiguredEndpoints() throws Exception {
+ String metadata = SamlSso.getServiceProviderMetadata();
+
+ assertThat(metadata, containsString("entityID=\"" + SamlTestSupport.SP_ENTITY_ID + "\""));
+ assertThat(metadata, containsString("Location=\"" + SamlTestSupport.ACS_URL + "\""));
+ assertThat(metadata, containsString("Location=\"" + SamlTestSupport.SLS_URL + "\""));
+ // The ACS URL must be exactly what the IdP has registered
+ assertFalse(metadata.contains("client_name="));
+ String spCertBase64 = Base64.getEncoder().encodeToString(saml.spCert.getEncoded());
+ assertThat(metadata.replaceAll("\\s", ""), containsString(spCertBase64));
+ }
+
+ @Test
+ public void authenticate_ValidSignedResponse_ReturnsProfile() throws Exception {
+ String samlResponse =
+ saml.signedResponse("user-123", studentAttributes(), saml.idpKeys, saml.idpCert);
+
+ Optional profile =
+ SamlSso.authenticate(acsPost(samlResponse), new MockHttpServletResponse());
+
+ assertTrue(profile.isPresent());
+ assertEquals("user-123", profile.get().getId());
+ assertEquals("_session-user-123", profile.get().getSessionIndex());
+ SsoUserMapper.Result user = SsoUserMapper.map(profile.get().getAttributes(), ssoProperties());
+ assertTrue(user.isValid());
+ assertEquals("jdoe", user.ssoName);
+ assertEquals("Jane Doe", user.userName);
+ assertEquals("player", user.userRole);
+ }
+
+ @Test
+ public void authenticate_TamperedResponse_IsRejected() throws Exception {
+ String signed =
+ saml.signedResponse("user-123", studentAttributes(), saml.idpKeys, saml.idpCert);
+ String xml = new String(Base64.getDecoder().decode(signed), "UTF-8");
+ String tampered =
+ Base64.getEncoder().encodeToString(xml.replace("students", "teachers").getBytes("UTF-8"));
+
+ Optional profile =
+ SamlSso.authenticate(acsPost(tampered), new MockHttpServletResponse());
+
+ assertFalse(profile.isPresent());
+ }
+
+ @Test
+ public void authenticate_SignedByUnknownKey_IsRejected() throws Exception {
+ KeyPair attackerKeys = SamlTestSupport.newKeyPair();
+ X509Certificate attackerCert = SamlTestSupport.selfSigned(attackerKeys, "idp.test");
+ String samlResponse =
+ saml.signedResponse("user-123", studentAttributes(), attackerKeys, attackerCert);
+
+ Optional profile =
+ SamlSso.authenticate(acsPost(samlResponse), new MockHttpServletResponse());
+
+ assertFalse(profile.isPresent());
+ }
+
+ @Test
+ public void authenticate_ReplayedResponse_IsRejected() throws Exception {
+ String samlResponse =
+ saml.signedResponse("user-123", studentAttributes(), saml.idpKeys, saml.idpCert);
+
+ assertTrue(
+ SamlSso.authenticate(acsPost(samlResponse), new MockHttpServletResponse()).isPresent());
+ assertFalse(
+ SamlSso.authenticate(acsPost(samlResponse), new MockHttpServletResponse()).isPresent());
+ }
+
+ @Test
+ public void authenticate_NoSamlResponse_ReturnsEmpty() {
+ MockHttpServletRequest request = new MockHttpServletRequest("POST", "/saml/acs");
+
+ assertFalse(SamlSso.authenticate(request, new MockHttpServletResponse()).isPresent());
+ }
+
+ @Test
+ public void logout_SendsLogoutRequestWithNameIdAndSessionIndex() throws Exception {
+ String samlResponse =
+ saml.signedResponse("user-123", studentAttributes(), saml.idpKeys, saml.idpCert);
+ SAML2Profile profile =
+ SamlSso.authenticate(acsPost(samlResponse), new MockHttpServletResponse()).get();
+ MockHttpServletResponse response = new MockHttpServletResponse();
+
+ boolean started =
+ SamlSso.logout(new MockHttpServletRequest("GET", "/logout"), response, profile);
+
+ assertTrue(started);
+ String location = response.getRedirectedUrl();
+ assertThat(location, startsWith(SamlTestSupport.IDP_SLO_URL + "?"));
+ String logoutRequest = SamlTestSupport.decodeRedirectRequest(location);
+ assertThat(logoutRequest, containsString("LogoutRequest"));
+ assertThat(logoutRequest, containsString(">user-123<"));
+ assertThat(logoutRequest, containsString("_session-user-123"));
+ }
+
+ @Test
+ public void logout_KeepsNameIdQualifiers() throws Exception {
+ String samlResponse =
+ saml.signedResponse(
+ "user-123",
+ SamlTestSupport.IDP_ENTITY_ID,
+ SamlTestSupport.SP_ENTITY_ID,
+ studentAttributes(),
+ saml.idpKeys,
+ saml.idpCert);
+ SAML2Profile profile =
+ SamlSso.authenticate(acsPost(samlResponse), new MockHttpServletResponse()).get();
+ MockHttpServletResponse response = new MockHttpServletResponse();
+
+ SamlSso.logout(new MockHttpServletRequest("GET", "/logout"), response, profile);
+
+ String logoutRequest = SamlTestSupport.decodeRedirectRequest(response.getRedirectedUrl());
+ assertThat(
+ logoutRequest, containsString("NameQualifier=\"" + SamlTestSupport.IDP_ENTITY_ID + "\""));
+ assertThat(
+ logoutRequest, containsString("SPNameQualifier=\"" + SamlTestSupport.SP_ENTITY_ID + "\""));
+ }
+
+ @Test
+ public void login_RequestedAuthnContextWithoutComparison_DefaultsToExact() throws Exception {
+ Properties props = saml.properties();
+ props.setProperty(
+ SamlSso.REQUESTED_AUTHN_CONTEXT,
+ "urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport");
+ SamlTestSupport.install(props);
+ MockHttpServletResponse response = new MockHttpServletResponse();
+
+ SamlSso.login(new MockHttpServletRequest("GET", "/saml/dologin"), response);
+
+ String authnRequest = SamlTestSupport.decodeRedirectRequest(response.getRedirectedUrl());
+ assertThat(authnRequest, containsString("Comparison=\"exact\""));
+ assertThat(authnRequest, containsString("PasswordProtectedTransport"));
+ }
+
+ @Test
+ public void login_LegacyComparisonKey_IsRead() throws Exception {
+ Properties props = saml.properties();
+ props.setProperty(
+ SamlSso.REQUESTED_AUTHN_CONTEXT,
+ "urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport");
+ props.setProperty(SamlSso.LEGACY_REQUESTED_AUTHN_CONTEXT_COMPARISON, "minimum");
+ SamlTestSupport.install(props);
+ MockHttpServletResponse response = new MockHttpServletResponse();
+
+ SamlSso.login(new MockHttpServletRequest("GET", "/saml/dologin"), response);
+
+ String authnRequest = SamlTestSupport.decodeRedirectRequest(response.getRedirectedUrl());
+ assertThat(authnRequest, containsString("Comparison=\"minimum\""));
+ }
+
+ @Test
+ public void getServiceProviderMetadata_AdvertisesOnlyVerifiableLogoutBindings() {
+ String metadata = SamlSso.getServiceProviderMetadata();
+
+ assertThat(metadata, containsString("bindings:HTTP-POST\""));
+ assertThat(metadata, containsString("bindings:HTTP-Redirect\""));
+ assertFalse(metadata.contains("HTTP-POST-SimpleSign"));
+ assertFalse(metadata.contains("bindings:SOAP"));
+ }
+
+ @Test
+ public void build_WithoutSpKeyPair_AuthenticatesSignedAssertions() throws Exception {
+ Properties props = saml.properties();
+ props.remove(SamlSso.SP_CERT);
+ props.remove(SamlSso.SP_KEY);
+ props.setProperty(SamlSso.AUTHN_REQUEST_SIGNED, "false");
+ props.setProperty(SamlSso.LOGOUT_REQUEST_SIGNED, "false");
+ SamlTestSupport.install(props);
+ String samlResponse =
+ saml.signedResponse("user-123", studentAttributes(), saml.idpKeys, saml.idpCert);
+
+ Optional profile =
+ SamlSso.authenticate(acsPost(samlResponse), new MockHttpServletResponse());
+
+ assertTrue(profile.isPresent());
+ assertEquals("user-123", profile.get().getId());
+ }
+
+ @Test
+ public void getServiceProviderMetadata_WithoutSpKeyPair_PublishesNoKey() throws Exception {
+ Properties props = saml.properties();
+ props.remove(SamlSso.SP_CERT);
+ props.remove(SamlSso.SP_KEY);
+ props.setProperty(SamlSso.AUTHN_REQUEST_SIGNED, "false");
+ props.setProperty(SamlSso.LOGOUT_REQUEST_SIGNED, "false");
+ SamlTestSupport.install(props);
+
+ String metadata = SamlSso.getServiceProviderMetadata();
+
+ assertFalse(metadata.contains("KeyDescriptor"));
+ assertFalse(metadata.contains("X509Certificate"));
+ }
+
+ @Test
+ public void getServiceProviderMetadata_WithSpKeyPair_PublishesIt() {
+ assertThat(SamlSso.getServiceProviderMetadata(), containsString("KeyDescriptor"));
+ }
+
+ @Test
+ public void build_WithoutSpKeyPair_WhenSigningRequired_NamesTheSettings() throws Exception {
+ Properties props = saml.properties();
+ props.remove(SamlSso.SP_CERT);
+ props.remove(SamlSso.SP_KEY);
+
+ IllegalStateException e = assertThrows(IllegalStateException.class, () -> SamlSso.build(props));
+
+ assertThat(e.getMessage(), containsString(SamlSso.SP_CERT));
+ assertThat(e.getMessage(), containsString(SamlSso.AUTHN_REQUEST_SIGNED));
+ }
+
+ @Test
+ public void build_OnlyOneOfSpCertAndKey_IsRejected() throws Exception {
+ Properties props = saml.properties();
+ props.remove(SamlSso.SP_KEY);
+
+ IllegalStateException e = assertThrows(IllegalStateException.class, () -> SamlSso.build(props));
+
+ assertThat(e.getMessage(), containsString("or neither"));
+ }
+
+ private Optional authenticateWithRequestedContexts(
+ String requested, String comparison, String returned) throws Exception {
+ Properties props = saml.properties();
+ props.setProperty(SamlSso.REQUESTED_AUTHN_CONTEXT, requested);
+ if (comparison != null) {
+ props.setProperty(SamlSso.REQUESTED_AUTHN_CONTEXT_COMPARISON, comparison);
+ }
+ SamlTestSupport.install(props);
+ saml.authnContextClassRef = returned;
+ try {
+ String samlResponse =
+ saml.signedResponse("user-123", studentAttributes(), saml.idpKeys, saml.idpCert);
+ return SamlSso.authenticate(acsPost(samlResponse), new MockHttpServletResponse());
+ } finally {
+ saml.authnContextClassRef = AuthnContext.PPT_AUTHN_CTX;
+ }
+ }
+
+ @Test
+ public void authenticate_OneOfSeveralRequestedContexts_IsAccepted() throws Exception {
+ assertTrue(
+ authenticateWithRequestedContexts(
+ AuthnContext.PPT_AUTHN_CTX + ", " + AuthnContext.X509_AUTHN_CTX,
+ null,
+ AuthnContext.X509_AUTHN_CTX)
+ .isPresent());
+ }
+
+ @Test
+ public void authenticate_ExactComparison_UnrequestedContext_IsRejected() throws Exception {
+ assertFalse(
+ authenticateWithRequestedContexts(
+ AuthnContext.PPT_AUTHN_CTX + ", " + AuthnContext.X509_AUTHN_CTX,
+ "exact",
+ AuthnContext.KERBEROS_AUTHN_CTX)
+ .isPresent());
+ }
+
+ @Test
+ public void authenticate_MinimumComparison_StrongerUnlistedContext_IsAccepted() throws Exception {
+ assertTrue(
+ authenticateWithRequestedContexts(
+ AuthnContext.PPT_AUTHN_CTX, "minimum", AuthnContext.X509_AUTHN_CTX)
+ .isPresent());
+ }
+
+ @Test
+ public void authenticate_NoRequestedContext_AcceptsAnyContext() throws Exception {
+ String samlResponse =
+ saml.signedResponse("user-123", studentAttributes(), saml.idpKeys, saml.idpCert);
+
+ assertTrue(
+ SamlSso.authenticate(acsPost(samlResponse), new MockHttpServletResponse()).isPresent());
+ }
+
+ @Test
+ public void getServiceProviderMetadata_WithoutSlsUrl_AdvertisesNoLogoutEndpoint()
+ throws Exception {
+ Properties props = saml.properties();
+ props.remove(SamlSso.SP_SLS_URL);
+ SamlTestSupport.install(props);
+
+ String metadata = SamlSso.getServiceProviderMetadata();
+
+ assertFalse(metadata.contains("SingleLogoutService"));
+ assertThat(metadata, containsString("Location=\"" + SamlTestSupport.ACS_URL + "\""));
+ }
+
+ /** Two tabs start a login before either finishes: both callbacks must succeed */
+ @Test
+ public void overlappingLogins_InOneBrowser_BothSucceed() throws Exception {
+ saml.startLogin();
+ String first = SamlTestSupport.lastRequestId();
+ saml.startLogin();
+ String second = SamlTestSupport.lastRequestId();
+
+ assertTrue(
+ SamlSso.authenticate(
+ acsPost(saml.signedResponseTo(first, "user-123", studentAttributes())),
+ new MockHttpServletResponse())
+ .isPresent());
+ assertTrue(
+ SamlSso.authenticate(
+ acsPost(saml.signedResponseTo(second, "user-123", studentAttributes())),
+ new MockHttpServletResponse())
+ .isPresent());
+ }
+
+ /**
+ * A second tab starts a login just before the first tab's cookie would expire: the cookie must
+ * last as long as the newest login, or that login's callback arrives without it
+ */
+ @Test
+ public void overlappingLogins_LaterLoginKeepsTheCookieAlive() throws Exception {
+ saml.startLogin();
+ SamlTestSupport.advanceBrowserClock(Duration.ofMinutes(9));
+ saml.startLogin();
+ String second = SamlTestSupport.lastRequestId();
+ SamlTestSupport.advanceBrowserClock(Duration.ofMinutes(2));
+
+ assertTrue(
+ SamlSso.authenticate(
+ acsPost(saml.signedResponseTo(second, "user-123", studentAttributes())),
+ new MockHttpServletResponse())
+ .isPresent());
+ }
+
+ /** Two tabs open the login page before the browser holds any binding cookie: both must work */
+ @Test
+ public void concurrentFirstLogins_InOneBrowser_BothSucceed() throws Exception {
+ List requests = SamlTestSupport.startConcurrentLogins(2);
+
+ assertTrue(
+ SamlSso.authenticate(
+ acsPost(saml.signedResponseTo(requests.get(0), "user-123", studentAttributes())),
+ new MockHttpServletResponse())
+ .isPresent());
+ assertTrue(
+ SamlSso.authenticate(
+ acsPost(saml.signedResponseTo(requests.get(1), "user-123", studentAttributes())),
+ new MockHttpServletResponse())
+ .isPresent());
+ }
+
+ /** A completed login deletes its binding cookie, so cookies don't pile up in the browser */
+ @Test
+ public void authenticate_Success_DeletesThatLoginsCookie() throws Exception {
+ Cookie cookie = saml.startLogin();
+ String samlResponse =
+ saml.signedResponse("user-123", studentAttributes(), saml.idpKeys, saml.idpCert);
+ MockHttpServletResponse response = new MockHttpServletResponse();
+
+ assertTrue(SamlSso.authenticate(acsPost(samlResponse), response).isPresent());
+
+ Cookie deleted = response.getCookie(cookie.getName());
+ assertNotNull(deleted);
+ assertEquals(0, deleted.getMaxAge());
+ assertEquals(cookie.getPath(), deleted.getPath());
+ }
+
+ @Test
+ public void bindingCookie_IsScopedToTheSamlDirectory() throws Exception {
+ Cookie cookie = saml.startLogin();
+
+ assertEquals("/saml/", cookie.getPath());
+ assertTrue(cookie.isHttpOnly());
+ assertTrue(cookie.getSecure());
+ assertEquals("None", cookie.getAttribute("SameSite"));
+ }
+
+ @Test
+ public void authenticate_UnsolicitedResponse_IsRejected() throws Exception {
+ saml.unsolicited = true;
+ try {
+ String samlResponse =
+ saml.signedResponse("user-123", studentAttributes(), saml.idpKeys, saml.idpCert);
+
+ assertFalse(
+ SamlSso.authenticate(acsPost(samlResponse), new MockHttpServletResponse()).isPresent());
+ } finally {
+ saml.unsolicited = false;
+ }
+ }
+
+ /**
+ * Login CSRF: the attacker's own valid assertion, with only the unsigned envelope's InResponseTo
+ * changed to the victim browser's pending request, must not log the victim's browser in
+ */
+ @Test
+ public void authenticate_EnvelopeRetargetedToAnotherBrowsersRequest_IsRejected()
+ throws Exception {
+ saml.startLogin();
+ String attackersRequest = SamlTestSupport.lastRequestId();
+ SamlTestSupport.newBrowser();
+ saml.startLogin();
+ String victimsRequest = SamlTestSupport.lastRequestId();
+
+ saml.envelopeInResponseTo = victimsRequest;
+ try {
+ String forged = saml.signedResponseTo(attackersRequest, "attacker", studentAttributes());
+
+ assertFalse(SamlSso.authenticate(acsPost(forged), new MockHttpServletResponse()).isPresent());
+ } finally {
+ saml.envelopeInResponseTo = null;
+ }
+ }
+
+ /**
+ * An assertion whose bearer confirmation names no request (as an IdP-initiated one) can't be
+ * bound to a login, even inside an envelope that names this browser's pending request
+ */
+ @Test
+ public void authenticate_ConfirmationWithoutInResponseTo_IsRejected() throws Exception {
+ saml.startLogin();
+ String request = SamlTestSupport.lastRequestId();
+
+ saml.omitConfirmationInResponseTo = true;
+ try {
+ String samlResponse = saml.signedResponseTo(request, "user-123", studentAttributes());
+
+ assertFalse(
+ SamlSso.authenticate(acsPost(samlResponse), new MockHttpServletResponse()).isPresent());
+ } finally {
+ saml.omitConfirmationInResponseTo = false;
+ }
+ }
+
+ /** Login CSRF: a valid response must not log in a browser other than the one that asked */
+ @Test
+ public void authenticate_ResponseDeliveredThroughAnotherBrowser_IsRejected() throws Exception {
+ saml.startLogin();
+ String request = SamlTestSupport.lastRequestId();
+ String samlResponse =
+ saml.signedResponse("attacker", studentAttributes(), saml.idpKeys, saml.idpCert);
+ MockHttpServletRequest victimWithoutCookie = acsPost(samlResponse);
+ victimWithoutCookie.setCookies();
+ MockHttpServletRequest victimWithOwnCookie = acsPost(samlResponse);
+ victimWithOwnCookie.setCookies(
+ new jakarta.servlet.http.Cookie(SamlRequestStore.cookieName(request), "victims-own-token"));
+
+ assertFalse(
+ SamlSso.authenticate(victimWithoutCookie, new MockHttpServletResponse()).isPresent());
+ assertFalse(
+ SamlSso.authenticate(victimWithOwnCookie, new MockHttpServletResponse()).isPresent());
+ }
+
+ @Test
+ public void authenticate_ResponseToAnotherBrowsersLogin_IsRejected() throws Exception {
+ saml.startLogin();
+ String samlResponse =
+ saml.signedResponse("attacker", studentAttributes(), saml.idpKeys, saml.idpCert);
+ // The victim's (different) browser has started a login of its own, with its own cookie
+ SamlTestSupport.newBrowser();
+ jakarta.servlet.http.Cookie victimCookie = saml.startLogin();
+ MockHttpServletRequest victim = acsPost(samlResponse);
+ victim.setCookies(victimCookie);
+
+ assertFalse(SamlSso.authenticate(victim, new MockHttpServletResponse()).isPresent());
+ }
+
+ @Test
+ public void processLogoutMessage_NoMessage_IsInvalid() {
+ MockHttpServletRequest request = new MockHttpServletRequest("GET", "/saml/sls");
+
+ assertEquals(
+ SamlSso.LogoutMessage.INVALID,
+ SamlSso.processLogoutMessage(request, new MockHttpServletResponse()));
+ }
+
+ private static Properties ssoProperties() {
+ Properties props = new Properties();
+ props.setProperty("sso.saml.ssoName", "uid");
+ props.setProperty("sso.saml.userName", "displayName");
+ props.setProperty("sso.saml.affiliation", "groups");
+ props.setProperty("sso.saml.adminAffiliation", "staff");
+ props.setProperty("sso.saml.playerAffiliation", "students");
+ return props;
+ }
+}
diff --git a/src/test/java/utils/SamlTestSupport.java b/src/test/java/utils/SamlTestSupport.java
new file mode 100644
index 000000000..ae31cce5f
--- /dev/null
+++ b/src/test/java/utils/SamlTestSupport.java
@@ -0,0 +1,647 @@
+package utils;
+
+import jakarta.servlet.http.Cookie;
+import java.io.ByteArrayOutputStream;
+import java.math.BigInteger;
+import java.net.URLDecoder;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.cert.X509Certificate;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.Base64;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+import java.util.UUID;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.zip.Deflater;
+import java.util.zip.Inflater;
+import javax.xml.namespace.QName;
+import net.shibboleth.shared.xml.SerializeSupport;
+import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
+import org.bouncycastle.asn1.x500.X500Name;
+import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
+import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;
+import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
+import org.opensaml.core.xml.XMLObject;
+import org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport;
+import org.opensaml.core.xml.schema.XSString;
+import org.opensaml.saml.common.SAMLVersion;
+import org.opensaml.saml.common.SignableSAMLObject;
+import org.opensaml.saml.saml2.core.Assertion;
+import org.opensaml.saml.saml2.core.Attribute;
+import org.opensaml.saml.saml2.core.AttributeStatement;
+import org.opensaml.saml.saml2.core.AttributeValue;
+import org.opensaml.saml.saml2.core.Audience;
+import org.opensaml.saml.saml2.core.AudienceRestriction;
+import org.opensaml.saml.saml2.core.AuthnContext;
+import org.opensaml.saml.saml2.core.AuthnContextClassRef;
+import org.opensaml.saml.saml2.core.AuthnStatement;
+import org.opensaml.saml.saml2.core.Conditions;
+import org.opensaml.saml.saml2.core.Issuer;
+import org.opensaml.saml.saml2.core.LogoutRequest;
+import org.opensaml.saml.saml2.core.LogoutResponse;
+import org.opensaml.saml.saml2.core.NameID;
+import org.opensaml.saml.saml2.core.NameIDType;
+import org.opensaml.saml.saml2.core.Response;
+import org.opensaml.saml.saml2.core.SessionIndex;
+import org.opensaml.saml.saml2.core.Status;
+import org.opensaml.saml.saml2.core.StatusCode;
+import org.opensaml.saml.saml2.core.Subject;
+import org.opensaml.saml.saml2.core.SubjectConfirmation;
+import org.opensaml.saml.saml2.core.SubjectConfirmationData;
+import org.opensaml.security.x509.BasicX509Credential;
+import org.opensaml.xmlsec.signature.Signature;
+import org.opensaml.xmlsec.signature.support.SignatureConstants;
+import org.opensaml.xmlsec.signature.support.Signer;
+import org.pac4j.saml.client.SAML2Client;
+import org.springframework.mock.web.MockHttpServletRequest;
+import org.springframework.mock.web.MockHttpServletResponse;
+
+/**
+ * Test fixtures for SAML SSO: generated SP/IdP key pairs, onelogin.saml.properties contents for
+ * them, and signed SAML responses as an IdP would send them.
+ */
+public class SamlTestSupport {
+
+ public static final String SP_ENTITY_ID = "https://shepherd.test/saml/metadata.xml";
+ public static final String ACS_URL = "https://shepherd.test/saml/acs";
+ public static final String SLS_URL = "https://shepherd.test/saml/sls";
+ public static final String IDP_ENTITY_ID = "https://idp.test/metadata";
+ public static final String IDP_SSO_URL = "https://idp.test/sso";
+ public static final String IDP_SLO_URL = "https://idp.test/slo";
+
+ /** The AuthnContextClassRef that {@link #signedResponse} reports */
+ public String authnContextClassRef = AuthnContext.PPT_AUTHN_CTX;
+
+ public final KeyPair spKeys;
+ public final X509Certificate spCert;
+ public final KeyPair idpKeys;
+ public final X509Certificate idpCert;
+
+ public SamlTestSupport() throws Exception {
+ spKeys = newKeyPair();
+ spCert = selfSigned(spKeys, "shepherd.test");
+ idpKeys = newKeyPair();
+ idpCert = selfSigned(idpKeys, "idp.test");
+ }
+
+ /** onelogin.saml.properties as an operator would write it, with PEM armour */
+ public Properties properties() throws Exception {
+ Properties props = new Properties();
+ props.setProperty(SamlSso.STRICT, "true");
+ props.setProperty(SamlSso.SP_ENTITY_ID, SP_ENTITY_ID);
+ props.setProperty(SamlSso.SP_ACS_URL, ACS_URL);
+ props.setProperty(SamlSso.SP_ACS_BINDING, "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST");
+ props.setProperty(SamlSso.SP_SLS_URL, SLS_URL);
+ props.setProperty(
+ SamlSso.SP_NAMEID_FORMAT, "urn:oasis:names:tc:SAML:2.0:nameid-format:persistent");
+ props.setProperty(SamlSso.SP_CERT, certificatePem(spCert));
+ props.setProperty(SamlSso.SP_KEY, pkcs8Pem(spKeys));
+ props.setProperty(SamlSso.IDP_ENTITY_ID, IDP_ENTITY_ID);
+ props.setProperty(SamlSso.IDP_SSO_URL, IDP_SSO_URL);
+ props.setProperty(
+ SamlSso.IDP_SSO_BINDING, "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect");
+ props.setProperty(SamlSso.IDP_SLO_URL, IDP_SLO_URL);
+ props.setProperty(
+ SamlSso.IDP_SLO_BINDING, "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect");
+ props.setProperty(SamlSso.IDP_CERT, certificatePem(idpCert));
+ props.setProperty(SamlSso.AUTHN_REQUEST_SIGNED, "true");
+ props.setProperty(SamlSso.LOGOUT_REQUEST_SIGNED, "true");
+ props.setProperty(SamlSso.WANT_ASSERTIONS_SIGNED, "true");
+ props.setProperty(
+ SamlSso.SIGNATURE_ALGORITHM, "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256");
+ return props;
+ }
+
+ /** Builds a client from {@link #properties()} and makes it the one SamlSso uses */
+ public SAML2Client installClient() throws Exception {
+ return install(properties());
+ }
+
+ /** Builds a client from the given properties and makes it the one SamlSso uses */
+ public static SAML2Client install(Properties props) {
+ SamlSso.Sso sso = SamlSso.build(props);
+ SamlSso.set(sso);
+ // A fresh client has an empty request store: forget the previous browser's login state
+ newBrowser();
+ LoginMethod.setSaml(true);
+ return sso.client;
+ }
+
+ /** Clears the client installed by {@link #installClient()} */
+ public static void uninstallClient() {
+ SamlSso.set(null);
+ LoginMethod.setSaml(null);
+ }
+
+ /** Switches SSO (sso.enabled) on or off without touching the installed client */
+ public static void setSsoEnabled(boolean enabled) {
+ LoginMethod.setSaml(enabled);
+ }
+
+ /** When true, {@link #signedResponse} builds an unsolicited response (no InResponseTo) */
+ public boolean unsolicited;
+
+ /**
+ * When set, {@link #signedResponse} puts this request ID in the Response envelope's InResponseTo
+ * (which isn't signed unless the whole message is) while the signed assertion's bearer
+ * confirmation keeps the request it answers: an attacker editing the envelope
+ */
+ public String envelopeInResponseTo;
+
+ /**
+ * When true, {@link #signedResponse} leaves InResponseTo out of the bearer confirmation, as in an
+ * assertion issued without a request (IdP-initiated)
+ */
+ public boolean omitConfirmationInResponseTo;
+
+ /*
+ * One simulated browser, matching the single installed client: the AuthnRequest ID of its most
+ * recent login, and its cookie jar, where each cookie is dropped when its Max-Age runs out on a
+ * browser clock that tests can move forward. Reset by install().
+ */
+ private static String pendingRequestId;
+ private static final Map cookieJar = new LinkedHashMap<>();
+ private static final Map cookieExpires = new HashMap<>();
+ private static Cookie lastLoginCookie;
+ private static Duration browserClock = Duration.ZERO;
+
+ /**
+ * Starts an SP-initiated login as a browser would, sending the cookies whose Path covers the
+ * login URL. The next {@link #signedResponse} answers it.
+ *
+ * @return the request-binding cookie this login set, which the ACS request must carry
+ */
+ public Cookie startLogin() throws Exception {
+ startConcurrentLogins(1);
+ return lastLoginCookie;
+ }
+
+ /**
+ * Starts logins in several tabs at once: every request leaves with the cookies the browser held
+ * before any of them, and only then are the responses' cookies stored, in order. The next {@link
+ * #signedResponse} answers the last one.
+ *
+ * @return the AuthnRequest IDs, in the order the logins were started
+ */
+ public static List startConcurrentLogins(int tabs) throws Exception {
+ List ids = new ArrayList<>();
+ List responses = new ArrayList<>();
+ for (int i = 0; i < tabs; i++) {
+ MockHttpServletRequest request = new MockHttpServletRequest("GET", "/saml/dologin");
+ request.setScheme("https");
+ request.setSecure(true);
+ request.setServerName("shepherd.test");
+ request.setServerPort(443);
+ Cookie[] sent = cookiesFor("/saml/dologin");
+ if (sent.length > 0) {
+ request.setCookies(sent);
+ }
+ MockHttpServletResponse response = new MockHttpServletResponse();
+ SamlSso.login(request, response);
+ Matcher id =
+ Pattern.compile(" ID=\"([^\"]+)\"")
+ .matcher(decodeRedirectRequest(response.getRedirectedUrl()));
+ if (!id.find()) {
+ throw new IllegalStateException("No ID in AuthnRequest");
+ }
+ ids.add(id.group(1));
+ responses.add(response);
+ }
+ for (MockHttpServletResponse response : responses) {
+ for (Cookie cookie : response.getCookies()) {
+ storeCookie(cookie);
+ if (cookie.getName().startsWith(SamlRequestStore.COOKIE) && cookie.getMaxAge() != 0) {
+ lastLoginCookie = cookie;
+ }
+ }
+ }
+ pendingRequestId = ids.get(ids.size() - 1);
+ return ids;
+ }
+
+ /** Stores a Set-Cookie as a browser does: replacing any cookie of that name, or deleting it */
+ private static void storeCookie(Cookie cookie) {
+ if (cookie.getMaxAge() == 0) {
+ cookieJar.remove(cookie.getName());
+ cookieExpires.remove(cookie.getName());
+ return;
+ }
+ cookieJar.put(cookie.getName(), cookie);
+ cookieExpires.put(
+ cookie.getName(),
+ cookie.getMaxAge() < 0 ? Instant.MAX : browserNow().plusSeconds(cookie.getMaxAge()));
+ }
+
+ /** Switches to a different browser, with no cookies and no login in progress */
+ public static void newBrowser() {
+ pendingRequestId = null;
+ cookieJar.clear();
+ cookieExpires.clear();
+ lastLoginCookie = null;
+ browserClock = Duration.ZERO;
+ }
+
+ /**
+ * Moves the browser's clock forward, so its cookies expire as their Max-Age says. The server's
+ * own clock is not moved.
+ */
+ public static void advanceBrowserClock(Duration elapsed) {
+ browserClock = browserClock.plus(elapsed);
+ }
+
+ private static Instant browserNow() {
+ return Instant.now().plus(browserClock);
+ }
+
+ /** The AuthnRequest ID of the most recently started login */
+ public static String lastRequestId() {
+ return pendingRequestId;
+ }
+
+ /** The binding cookie set by the most recently started login, if any */
+ public Cookie browserCookie() {
+ return lastLoginCookie;
+ }
+
+ /** The cookies the browser sends to a URL path, following the cookie Path and Max-Age rules */
+ public static Cookie[] cookiesFor(String requestPath) {
+ cookieJar.keySet().removeIf(name -> !browserNow().isBefore(cookieExpires.get(name)));
+ return cookieJar.values().stream()
+ .filter(c -> requestPath.startsWith(c.getPath() == null ? "/" : c.getPath()))
+ .toArray(Cookie[]::new);
+ }
+
+ /** As {@link #signedResponse}, answering the given AuthnRequest ID */
+ public String signedResponseTo(
+ String requestId, String nameId, Map> attributes) throws Exception {
+ pendingRequestId = requestId;
+ return signedResponse(nameId, attributes, idpKeys, idpCert);
+ }
+
+ /**
+ * Builds a base64 SAMLResponse for the HTTP-POST binding, with the assertion signed by the given
+ * key pair.
+ */
+ public String signedResponse(
+ String nameId,
+ Map> attributes,
+ KeyPair signingKeys,
+ X509Certificate signingCert)
+ throws Exception {
+ return signedResponse(nameId, null, null, attributes, signingKeys, signingCert);
+ }
+
+ /** As {@link #signedResponse}, with NameQualifier / SPNameQualifier on the NameID */
+ public String signedResponse(
+ String nameId,
+ String nameQualifier,
+ String spNameQualifier,
+ Map> attributes,
+ KeyPair signingKeys,
+ X509Certificate signingCert)
+ throws Exception {
+ Instant now = Instant.now();
+
+ Response response = build(Response.DEFAULT_ELEMENT_NAME);
+ response.setID("_" + UUID.randomUUID());
+ response.setIssueInstant(now);
+ response.setVersion(SAMLVersion.VERSION_20);
+ response.setDestination(ACS_URL);
+ String inResponseTo = null;
+ if (!unsolicited) {
+ if (pendingRequestId == null) {
+ startLogin();
+ }
+ inResponseTo = pendingRequestId;
+ pendingRequestId = null;
+ response.setInResponseTo(envelopeInResponseTo != null ? envelopeInResponseTo : inResponseTo);
+ }
+ response.setIssuer(issuer());
+ Status status = build(Status.DEFAULT_ELEMENT_NAME);
+ StatusCode statusCode = build(StatusCode.DEFAULT_ELEMENT_NAME);
+ statusCode.setValue(StatusCode.SUCCESS);
+ status.setStatusCode(statusCode);
+ response.setStatus(status);
+
+ Assertion assertion = build(Assertion.DEFAULT_ELEMENT_NAME);
+ assertion.setID("_" + UUID.randomUUID());
+ assertion.setIssueInstant(now);
+ assertion.setVersion(SAMLVersion.VERSION_20);
+ assertion.setIssuer(issuer());
+
+ Subject subject = build(Subject.DEFAULT_ELEMENT_NAME);
+ NameID name = build(NameID.DEFAULT_ELEMENT_NAME);
+ name.setValue(nameId);
+ name.setFormat(NameIDType.PERSISTENT);
+ name.setNameQualifier(nameQualifier);
+ name.setSPNameQualifier(spNameQualifier);
+ subject.setNameID(name);
+ SubjectConfirmation confirmation = build(SubjectConfirmation.DEFAULT_ELEMENT_NAME);
+ confirmation.setMethod(SubjectConfirmation.METHOD_BEARER);
+ SubjectConfirmationData confirmationData = build(SubjectConfirmationData.DEFAULT_ELEMENT_NAME);
+ confirmationData.setRecipient(ACS_URL);
+ confirmationData.setInResponseTo(omitConfirmationInResponseTo ? null : inResponseTo);
+ confirmationData.setNotOnOrAfter(now.plus(Duration.ofMinutes(5)));
+ confirmation.setSubjectConfirmationData(confirmationData);
+ subject.getSubjectConfirmations().add(confirmation);
+ assertion.setSubject(subject);
+
+ Conditions conditions = build(Conditions.DEFAULT_ELEMENT_NAME);
+ conditions.setNotBefore(now.minus(Duration.ofMinutes(1)));
+ conditions.setNotOnOrAfter(now.plus(Duration.ofMinutes(5)));
+ AudienceRestriction audienceRestriction = build(AudienceRestriction.DEFAULT_ELEMENT_NAME);
+ Audience audience = build(Audience.DEFAULT_ELEMENT_NAME);
+ audience.setURI(SP_ENTITY_ID);
+ audienceRestriction.getAudiences().add(audience);
+ conditions.getAudienceRestrictions().add(audienceRestriction);
+ assertion.setConditions(conditions);
+
+ AuthnStatement authn = build(AuthnStatement.DEFAULT_ELEMENT_NAME);
+ authn.setAuthnInstant(now);
+ authn.setSessionIndex("_session-" + nameId);
+ AuthnContext authnContext = build(AuthnContext.DEFAULT_ELEMENT_NAME);
+ AuthnContextClassRef classRef = build(AuthnContextClassRef.DEFAULT_ELEMENT_NAME);
+ classRef.setURI(authnContextClassRef);
+ authnContext.setAuthnContextClassRef(classRef);
+ authn.setAuthnContext(authnContext);
+ assertion.getAuthnStatements().add(authn);
+
+ AttributeStatement attributeStatement = build(AttributeStatement.DEFAULT_ELEMENT_NAME);
+ for (Map.Entry> entry : attributes.entrySet()) {
+ Attribute attribute = build(Attribute.DEFAULT_ELEMENT_NAME);
+ attribute.setName(entry.getKey());
+ for (String value : entry.getValue()) {
+ XSString xsValue =
+ (XSString)
+ XMLObjectProviderRegistrySupport.getBuilderFactory()
+ .getBuilder(XSString.TYPE_NAME)
+ .buildObject(AttributeValue.DEFAULT_ELEMENT_NAME, XSString.TYPE_NAME);
+ xsValue.setValue(value);
+ attribute.getAttributeValues().add(xsValue);
+ }
+ attributeStatement.getAttributes().add(attribute);
+ }
+ assertion.getAttributeStatements().add(attributeStatement);
+
+ Signature signature = build(Signature.DEFAULT_ELEMENT_NAME);
+ signature.setSigningCredential(new BasicX509Credential(signingCert, signingKeys.getPrivate()));
+ signature.setSignatureAlgorithm(SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA256);
+ signature.setCanonicalizationAlgorithm(SignatureConstants.ALGO_ID_C14N_EXCL_OMIT_COMMENTS);
+ assertion.setSignature(signature);
+ response.getAssertions().add(assertion);
+
+ XMLObjectProviderRegistrySupport.getMarshallerFactory()
+ .getMarshaller(response)
+ .marshall(response);
+ Signer.signObject(signature);
+
+ String xml = SerializeSupport.nodeToString(response.getDOM());
+ return Base64.getEncoder().encodeToString(xml.getBytes(StandardCharsets.UTF_8));
+ }
+
+ /** An IdP LogoutResponse to an SP-initiated logout */
+ public LogoutResponse logoutResponse() {
+ LogoutResponse response = build(LogoutResponse.DEFAULT_ELEMENT_NAME);
+ response.setID("_" + UUID.randomUUID());
+ response.setIssueInstant(Instant.now());
+ response.setVersion(SAMLVersion.VERSION_20);
+ response.setDestination(SLS_URL);
+ response.setIssuer(issuer());
+ Status status = build(Status.DEFAULT_ELEMENT_NAME);
+ StatusCode statusCode = build(StatusCode.DEFAULT_ELEMENT_NAME);
+ statusCode.setValue(StatusCode.SUCCESS);
+ status.setStatusCode(statusCode);
+ response.setStatus(status);
+ return response;
+ }
+
+ /** An IdP-initiated LogoutRequest naming several logins of one user */
+ public LogoutRequest logoutRequest(String nameId, List sessionIndexes) {
+ LogoutRequest request = logoutRequest(nameId, (String) null);
+ for (String value : sessionIndexes) {
+ SessionIndex index = build(SessionIndex.DEFAULT_ELEMENT_NAME);
+ index.setValue(value);
+ request.getSessionIndexes().add(index);
+ }
+ return request;
+ }
+
+ /** An IdP-initiated LogoutRequest for a login */
+ public LogoutRequest logoutRequest(String nameId, String sessionIndex) {
+ LogoutRequest request = build(LogoutRequest.DEFAULT_ELEMENT_NAME);
+ request.setID("_" + UUID.randomUUID());
+ request.setIssueInstant(Instant.now());
+ request.setVersion(SAMLVersion.VERSION_20);
+ request.setDestination(SLS_URL);
+ request.setIssuer(issuer());
+ NameID name = build(NameID.DEFAULT_ELEMENT_NAME);
+ name.setValue(nameId);
+ name.setFormat(NameIDType.PERSISTENT);
+ request.setNameID(name);
+ if (sessionIndex != null) {
+ SessionIndex index = build(SessionIndex.DEFAULT_ELEMENT_NAME);
+ index.setValue(sessionIndex);
+ request.getSessionIndexes().add(index);
+ }
+ return request;
+ }
+
+ /** Serialises a SAML message, with an enveloped XML signature when signing keys are given */
+ public static String toXml(
+ SignableSAMLObject message, KeyPair signingKeys, X509Certificate signingCert)
+ throws Exception {
+ Signature signature = null;
+ if (signingKeys != null) {
+ signature = build(Signature.DEFAULT_ELEMENT_NAME);
+ signature.setSigningCredential(
+ new BasicX509Credential(signingCert, signingKeys.getPrivate()));
+ signature.setSignatureAlgorithm(SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA256);
+ signature.setCanonicalizationAlgorithm(SignatureConstants.ALGO_ID_C14N_EXCL_OMIT_COMMENTS);
+ message.setSignature(signature);
+ }
+ XMLObjectProviderRegistrySupport.getMarshallerFactory()
+ .getMarshaller(message)
+ .marshall(message);
+ if (signature != null) {
+ Signer.signObject(signature);
+ }
+ return SerializeSupport.nodeToString(message.getDOM());
+ }
+
+ /**
+ * Builds an HTTP-Redirect binding query string carrying a SAML message, with a query-string
+ * signature when signing keys are given.
+ */
+ public static String redirectQuery(String param, String xml, KeyPair signingKeys)
+ throws Exception {
+ return redirectQuery(
+ param, xml, signingKeys, SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA256, "SHA256withRSA");
+ }
+
+ /** As {@link #redirectQuery(String, String, KeyPair)}, with a chosen signature algorithm */
+ public static String redirectQuery(
+ String param, String xml, KeyPair signingKeys, String sigAlgUri, String jcaAlgorithm)
+ throws Exception {
+ Deflater deflater = new Deflater(Deflater.DEFLATED, true);
+ deflater.setInput(xml.getBytes(StandardCharsets.UTF_8));
+ deflater.finish();
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ byte[] buffer = new byte[4096];
+ while (!deflater.finished()) {
+ out.write(buffer, 0, deflater.deflate(buffer));
+ }
+ String query =
+ param
+ + "="
+ + URLEncoder.encode(
+ Base64.getEncoder().encodeToString(out.toByteArray()), StandardCharsets.UTF_8);
+ if (signingKeys == null) {
+ return query;
+ }
+ query += "&SigAlg=" + URLEncoder.encode(sigAlgUri, StandardCharsets.UTF_8);
+ java.security.Signature signer = java.security.Signature.getInstance(jcaAlgorithm);
+ signer.initSign(signingKeys.getPrivate());
+ signer.update(query.getBytes(StandardCharsets.UTF_8));
+ return query
+ + "&Signature="
+ + URLEncoder.encode(
+ Base64.getEncoder().encodeToString(signer.sign()), StandardCharsets.UTF_8);
+ }
+
+ /**
+ * A request to the single logout service carrying the given HTTP-Redirect query string. Like a
+ * servlet container, parameter names and values are both URL-decoded, in order.
+ */
+ public static MockHttpServletRequest slsGet(String query) {
+ MockHttpServletRequest request = slsRequest("GET");
+ request.setQueryString(query);
+ for (String pair : query.split("&")) {
+ int eq = pair.indexOf('=');
+ request.addParameter(
+ URLDecoder.decode(pair.substring(0, eq), StandardCharsets.UTF_8),
+ URLDecoder.decode(pair.substring(eq + 1), StandardCharsets.UTF_8));
+ }
+ return request;
+ }
+
+ /** A request to the single logout service carrying an HTTP-POST binding message */
+ public static MockHttpServletRequest slsPost(String param, String xml) {
+ MockHttpServletRequest request = slsRequest("POST");
+ request.addParameter(
+ param, Base64.getEncoder().encodeToString(xml.getBytes(StandardCharsets.UTF_8)));
+ return request;
+ }
+
+ private static MockHttpServletRequest slsRequest(String method) {
+ MockHttpServletRequest request = new MockHttpServletRequest(method, "/saml/sls");
+ request.setScheme("https");
+ request.setSecure(true);
+ request.setServerName("shepherd.test");
+ request.setServerPort(443);
+ return request;
+ }
+
+ /** Decodes the SAMLRequest parameter of an HTTP-Redirect binding URL */
+ public static String decodeRedirectRequest(String location) throws Exception {
+ String query = location.substring(location.indexOf('?') + 1);
+ for (String param : query.split("&")) {
+ if (param.startsWith("SAMLRequest=")) {
+ byte[] deflated =
+ Base64.getDecoder()
+ .decode(URLDecoder.decode(param.substring(12), StandardCharsets.UTF_8));
+ Inflater inflater = new Inflater(true);
+ inflater.setInput(deflated);
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ byte[] buffer = new byte[4096];
+ while (!inflater.finished()) {
+ out.write(buffer, 0, inflater.inflate(buffer));
+ }
+ return out.toString(StandardCharsets.UTF_8);
+ }
+ }
+ throw new IllegalArgumentException("No SAMLRequest in " + location);
+ }
+
+ /** An EC P-256 key pair, as used by IdPs that sign with ecdsa-sha256 */
+ public static KeyPair newEcKeyPair() throws Exception {
+ KeyPairGenerator generator = KeyPairGenerator.getInstance("EC");
+ generator.initialize(256);
+ return generator.generateKeyPair();
+ }
+
+ public static KeyPair newKeyPair() throws Exception {
+ KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA");
+ generator.initialize(2048);
+ return generator.generateKeyPair();
+ }
+
+ public static X509Certificate selfSigned(KeyPair keys, String commonName) throws Exception {
+ X500Name name = new X500Name("CN=" + commonName);
+ Instant now = Instant.now();
+ JcaX509v3CertificateBuilder builder =
+ new JcaX509v3CertificateBuilder(
+ name,
+ BigInteger.valueOf(now.toEpochMilli()),
+ Date.from(now.minus(Duration.ofDays(1))),
+ Date.from(now.plus(Duration.ofDays(365))),
+ name,
+ keys.getPublic());
+ return new JcaX509CertificateConverter()
+ .getCertificate(
+ builder.build(
+ new JcaContentSignerBuilder(
+ "EC".equals(keys.getPrivate().getAlgorithm())
+ ? "SHA256withECDSA"
+ : "SHA256withRSA")
+ .build(keys.getPrivate())));
+ }
+
+ public static String certificatePem(X509Certificate cert) throws Exception {
+ return pem("CERTIFICATE", cert.getEncoded());
+ }
+
+ public static String pkcs8Pem(KeyPair keys) {
+ return pem("PRIVATE KEY", keys.getPrivate().getEncoded());
+ }
+
+ public static String pkcs1Pem(KeyPair keys) throws Exception {
+ byte[] pkcs1 =
+ PrivateKeyInfo.getInstance(keys.getPrivate().getEncoded())
+ .parsePrivateKey()
+ .toASN1Primitive()
+ .getEncoded();
+ return pem("RSA PRIVATE KEY", pkcs1);
+ }
+
+ private static String pem(String type, byte[] der) {
+ return "-----BEGIN "
+ + type
+ + "-----\n"
+ + Base64.getMimeEncoder(64, "\n".getBytes(StandardCharsets.US_ASCII)).encodeToString(der)
+ + "\n-----END "
+ + type
+ + "-----";
+ }
+
+ private static Issuer issuer() {
+ Issuer issuer = build(Issuer.DEFAULT_ELEMENT_NAME);
+ issuer.setValue(IDP_ENTITY_ID);
+ return issuer;
+ }
+
+ @SuppressWarnings("unchecked")
+ private static T build(QName name) {
+ return (T)
+ XMLObjectProviderRegistrySupport.getBuilderFactory().getBuilder(name).buildObject(name);
+ }
+}
diff --git a/src/test/java/utils/SsoUserMapperTest.java b/src/test/java/utils/SsoUserMapperTest.java
new file mode 100644
index 000000000..32267ec27
--- /dev/null
+++ b/src/test/java/utils/SsoUserMapperTest.java
@@ -0,0 +1,135 @@
+package utils;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Properties;
+import org.junit.jupiter.api.Test;
+
+public class SsoUserMapperTest {
+
+ private static Properties ssoProperties() {
+ Properties props = new Properties();
+ props.setProperty("sso.saml.ssoName", "uid");
+ props.setProperty("sso.saml.userName", "displayName");
+ props.setProperty("sso.saml.affiliation", "groups");
+ props.setProperty("sso.saml.adminAffiliation", "staff, teachers");
+ props.setProperty("sso.saml.playerAffiliation", "students");
+ return props;
+ }
+
+ private static Map attributes(Object groups) {
+ Map attributes = new HashMap<>();
+ attributes.put("uid", Collections.singletonList("jdoe"));
+ attributes.put("displayName", Collections.singletonList("Jane Doe"));
+ attributes.put("groups", groups);
+ return attributes;
+ }
+
+ @Test
+ public void map_AdminAffiliation_IsAdmin() {
+ SsoUserMapper.Result result =
+ SsoUserMapper.map(attributes(Arrays.asList("students", "teachers")), ssoProperties());
+
+ assertTrue(result.isValid());
+ assertEquals("jdoe", result.ssoName);
+ assertEquals("Jane Doe", result.userName);
+ assertEquals("admin", result.userRole);
+ }
+
+ @Test
+ public void map_PlayerAffiliation_IsPlayer() {
+ SsoUserMapper.Result result =
+ SsoUserMapper.map(attributes(Collections.singletonList("students")), ssoProperties());
+
+ assertTrue(result.isValid());
+ assertEquals("player", result.userRole);
+ }
+
+ @Test
+ public void map_SingleValuedAttributes_AreRead() {
+ Map attributes = new HashMap<>();
+ attributes.put("uid", "jdoe");
+ attributes.put("displayName", "Jane Doe");
+ attributes.put("groups", "staff");
+
+ SsoUserMapper.Result result = SsoUserMapper.map(attributes, ssoProperties());
+
+ assertTrue(result.isValid());
+ assertEquals("jdoe", result.ssoName);
+ assertEquals("admin", result.userRole);
+ }
+
+ @Test
+ public void map_NoMatchingAffiliation_ListsAllowedGroups() {
+ SsoUserMapper.Result result =
+ SsoUserMapper.map(attributes(Collections.singletonList("alumni")), ssoProperties());
+
+ assertFalse(result.isValid());
+ assertEquals(
+ "Authorization failed. Please ensure that you are a member of one of the following"
+ + " groups: staff, teachers, students, ",
+ result.error);
+ }
+
+ @Test
+ public void map_NoAffiliationAttribute_IsRejected() {
+ Map attributes = attributes(null);
+ attributes.remove("groups");
+
+ assertFalse(SsoUserMapper.map(attributes, ssoProperties()).isValid());
+ }
+
+ @Test
+ public void map_MissingUserName_IsRejected() {
+ Map attributes = attributes(Collections.singletonList("staff"));
+ attributes.remove("displayName");
+
+ SsoUserMapper.Result result = SsoUserMapper.map(attributes, ssoProperties());
+
+ assertFalse(result.isValid());
+ assertEquals("Required SSO attributes are missing", result.error);
+ }
+
+ @Test
+ public void map_BlankSsoName_IsRejected() {
+ for (String blank : new String[] {"", " "}) {
+ Map attributes = attributes(Collections.singletonList("staff"));
+ attributes.put("uid", Collections.singletonList(blank));
+
+ SsoUserMapper.Result result = SsoUserMapper.map(attributes, ssoProperties());
+
+ assertFalse(result.isValid());
+ assertEquals("Required SSO attributes are missing", result.error);
+ }
+ }
+
+ @Test
+ public void map_BlankUserName_IsRejected() {
+ Map attributes = attributes(Collections.singletonList("staff"));
+ attributes.put("displayName", " ");
+
+ assertFalse(SsoUserMapper.map(attributes, ssoProperties()).isValid());
+ }
+
+ @Test
+ public void map_NonBlankValue_IsKeptAsSent() {
+ Map attributes = attributes(Collections.singletonList("staff"));
+ attributes.put("uid", Collections.singletonList(" jdoe "));
+
+ assertEquals(" jdoe ", SsoUserMapper.map(attributes, ssoProperties()).ssoName);
+ }
+
+ @Test
+ public void map_NoAttributes_IsRejected() {
+ SsoUserMapper.Result result = SsoUserMapper.map(Collections.emptyMap(), ssoProperties());
+
+ assertFalse(result.isValid());
+ assertEquals("You don't have any attributes", result.error);
+ }
+}
diff --git a/src/test/scripts/pgp-verification-test.sh b/src/test/scripts/pgp-verification-test.sh
new file mode 100755
index 000000000..2fe6b9fc7
--- /dev/null
+++ b/src/test/scripts/pgp-verification-test.sh
@@ -0,0 +1,64 @@
+#!/usr/bin/env bash
+# Checks that the Shibboleth repository filter (.mvn/maven.config) is enforced, and that the
+# build's PGP verification (pgpverify-maven-plugin, .mvn/pgp-keys.map) rejects a tampered
+# OpenSAML artifact and an OpenSAML artifact signed by a key other than the pinned Shibboleth
+# release key. Uses a private local repository, so it never touches
+# ~/.m2. Run from the project root: bash src/test/scripts/pgp-verification-test.sh
+set -euo pipefail
+
+work=$(mktemp -d)
+trap 'rm -rf "$work"' EXIT
+repo="$work/repository"
+mvn_validate() {
+ mvn -B validate -Dmaven.repo.local="$repo" "$@" >"$work/build.log" 2>&1
+}
+fail() {
+ echo "FAIL: $1"
+ tail -40 "$work/build.log"
+ exit 1
+}
+
+echo "0. The Shibboleth repository may serve only allow-listed groupIds"
+# With org.opensaml dropped from the allow-list, OpenSAML must not be fetched from Shibboleth's
+# repository (and is not on Central), so resolution has to fail. If it succeeds, the repository
+# filter in .mvn/maven.config is not being applied by this Maven.
+allowlist=.mvn/rrf/groupId-shibboleth-releases.txt
+cp "$allowlist" "$work/allowlist.orig"
+grep -v '^org\.opensaml$' "$work/allowlist.orig" >"$allowlist"
+restore_allowlist() { cp "$work/allowlist.orig" "$allowlist"; }
+trap 'restore_allowlist; rm -rf "$work"' EXIT
+if mvn_validate -Dpgpverify.skip -Dmaven.repo.local="$work/filter-repo" dependency:resolve; then
+ fail "OpenSAML was fetched although org.opensaml is not allow-listed: repository filter not applied"
+fi
+if grep -q "Downloaded from shibboleth-releases.*org/opensaml" "$work/build.log"; then
+ fail "OpenSAML was downloaded from Shibboleth although not allow-listed"
+fi
+grep -E "org\.opensaml:opensaml-core-api:jar:[^ ]* (is missing|was not found)|Could not find artifact org\.opensaml" \
+ "$work/build.log" | head -1 || fail "resolution failed for an unrelated reason"
+restore_allowlist
+
+echo "1. Untampered dependencies verify"
+mvn_validate || fail "verification failed on genuine artifacts"
+
+jar=$(find "$repo/org/opensaml/opensaml-core-api" -name 'opensaml-core-api-*.jar' | head -1)
+[ -n "$jar" ] || fail "opensaml-core-api jar not found in $repo"
+
+echo "2. OpenSAML pinned to a different key is rejected"
+sed 's/^org\.opensaml = .*/org.opensaml = 0x0000000000000000000000000000000000000001/' \
+ .mvn/pgp-keys.map >"$work/wrong-key.map"
+if mvn_validate -o -Dpgp.keysMap="$work/wrong-key.map"; then
+ fail "build passed although OpenSAML is not signed by the pinned key"
+fi
+grep -E "^\[ERROR\] Not allowed artifact org\.opensaml" "$work/build.log" | head -1 ||
+ fail "failure was not the OpenSAML key check"
+
+echo "3. A tampered OpenSAML jar is rejected"
+cp "$jar" "$work/original.jar"
+printf 'tampered' >>"$jar"
+if mvn_validate -o; then
+ fail "build passed with a tampered $(basename "$jar")"
+fi
+grep -E "^\[ERROR\].*opensaml-core-api" "$work/build.log" | head -1 ||
+ fail "failure was not the opensaml-core-api signature check"
+
+echo "PASS: PGP verification rejects tampered and wrongly-signed OpenSAML artifacts"
diff --git a/src/test/scripts/tomcat-config-test.sh b/src/test/scripts/tomcat-config-test.sh
new file mode 100755
index 000000000..980251f60
--- /dev/null
+++ b/src/test/scripts/tomcat-config-test.sh
@@ -0,0 +1,34 @@
+#!/usr/bin/env bash
+# Checks the Tomcat image built by `docker compose build`: the Dockerfile must have substituted
+# every placeholder in docker/tomcat/serverxml.patch, so conf/server.xml carries the configured
+# HTTPS port and keystore settings rather than literal HTTPS_PORT / TLS_KEYSTORE_* / ALIAS.
+# Run from the project root after `docker compose build`. Reads IMAGE_TOMCAT and HTTPS_PORT from
+# the environment, falling back to .env.
+set -euo pipefail
+
+env_value() {
+ local name=$1
+ if [ -n "${!name:-}" ]; then
+ echo "${!name}"
+ else
+ sed -n "s/^${name}=//p" .env | tail -1
+ fi
+}
+image=$(env_value IMAGE_TOMCAT)
+https_port=$(env_value HTTPS_PORT)
+server_xml=$(docker run --rm --entrypoint cat "$image" /usr/local/tomcat/conf/server.xml)
+
+fail() {
+ echo "FAIL: $1"
+ exit 1
+}
+
+if grep -nE 'HTTPS_PORT|TLS_KEYSTORE_FILE|TLS_KEYSTORE_PASS|"ALIAS"' <<<"$server_xml"; then
+ fail "unsubstituted placeholder(s) in $image conf/server.xml"
+fi
+grep -q "redirectPort=\"${https_port}\"" <<<"$server_xml" ||
+ fail "HTTP connector does not redirect to HTTPS_PORT=${https_port}"
+grep -q 'certificateKeystoreFile="conf/' <<<"$server_xml" ||
+ fail "HTTPS connector has no certificateKeystoreFile"
+
+echo "PASS: $image conf/server.xml redirects to port ${https_port} and has no placeholders left"