diff --git a/.env b/.env index 9c189ff3b..842dd115a 100644 --- a/.env +++ b/.env @@ -1,6 +1,6 @@ VERSION=3.1 -TOMCAT_DOCKER_VERSION=9.0-jre17-temurin +TOMCAT_DOCKER_VERSION=11.0-jre17-temurin DB_VERSION=10.6.11 MONGODB_VERSION=4.1.13 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5fbcc85cd..d28b78d44 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -35,6 +35,7 @@ jobs: src: - 'src/**' - 'pom.xml' + - '.mvn/**' - 'docker-compose.yml' - 'docker/**' - '.env' @@ -75,8 +76,12 @@ jobs: cache: maven - name: Build Maven with Docker Profile run: mvn clean install -Pdocker -DskipTests -B + - name: Check PGP verification rejects tampered and wrongly-signed OpenSAML + run: bash src/test/scripts/pgp-verification-test.sh - name: Docker Compose Build run: docker compose build + - name: Check Tomcat config placeholders were substituted + run: bash src/test/scripts/tomcat-config-test.sh unit-tests: needs: detect-changes diff --git a/.mvn/maven.config b/.mvn/maven.config new file mode 100644 index 000000000..eb0f85399 --- /dev/null +++ b/.mvn/maven.config @@ -0,0 +1,2 @@ +-Daether.remoteRepositoryFilter.groupId=true +-Daether.remoteRepositoryFilter.groupId.basedir=${maven.multiModuleProjectDirectory}/.mvn/rrf diff --git a/.mvn/pgp-keys.map b/.mvn/pgp-keys.map new file mode 100644 index 000000000..ef7086dec --- /dev/null +++ b/.mvn/pgp-keys.map @@ -0,0 +1,74 @@ +# PGP keys map for pgpverify-maven-plugin (see pom.xml). Every dependency must match a rule here. +# +# OpenSAML / Shibboleth come from Shibboleth's own repository (not Maven Central), so they are +# pinned to the Shibboleth release key: Philip David Smart (Jisc), fingerprint +# B5B5 DD33 2142 AD65 7E8D 87AC 7D27 E610 B8A3 DC52, as published at +# https://shibboleth.net/downloads/PGP_KEYS. Never add a wildcard that could also match these +# groups: any matching rule is enough for an artifact to pass, which would undo the pin. +org.opensaml = 0xB5B5DD332142AD657E8D87AC7D27E610B8A3DC52 +net.shibboleth = 0xB5B5DD332142AD657E8D87AC7D27E610B8A3DC52 + +# Everything else comes from Maven Central only (.mvn/rrf), and must carry a valid signature by +# any key. A new dependency from an unlisted groupId fails the build ("Not allowed artifact"): +# review it, then add its groupId here. +com.fasterxml.jackson.core = any +com.fasterxml.jackson.datatype = any +com.github.ben-manes.caffeine = any +com.github.waffle = any +com.google.code.findbugs = any +com.google.errorprone = any +com.google.guava = any +com.google.j2objc = any +com.google.protobuf = any +com.googlecode.json-simple = any +com.mysql = any +com.zaxxer = any +commons-codec = any +commons-io = any +commons-logging = any +de.mkammerer = any +io.dropwizard.metrics = any +io.github.cdimascio = any +io.micrometer = any +jakarta.activation = any +jakarta.el = any +jakarta.mail = any +jakarta.servlet.jsp.jstl = any +junit = any +net.bytebuddy = any +net.java.dev.jna = any +net.sf.jtidy = any +org.apache.commons = any +org.apache.httpcomponents.client5 = any +org.apache.httpcomponents.core5 = any +org.apache.logging.log4j = any +org.apache.santuario = any +org.apache.velocity = any +org.apiguardian = any +org.bouncycastle = any +org.checkerframework = any +org.cryptacular = any +org.eclipse.angus = any +org.glassfish = any +org.glassfish.web = any +org.hamcrest = any +org.jetbrains = any +org.jetbrains.kotlin = any +org.json = any +org.jsoup = any +org.jspecify = any +org.junit.jupiter = any +org.junit.platform = any +org.mariadb.jdbc = any +org.mockito = any +org.mongodb = any +org.objenesis = any +org.opentest4j = any +org.owasp.encoder = any +org.pac4j = any +org.slf4j = any +org.springframework = any + +# Unsigned legacy artifact pulled in via json-simple -> junit 4.10 (to be removed, see #944). +# Exact version only. +org.hamcrest:hamcrest-core:1.1 = noSig diff --git a/.mvn/rrf/groupId-shibboleth-releases.txt b/.mvn/rrf/groupId-shibboleth-releases.txt new file mode 100644 index 000000000..5f1727b3a --- /dev/null +++ b/.mvn/rrf/groupId-shibboleth-releases.txt @@ -0,0 +1,2 @@ +net.shibboleth +org.opensaml diff --git a/AGENTS.md b/AGENTS.md index 7b98c5666..8fab41324 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -26,9 +26,18 @@ Before committing Java changes, run: mvn spotless:apply ``` -## Java version +## Java version and platform -The project targets **Java 8**. All code must compile against Java 8, even though CI uses Java 17 for linting and building. Do not use Java 9+ APIs or language features. +The project targets **Java 17** (`17` in `pom.xml`). Java 17 APIs and language features are fine; do not use anything newer. + +The web app runs on **Jakarta EE 11**: Servlet 6.1 on **Tomcat 11** (`TOMCAT_DOCKER_VERSION` in `.env`). + +- Use `jakarta.servlet.*`, `jakarta.mail.*`, etc. Never add `javax.servlet` or other Java EE `javax.*` dependencies; they will not run on Tomcat 11. JDK packages that keep the `javax` name (`javax.crypto`, `javax.xml.parsers`, `javax.sql`) are fine. +- JSTL taglibs use the `jakarta.tags.*` URIs (declared once in `src/main/webapp/translation.jsp`). +- Spring 7 is used in two places: + - **Runtime:** pac4j-saml pulls in Spring core, beans, JDBC, TX and ORM at compile scope, and `utils/SamlSso.java` uses spring-core's `ByteArrayResource`. Don't remove or test-scope these, or SSO breaks in the WAR. + - **Tests:** `spring-test`'s `MockHttpServletRequest` / `MockHttpServletResponse` / `MockServletConfig`. `spring-web` and `spring-context` are test-scoped and only support those mocks. Never declare `spring-core` test-scoped: a test-scoped direct declaration overrides pac4j's compile-scope copy and drops it from the WAR. + - The version is set once by the `spring.version` property, which also drives the `spring-framework-bom` import that pins every Spring module, direct or transitive. ## Build and test @@ -57,6 +66,41 @@ docker run -d --name secshep_test_db \ The docker-compose `db` service is **not suitable for quick test runs** — it requires `mvn -Pdocker validate` first to generate SQL init scripts, and those scripts run on first container startup to create all challenge schemas. +### Running the full CI pipeline locally + +Before pushing, run every step of `.github/workflows/test.yml` in the same order, not just the unit tests. Running only `mvn test` misses compile failures on other branches, Docker image problems and integration-test failures, and those then show up in CI instead. In a stack of PRs, run them on each branch whose content changes (at least the bottom branch you changed and the top of the stack). + +Use JDK 17 (`export JAVA_HOME=$(/usr/libexec/java_home -v 17)` on macOS). Spotless fails on newer JDKs. + +Run the pipeline in a throwaway worktree of the exact commit you will push, for example `git worktree add --detach ../shepherd-ci HEAD`, then `cd` into it and remove it afterwards with `git worktree remove --force ../shepherd-ci`. Running Maven in your main checkout while the pipeline runs rebuilds `target/` under it and breaks the run. The worktree also keeps the `moduleSchemas.sql` edit below out of your checkout. + +```bash +mvn spotless:check -B # lint-java +mvn clean install -Pdocker -DskipTests -B # build +bash src/test/scripts/pgp-verification-test.sh # build +docker compose build # build +bash src/test/scripts/tomcat-config-test.sh # build +TEST_MYSQL_PASSWORD=CowSaysMoo mvn test -B -e # unit-tests +``` + +For the integration tests, start the `secshep_test_db` container above, then repeat what CI does before running them: + +```bash +# CI rewrites the challenge DB users to accept connections from any host. The test DB runs in Docker, +# so without this the challenge tests fail with "Access denied for user 'csrfChalFour'@'192.168.65.1'". +# Never commit this change. +sed -i.bak "s/'@'localhost'/'@'%'/g" src/main/resources/database/moduleSchemas.sql +docker ps -aq --filter 'name=^mongo-[0-9]+$' | xargs -r docker rm -f # leftovers hold port 27017 +mvn -Pdocker validate -B +TEST_MYSQL_PASSWORD=CowSaysMoo mvn verify -DskipUTs=true -DmongoDocker -B +``` + +Each integration-test run overwrites the local `mongo:4.1.13` tag (see "Integration tests fail building the Mongo image" below). Restore the official image before `docker compose build` and before every integration-test run, not only after a failure. + +When a step fails, look at the full Maven output for the root cause. `-q` and filtered output can hide it. + +Only infrastructure outages (Docker Hub errors, `build.shibboleth.net` timeouts) are acceptable reasons to learn about a failure from CI rather than locally. + ### Docker DB init fails with SQL syntax error If the MariaDB container exits immediately with `ERROR 1064 (42000) at line 181` referencing a `CREATE PROCEDURE` statement, this is a **stale Docker image cache** issue, not a bug in the SQL. @@ -84,6 +128,16 @@ TEST_MYSQL_PASSWORD=CowSaysMoo mvn test -B Do not commit `.env` changes that break CI. +### Upgrading a local Docker environment to Tomcat 11 + +The Tomcat container mounts a named `conf` volume over `/usr/local/tomcat/conf`. A volume created by an older Tomcat 9/10.1 image keeps its old `server.xml` and `web.xml`, and Tomcat 11 will not see the new image's config, so the volume has to be recreated. Data volumes are unaffected. + +The volume also holds the TLS keystore and `database.properties` / `mongo.properties`. Back it up before removing it. The archive contains the TLS private key and the database password, so create it mode 600 in a mode-700 directory **outside the repository**. The repository root is the Tomcat image's Docker build context, and a root-owned archive there breaks the next build. Run `mkdir -p ~/shepherd-conf-backup && chmod 700 ~/shepherd-conf-backup`, then `docker run --rm -v securityshepherd_conf:/conf -v ~/shepherd-conf-backup:/backup alpine sh -c 'umask 077 && touch /backup/shepherd-conf-backup.tgz && chmod 600 /backup/shepherd-conf-backup.tgz && tar czf /backup/shepherd-conf-backup.tgz -C /conf .'`, and delete the directory after the upgrade. Then run `docker compose down && docker volume rm securityshepherd_conf` and `docker compose up -d --build` (detached, so the databases stay up while files are restored and `web` is restarted). Restore only customised keystore or `.properties` files from the backup. Never restore the old `server.xml` / `web.xml`: re-apply any hand edits to the new Tomcat 11 files instead. The full steps are in the README. + +### Integration tests fail building the Mongo image (`chown ... Operation not permitted`) + +The `it-test` profile (`-DmongoDocker`) tags the image it builds as `mongo:${MONGODB_VERSION}`. That overwrites the official image's local tag, so the next build uses the previous custom image (which runs as `mongodb`) as its base. Restore the official image with `docker rmi mongo:4.1.13 && docker pull mongo:4.1.13`. CI starts from a clean Docker and is unaffected. + ### First-time app setup After `docker compose up`, the app redirects to `https://localhost/setup.jsp` for initial database configuration. **Ask the user before performing setup** — they may prefer to configure it themselves via the browser. If they ask you to do it: @@ -133,7 +187,7 @@ The hostname must be the **Docker container name** (e.g. `secshep_mariadb`), not - `servlets/` — platform servlets (Login, Register, Setup, etc.) - `servlets/admin/` — admin servlets (`config/`, `moduleManagement/`, `userManagement/`) - `servlets/module/` — module framework (`SolutionSubmit.java`, `GetModule.java`, etc.) plus `challenge/` and `lesson/` (intentionally vulnerable — do not fix) - - `utils/` — shared utilities (validation, hashing, scoring, XSS/SQL filters) + - `utils/` — shared utilities (validation, hashing, scoring, XSS/SQL filters, SAML SSO via `SamlSso`) - `src/main/resources/` — config and challenge properties files - `src/test/java/` — unit tests - `src/it/java/` — integration tests (require running DB containers) @@ -147,6 +201,33 @@ The hostname must be the **Docker container name** (e.g. `secshep_mariadb`), not - Encoding must be `utf8mb4` (not `utf8`) for full Unicode support - Password hashing uses Argon2 (requires `libargon2` native library) +### SAML single sign-on + +SSO is optional, and is turned on by `sso.enabled=true` in `sso.properties`. It is implemented in `utils/SamlSso.java` on **pac4j-saml 6.x** (OpenSAML 5). The five servlets `SSOLogin`, `ACS`, `SLS`, `SSOMetadata` and `Logout` delegate to it. + +- Configuration lives in `onelogin.saml.properties`, which keeps the key names of the java-saml toolkit used previously; see `src/main/resources/onelogin.saml.properties.sample`. The SP's PEM certificate and key are loaded into an in-memory keystore. The IdP settings are turned into an IdP metadata document. Nothing is written to disk. +- `sso.properties` maps IdP attributes to users and roles (`utils/SsoUserMapper.java`). Blank identifiers are rejected. +- The SSO endpoints answer 404 unless `sso.enabled=true`, even when a SAML configuration is present. +- **Only SP-initiated login is supported.** `utils/SamlRequestStore.java` binds each AuthnRequest to the browser through a cookie of its own, `shepherd_saml_request_` (HttpOnly, Secure, SameSite=None, scoped to the ACS URL's directory such as `/saml/`, deleted once the login completes), so logins started in several tabs at once all work. Responses without a matching `InResponseTo` from that browser are refused, which blocks login CSRF, and so are unsolicited IdP-initiated responses. The bearer `SubjectConfirmationData` inside the signed assertion must name the same request, because pac4j checks only the Response's `InResponseTo`, which is unsigned unless the whole message is signed (`SamlSso.AlternativeAuthnContextClient`). + +### Dependency trust: OpenSAML and PGP verification + +pac4j-saml depends on **OpenSAML 5, which is not on Maven Central**. Shibboleth, its maintainer, publishes it only to its own repository (`build.shibboleth.net`, declared in `pom.xml` with checksum failures enforced). Three controls apply: + +- **Repository filtering** (`.mvn/maven.config`, `.mvn/rrf/groupId-shibboleth-releases.txt`): the Shibboleth repository may serve only the `org.opensaml` and `net.shibboleth` groupIds. Everything else comes from Central only. + - This needs **Maven 3.9.2 or newer**: older versions don't expand the path in `.mvn/maven.config` and silently skip the filter. `maven-enforcer-plugin` refuses to build on older Maven. + - The enforcer itself resolves dependencies before it fails, so if an older Maven was ever used on a machine, delete its local repository (`~/.m2/repository`) before building again. +- **PGP signature verification** (`pgpverify-maven-plugin` in `pom.xml`, rules in `.mvn/pgp-keys.map`): every dependency's signature is checked on every build (with the one unsigned exception below). + - `org.opensaml` / `net.shibboleth` are pinned to the Shibboleth release key `B5B5DD332142AD657E8D87AC7D27E610B8A3DC52`, from https://shibboleth.net/downloads/PGP_KEYS. Verify any new key there before changing the pin. + - Every other groupId is listed by name and needs a valid signature. A new dependency from an unlisted groupId fails with `Not allowed artifact`: review it, then add its groupId. + - **One exception:** `org.hamcrest:hamcrest-core:1.1` is unsigned and allowed with `noSig` for that exact version only. It arrives through `json-simple` → JUnit 4.10, and is to be removed with #944. Don't add `noSig` rules for anything else without the same scrutiny. + - **Never add a wildcard rule** (`*`, `org.*`, …): any matching rule lets an artifact pass, so a broad rule would silently undo the Shibboleth pin. + - Keys are fetched from `keys.openpgp.org` / `keyserver.ubuntu.com` and cached in the local repository. +- **Test:** `bash src/test/scripts/pgp-verification-test.sh` (run in CI's build job) proves three things on the Maven that runs it: the repository filter is enforced, a tampered OpenSAML jar fails the build, and an OpenSAML artifact signed by another key fails the build. + +A build failing with `Could not find artifact org.opensaml:...` / `build.shibboleth.net failed to respond` means that repository was unreachable. Re-run the build. +- Tests generate their own key pairs and signed SAML responses (`src/test/java/utils/SamlTestSupport.java`). Do not commit private keys. + ### SQL schema files - `src/main/resources/database/coreSchema.sql` — **source of truth** for the core database: users table, settings table, stored procedures (user management, authentication, class management, scoring), and seed data (default admin user, default settings) @@ -161,7 +242,17 @@ Stored procedures use `DELIMITER` statements that are commented out in the sourc ### Always run tests before pushing -Run `mvn test -B` locally before pushing any code changes. If integration tests are relevant to your changes, run those too. Do not rely solely on CI to catch failures. +Before pushing any code change, run the whole CI pipeline locally, as described in "Running the full CI pipeline locally", and push only once every step passes. Do not rely on CI to catch failures. + +Also review your own diff for edge cases before pushing, especially in authentication and SAML code: expiry, overlapping or concurrent requests, cookie scope, and which fields are signed. Automated reviewers should not be the first to find them. + +### Fix only what your change introduces + +For every problem you find while working on a change, including comments from automated reviewers, first check whether the base branch already has it (`git show origin/dev:`): +- **Introduced by your change:** fix it in the same PR, with a test. +- **Already on the base branch:** open a GitHub issue ("Discovered during #NNN", with the repo's labels) and leave it out of the PR, even when the fix is one line in a file the PR touches. + +This keeps large changes, such as the Jakarta migration (#861), focused and reviewable. ### Bug fixes must include tests diff --git a/Dockerfile b/Dockerfile index 530313666..b85abec77 100644 --- a/Dockerfile +++ b/Dockerfile @@ -27,8 +27,12 @@ COPY docker/tomcat/webxml.patch webxml.patch RUN printf "databaseConnectionURL=$MARIADB_URI/\nDriverType=$DB_DRIVER\ndatabaseSchema=$DB_SCHEMA\ndatabaseUsername=$DB_USER\ndatabasePassword=$DB_PASS\ndatabaseOptions=useUnicode=true&character_set_server=utf8mb4\n" >> database.properties RUN printf "connectionHost=$MONGO_HOST\nconnectionPort=$MONGO_PORT\ndatabaseName=shepherdGames\nconnectTimeout=$MONGO_CONN_TIMEOUT\nsocketTimeout=$MONGO_SOCK_TIMEOUT\nserverSelectionTimeout=$MONGO_SVR_TIMEOUT" >> mongo.properties -RUN sed -i 's/keystoreFile="conf\/TLS_KEYSTORE_FILE" keystorePass="TLS_KEYSTORE_PASS" keyAlias="ALIAS">/keystoreFile="conf\/'"$TLS_KEYSTORE_FILE"'" keystorePass="'"$TLS_KEYSTORE_PASS"'" keyAlias="'"$ALIAS"'">/g' serverxml.patch &&\ - sed -i 's/redirectPort="HTTPS_PORT" \/>/redirectPort="'"$HTTPS_PORT"'" \/>/g' serverxml.patch +RUN sed -i \ + -e 's/certificateKeystoreFile="conf\/TLS_KEYSTORE_FILE"/certificateKeystoreFile="conf\/'"$TLS_KEYSTORE_FILE"'"/' \ + -e 's/certificateKeystorePassword="TLS_KEYSTORE_PASS"/certificateKeystorePassword="'"$TLS_KEYSTORE_PASS"'"/' \ + -e 's/certificateKeyAlias="ALIAS"/certificateKeyAlias="'"$ALIAS"'"/' \ + -e 's/redirectPort="HTTPS_PORT"/redirectPort="'"$HTTPS_PORT"'"/' \ + serverxml.patch FROM tomcat:${TOMCAT_DOCKER_VERSION} COPY --from=builder /workdir/ROOT.war /usr/local/tomcat/webapps/ diff --git a/README.md b/README.md index 9e04ed0fb..797586259 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,45 @@ To login use the following credentials (you will be asked to update after login) Note: Environment variables can be configured in dotenv ```.env``` file in the root dir. +#### Upgrading an existing Docker environment +Security Shepherd now runs on Tomcat 11. Tomcat's `conf` directory lives in the `conf` Docker volume, and Docker only copies the image's files into a volume when the volume is first created. An existing environment would therefore start Tomcat 11 with its old Tomcat 9/10.1 `server.xml` and `web.xml`. That volume has to be recreated. Database data is in the separate `data` and `mongodata` volumes, so it is kept. + +The `conf` volume also holds the TLS keystore and the `database.properties` / `mongo.properties` connection settings, so **back it up first**: + +```console +docker-compose down +# The volume is named _conf; check with: docker volume ls +# The archive holds the TLS private key and database password. Keep it outside the repository +# (the repository root is the Docker build context) and readable by you only +BACKUP_DIR="$HOME/shepherd-conf-backup" +mkdir -p "$BACKUP_DIR" && chmod 700 "$BACKUP_DIR" +docker run --rm -v securityshepherd_conf:/conf -v "$BACKUP_DIR":/backup alpine sh -c \ + 'umask 077 && touch /backup/shepherd-conf-backup.tgz && chmod 600 /backup/shepherd-conf-backup.tgz \ + && tar czf /backup/shepherd-conf-backup.tgz -C /conf .' +docker volume rm securityshepherd_conf +mvn -Pdocker clean install -DskipTests +# Detached, so the databases keep running while you restore files below +docker-compose up -d --build +``` + +The new volume starts from the image: a freshly built keystore, and connection settings taken from `.env`. If you had customised either, copy just those files back from the backup. Leave `server.xml` and `web.xml` alone, because they are the Tomcat 11 versions. + +```console +BACKUP_DIR="$HOME/shepherd-conf-backup" +# Connection settings changed through the setup page: +docker run --rm -v securityshepherd_conf:/conf -v "$BACKUP_DIR":/backup alpine \ + tar xzf /backup/shepherd-conf-backup.tgz -C /conf ./database.properties ./mongo.properties +# Your own TLS certificate: restore the keystore file named by TLS_KEYSTORE_FILE, and make sure +# TLS_KEYSTORE_PASS and ALIAS in .env match it before building +docker run --rm -v securityshepherd_conf:/conf -v "$BACKUP_DIR":/backup alpine \ + tar xzf /backup/shepherd-conf-backup.tgz -C /conf ./ +docker-compose restart web +``` + +Delete `$BACKUP_DIR` (or move it somewhere safe) once the upgrade is working. On Linux hosts the archive is owned by root, but you can still delete it because you own the directory. + +If you had hand-edited the old `server.xml` or `web.xml` (custom connectors, ports, ciphers and so on), re-apply those edits to the new Tomcat 11 files, using the backup as a reference. Don't copy the old files back. + #### Full Guide [Docker-Environment-Setup](https://github.com/OWASP/SecurityShepherd/wiki/Docker-Environment-Setup) diff --git a/docker/tomcat/serverxml.patch b/docker/tomcat/serverxml.patch index c573c2138..fac343a67 100644 --- a/docker/tomcat/serverxml.patch +++ b/docker/tomcat/serverxml.patch @@ -1,38 +1,27 @@ ---- server.xml 2024-01-01 00:00:00.000000000 +0000 -+++ server.https.xml 2024-01-01 00:00:00.000000000 +0000 -@@ -67,21 +67,20 @@ - Java AJP Connector: /docs/config/ajp.html - APR (HTTP/AJP) Connector: /docs/apr.html - Define a non-SSL/TLS HTTP/1.1 Connector on port 8080 -- --> +--- server.xml ++++ server.https.xml +@@ -69,7 +69,7 @@ + --> -+ -+ --> +- redirectPort="8443" /> ++ redirectPort="HTTPS_PORT" /> -- - + + ++ maxThreads="150" scheme="https" secure="true"> ++ ++ ++ + - + + 7.0.10 + 6.6.0 + + ${project.basedir}/.mvn/pgp-keys.map + + + + shibboleth-releases + Shibboleth Releases Repository + https://build.shibboleth.net/maven/releases/ + + true + fail + + + false + + + + + + + + + org.springframework + spring-framework-bom + ${spring.version} + pom + import + + + + + + + org.pac4j + pac4j-saml + ${pac4j.version} + + + org.pac4j + pac4j-jakartaee + ${pac4j.version} + + - com.onelogin - java-saml - 2.9.0 + org.apache.logging.log4j + log4j-slf4j2-impl + 2.25.4 de.mkammerer @@ -47,7 +99,7 @@ commons-io commons-io - 2.18.0 + 2.22.0 @@ -68,11 +120,11 @@ jtidy r938 - + - com.sun.mail - javax.mail - 1.6.2 + org.eclipse.angus + angus-mail + 2.0.5 @@ -81,23 +133,26 @@ mongo-java-driver 3.12.14 + - javax - javaee-api - 8.0.1 + jakarta.servlet + jakarta.servlet-api + 6.1.0 provided + + - javax.servlet - javax.servlet-api - 4.0.1 - provided + jakarta.servlet.jsp.jstl + jakarta.servlet.jsp.jstl-api + 3.0.2 - - jstl - jstl - 1.2 + org.glassfish.web + jakarta.servlet.jsp.jstl + 3.0.1 @@ -139,7 +194,7 @@ org.springframework spring-web - 6.1.21 + ${spring.version} test @@ -147,38 +202,23 @@ org.springframework spring-test - 5.3.31 + ${spring.version} test - - - org.springframework - spring-core - 6.0.2 - test - - - - org.springframework - spring-mock - 2.0.8 - test - - - - com.github.fakemongo - fongo - 2.1.1 - test - + org.springframework spring-context - 5.3.31 + ${spring.version} + test @@ -405,6 +445,51 @@ + + + org.apache.maven.plugins + maven-enforcer-plugin + 3.6.3 + + + require-maven-for-repository-filter + + enforce + + + + + + [3.9.2,) + Maven 3.9.2 or newer is required: older versions ignore the Shibboleth repository restriction in .mvn/maven.config. + + + + + + + + org.simplify4u.plugins + pgpverify-maven-plugin + 1.19.1 + + + + check + + + + + ${pgp.keysMap} + true + + true + hkps://keys.openpgp.org,hkps://keyserver.ubuntu.com + + org.codehaus.mojo properties-maven-plugin diff --git a/src/it/java/servlets/LoginIT.java b/src/it/java/servlets/LoginIT.java index 3198098ae..18e460316 100644 --- a/src/it/java/servlets/LoginIT.java +++ b/src/it/java/servlets/LoginIT.java @@ -5,9 +5,9 @@ import dbProcs.Getter; import dbProcs.GetterIT; import dbProcs.Setter; +import jakarta.servlet.http.HttpSession; import java.io.IOException; import java.sql.SQLException; -import javax.servlet.http.HttpSession; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.junit.jupiter.api.BeforeAll; diff --git a/src/it/java/servlets/LogoutIT.java b/src/it/java/servlets/LogoutIT.java index a8ccd57c5..559e0c061 100644 --- a/src/it/java/servlets/LogoutIT.java +++ b/src/it/java/servlets/LogoutIT.java @@ -3,9 +3,9 @@ import static org.junit.jupiter.api.Assertions.fail; import dbProcs.GetterIT; +import jakarta.servlet.http.HttpSession; import java.io.IOException; import java.sql.SQLException; -import javax.servlet.http.HttpSession; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.junit.jupiter.api.BeforeAll; diff --git a/src/it/java/servlets/SetupIT.java b/src/it/java/servlets/SetupIT.java index 0b98dd32d..5f304ff4f 100644 --- a/src/it/java/servlets/SetupIT.java +++ b/src/it/java/servlets/SetupIT.java @@ -8,11 +8,11 @@ import dbProcs.ConnectionPool; import dbProcs.Constants; +import jakarta.servlet.ServletException; import java.io.File; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.sql.SQLException; -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/it/java/servlets/admin/config/DisableCheatsIT.java b/src/it/java/servlets/admin/config/DisableCheatsIT.java index 951f6ba45..8a321ee0e 100644 --- a/src/it/java/servlets/admin/config/DisableCheatsIT.java +++ b/src/it/java/servlets/admin/config/DisableCheatsIT.java @@ -3,10 +3,10 @@ import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.fail; +import jakarta.servlet.ServletException; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.sql.SQLException; -import javax.servlet.ServletException; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.junit.jupiter.api.BeforeAll; diff --git a/src/it/java/servlets/admin/config/DisableFeedbackIT.java b/src/it/java/servlets/admin/config/DisableFeedbackIT.java index 5015f47cd..22dae5a69 100644 --- a/src/it/java/servlets/admin/config/DisableFeedbackIT.java +++ b/src/it/java/servlets/admin/config/DisableFeedbackIT.java @@ -2,9 +2,9 @@ import static org.junit.jupiter.api.Assertions.fail; +import jakarta.servlet.ServletException; import java.io.IOException; import java.sql.SQLException; -import javax.servlet.ServletException; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.junit.jupiter.api.BeforeAll; diff --git a/src/it/java/servlets/admin/config/DisableScoreboardIT.java b/src/it/java/servlets/admin/config/DisableScoreboardIT.java index e9ecdbed7..2c4b07276 100644 --- a/src/it/java/servlets/admin/config/DisableScoreboardIT.java +++ b/src/it/java/servlets/admin/config/DisableScoreboardIT.java @@ -2,9 +2,9 @@ import static org.junit.jupiter.api.Assertions.fail; +import jakarta.servlet.ServletException; import java.io.IOException; import java.sql.SQLException; -import javax.servlet.ServletException; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.junit.jupiter.api.BeforeAll; diff --git a/src/it/java/servlets/admin/config/EnableCheatsIT.java b/src/it/java/servlets/admin/config/EnableCheatsIT.java index 0b74ec4a7..6cbd83dfa 100644 --- a/src/it/java/servlets/admin/config/EnableCheatsIT.java +++ b/src/it/java/servlets/admin/config/EnableCheatsIT.java @@ -2,9 +2,9 @@ import static org.junit.jupiter.api.Assertions.fail; +import jakarta.servlet.ServletException; import java.io.IOException; import java.sql.SQLException; -import javax.servlet.ServletException; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.junit.jupiter.api.BeforeAll; diff --git a/src/it/java/servlets/admin/config/EnableFeedbackIT.java b/src/it/java/servlets/admin/config/EnableFeedbackIT.java index 9d3276d4c..a3f60f8e6 100644 --- a/src/it/java/servlets/admin/config/EnableFeedbackIT.java +++ b/src/it/java/servlets/admin/config/EnableFeedbackIT.java @@ -2,9 +2,9 @@ import static org.junit.jupiter.api.Assertions.fail; +import jakarta.servlet.ServletException; import java.io.IOException; import java.sql.SQLException; -import javax.servlet.ServletException; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.junit.jupiter.api.BeforeAll; diff --git a/src/it/java/servlets/admin/config/SetCtfModeIT.java b/src/it/java/servlets/admin/config/SetCtfModeIT.java index ea7ba6b83..1fe1fbf2a 100644 --- a/src/it/java/servlets/admin/config/SetCtfModeIT.java +++ b/src/it/java/servlets/admin/config/SetCtfModeIT.java @@ -2,9 +2,9 @@ import static org.junit.jupiter.api.Assertions.fail; +import jakarta.servlet.ServletException; import java.io.IOException; import java.sql.SQLException; -import javax.servlet.ServletException; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.junit.jupiter.api.BeforeAll; diff --git a/src/it/java/servlets/admin/config/SetOpenFloorModeIT.java b/src/it/java/servlets/admin/config/SetOpenFloorModeIT.java index bf943868e..97cd5b1bc 100644 --- a/src/it/java/servlets/admin/config/SetOpenFloorModeIT.java +++ b/src/it/java/servlets/admin/config/SetOpenFloorModeIT.java @@ -2,9 +2,9 @@ import static org.junit.jupiter.api.Assertions.fail; +import jakarta.servlet.ServletException; import java.io.IOException; import java.sql.SQLException; -import javax.servlet.ServletException; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.junit.jupiter.api.BeforeAll; diff --git a/src/it/java/servlets/admin/config/SetTournamentModeIT.java b/src/it/java/servlets/admin/config/SetTournamentModeIT.java index cc48a5cb6..1eab5ebaa 100644 --- a/src/it/java/servlets/admin/config/SetTournamentModeIT.java +++ b/src/it/java/servlets/admin/config/SetTournamentModeIT.java @@ -2,9 +2,9 @@ import static org.junit.jupiter.api.Assertions.fail; +import jakarta.servlet.ServletException; import java.io.IOException; import java.sql.SQLException; -import javax.servlet.ServletException; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.junit.jupiter.api.BeforeAll; diff --git a/src/it/java/servlets/admin/config/ToggleRegistrationIT.java b/src/it/java/servlets/admin/config/ToggleRegistrationIT.java index 65246e3d3..634bb3cfc 100644 --- a/src/it/java/servlets/admin/config/ToggleRegistrationIT.java +++ b/src/it/java/servlets/admin/config/ToggleRegistrationIT.java @@ -2,9 +2,9 @@ import static org.junit.jupiter.api.Assertions.fail; +import jakarta.servlet.ServletException; import java.io.IOException; import java.sql.SQLException; -import javax.servlet.ServletException; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.junit.jupiter.api.BeforeAll; diff --git a/src/it/java/servlets/admin/moduleManagement/CloseAllModulesTestIT.java b/src/it/java/servlets/admin/moduleManagement/CloseAllModulesTestIT.java index 37ff10b1c..d093bd94a 100644 --- a/src/it/java/servlets/admin/moduleManagement/CloseAllModulesTestIT.java +++ b/src/it/java/servlets/admin/moduleManagement/CloseAllModulesTestIT.java @@ -2,9 +2,9 @@ import static org.junit.jupiter.api.Assertions.fail; +import jakarta.servlet.ServletException; import java.io.IOException; import java.sql.SQLException; -import javax.servlet.ServletException; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.junit.jupiter.api.BeforeAll; diff --git a/src/it/java/servlets/admin/moduleManagement/EnableModuleBlockIT.java b/src/it/java/servlets/admin/moduleManagement/EnableModuleBlockIT.java index 6df6cf9a7..1c8ebaefd 100644 --- a/src/it/java/servlets/admin/moduleManagement/EnableModuleBlockIT.java +++ b/src/it/java/servlets/admin/moduleManagement/EnableModuleBlockIT.java @@ -2,9 +2,9 @@ import static org.junit.jupiter.api.Assertions.fail; +import jakarta.servlet.ServletException; import java.io.IOException; import java.sql.SQLException; -import javax.servlet.ServletException; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.junit.jupiter.api.BeforeAll; diff --git a/src/it/java/servlets/admin/moduleManagement/GetFeedbackIT.java b/src/it/java/servlets/admin/moduleManagement/GetFeedbackIT.java index 6f2a1c4cc..7bb92956a 100644 --- a/src/it/java/servlets/admin/moduleManagement/GetFeedbackIT.java +++ b/src/it/java/servlets/admin/moduleManagement/GetFeedbackIT.java @@ -2,10 +2,10 @@ import static org.junit.jupiter.api.Assertions.fail; +import jakarta.servlet.ServletException; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.sql.SQLException; -import javax.servlet.ServletException; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.junit.jupiter.api.BeforeAll; diff --git a/src/it/java/servlets/admin/moduleManagement/GetJsonProgressIT.java b/src/it/java/servlets/admin/moduleManagement/GetJsonProgressIT.java index c232bf8b9..e74e3108a 100644 --- a/src/it/java/servlets/admin/moduleManagement/GetJsonProgressIT.java +++ b/src/it/java/servlets/admin/moduleManagement/GetJsonProgressIT.java @@ -2,9 +2,9 @@ import static org.junit.jupiter.api.Assertions.fail; +import jakarta.servlet.ServletException; import java.io.IOException; import java.sql.SQLException; -import javax.servlet.ServletException; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.junit.jupiter.api.BeforeAll; diff --git a/src/it/java/servlets/admin/moduleManagement/OpenAllModulesIT.java b/src/it/java/servlets/admin/moduleManagement/OpenAllModulesIT.java index 6ec680cbe..f33ca778b 100644 --- a/src/it/java/servlets/admin/moduleManagement/OpenAllModulesIT.java +++ b/src/it/java/servlets/admin/moduleManagement/OpenAllModulesIT.java @@ -3,10 +3,10 @@ import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; +import jakarta.servlet.ServletException; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.sql.SQLException; -import javax.servlet.ServletException; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.junit.jupiter.api.BeforeAll; diff --git a/src/it/java/servlets/admin/moduleManagement/OpenOrCloseByCategoryIT.java b/src/it/java/servlets/admin/moduleManagement/OpenOrCloseByCategoryIT.java index 8b1d22cba..ecdb9d884 100644 --- a/src/it/java/servlets/admin/moduleManagement/OpenOrCloseByCategoryIT.java +++ b/src/it/java/servlets/admin/moduleManagement/OpenOrCloseByCategoryIT.java @@ -3,9 +3,9 @@ import static org.junit.jupiter.api.Assertions.fail; import dbProcs.Setter; +import jakarta.servlet.ServletException; import java.io.IOException; import java.sql.SQLException; -import javax.servlet.ServletException; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.junit.jupiter.api.BeforeAll; diff --git a/src/it/java/servlets/admin/userManagement/DeletePlayersIT.java b/src/it/java/servlets/admin/userManagement/DeletePlayersIT.java index 34366824d..021ccc059 100644 --- a/src/it/java/servlets/admin/userManagement/DeletePlayersIT.java +++ b/src/it/java/servlets/admin/userManagement/DeletePlayersIT.java @@ -4,9 +4,9 @@ import dbProcs.Getter; import dbProcs.Setter; +import jakarta.servlet.ServletException; import java.io.IOException; import java.sql.SQLException; -import javax.servlet.ServletException; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.junit.jupiter.api.BeforeAll; diff --git a/src/it/java/servlets/api/LevelsIT.java b/src/it/java/servlets/api/LevelsIT.java index 23bdddc37..22c7029e0 100644 --- a/src/it/java/servlets/api/LevelsIT.java +++ b/src/it/java/servlets/api/LevelsIT.java @@ -3,9 +3,9 @@ import static org.junit.jupiter.api.Assertions.fail; import dbProcs.Setter; +import jakarta.servlet.ServletException; import java.io.IOException; import java.sql.SQLException; -import javax.servlet.ServletException; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.json.JSONArray; diff --git a/src/it/java/servlets/module/GetModuleIT.java b/src/it/java/servlets/module/GetModuleIT.java index cef551958..400b788d1 100644 --- a/src/it/java/servlets/module/GetModuleIT.java +++ b/src/it/java/servlets/module/GetModuleIT.java @@ -4,9 +4,9 @@ import dbProcs.GetterIT; import dbProcs.Setter; +import jakarta.servlet.ServletException; import java.io.IOException; import java.sql.SQLException; -import javax.servlet.ServletException; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.junit.jupiter.api.BeforeAll; diff --git a/src/it/java/servlets/module/challenge/BrokenCryptoHomeMadeIT.java b/src/it/java/servlets/module/challenge/BrokenCryptoHomeMadeIT.java index 4c2c871f5..e78e6ef62 100644 --- a/src/it/java/servlets/module/challenge/BrokenCryptoHomeMadeIT.java +++ b/src/it/java/servlets/module/challenge/BrokenCryptoHomeMadeIT.java @@ -4,11 +4,11 @@ import dbProcs.GetterIT; import dbProcs.Setter; +import jakarta.servlet.ServletException; import java.io.IOException; import java.sql.SQLException; import java.util.Locale; import java.util.ResourceBundle; -import javax.servlet.ServletException; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.junit.jupiter.api.BeforeAll; diff --git a/src/it/java/servlets/module/challenge/NoSqlInjection1IT.java b/src/it/java/servlets/module/challenge/NoSqlInjection1IT.java index 1cfddd769..f395abf93 100644 --- a/src/it/java/servlets/module/challenge/NoSqlInjection1IT.java +++ b/src/it/java/servlets/module/challenge/NoSqlInjection1IT.java @@ -4,9 +4,9 @@ import dbProcs.GetterIT; import dbProcs.Setter; +import jakarta.servlet.ServletException; import java.io.IOException; import java.sql.SQLException; -import javax.servlet.ServletException; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.junit.jupiter.api.BeforeAll; diff --git a/src/it/java/servlets/module/lesson/CsrfLessonIT.java b/src/it/java/servlets/module/lesson/CsrfLessonIT.java index f56f93655..c6e549ce7 100644 --- a/src/it/java/servlets/module/lesson/CsrfLessonIT.java +++ b/src/it/java/servlets/module/lesson/CsrfLessonIT.java @@ -4,9 +4,9 @@ import dbProcs.GetterIT; import dbProcs.Setter; +import jakarta.servlet.ServletException; import java.io.IOException; import java.sql.SQLException; -import javax.servlet.ServletException; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.junit.jupiter.api.BeforeAll; diff --git a/src/it/java/servlets/module/lesson/SessionManagementLessonIT.java b/src/it/java/servlets/module/lesson/SessionManagementLessonIT.java index 577249667..97f4c39ac 100644 --- a/src/it/java/servlets/module/lesson/SessionManagementLessonIT.java +++ b/src/it/java/servlets/module/lesson/SessionManagementLessonIT.java @@ -4,9 +4,9 @@ import dbProcs.GetterIT; import dbProcs.Setter; +import jakarta.servlet.http.Cookie; import java.io.IOException; import java.sql.SQLException; -import javax.servlet.http.Cookie; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.junit.jupiter.api.BeforeAll; diff --git a/src/main/java/listeners/DatabaseLifecycleListener.java b/src/main/java/listeners/DatabaseLifecycleListener.java index 3ba64ecd4..ef94963b9 100644 --- a/src/main/java/listeners/DatabaseLifecycleListener.java +++ b/src/main/java/listeners/DatabaseLifecycleListener.java @@ -2,9 +2,9 @@ import dbProcs.ConnectionPool; import dbProcs.MongoDatabase; -import javax.servlet.ServletContextEvent; -import javax.servlet.ServletContextListener; -import javax.servlet.annotation.WebListener; +import jakarta.servlet.ServletContextEvent; +import jakarta.servlet.ServletContextListener; +import jakarta.servlet.annotation.WebListener; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import servlets.Setup; diff --git a/src/main/java/servlets/ACS.java b/src/main/java/servlets/ACS.java index 3882c478b..9a8d7237a 100644 --- a/src/main/java/servlets/ACS.java +++ b/src/main/java/servlets/ACS.java @@ -1,26 +1,24 @@ package servlets; -import com.onelogin.saml2.Auth; -import com.onelogin.saml2.exception.Error; -import com.onelogin.saml2.exception.SettingsException; 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.InputStream; -import java.util.Arrays; -import java.util.List; -import java.util.Map; +import java.util.Optional; import java.util.Properties; -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 org.pac4j.saml.profile.SAML2Profile; import utils.Hash; +import utils.LoginMethod; +import utils.SamlSso; import utils.ShepherdLogManager; +import utils.SsoUserMapper; import utils.UserKicker; /** @@ -48,6 +46,12 @@ public class ACS extends HttpServlet { public void doPost(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.ACS ***"); @@ -57,245 +61,96 @@ public void doPost(HttpServletRequest request, HttpServletResponse response) Object language = ses.getAttribute("lang"); - String errorMessage = new String(); - - request.setCharacterEncoding("UTF-8"); - response.setContentType("text/plain"); log.debug("Getting ApplicationRoot"); String ApplicationRoot = getServletContext().getRealPath(""); log.debug("Servlet root = " + ApplicationRoot); - boolean mustRedirect = false; - boolean ssoValid = false; - - // Start SAML processing - log.debug("Processing SAML data..."); - - 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.processResponse(); - } catch (Exception e) { - throw new RuntimeException("SAML error when processing response: " + e.toString()); - } - + Optional profile = SamlSso.authenticate(request, response); log.debug("SAML data processed"); - if (!auth.isAuthenticated()) { + if (!profile.isPresent()) { log.debug("User not authenticated"); - - errorMessage += "Not authenticated"; - ses.setAttribute("loginFailed", errorMessage); + ses.setAttribute("loginFailed", "Not authenticated"); response.sendRedirect("../login.jsp"); + log.debug("**** End servlets.ACS ***"); + return; + } - } else { - log.debug("User authenticated"); - List errors = auth.getErrors(); - if (!errors.isEmpty()) { - errorMessage += StringUtils.join(errors, ", "); - log.debug("SAML errors found: " + StringUtils.join(errors, ", ")); - } else { - Map> attributes = auth.getAttributes(); - String nameId = auth.getNameId(); - String nameIdFormat = auth.getNameIdFormat(); - String sessionIndex = auth.getSessionIndex(); - String nameidNameQualifier = auth.getNameIdNameQualifier(); - String nameidSPNameQualifier = auth.getNameIdSPNameQualifier(); - - ses.setAttribute("attributes", attributes); - ses.setAttribute("nameId", nameId); - ses.setAttribute("nameIdFormat", nameIdFormat); - ses.setAttribute("sessionIndex", sessionIndex); - - ses.setAttribute("nameidNameQualifier", nameidNameQualifier); - ses.setAttribute("nameidSPNameQualifier", nameidSPNameQualifier); - - if (attributes.isEmpty()) { - errorMessage += "You don't have any attributes"; - log.debug("No SAML attributes found"); - - } else { - log.debug("Unpacking SAML attributes..."); - - String ssoName = null; - String userName = null; - String userRole = null; - - ClassLoader classLoader = getClass().getClassLoader(); - - log.debug("Loading saml unpack properties file"); - - String unpackFileName = "sso.properties"; - - try (InputStream inputStream = classLoader.getResourceAsStream(unpackFileName)) { - if (inputStream != null) { - Properties prop = new Properties(); - prop.load(inputStream); - if (prop != null) { - - log.debug("Saml unpack properties file loaded, unpacking saml data"); - - // Get id and name from SAML data - - String ssoNameKey = prop.getProperty("sso.saml.ssoName"); - - ssoName = attributes.get(ssoNameKey).get(0); - - log.debug("ssoName = " + ssoName); - - String userNameKey = prop.getProperty("sso.saml.userName"); - - userName = attributes.get(userNameKey).get(0); - - log.debug("userName = " + userName); - - String affiliationKey = prop.getProperty("sso.saml.affiliation"); - - List affiliations = attributes.get(affiliationKey); - - String adminAffiliation = prop.getProperty("sso.saml.adminAffiliation"); - String playerAffiliation = prop.getProperty("sso.saml.playerAffiliation"); - - List adminAffiliations = Arrays.asList(adminAffiliation.split(",[ ]*")); - List 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"