all things saml

This commit is contained in:
Anthony Stirling 2024-10-02 23:43:30 +01:00
parent 5832147b30
commit c59d3ff3e0
28 changed files with 777 additions and 303 deletions

View File

@ -32,6 +32,12 @@ java {
repositories { repositories {
mavenCentral() mavenCentral()
maven { url "https://jitpack.io" } maven { url "https://jitpack.io" }
maven {
url "https://build.shibboleth.net/nexus/content/repositories/releases/"
}
maven {
url "https://build.shibboleth.net/maven/releases/"
}
} }
licenseReport { licenseReport {
@ -135,6 +141,8 @@ dependencies {
implementation "org.springframework.boot:spring-boot-starter-data-jpa:$springBootVersion" implementation "org.springframework.boot:spring-boot-starter-data-jpa:$springBootVersion"
implementation "org.springframework.boot:spring-boot-starter-oauth2-client:$springBootVersion" implementation "org.springframework.boot:spring-boot-starter-oauth2-client:$springBootVersion"
implementation 'org.springframework.security:spring-security-saml2-service-provider:6.3.3'
implementation 'com.unboundid.product.scim2:scim2-sdk-client:2.3.5'
//2.2.x requires rebuild of DB file.. need migration path //2.2.x requires rebuild of DB file.. need migration path
runtimeOnly "com.h2database:h2:2.1.214" runtimeOnly "com.h2database:h2:2.1.214"
// implementation "com.h2database:h2:2.2.224" // implementation "com.h2database:h2:2.2.224"

View File

@ -17,9 +17,8 @@ public class EEAppConfig {
@Autowired ApplicationProperties applicationProperties; @Autowired ApplicationProperties applicationProperties;
@Autowired @Autowired private LicenseKeyChecker licenseKeyChecker;
private LicenseKeyChecker licenseKeyChecker;
@Bean(name = "runningEE") @Bean(name = "runningEE")
public boolean runningEnterpriseEdition() { public boolean runningEnterpriseEdition() {
return licenseKeyChecker.getEnterpriseEnabledResult(); return licenseKeyChecker.getEnterpriseEnabledResult();

View File

@ -4,11 +4,6 @@ import java.net.URI;
import java.net.http.HttpClient; import java.net.http.HttpClient;
import java.net.http.HttpRequest; import java.net.http.HttpRequest;
import java.net.http.HttpResponse; import java.net.http.HttpResponse;
import java.security.KeyFactory;
import java.security.PublicKey;
import java.security.Signature;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@ -26,26 +21,40 @@ public class KeygenLicenseVerifier {
private static final String BASE_URL = "https://api.keygen.sh/v1/accounts"; private static final String BASE_URL = "https://api.keygen.sh/v1/accounts";
private static final ObjectMapper objectMapper = new ObjectMapper(); private static final ObjectMapper objectMapper = new ObjectMapper();
// 23:26:20.344 [scheduling-1] INFO s.s.SPDF.EE.KeygenLicenseVerifier -
// validateLicenseResponse body:
// {"data":{"id":"808ed3c9-584b-46dd-8a80-c9217ef70915","type":"licenses","attributes":{"name":"userCounTest","key":"A7EW-KUPF-PRML-RRVL-HLMP-7THR-F7KE-XF4C","expiry":"2024-10-31T21:39:49.271Z","status":"ACTIVE","uses":0,"suspended":false,"scheme":null,"encrypted":false,"strict":true,"floating":true,"protected":true,"version":null,"maxMachines":1,"maxProcesses":null,"maxUsers":null,"maxCores":null,"maxUses":null,"requireHeartbeat":false,"requireCheckIn":false,"lastValidated":"2024-10-01T22:26:18.121Z","lastCheckIn":null,"nextCheckIn":null,"lastCheckOut":null,"metadata":{"users":10},"created":"2024-10-01T21:39:49.268Z","updated":"2024-10-01T21:39:49.268Z"},"relationships":{"account":{"links":{"related":"/v1/accounts/e5430f69-e834-4ae4-befd-b602aae5f372"},"data":{"type":"accounts","id":"e5430f69-e834-4ae4-befd-b602aae5f372"}},"environment":{"links":{"related":null},"data":null},"product":{"links":{"related":"/v1/accounts/e5430f69-e834-4ae4-befd-b602aae5f372/licenses/808ed3c9-584b-46dd-8a80-c9217ef70915/product"},"data":{"type":"products","id":"f9bb2423-62c9-4d39-8def-4fdc5aca751e"}},"policy":{"links":{"related":"/v1/accounts/e5430f69-e834-4ae4-befd-b602aae5f372/licenses/808ed3c9-584b-46dd-8a80-c9217ef70915/policy"},"data":{"type":"policies","id":"04caef06-9ac2-4084-bf3c-bca4a0d29143"}},"group":{"links":{"related":"/v1/accounts/e5430f69-e834-4ae4-befd-b602aae5f372/licenses/808ed3c9-584b-46dd-8a80-c9217ef70915/group"},"data":null},"owner":{"links":{"related":"/v1/accounts/e5430f69-e834-4ae4-befd-b602aae5f372/licenses/808ed3c9-584b-46dd-8a80-c9217ef70915/owner"},"data":null},"users":{"links":{"related":"/v1/accounts/e5430f69-e834-4ae4-befd-b602aae5f372/licenses/808ed3c9-584b-46dd-8a80-c9217ef70915/users"},"meta":{"count":0}},"machines":{"links":{"related":"/v1/accounts/e5430f69-e834-4ae4-befd-b602aae5f372/licenses/808ed3c9-584b-46dd-8a80-c9217ef70915/machines"},"meta":{"cores":0,"count":0}},"tokens":{"links":{"related":"/v1/accounts/e5430f69-e834-4ae4-befd-b602aae5f372/licenses/808ed3c9-584b-46dd-8a80-c9217ef70915/tokens"}},"entitlements":{"links":{"related":"/v1/accounts/e5430f69-e834-4ae4-befd-b602aae5f372/licenses/808ed3c9-584b-46dd-8a80-c9217ef70915/entitlements"}}},"links":{"self":"/v1/accounts/e5430f69-e834-4ae4-befd-b602aae5f372/licenses/808ed3c9-584b-46dd-8a80-c9217ef70915"}},"meta":{"ts":"2024-10-01T22:26:18.124Z","valid":false,"detail":"fingerprint is not activated (has no associated machines)","code":"NO_MACHINES","scope":{"fingerprint":"example-fingerprint"}}}
public boolean verifyLicense(String licenseKey) { public boolean verifyLicense(String licenseKey) {
try { try {
log.info("Checking license key");
String machineFingerprint = generateMachineFingerprint(); String machineFingerprint = generateMachineFingerprint();
// First, try to validate the license // First, try to validate the license
JsonNode validationResponse = validateLicense(licenseKey, machineFingerprint); JsonNode validationResponse = validateLicense(licenseKey, machineFingerprint);
log.debug(validationResponse.asText()); log.info(validationResponse.asText());
if (validationResponse != null) { if (validationResponse != null) {
boolean isValid = validationResponse.path("meta").path("valid").asBoolean(); boolean isValid = validationResponse.path("meta").path("valid").asBoolean();
String licenseId = validationResponse.path("data").path("id").asText(); String licenseId = validationResponse.path("data").path("id").asText();
if (!isValid) { if (!isValid) {
String code = validationResponse.path("meta").path("code").asText(); String code = validationResponse.path("meta").path("code").asText();
log.debug(code); log.debug(code);
if ("NO_MACHINE".equals(code) || "NO_MACHINES".equals(code) || "FINGERPRINT_SCOPE_MISMATCH".equals(code)) { if ("NO_MACHINE".equals(code)
log.info("License not activated for this machine. Attempting to activate..."); || "NO_MACHINES".equals(code)
boolean activated = activateMachine(licenseKey, licenseId, machineFingerprint); || "FINGERPRINT_SCOPE_MISMATCH".equals(code)) {
log.info(
"License not activated for this machine. Attempting to activate...");
boolean activated =
activateMachine(licenseKey, licenseId, machineFingerprint);
if (activated) { if (activated) {
// Revalidate after activation // Revalidate after activation
validationResponse = validateLicense(licenseKey, machineFingerprint); validationResponse = validateLicense(licenseKey, machineFingerprint);
isValid = validationResponse != null && validationResponse.path("meta").path("valid").asBoolean(); isValid =
validationResponse != null
&& validationResponse
.path("meta")
.path("valid")
.asBoolean();
} }
} }
} }
@ -54,7 +63,7 @@ public class KeygenLicenseVerifier {
return false; return false;
} catch (Exception e) { } catch (Exception e) {
log.error("Error verifying license: " + e.getMessage()); log.error("Error verifying license: " + e.getMessage());
return false; return false;
} }
} }
@ -65,7 +74,7 @@ public class KeygenLicenseVerifier {
String requestBody = String requestBody =
String.format( String.format(
"{\"meta\":{\"key\":\"%s\",\"scope\":{\"fingerprint\":\"%s\"}}}", "{\"meta\":{\"key\":\"%s\",\"scope\":{\"fingerprint\":\"%s\"}}}",
licenseKey, machineFingerprint ); licenseKey, machineFingerprint);
HttpRequest request = HttpRequest request =
HttpRequest.newBuilder() HttpRequest.newBuilder()
.uri( .uri(
@ -76,18 +85,18 @@ public class KeygenLicenseVerifier {
+ "/licenses/actions/validate-key")) + "/licenses/actions/validate-key"))
.header("Content-Type", "application/vnd.api+json") .header("Content-Type", "application/vnd.api+json")
.header("Accept", "application/vnd.api+json") .header("Accept", "application/vnd.api+json")
//.header("Authorization", "License " + licenseKey) // .header("Authorization", "License " + licenseKey)
.POST(HttpRequest.BodyPublishers.ofString(requestBody)) .POST(HttpRequest.BodyPublishers.ofString(requestBody))
.build(); .build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString()); HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
log.debug(" validateLicenseResponse body: " + response.body()); log.info(" validateLicenseResponse body: " + response.body());
JsonNode jsonResponse = objectMapper.readTree(response.body()); JsonNode jsonResponse = objectMapper.readTree(response.body());
if (response.statusCode() == 200) { if (response.statusCode() == 200) {
JsonNode metaNode = jsonResponse.path("meta"); JsonNode metaNode = jsonResponse.path("meta");
boolean isValid = metaNode.path("valid").asBoolean(); boolean isValid = metaNode.path("valid").asBoolean();
String detail = metaNode.path("detail").asText(); String detail = metaNode.path("detail").asText();
String code = metaNode.path("code").asText(); String code = metaNode.path("code").asText();
@ -95,15 +104,14 @@ public class KeygenLicenseVerifier {
log.debug("Validation detail: " + detail); log.debug("Validation detail: " + detail);
log.debug("Validation code: " + code); log.debug("Validation code: " + code);
} else { } else {
log.error("Error validating license. Status code: " + response.statusCode()); log.error("Error validating license. Status code: " + response.statusCode());
} }
return jsonResponse; return jsonResponse;
} }
private static boolean activateMachine(String licenseKey, String licenseId, String machineFingerprint) private static boolean activateMachine(
throws Exception { String licenseKey, String licenseId, String machineFingerprint) throws Exception {
HttpClient client = HttpClient.newHttpClient(); HttpClient client = HttpClient.newHttpClient();
String hostname; String hostname;
@ -112,27 +120,54 @@ public class KeygenLicenseVerifier {
} catch (Exception e) { } catch (Exception e) {
hostname = "Unknown"; hostname = "Unknown";
} }
JSONObject body = new JSONObject()
.put("data", new JSONObject()
.put("type", "machines")
.put("attributes", new JSONObject()
.put("fingerprint", machineFingerprint)
.put("platform", System.getProperty("os.name")) // Added platform parameter
.put("name", hostname)) // Added name parameter
.put("relationships", new JSONObject()
.put("license", new JSONObject()
.put("data", new JSONObject()
.put("type", "licenses")
.put("id", licenseId)))));
HttpRequest request = HttpRequest.newBuilder() JSONObject body =
.uri(URI.create(BASE_URL + "/" + ACCOUNT_ID + "/machines")) new JSONObject()
.header("Content-Type", "application/vnd.api+json") .put(
.header("Accept", "application/vnd.api+json") "data",
.header("Authorization", "License " + licenseKey) // Keep the license key authentication new JSONObject()
.POST(HttpRequest.BodyPublishers.ofString(body.toString())) // Send the JSON body .put("type", "machines")
.build(); .put(
"attributes",
new JSONObject()
.put("fingerprint", machineFingerprint)
.put(
"platform",
System.getProperty(
"os.name")) // Added
// platform
// parameter
.put(
"name",
hostname)) // Added name parameter
.put(
"relationships",
new JSONObject()
.put(
"license",
new JSONObject()
.put(
"data",
new JSONObject()
.put(
"type",
"licenses")
.put(
"id",
licenseId)))));
HttpRequest request =
HttpRequest.newBuilder()
.uri(URI.create(BASE_URL + "/" + ACCOUNT_ID + "/machines"))
.header("Content-Type", "application/vnd.api+json")
.header("Accept", "application/vnd.api+json")
.header(
"Authorization",
"License " + licenseKey) // Keep the license key authentication
.POST(
HttpRequest.BodyPublishers.ofString(
body.toString())) // Send the JSON body
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString()); HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
log.debug("activateMachine Response body: " + response.body()); log.debug("activateMachine Response body: " + response.body());
@ -141,16 +176,14 @@ public class KeygenLicenseVerifier {
return true; return true;
} else { } else {
log.error("Error activating machine. Status code: " + response.statusCode()); log.error("Error activating machine. Status code: " + response.statusCode());
return false; return false;
} }
} }
private static String generateMachineFingerprint() { private static String generateMachineFingerprint() {
// This is a simplified example. In a real-world scenario, you'd want to generate // This is a simplified example. In a real-world scenario, you'd want to generate
// a more robust and unique fingerprint based on hardware characteristics. // a more robust and unique fingerprint based on hardware characteristics.
return "example-fingerprint"; return "example-fingerprint";
} }
} }

View File

@ -2,41 +2,49 @@ package stirling.software.SPDF.EE;
import java.io.IOException; import java.io.IOException;
import org.springframework.boot.CommandLineRunner; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled; import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.model.ApplicationProperties; import stirling.software.SPDF.model.ApplicationProperties;
import stirling.software.SPDF.utils.GeneralUtils; import stirling.software.SPDF.utils.GeneralUtils;
@Component @Component
public class LicenseKeyChecker { @Slf4j
public class LicenseKeyChecker {
private final KeygenLicenseVerifier licenseService; private final KeygenLicenseVerifier licenseService;
private final ApplicationProperties applicationProperties; private final ApplicationProperties applicationProperties;
private static boolean enterpriseEnbaledResult = false; private boolean enterpriseEnbaledResult = false;
// Inject your license service or configuration // Inject your license service or configuration
@Autowired
public LicenseKeyChecker( public LicenseKeyChecker(
KeygenLicenseVerifier licenseService, ApplicationProperties applicationProperties) { KeygenLicenseVerifier licenseService, ApplicationProperties applicationProperties) {
this.licenseService = licenseService; this.licenseService = licenseService;
this.applicationProperties = new ApplicationProperties(); this.applicationProperties = applicationProperties;
} }
@Scheduled(fixedRate = 604800000, initialDelay = 1000) // 7 days in milliseconds
@Scheduled(fixedRate = 604800000) // 7 days in milliseconds
public void checkLicensePeriodically() { public void checkLicensePeriodically() {
checkLicense(); checkLicense();
} }
private void checkLicense() { private void checkLicense() {
if(!applicationProperties.getEnterpriseEdition().isEnabled()) { log.info(applicationProperties.toString());
enterpriseEnbaledResult = false; log.info(applicationProperties.getEnterpriseEdition().toString());
} else { if (!applicationProperties.getEnterpriseEdition().isEnabled()) {
enterpriseEnbaledResult = licenseService.verifyLicense(applicationProperties.getEnterpriseEdition().getKey()); System.out.println("gggggg");
} enterpriseEnbaledResult = false;
} else {
System.out.println("ssssssssssss");
enterpriseEnbaledResult =
licenseService.verifyLicense(
applicationProperties.getEnterpriseEdition().getKey());
}
} }
public void updateLicenseKey(String newKey) throws IOException { public void updateLicenseKey(String newKey) throws IOException {
@ -44,7 +52,7 @@ public class LicenseKeyChecker {
GeneralUtils.saveKeyToConfig("EnterpriseEdition.key", newKey, false); GeneralUtils.saveKeyToConfig("EnterpriseEdition.key", newKey, false);
checkLicense(); checkLicense();
} }
public boolean getEnterpriseEnabledResult() { public boolean getEnterpriseEnabledResult() {
return enterpriseEnbaledResult; return enterpriseEnbaledResult;
} }

View File

@ -10,6 +10,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import io.github.pixee.security.SystemCommand; import io.github.pixee.security.SystemCommand;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@Slf4j @Slf4j

View File

@ -20,7 +20,6 @@ import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader; import org.springframework.core.io.ResourceLoader;
import org.thymeleaf.spring6.SpringTemplateEngine; import org.thymeleaf.spring6.SpringTemplateEngine;
import stirling.software.SPDF.EE.LicenseKeyChecker;
import stirling.software.SPDF.model.ApplicationProperties; import stirling.software.SPDF.model.ApplicationProperties;
@Configuration @Configuration
@ -31,7 +30,6 @@ public class AppConfig {
@Autowired ApplicationProperties applicationProperties; @Autowired ApplicationProperties applicationProperties;
@Bean @Bean
@ConditionalOnProperty( @ConditionalOnProperty(
name = "system.customHTMLFiles", name = "system.customHTMLFiles",
@ -185,6 +183,4 @@ public class AppConfig {
public String uuid() { public String uuid() {
return applicationProperties.getAutomaticallyGenerated().getUUID(); return applicationProperties.getAutomaticallyGenerated().getUUID();
} }
} }

View File

@ -56,11 +56,13 @@ public class FingerprintBasedSessionFilter extends OncePerRequestFilter {
} }
session.setAttribute("userFingerprint", fingerprint); session.setAttribute("userFingerprint", fingerprint);
session.setAttribute(FingerprintBasedSessionManager.STARTUP_TIMESTAMP, FingerprintBasedSessionManager.APP_STARTUP_TIME); session.setAttribute(
FingerprintBasedSessionManager.STARTUP_TIMESTAMP,
FingerprintBasedSessionManager.APP_STARTUP_TIME);
sessionManager.registerFingerprint(fingerprint, sessionId); sessionManager.registerFingerprint(fingerprint, sessionId);
log.debug("Proceeding with request: {}", request.getRequestURI()); log.debug("Proceeding with request: {}", request.getRequestURI());
filterChain.doFilter(request, response); filterChain.doFilter(request, response);
} }
} }

View File

@ -5,7 +5,6 @@ import java.util.Map;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled; import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@ -16,17 +15,20 @@ import jakarta.servlet.http.HttpSessionListener;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Data; import lombok.Data;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@Slf4j @Slf4j
@Component @Component
public class FingerprintBasedSessionManager implements HttpSessionListener, HttpSessionAttributeListener { public class FingerprintBasedSessionManager
private static final ConcurrentHashMap<String, FingerprintInfo> activeFingerprints = new ConcurrentHashMap<>(); implements HttpSessionListener, HttpSessionAttributeListener {
private static final ConcurrentHashMap<String, FingerprintInfo> activeFingerprints =
new ConcurrentHashMap<>();
// To be reduced in later version to 8~ // To be reduced in later version to 8~
private static final int MAX_ACTIVE_FINGERPRINTS = 30; private static final int MAX_ACTIVE_FINGERPRINTS = 30;
static final String STARTUP_TIMESTAMP = "appStartupTimestamp"; static final String STARTUP_TIMESTAMP = "appStartupTimestamp";
static final long APP_STARTUP_TIME = System.currentTimeMillis(); static final long APP_STARTUP_TIME = System.currentTimeMillis();
private static final long FINGERPRINT_EXPIRATION = TimeUnit.MINUTES.toMillis(30); private static final long FINGERPRINT_EXPIRATION = TimeUnit.MINUTES.toMillis(30);
@Override @Override
public void sessionCreated(HttpSessionEvent se) { public void sessionCreated(HttpSessionEvent se) {
@ -40,16 +42,17 @@ public class FingerprintBasedSessionManager implements HttpSessionListener, Http
} }
synchronized (activeFingerprints) { synchronized (activeFingerprints) {
if (activeFingerprints.size() >= MAX_ACTIVE_FINGERPRINTS && !activeFingerprints.containsKey(fingerprint)) { if (activeFingerprints.size() >= MAX_ACTIVE_FINGERPRINTS
&& !activeFingerprints.containsKey(fingerprint)) {
log.info("Max fingerprints reached. Marking session as blocked: {}", sessionId); log.info("Max fingerprints reached. Marking session as blocked: {}", sessionId);
session.setAttribute("blocked", true); session.setAttribute("blocked", true);
} else { } else {
activeFingerprints.put(fingerprint, new FingerprintInfo(sessionId, System.currentTimeMillis())); activeFingerprints.put(
fingerprint, new FingerprintInfo(sessionId, System.currentTimeMillis()));
log.info( log.info(
"New fingerprint registered: {}. Total active fingerprints: {}", "New fingerprint registered: {}. Total active fingerprints: {}",
fingerprint, fingerprint,
activeFingerprints.size() activeFingerprints.size());
);
} }
session.setAttribute(STARTUP_TIMESTAMP, APP_STARTUP_TIME); session.setAttribute(STARTUP_TIMESTAMP, APP_STARTUP_TIME);
} }
@ -64,23 +67,24 @@ public class FingerprintBasedSessionManager implements HttpSessionListener, Http
synchronized (activeFingerprints) { synchronized (activeFingerprints) {
activeFingerprints.remove(fingerprint); activeFingerprints.remove(fingerprint);
log.info( log.info(
"Fingerprint removed: {}. Total active fingerprints: {}", "Fingerprint removed: {}. Total active fingerprints: {}",
fingerprint, fingerprint,
activeFingerprints.size() activeFingerprints.size());
);
} }
} }
} }
public boolean isFingerPrintAllowed(String fingerprint) { public boolean isFingerPrintAllowed(String fingerprint) {
synchronized (activeFingerprints) { synchronized (activeFingerprints) {
return activeFingerprints.size() < MAX_ACTIVE_FINGERPRINTS || activeFingerprints.containsKey(fingerprint); return activeFingerprints.size() < MAX_ACTIVE_FINGERPRINTS
|| activeFingerprints.containsKey(fingerprint);
} }
} }
public void registerFingerprint(String fingerprint, String sessionId) { public void registerFingerprint(String fingerprint, String sessionId) {
synchronized (activeFingerprints) { synchronized (activeFingerprints) {
activeFingerprints.put(fingerprint, new FingerprintInfo(sessionId, System.currentTimeMillis())); activeFingerprints.put(
fingerprint, new FingerprintInfo(sessionId, System.currentTimeMillis()));
} }
} }
@ -97,7 +101,8 @@ public class FingerprintBasedSessionManager implements HttpSessionListener, Http
int removedCount = 0; int removedCount = 0;
synchronized (activeFingerprints) { synchronized (activeFingerprints) {
Iterator<Map.Entry<String, FingerprintInfo>> iterator = activeFingerprints.entrySet().iterator(); Iterator<Map.Entry<String, FingerprintInfo>> iterator =
activeFingerprints.entrySet().iterator();
while (iterator.hasNext()) { while (iterator.hasNext()) {
Map.Entry<String, FingerprintInfo> entry = iterator.next(); Map.Entry<String, FingerprintInfo> entry = iterator.next();
FingerprintInfo info = entry.getValue(); FingerprintInfo info = entry.getValue();
@ -120,11 +125,10 @@ public class FingerprintBasedSessionManager implements HttpSessionListener, Http
} }
} }
@Data @AllArgsConstructor @Data
@AllArgsConstructor
private static class FingerprintInfo { private static class FingerprintInfo {
private String sessionId; private String sessionId;
private long lastAccessTime; private long lastAccessTime;
} }
} }

