mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2026-05-01 23:16:31 +02:00
Fixed tests
This commit is contained in:
@@ -121,6 +121,7 @@ public class ApplicationProperties {
|
||||
private String customGlobalAPIKey;
|
||||
private Jwt jwt = new Jwt();
|
||||
private Validation validation = new Validation();
|
||||
private RateLimit rateLimit = new RateLimit();
|
||||
|
||||
public Boolean isAltLogin() {
|
||||
return saml2.getEnabled() || oauth2.getEnabled();
|
||||
@@ -344,6 +345,12 @@ public class ApplicationProperties {
|
||||
private boolean hardFail = false;
|
||||
}
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class RateLimit {
|
||||
private int maxRequests = 1000;
|
||||
private String resetSchedule = "0 0 0 * * MON"; // Cron expression: At 00:00 every Monday
|
||||
}
|
||||
}
|
||||
|
||||
@Data
|
||||
|
||||
@@ -61,6 +61,3 @@ java.io.tmpdir=${stirling.tempfiles.directory:${java.io.tmpdir}/stirling-pdf}
|
||||
|
||||
# V2 features
|
||||
v2=true
|
||||
|
||||
# OAuth2 configuration
|
||||
security.oauth2.enabled=true
|
||||
|
||||
@@ -60,6 +60,7 @@ security:
|
||||
privateKey: classpath:saml-private-key.key # Your private key. Generated from your keypair
|
||||
spCert: classpath:saml-public-cert.crt # Your signing certificate. Generated from your keypair
|
||||
jwt: # This feature is currently under development and not yet fully supported. Do not use in production.
|
||||
issuer: https://stirling.com # The issuer field to include in the JWTs
|
||||
persistence: true # Set to 'true' to enable JWT key store
|
||||
enableKeyRotation: true # Set to 'true' to enable key pair rotation
|
||||
enableKeyCleanup: true # Set to 'true' to enable key pair cleanup
|
||||
|
||||
@@ -114,8 +114,11 @@ public class CustomLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler {
|
||||
// Set service provider keys for the SamlClient
|
||||
samlClient.setSPKeys(certificate, privateKey);
|
||||
|
||||
// Redirect to identity provider for logout. todo: add relay state
|
||||
samlClient.redirectToIdentityProvider(response, null, nameIdValue);
|
||||
// Build relay state to return user to login page after IdP logout
|
||||
String relayState = UrlUtils.getOrigin(request) + request.getContextPath() + LOGOUT_PATH;
|
||||
|
||||
// Redirect to identity provider for logout with relay state
|
||||
samlClient.redirectToIdentityProvider(response, relayState, nameIdValue);
|
||||
} catch (Exception e) {
|
||||
log.error(
|
||||
"Error retrieving logout URL from Provider {} for user {}",
|
||||
|
||||
@@ -13,7 +13,7 @@ public class RateLimitResetScheduler {
|
||||
|
||||
private final IPRateLimitingFilter rateLimitingFilter;
|
||||
|
||||
@Scheduled(cron = "0 0 0 * * MON") // At 00:00 every Monday TODO: configurable
|
||||
@Scheduled(cron = "${security.rate-limit.reset-schedule:0 0 0 * * MON}")
|
||||
public void resetRateLimit() {
|
||||
rateLimitingFilter.resetRequestCounts();
|
||||
}
|
||||
|
||||
@@ -132,23 +132,16 @@ public class SecurityConfiguration {
|
||||
if (loginEnabledValue) {
|
||||
boolean v2Enabled = appConfig.v2Enabled();
|
||||
|
||||
http.addFilterBefore(
|
||||
userAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
|
||||
.addFilterBefore(
|
||||
rateLimitingFilter(), UsernamePasswordAuthenticationFilter.class)
|
||||
.addFilterAfter(firstLoginFilter, IPRateLimitingFilter.class);
|
||||
// http.addFilterAfter(firstLoginFilter, IPRateLimitingFilter.class);
|
||||
|
||||
if (v2Enabled) {
|
||||
http.addFilterBefore(
|
||||
jwtAuthenticationFilter(),
|
||||
UsernamePasswordAuthenticationFilter.class)
|
||||
.exceptionHandling(
|
||||
exceptionHandling ->
|
||||
exceptionHandling.authenticationEntryPoint(
|
||||
jwtAuthenticationEntryPoint))
|
||||
.addFilterAfter(
|
||||
userAuthenticationFilter,
|
||||
JwtAuthenticationFilter.class); // Run AFTER JWT filter
|
||||
} else {
|
||||
http.addFilterBefore(
|
||||
userAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
|
||||
http.addFilterBefore(jwtAuthenticationFilter(), UserAuthenticationFilter.class);
|
||||
}
|
||||
http.addFilterAfter(rateLimitingFilter(), UserAuthenticationFilter.class)
|
||||
.addFilterAfter(firstLoginFilter, UsernamePasswordAuthenticationFilter.class);
|
||||
|
||||
if (!securityProperties.getCsrfDisabled()) {
|
||||
CookieCsrfTokenRepository cookieRepo =
|
||||
@@ -324,7 +317,8 @@ public class SecurityConfiguration {
|
||||
userInfoEndpoint
|
||||
.oidcUserService(
|
||||
new CustomOAuth2UserService(
|
||||
securityProperties.getOauth2(),
|
||||
securityProperties
|
||||
.getOauth2(),
|
||||
userService,
|
||||
loginAttemptService))
|
||||
.userAuthoritiesMapper(
|
||||
@@ -378,8 +372,7 @@ public class SecurityConfiguration {
|
||||
|
||||
@Bean
|
||||
public IPRateLimitingFilter rateLimitingFilter() {
|
||||
// Example limit TODO add config level
|
||||
int maxRequestsPerIp = 1000000;
|
||||
int maxRequestsPerIp = securityProperties.getRateLimit().getMaxRequests();
|
||||
return new IPRateLimitingFilter(maxRequestsPerIp, maxRequestsPerIp);
|
||||
}
|
||||
|
||||
|
||||
@@ -121,8 +121,11 @@ public class CustomSaml2AuthenticationSuccessHandler
|
||||
String ssoProviderId = saml2Principal.nameId();
|
||||
String ssoProvider = "saml2"; // fixme
|
||||
|
||||
log.debug("Processing SSO post-login for user: {} (Provider: {}, ProviderId: {})",
|
||||
username, ssoProvider, ssoProviderId);
|
||||
log.debug(
|
||||
"Processing SSO post-login for user: {} (Provider: {}, ProviderId: {})",
|
||||
username,
|
||||
ssoProvider,
|
||||
ssoProviderId);
|
||||
|
||||
userService.processSSOPostLogin(
|
||||
username,
|
||||
|
||||
@@ -14,7 +14,6 @@ import org.springframework.security.oauth2.core.oidc.user.OidcUser;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.common.model.ApplicationProperties;
|
||||
import stirling.software.common.model.ApplicationProperties.Security.OAUTH2;
|
||||
import stirling.software.common.model.enumeration.UsernameAttribute;
|
||||
import stirling.software.proprietary.security.model.User;
|
||||
|
||||
@@ -42,17 +41,20 @@ public class CustomOAuth2UserService implements OAuth2UserService<OidcUserReques
|
||||
public OidcUser loadUser(OidcUserRequest userRequest) throws OAuth2AuthenticationException {
|
||||
try {
|
||||
OidcUser user = delegate.loadUser(userRequest);
|
||||
String usernameAttributeKey = UsernameAttribute
|
||||
.valueOf(oauth2Properties.getUseAsUsername().toUpperCase())
|
||||
.getName();
|
||||
String usernameAttributeKey =
|
||||
UsernameAttribute.valueOf(oauth2Properties.getUseAsUsername().toUpperCase())
|
||||
.getName();
|
||||
|
||||
// Extract SSO provider information
|
||||
String ssoProviderId = user.getSubject(); // Standard OIDC 'sub' claim
|
||||
String ssoProvider = userRequest.getClientRegistration().getRegistrationId();
|
||||
String username = user.getAttribute(usernameAttributeKey);
|
||||
|
||||
log.debug("OAuth2 login - Provider: {}, ProviderId: {}, Username: {}",
|
||||
ssoProvider, ssoProviderId, username);
|
||||
log.debug(
|
||||
"OAuth2 login - Provider: {}, ProviderId: {}, Username: {}",
|
||||
ssoProvider,
|
||||
ssoProviderId,
|
||||
username);
|
||||
|
||||
Optional<User> internalUser = userService.findByUsernameIgnoreCase(username);
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ import java.util.Map;
|
||||
import org.springframework.security.core.Authentication;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
|
||||
public interface JwtServiceInterface {
|
||||
|
||||
|
||||
@@ -74,7 +74,8 @@ public class UserService implements UserServiceInterface {
|
||||
// Find user by SSO provider ID first
|
||||
Optional<User> existingUser;
|
||||
if (ssoProviderId != null && ssoProvider != null) {
|
||||
existingUser = userRepository.findBySsoProviderAndSsoProviderId(ssoProvider, ssoProviderId);
|
||||
existingUser =
|
||||
userRepository.findBySsoProviderAndSsoProviderId(ssoProvider, ssoProviderId);
|
||||
|
||||
if (existingUser.isPresent()) {
|
||||
log.debug("User found by SSO provider ID: {}", ssoProviderId);
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package stirling.software.proprietary.security;
|
||||
|
||||
import static org.mockito.Mockito.*;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
@@ -38,7 +40,6 @@ class CustomLogoutSuccessHandlerTest {
|
||||
|
||||
when(response.isCommitted()).thenReturn(false);
|
||||
when(jwtService.extractToken(request)).thenReturn(token);
|
||||
doNothing().when(jwtService).clearToken(response);
|
||||
when(request.getContextPath()).thenReturn("");
|
||||
when(response.encodeRedirectURL(logoutPath)).thenReturn(logoutPath);
|
||||
|
||||
@@ -56,14 +57,12 @@ class CustomLogoutSuccessHandlerTest {
|
||||
|
||||
when(response.isCommitted()).thenReturn(false);
|
||||
when(jwtService.extractToken(request)).thenReturn(token);
|
||||
doNothing().when(jwtService).clearToken(response);
|
||||
when(request.getContextPath()).thenReturn("");
|
||||
when(response.encodeRedirectURL(logoutPath)).thenReturn(logoutPath);
|
||||
|
||||
customLogoutSuccessHandler.onLogoutSuccess(request, response, null);
|
||||
|
||||
verify(response).sendRedirect(logoutPath);
|
||||
verify(jwtService).clearToken(response);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -127,7 +127,6 @@ class JwtAuthenticationFilterTest {
|
||||
.setAuthentication(any(UsernamePasswordAuthenticationToken.class));
|
||||
verify(jwtService)
|
||||
.generateToken(any(UsernamePasswordAuthenticationToken.class), eq(claims));
|
||||
verify(jwtService).addToken(response, newToken);
|
||||
verify(filterChain).doFilter(request, response);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,8 +8,6 @@ import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.Mockito.atLeast;
|
||||
import static org.mockito.Mockito.contains;
|
||||
import static org.mockito.Mockito.eq;
|
||||
import static org.mockito.Mockito.lenient;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
@@ -17,7 +15,6 @@ import static org.mockito.Mockito.when;
|
||||
|
||||
import java.security.KeyPair;
|
||||
import java.security.KeyPairGenerator;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Base64;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
@@ -27,13 +24,10 @@ import java.util.Optional;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.security.core.Authentication;
|
||||
|
||||
import jakarta.servlet.http.Cookie;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
|
||||
@@ -59,7 +53,7 @@ class JwtServiceTest {
|
||||
private JwtVerificationKey testVerificationKey;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() throws NoSuchAlgorithmException {
|
||||
void setUp() throws Exception {
|
||||
// Generate a test keypair
|
||||
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
|
||||
keyPairGenerator.initialize(2048);
|
||||
@@ -71,6 +65,11 @@ class JwtServiceTest {
|
||||
testVerificationKey = new JwtVerificationKey("test-key-id", encodedPublicKey);
|
||||
|
||||
jwtService = new JwtService(true, keystoreService);
|
||||
|
||||
// Set the issuer field using reflection since @Value annotation isn't populated in tests
|
||||
java.lang.reflect.Field issuerField = JwtService.class.getDeclaredField("issuer");
|
||||
issuerField.setAccessible(true);
|
||||
issuerField.set(jwtService, "https://stirling.com");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -224,7 +223,8 @@ class JwtServiceTest {
|
||||
assertEquals("admin", extractedClaims.get("role"));
|
||||
assertEquals("IT", extractedClaims.get("department"));
|
||||
assertEquals(username, extractedClaims.get("sub"));
|
||||
assertEquals("Stirling PDF", extractedClaims.get("iss"));
|
||||
// Issuer is now configurable, so check it exists rather than exact value
|
||||
assertNotNull(extractedClaims.get("iss"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -239,62 +239,27 @@ class JwtServiceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void testExtractTokenWithCookie() {
|
||||
void testExtractTokenWithAuthorizationHeader() {
|
||||
String token = "test-token";
|
||||
Cookie[] cookies = {new Cookie("stirling_jwt", token)};
|
||||
when(request.getCookies()).thenReturn(cookies);
|
||||
when(request.getHeader("Authorization")).thenReturn("Bearer " + token);
|
||||
|
||||
assertEquals(token, jwtService.extractToken(request));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testExtractTokenWithNoCookies() {
|
||||
when(request.getCookies()).thenReturn(null);
|
||||
void testExtractTokenWithNoAuthorizationHeader() {
|
||||
when(request.getHeader("Authorization")).thenReturn(null);
|
||||
|
||||
assertNull(jwtService.extractToken(request));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testExtractTokenWithWrongCookie() {
|
||||
Cookie[] cookies = {new Cookie("OTHER_COOKIE", "value")};
|
||||
when(request.getCookies()).thenReturn(cookies);
|
||||
void testExtractTokenWithInvalidAuthorizationHeaderFormat() {
|
||||
when(request.getHeader("Authorization")).thenReturn("InvalidFormat token");
|
||||
|
||||
assertNull(jwtService.extractToken(request));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testExtractTokenWithInvalidAuthorizationHeader() {
|
||||
when(request.getCookies()).thenReturn(null);
|
||||
|
||||
assertNull(jwtService.extractToken(request));
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(booleans = {true, false})
|
||||
void testAddToken(boolean secureCookie) throws Exception {
|
||||
String token = "test-token";
|
||||
|
||||
// Create new JwtService instance with the secureCookie parameter
|
||||
JwtService testJwtService = createJwtServiceWithSecureCookie(secureCookie);
|
||||
|
||||
testJwtService.addToken(response, token);
|
||||
|
||||
verify(response).addHeader(eq("Set-Cookie"), contains("stirling_jwt=" + token));
|
||||
verify(response).addHeader(eq("Set-Cookie"), contains("HttpOnly"));
|
||||
|
||||
if (secureCookie) {
|
||||
verify(response).addHeader(eq("Set-Cookie"), contains("Secure"));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testClearToken() {
|
||||
jwtService.clearToken(response);
|
||||
|
||||
verify(response).addHeader(eq("Set-Cookie"), contains("stirling_jwt="));
|
||||
verify(response).addHeader(eq("Set-Cookie"), contains("Max-Age=0"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGenerateTokenWithKeyId() throws Exception {
|
||||
String username = "testuser";
|
||||
@@ -373,17 +338,4 @@ class JwtServiceTest {
|
||||
// Verify fallback logic was used
|
||||
verify(keystoreService, atLeast(1)).getActiveKey();
|
||||
}
|
||||
|
||||
private JwtService createJwtServiceWithSecureCookie(boolean secureCookie) throws Exception {
|
||||
// Use reflection to create JwtService with custom secureCookie value
|
||||
JwtService testService = new JwtService(true, keystoreService);
|
||||
|
||||
// Set the secureCookie field using reflection
|
||||
java.lang.reflect.Field secureCookieField =
|
||||
JwtService.class.getDeclaredField("secureCookie");
|
||||
secureCookieField.setAccessible(true);
|
||||
secureCookieField.set(testService, secureCookie);
|
||||
|
||||
return testService;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user