View File

@ -1,57 +1,56 @@
package stirling.software.SPDF.config.security; package stirling.software.SPDF.config.security;
import java.util.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Lazy;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper; import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.client.registration.ClientRegistration; import org.springframework.security.saml2.provider.service.authentication.OpenSaml4AuthenticationProvider;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository;
import org.springframework.security.oauth2.client.registration.ClientRegistrations; import org.springframework.security.saml2.provider.service.web.authentication.Saml2WebSsoAuthenticationFilter;
import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository;
import org.springframework.security.oauth2.core.user.OAuth2UserAuthority;
import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository; import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
import org.springframework.security.web.savedrequest.NullRequestCache; import org.springframework.security.web.savedrequest.NullRequestCache;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.config.security.oauth2.CustomOAuth2AuthenticationFailureHandler; import stirling.software.SPDF.config.security.oauth2.CustomOAuth2AuthenticationFailureHandler;
import stirling.software.SPDF.config.security.oauth2.CustomOAuth2AuthenticationSuccessHandler; import stirling.software.SPDF.config.security.oauth2.CustomOAuth2AuthenticationSuccessHandler;
import stirling.software.SPDF.config.security.oauth2.CustomOAuth2LogoutSuccessHandler; import stirling.software.SPDF.config.security.oauth2.CustomOAuth2LogoutSuccessHandler;
import stirling.software.SPDF.config.security.oauth2.CustomOAuth2UserService; import stirling.software.SPDF.config.security.oauth2.CustomOAuth2UserService;
import stirling.software.SPDF.config.security.saml.ConvertResponseToAuthentication;
import stirling.software.SPDF.config.security.saml.CustomSAMLAuthenticationFailureHandler;
import stirling.software.SPDF.config.security.saml.CustomSAMLAuthenticationSuccessHandler;
import stirling.software.SPDF.config.security.session.SessionPersistentRegistry; import stirling.software.SPDF.config.security.session.SessionPersistentRegistry;
import stirling.software.SPDF.model.ApplicationProperties; import stirling.software.SPDF.model.ApplicationProperties;
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2;
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2.Client;
import stirling.software.SPDF.model.User;
import stirling.software.SPDF.model.provider.GithubProvider;
import stirling.software.SPDF.model.provider.GoogleProvider;
import stirling.software.SPDF.model.provider.KeycloakProvider;
import stirling.software.SPDF.repository.JPATokenRepositoryImpl; import stirling.software.SPDF.repository.JPATokenRepositoryImpl;
@Configuration @Configuration
@EnableWebSecurity @EnableWebSecurity
@EnableMethodSecurity @EnableMethodSecurity
@Slf4j
public class SecurityConfiguration { public class SecurityConfiguration {
@Autowired private CustomUserDetailsService userDetailsService; @Autowired private CustomUserDetailsService userDetailsService;
private static final Logger logger = LoggerFactory.getLogger(SecurityConfiguration.class); @Autowired(required = false)
private GrantedAuthoritiesMapper userAuthoritiesMapper;
@Autowired(required = false)
private RelyingPartyRegistrationRepository relyingPartyRegistrationRepository;
@Bean @Bean
public PasswordEncoder passwordEncoder() { public PasswordEncoder passwordEncoder() {
@ -73,10 +72,14 @@ public class SecurityConfiguration {
@Autowired private FirstLoginFilter firstLoginFilter; @Autowired private FirstLoginFilter firstLoginFilter;
@Autowired private SessionPersistentRegistry sessionRegistry; @Autowired private SessionPersistentRegistry sessionRegistry;
@Autowired private ConvertResponseToAuthentication convertResponseToAuthentication;
@Bean @Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.authenticationManager(authenticationManager(http));
if (loginEnabledValue) { if (loginEnabledValue) {
http.addFilterBefore( http.addFilterBefore(
userAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); userAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
http.csrf(csrf -> csrf.disable()); http.csrf(csrf -> csrf.disable());
http.addFilterBefore(rateLimitingFilter(), UsernamePasswordAuthenticationFilter.class); http.addFilterBefore(rateLimitingFilter(), UsernamePasswordAuthenticationFilter.class);
@ -85,7 +88,7 @@ public class SecurityConfiguration {
sessionManagement -> sessionManagement ->
sessionManagement sessionManagement
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED) .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
.maximumSessions(4) .maximumSessions(10)
.maxSessionsPreventsLogin(false) .maxSessionsPreventsLogin(false)
.sessionRegistry(sessionRegistry) .sessionRegistry(sessionRegistry)
.expiredUrl("/login?logout=true")); .expiredUrl("/login?logout=true"));
@ -134,6 +137,7 @@ public class SecurityConfiguration {
return trimmedUri.startsWith("/login") return trimmedUri.startsWith("/login")
|| trimmedUri.startsWith("/oauth") || trimmedUri.startsWith("/oauth")
|| trimmedUri.startsWith("/saml2")
|| trimmedUri.endsWith(".svg") || trimmedUri.endsWith(".svg")
|| trimmedUri.startsWith( || trimmedUri.startsWith(
"/register") "/register")
@ -183,13 +187,37 @@ public class SecurityConfiguration {
userService, userService,
loginAttemptService)) loginAttemptService))
.userAuthoritiesMapper( .userAuthoritiesMapper(
userAuthoritiesMapper()))) userAuthoritiesMapper)))
.logout( .logout(
logout -> logout ->
logout.logoutSuccessHandler( logout.logoutSuccessHandler(
new CustomOAuth2LogoutSuccessHandler( new CustomOAuth2LogoutSuccessHandler(
applicationProperties))); applicationProperties)));
} }
// Handle SAML
if (applicationProperties.getSecurity().getSaml() != null
&& applicationProperties.getSecurity().getSaml().getEnabled()
&& !applicationProperties
.getSecurity()
.getLoginMethod()
.equalsIgnoreCase("normal")) {
http.saml2Login(
saml2 -> {
saml2.loginPage("/saml2")
.relyingPartyRegistrationRepository(
relyingPartyRegistrationRepository)
.successHandler(
new CustomSAMLAuthenticationSuccessHandler(
loginAttemptService,
userService,
applicationProperties))
.failureHandler(
new CustomSAMLAuthenticationFailureHandler());
})
.addFilterBefore(
userAuthenticationFilter, Saml2WebSsoAuthenticationFilter.class);
}
} else { } else {
http.csrf(csrf -> csrf.disable()) http.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(authz -> authz.anyRequest().permitAll()); .authorizeHttpRequests(authz -> authz.anyRequest().permitAll());
@ -198,176 +226,32 @@ public class SecurityConfiguration {
return http.build(); return http.build();
} }
// Client Registration Repository for OAUTH2 OIDC Login
@Bean @Bean
@ConditionalOnProperty( public AuthenticationProvider samlAuthenticationProvider() {
value = "security.oauth2.enabled", OpenSaml4AuthenticationProvider authenticationProvider =
havingValue = "true", new OpenSaml4AuthenticationProvider();
matchIfMissing = false) authenticationProvider.setResponseAuthenticationConverter(convertResponseToAuthentication);
public ClientRegistrationRepository clientRegistrationRepository() { return authenticationProvider;
List<ClientRegistration> registrations = new ArrayList<>();
githubClientRegistration().ifPresent(registrations::add);
oidcClientRegistration().ifPresent(registrations::add);
googleClientRegistration().ifPresent(registrations::add);
keycloakClientRegistration().ifPresent(registrations::add);
if (registrations.isEmpty()) {
logger.error("At least one OAuth2 provider must be configured");
System.exit(1);
}
return new InMemoryClientRegistrationRepository(registrations);
} }
private Optional<ClientRegistration> googleClientRegistration() {
OAUTH2 oauth = applicationProperties.getSecurity().getOauth2();
if (oauth == null || !oauth.getEnabled()) {
return Optional.empty();
}
Client client = oauth.getClient();
if (client == null) {
return Optional.empty();
}
GoogleProvider google = client.getGoogle();
return google != null && google.isSettingsValid()
? Optional.of(
ClientRegistration.withRegistrationId(google.getName())
.clientId(google.getClientId())
.clientSecret(google.getClientSecret())
.scope(google.getScopes())
.authorizationUri(google.getAuthorizationuri())
.tokenUri(google.getTokenuri())
.userInfoUri(google.getUserinfouri())
.userNameAttributeName(google.getUseAsUsername())
.clientName(google.getClientName())
.redirectUri("{baseUrl}/login/oauth2/code/" + google.getName())
.authorizationGrantType(
org.springframework.security.oauth2.core
.AuthorizationGrantType.AUTHORIZATION_CODE)
.build())
: Optional.empty();
}
private Optional<ClientRegistration> keycloakClientRegistration() {
OAUTH2 oauth = applicationProperties.getSecurity().getOauth2();
if (oauth == null || !oauth.getEnabled()) {
return Optional.empty();
}
Client client = oauth.getClient();
if (client == null) {
return Optional.empty();
}
KeycloakProvider keycloak = client.getKeycloak();
return keycloak != null && keycloak.isSettingsValid()
? Optional.of(
ClientRegistrations.fromIssuerLocation(keycloak.getIssuer())
.registrationId(keycloak.getName())
.clientId(keycloak.getClientId())
.clientSecret(keycloak.getClientSecret())
.scope(keycloak.getScopes())
.userNameAttributeName(keycloak.getUseAsUsername())
.clientName(keycloak.getClientName())
.build())
: Optional.empty();
}
private Optional<ClientRegistration> githubClientRegistration() {
OAUTH2 oauth = applicationProperties.getSecurity().getOauth2();
if (oauth == null || !oauth.getEnabled()) {
return Optional.empty();
}
Client client = oauth.getClient();
if (client == null) {
return Optional.empty();
}
GithubProvider github = client.getGithub();
return github != null && github.isSettingsValid()
? Optional.of(
ClientRegistration.withRegistrationId(github.getName())
.clientId(github.getClientId())
.clientSecret(github.getClientSecret())
.scope(github.getScopes())
.authorizationUri(github.getAuthorizationuri())
.tokenUri(github.getTokenuri())
.userInfoUri(github.getUserinfouri())
.userNameAttributeName(github.getUseAsUsername())
.clientName(github.getClientName())
.redirectUri("{baseUrl}/login/oauth2/code/" + github.getName())
.authorizationGrantType(
org.springframework.security.oauth2.core
.AuthorizationGrantType.AUTHORIZATION_CODE)
.build())
: Optional.empty();
}
private Optional<ClientRegistration> oidcClientRegistration() {
OAUTH2 oauth = applicationProperties.getSecurity().getOauth2();
if (oauth == null
|| oauth.getIssuer() == null
|| oauth.getIssuer().isEmpty()
|| oauth.getClientId() == null
|| oauth.getClientId().isEmpty()
|| oauth.getClientSecret() == null
|| oauth.getClientSecret().isEmpty()
|| oauth.getScopes() == null
|| oauth.getScopes().isEmpty()
|| oauth.getUseAsUsername() == null
|| oauth.getUseAsUsername().isEmpty()) {
return Optional.empty();
}
return Optional.of(
ClientRegistrations.fromIssuerLocation(oauth.getIssuer())
.registrationId("oidc")
.clientId(oauth.getClientId())
.clientSecret(oauth.getClientSecret())
.scope(oauth.getScopes())
.userNameAttributeName(oauth.getUseAsUsername())
.clientName("OIDC")
.build());
}
/*
This following function is to grant Authorities to the OAUTH2 user from the values stored in the database.
This is required for the internal; 'hasRole()' function to give out the correct role.
*/
@Bean @Bean
@ConditionalOnProperty( public AuthenticationProvider daoAuthenticationProvider() {
value = "security.oauth2.enabled", DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
havingValue = "true", provider.setUserDetailsService(userDetailsService); // UserDetailsService
matchIfMissing = false) provider.setPasswordEncoder(passwordEncoder()); // PasswordEncoder
GrantedAuthoritiesMapper userAuthoritiesMapper() { return provider;
return (authorities) -> { }
Set<GrantedAuthority> mappedAuthorities = new HashSet<>();
authorities.forEach( @Bean
authority -> { public AuthenticationManager authenticationManager(HttpSecurity http) throws Exception {
// Add existing OAUTH2 Authorities AuthenticationManagerBuilder authenticationManagerBuilder =
mappedAuthorities.add(new SimpleGrantedAuthority(authority.getAuthority())); http.getSharedObject(AuthenticationManagerBuilder.class);
// Add Authorities from database for existing user, if user is present. authenticationManagerBuilder
if (authority instanceof OAuth2UserAuthority oauth2Auth) { .authenticationProvider(daoAuthenticationProvider()) // Benutzername/Passwort
String useAsUsername = .authenticationProvider(samlAuthenticationProvider()); // SAML
applicationProperties
.getSecurity() return authenticationManagerBuilder.build();
.getOauth2()
.getUseAsUsername();
Optional<User> userOpt =
userService.findByUsernameIgnoreCase(
(String) oauth2Auth.getAttributes().get(useAsUsername));
if (userOpt.isPresent()) {
User user = userOpt.get();
if (user != null) {
mappedAuthorities.add(
new SimpleGrantedAuthority(
userService.findRole(user).getAuthority()));
}
}
}
});
return mappedAuthorities;
};
} }
@Bean @Bean
@ -385,4 +269,13 @@ public class SecurityConfiguration {
public boolean activSecurity() { public boolean activSecurity() {
return true; return true;
} }
// Only Dev test
@Bean
public WebSecurityCustomizer webSecurityCustomizer() {
return (web) ->
web.ignoring()
.requestMatchers(
"/css/**", "/images/**", "/js/**", "/**.svg", "/pdfjs-legacy/**");
}
} }

View File

@ -0,0 +1,68 @@
package stirling.software.SPDF.config.security.saml;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import org.opensaml.saml.saml2.core.Assertion;
import org.springframework.core.convert.converter.Converter;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.saml2.provider.service.authentication.OpenSaml4AuthenticationProvider.ResponseToken;
import org.springframework.security.saml2.provider.service.authentication.Saml2Authentication;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import lombok.extern.slf4j.Slf4j;
@Component
@Slf4j
public class ConvertResponseToAuthentication
implements Converter<ResponseToken, Saml2Authentication> {
private final Saml2AuthorityAttributeLookup saml2AuthorityAttributeLookup;
public ConvertResponseToAuthentication(
Saml2AuthorityAttributeLookup saml2AuthorityAttributeLookup) {
this.saml2AuthorityAttributeLookup = saml2AuthorityAttributeLookup;
}
@Override
public Saml2Authentication convert(ResponseToken responseToken) {
final Assertion assertion =
CollectionUtils.firstElement(responseToken.getResponse().getAssertions());
final Map<String, List<Object>> attributes =
SamlAssertionUtils.getAssertionAttributes(assertion);
final String registrationId =
responseToken.getToken().getRelyingPartyRegistration().getRegistrationId();
final ScimSaml2AuthenticatedPrincipal principal =
new ScimSaml2AuthenticatedPrincipal(
assertion,
attributes,
saml2AuthorityAttributeLookup.getIdentityMappings(registrationId));
final Collection<? extends GrantedAuthority> assertionAuthorities =
getAssertionAuthorities(
attributes,
saml2AuthorityAttributeLookup.getAuthorityAttribute(registrationId));
return new Saml2Authentication(
principal, responseToken.getToken().getSaml2Response(), assertionAuthorities);
}
private static Collection<? extends GrantedAuthority> getAssertionAuthorities(
final Map<String, List<Object>> attributes, final String authoritiesAttributeName) {
if (attributes == null || attributes.isEmpty()) {
return Collections.emptySet();
}
final List<Object> groups = new ArrayList<>(attributes.get(authoritiesAttributeName));
return groups.stream()
.filter(String.class::isInstance)
.map(String.class::cast)
.map(String::toLowerCase)
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toSet());
}
}

View File

@ -0,0 +1,51 @@
package stirling.software.SPDF.config.security.saml;
import java.io.IOException;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.DisabledException;
import org.springframework.security.authentication.LockedException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationException;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class CustomSAMLAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(
HttpServletRequest request,
HttpServletResponse response,
AuthenticationException exception)
throws IOException, ServletException {
if (exception instanceof BadCredentialsException) {
log.error("BadCredentialsException", exception);
getRedirectStrategy().sendRedirect(request, response, "/login?error=badcredentials");
return;
}
if (exception instanceof DisabledException) {
log.error("User is deactivated: ", exception);
getRedirectStrategy().sendRedirect(request, response, "/logout?userIsDisabled=true");
return;
}
if (exception instanceof LockedException) {
log.error("Account locked: ", exception);
getRedirectStrategy().sendRedirect(request, response, "/logout?error=locked");
return;
}
if (exception instanceof Saml2AuthenticationException) {
log.error("SAML2 Authentication error: ", exception);
getRedirectStrategy()
.sendRedirect(request, response, "/logout?error=saml2AuthenticationError");
return;
}
log.error("Unhandled authentication exception", exception);
super.onAuthenticationFailure(request, response, exception);
}
}

View File

@ -0,0 +1,108 @@
package stirling.software.SPDF.config.security.saml;
import java.io.IOException;
import org.springframework.security.authentication.LockedException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
import org.springframework.security.web.savedrequest.SavedRequest;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.config.security.LoginAttemptService;
import stirling.software.SPDF.config.security.UserService;
import stirling.software.SPDF.model.ApplicationProperties;
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2;
import stirling.software.SPDF.model.AuthenticationType;
import stirling.software.SPDF.utils.RequestUriUtils;
@Slf4j
public class CustomSAMLAuthenticationSuccessHandler
extends SavedRequestAwareAuthenticationSuccessHandler {
private LoginAttemptService loginAttemptService;
private UserService userService;
private ApplicationProperties applicationProperties;
public CustomSAMLAuthenticationSuccessHandler(
LoginAttemptService loginAttemptService,
UserService userService,
ApplicationProperties applicationProperties) {
this.loginAttemptService = loginAttemptService;
this.userService = userService;
this.applicationProperties = applicationProperties;
}
@Override
public void onAuthenticationSuccess(
HttpServletRequest request, HttpServletResponse response, Authentication authentication)
throws ServletException, IOException {
Object principal = authentication.getPrincipal();
String username = "";
if (principal instanceof OAuth2User) {
OAuth2User oauthUser = (OAuth2User) principal;
username = oauthUser.getName();
} else if (principal instanceof UserDetails) {
UserDetails oauthUser = (UserDetails) principal;
username = oauthUser.getUsername();
} else if (principal instanceof ScimSaml2AuthenticatedPrincipal) {
ScimSaml2AuthenticatedPrincipal samlPrincipal =
(ScimSaml2AuthenticatedPrincipal) principal;
username = samlPrincipal.getName();
}
// Get the saved request
HttpSession session = request.getSession(false);
String contextPath = request.getContextPath();
SavedRequest savedRequest =
(session != null)
? (SavedRequest) session.getAttribute("SPRING_SECURITY_SAVED_REQUEST")
: null;
if (savedRequest != null
&& !RequestUriUtils.isStaticResource(contextPath, savedRequest.getRedirectUrl())) {
// Redirect to the original destination
super.onAuthenticationSuccess(request, response, authentication);
} else {
OAUTH2 oAuth = applicationProperties.getSecurity().getOauth2();
if (loginAttemptService.isBlocked(username)) {
if (session != null) {
session.removeAttribute("SPRING_SECURITY_SAVED_REQUEST");
}
throw new LockedException(
"Your account has been locked due to too many failed login attempts.");
}
if (userService.usernameExistsIgnoreCase(username)
&& userService.hasPassword(username)
&& !userService.isAuthenticationTypeByUsername(
username, AuthenticationType.OAUTH2)
&& oAuth.getAutoCreateUser()) {
response.sendRedirect(contextPath + "/logout?oauth2AuthenticationErrorWeb=true");
return;
}
try {
if (oAuth.getBlockRegistration()
&& !userService.usernameExistsIgnoreCase(username)) {
response.sendRedirect(contextPath + "/logout?oauth2_admin_blocked_user=true");
return;
}
if (principal instanceof OAuth2User) {
userService.processOAuth2PostLogin(username, oAuth.getAutoCreateUser());
}
response.sendRedirect(contextPath + "/");
return;
} catch (IllegalArgumentException e) {
response.sendRedirect(contextPath + "/logout?invalidUsername=true");
return;
}
}
}
}

View File

@ -0,0 +1,38 @@
package stirling.software.SPDF.config.security.saml;
import java.io.IOException;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class SAMLLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler {
@Override
public void onLogoutSuccess(
HttpServletRequest request, HttpServletResponse response, Authentication authentication)
throws IOException, ServletException {
String redirectUrl = determineTargetUrl(request, response, authentication);
if (response.isCommitted()) {
log.debug("Response has already been committed. Unable to redirect to " + redirectUrl);
return;
}
getRedirectStrategy().sendRedirect(request, response, redirectUrl);
}
protected String determineTargetUrl(
HttpServletRequest request,
HttpServletResponse response,
Authentication authentication) {
// Default to the root URL
return "/";
}
}

View File

@ -0,0 +1,7 @@
package stirling.software.SPDF.config.security.saml;
public interface Saml2AuthorityAttributeLookup {
String getAuthorityAttribute(String registrationId);
SimpleScimMappings getIdentityMappings(String registrationId);
}

View File

@ -0,0 +1,17 @@
package stirling.software.SPDF.config.security.saml;
import org.springframework.stereotype.Component;
@Component
public class Saml2AuthorityAttributeLookupImpl implements Saml2AuthorityAttributeLookup {
@Override
public String getAuthorityAttribute(String registrationId) {
return "authorityAttributeName";
}
@Override
public SimpleScimMappings getIdentityMappings(String registrationId) {
return new SimpleScimMappings();
}
}

View File

@ -0,0 +1,63 @@
package stirling.software.SPDF.config.security.saml;
import java.time.Instant;
import java.util.*;
import org.opensaml.core.xml.XMLObject;
import org.opensaml.core.xml.schema.*;
import org.opensaml.saml.saml2.core.Assertion;
public class SamlAssertionUtils {
public static Map<String, List<Object>> getAssertionAttributes(Assertion assertion) {
Map<String, List<Object>> attributeMap = new LinkedHashMap<>();
assertion
.getAttributeStatements()
.forEach(
attributeStatement -> {
attributeStatement
.getAttributes()
.forEach(
attribute -> {
List<Object> attributeValues = new ArrayList<>();
attribute
.getAttributeValues()
.forEach(
xmlObject -> {
Object attributeValue =
getXmlObjectValue(
xmlObject);
if (attributeValue != null) {
attributeValues.add(
attributeValue);
}
});
attributeMap.put(
attribute.getName(), attributeValues);
});
});
return attributeMap;
}
public static Object getXmlObjectValue(XMLObject xmlObject) {
if (xmlObject instanceof XSAny) {
return ((XSAny) xmlObject).getTextContent();
} else if (xmlObject instanceof XSString) {
return ((XSString) xmlObject).getValue();
} else if (xmlObject instanceof XSInteger) {
return ((XSInteger) xmlObject).getValue();
} else if (xmlObject instanceof XSURI) {
return ((XSURI) xmlObject).getURI();
} else if (xmlObject instanceof XSBoolean) {
return ((XSBoolean) xmlObject).getValue().getValue();
} else if (xmlObject instanceof XSDateTime) {
Instant dateTime = ((XSDateTime) xmlObject).getValue();
return (dateTime != null) ? Instant.ofEpochMilli(dateTime.toEpochMilli()) : null;
}
return null;
}
}

View File

@ -0,0 +1,42 @@
package stirling.software.SPDF.config.security.saml;
import java.security.cert.CertificateException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.saml2.provider.service.registration.InMemoryRelyingPartyRegistrationRepository;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrations;
import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.model.ApplicationProperties;
@Configuration
@Slf4j
public class SamlConfig {
@Autowired ApplicationProperties applicationProperties;
@Bean
@ConditionalOnProperty(
value = "security.saml.enabled",
havingValue = "true",
matchIfMissing = false)
public RelyingPartyRegistrationRepository relyingPartyRegistrationRepository()
throws CertificateException {
RelyingPartyRegistration registration =
RelyingPartyRegistrations.fromMetadataLocation(
applicationProperties
.getSecurity()
.getSaml()
.getIdpMetadataLocation())
.entityId(applicationProperties.getSecurity().getSaml().getEntityId())
.registrationId(
applicationProperties.getSecurity().getSaml().getRegistrationId())
.build();
return new InMemoryRelyingPartyRegistrationRepository(registration);
}
}

View File

@ -0,0 +1,89 @@
package stirling.software.SPDF.config.security.saml;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import org.opensaml.saml.saml2.core.Assertion;
import org.springframework.security.core.AuthenticatedPrincipal;
import org.springframework.util.Assert;
import com.unboundid.scim2.common.types.Email;
import com.unboundid.scim2.common.types.Name;
import com.unboundid.scim2.common.types.UserResource;
public class ScimSaml2AuthenticatedPrincipal implements AuthenticatedPrincipal, Serializable {
private static final long serialVersionUID = 1L;
private final transient UserResource userResource;
public ScimSaml2AuthenticatedPrincipal(
final Assertion assertion,
final Map<String, List<Object>> attributes,
final SimpleScimMappings attributeMappings) {
Assert.notNull(assertion, "assertion cannot be null");
Assert.notNull(assertion.getSubject(), "assertion subject cannot be null");
Assert.notNull(
assertion.getSubject().getNameID(), "assertion subject NameID cannot be null");
Assert.notNull(attributes, "attributes cannot be null");
Assert.notNull(attributeMappings, "attributeMappings cannot be null");
final Name name =
new Name()
.setFamilyName(
getAttribute(
attributes,
attributeMappings,
SimpleScimMappings::getFamilyName))
.setGivenName(
getAttribute(
attributes,
attributeMappings,
SimpleScimMappings::getGivenName));
final List<Email> emails = new ArrayList<>(1);
emails.add(
new Email()
.setValue(
getAttribute(
attributes,
attributeMappings,
SimpleScimMappings::getEmail))
.setPrimary(true));
userResource =
new UserResource()
.setUserName(assertion.getSubject().getNameID().getValue())
.setName(name)
.setEmails(emails);
}
private static String getAttribute(
final Map<String, List<Object>> attributes,
final SimpleScimMappings simpleScimMappings,
final Function<SimpleScimMappings, String> attributeMapper) {
final String key = attributeMapper.apply(simpleScimMappings);
final List<Object> values = attributes.getOrDefault(key, Collections.emptyList());
return values.stream()
.filter(String.class::isInstance)
.map(String.class::cast)
.findFirst()
.orElse(null);
}
@Override
public String getName() {
return this.userResource.getUserName();
}
public UserResource getUserResource() {
return this.userResource;
}
}

View File

@ -0,0 +1,10 @@
package stirling.software.SPDF.config.security.saml;
import lombok.Data;
@Data
public class SimpleScimMappings {
String givenName;
String familyName;
String email;
}

View File

@ -193,7 +193,6 @@ public class UserController {
Map<String, String[]> paramMap = request.getParameterMap(); Map<String, String[]> paramMap = request.getParameterMap();
Map<String, String> updates = new HashMap<>(); Map<String, String> updates = new HashMap<>();
for (Map.Entry<String, String[]> entry : paramMap.entrySet()) { for (Map.Entry<String, String[]> entry : paramMap.entrySet()) {
updates.put(entry.getKey(), entry.getValue()[0]); updates.put(entry.getKey(), entry.getValue()[0]);
} }

View File

@ -25,6 +25,7 @@ import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.model.api.misc.PrintFileRequest; import stirling.software.SPDF.model.api.misc.PrintFileRequest;

View File

@ -13,6 +13,9 @@ import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource; import org.springframework.context.annotation.PropertySource;
import org.springframework.core.Ordered; import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order; import org.springframework.core.annotation.Order;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.Resource;
import lombok.Data; import lombok.Data;
import lombok.ToString; import lombok.ToString;
@ -60,6 +63,7 @@ public class ApplicationProperties {
private Boolean csrfDisabled; private Boolean csrfDisabled;
private InitialLogin initialLogin = new InitialLogin(); private InitialLogin initialLogin = new InitialLogin();
private OAUTH2 oauth2 = new OAUTH2(); private OAUTH2 oauth2 = new OAUTH2();
private SAML saml = new SAML();
private int loginAttemptCount; private int loginAttemptCount;
private long loginResetTimeMinutes; private long loginResetTimeMinutes;
private String loginMethod = "all"; private String loginMethod = "all";
@ -70,6 +74,34 @@ public class ApplicationProperties {
@ToString.Exclude private String password; @ToString.Exclude private String password;
} }
@Data
public static class SAML {
private Boolean enabled = false;
private String entityId;
private String registrationId;
private String spBaseUrl;
private String idpMetadataLocation;
private KeyStore keystore;
@Data
public static class KeyStore {
private String keystoreLocation;
private String keystorePassword;
private String keyAlias;
private String keyPassword;
private String realmCertificateAlias;
public Resource getKeystoreResource() {
if (keystoreLocation.startsWith("classpath:")) {
return new ClassPathResource(
keystoreLocation.substring("classpath:".length()));
} else {
return new FileSystemResource(keystoreLocation);
}
}
}
}
@Data @Data
public static class OAUTH2 { public static class OAUTH2 {
private Boolean enabled = false; private Boolean enabled = false;

View File

@ -17,7 +17,7 @@ public class MetricsAggregatorService {
private final MeterRegistry meterRegistry; private final MeterRegistry meterRegistry;
private final PostHogService postHogService; private final PostHogService postHogService;
private final Map<String, Double> lastSentMetrics = new ConcurrentHashMap<>(); private final Map<String, Double> lastSentMetrics = new ConcurrentHashMap<>();
@Autowired @Autowired
public MetricsAggregatorService(MeterRegistry meterRegistry, PostHogService postHogService) { public MetricsAggregatorService(MeterRegistry meterRegistry, PostHogService postHogService) {
this.meterRegistry = meterRegistry; this.meterRegistry = meterRegistry;
@ -28,25 +28,26 @@ public class MetricsAggregatorService {
public void aggregateAndSendMetrics() { public void aggregateAndSendMetrics() {
Map<String, Object> metrics = new HashMap<>(); Map<String, Object> metrics = new HashMap<>();
Search.in(meterRegistry) Search.in(meterRegistry)
.name("http.requests") .name("http.requests")
.counters() .counters()
.forEach(counter -> { .forEach(
String key = String.format( counter -> {
"http_requests_%s_%s", String key =
counter.getId().getTag("method"), String.format(
counter.getId().getTag("uri").replace("/", "_")); "http_requests_%s_%s",
counter.getId().getTag("method"),
double currentCount = counter.count(); counter.getId().getTag("uri").replace("/", "_"));
double lastCount = lastSentMetrics.getOrDefault(key, 0.0);
double difference = currentCount - lastCount; double currentCount = counter.count();
double lastCount = lastSentMetrics.getOrDefault(key, 0.0);
if (difference > 0) { double difference = currentCount - lastCount;
metrics.put(key, difference);
lastSentMetrics.put(key, currentCount); if (difference > 0) {
} metrics.put(key, difference);
}); lastSentMetrics.put(key, currentCount);
}
});
// Send aggregated metrics to PostHog // Send aggregated metrics to PostHog
if (!metrics.isEmpty()) { if (!metrics.isEmpty()) {
postHogService.captureEvent("aggregated_metrics", metrics); postHogService.captureEvent("aggregated_metrics", metrics);

View File

@ -39,15 +39,20 @@ public class PostHogService {
} }
private void captureSystemInfo() { private void captureSystemInfo() {
if(!Boolean.getBoolean(applicationProperties.getSystem().getEnableAnalytics())) {
return;
}
try { try {
postHog.capture(uniqueId, "system_info_captured", captureServerMetrics()); postHog.capture(uniqueId, "system_info_captured", captureServerMetrics());
} catch (Exception e) { } catch (Exception e) {
// Handle exceptions // Handle exceptions
} }
} }
public void captureEvent(String eventName, Map<String, Object> properties) { public void captureEvent(String eventName, Map<String, Object> properties) {
if(!Boolean.getBoolean(applicationProperties.getSystem().getEnableAnalytics())) {
return;
}
postHog.capture(uniqueId, eventName, properties); postHog.capture(uniqueId, eventName, properties);
} }

View File

@ -49,5 +49,5 @@ springdoc.api-docs.path=/v1/api-docs
springdoc.swagger-ui.url=/v1/api-docs springdoc.swagger-ui.url=/v1/api-docs
posthog.api.key=phc_Bh95TRT3qZveAxJpBmJcPpSpW2dJeiKlgin8c7xXGna posthog.api.key=phc_fiR65u5j6qmXTYL56MNrLZSWqLaDW74OrZH0Insd2xq
posthog.host=https://eu.i.posthog.com posthog.host=https://eu.i.posthog.com

View File

@ -49,7 +49,7 @@ security:
provider: google # Set this to your OAuth provider's name, e.g., 'google' or 'keycloak' provider: google # Set this to your OAuth provider's name, e.g., 'google' or 'keycloak'
# Enterprise edition settings unused for now please ignore! # Enterprise edition settings unused for now please ignore!
EnterpriseEdition: enterpriseEdition:
enabled: false # set to 'true' to enable enterprise edition enabled: false # set to 'true' to enable enterprise edition
key: 00000000-0000-0000-0000-000000000000 key: 00000000-0000-0000-0000-000000000000
CustomMetadata: CustomMetadata:

View File

@ -278,7 +278,7 @@
let allKeys = new Set([...Object.keys(accountSettings), ...Object.keys(localStorage)]); let allKeys = new Set([...Object.keys(accountSettings), ...Object.keys(localStorage)]);
allKeys.forEach(key => { allKeys.forEach(key => {
if(key === 'debug' || key === '0' || key === '1') return; // Ignoring specific keys if(key === 'debug' || key === '0' || key === '1' || key.includes('pdfjs') || key.includes('posthog') || key.includes('pageViews')) return; // Ignoring specific keys
const accountValue = accountSettings[key] || '-'; const accountValue = accountSettings[key] || '-';
const browserValue = localStorage.getItem(key) || '-'; const browserValue = localStorage.getItem(key) || '-';
@ -299,7 +299,7 @@
// Then, set the account settings to local storage // Then, set the account settings to local storage
for (let key in accountSettings) { for (let key in accountSettings) {
if(key !== 'debug' && key !== '0' && key !== '1') { // Only sync non-ignored keys if(key !== 'debug' && key !== '0' && key !== '1' && !key.includes('pdfjs') && !key.includes('posthog') && !key.includes('pageViews')) { // Only sync non-ignored keys
localStorage.setItem(key, accountSettings[key]); localStorage.setItem(key, accountSettings[key]);
} }
} }
@ -316,7 +316,7 @@
for (let i = 0; i < localStorage.length; i++) { for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i); const key = localStorage.key(i);
if(key !== 'debug' && key !== '0' && key !== '1') { // Only send non-ignored keys if(key !== 'debug' && key !== '0' && key !== '1' && !key.includes('pdfjs') && !key.includes('posthog') && !key.includes('pageViews')) { // Only send non-ignored keys
let hiddenField = document.createElement("input"); let hiddenField = document.createElement("input");
hiddenField.type = "hidden"; hiddenField.type = "hidden";
hiddenField.name = key; hiddenField.name = key;

View File

@ -97,6 +97,7 @@
}(document, window.posthog || []); }(document, window.posthog || []);
posthog.init('phc_fiR65u5j6qmXTYL56MNrLZSWqLaDW74OrZH0Insd2xq', { posthog.init('phc_fiR65u5j6qmXTYL56MNrLZSWqLaDW74OrZH0Insd2xq', {
api_host: 'https://eu.i.posthog.com', api_host: 'https://eu.i.posthog.com',
persistence: 'localStorage',
person_profiles: 'always', person_profiles: 'always',
mask_all_text: true, mask_all_text: true,
mask_all_element_attributes: true mask_all_element_attributes: true
@ -107,8 +108,6 @@
'UUID': /*[[${@UUID}]]*/ '' 'UUID': /*[[${@UUID}]]*/ ''
}) })
} }
</script> </script>