Completed SAML2 JWT auth, fixed InResponseTo error

This commit is contained in:
Dario Ghunney Ware 2025-07-18 10:25:14 +01:00
parent b53ac89541
commit ae8980f656
12 changed files with 369 additions and 213 deletions

View File

@ -861,7 +861,7 @@ login.rememberme=Remember me
login.invalid=Invalid username or password. login.invalid=Invalid username or password.
login.locked=Your account has been locked. login.locked=Your account has been locked.
login.signinTitle=Please sign in login.signinTitle=Please sign in
login.ssoSignIn=Login via Single Sign-on login.ssoSignIn=Login via Single Sign-On
login.oAuth2AutoCreateDisabled=OAUTH2 Auto-Create User Disabled login.oAuth2AutoCreateDisabled=OAUTH2 Auto-Create User Disabled
login.oAuth2AdminBlockedUser=Registration or logging in of non-registered users is currently blocked. Please contact the administrator. login.oAuth2AdminBlockedUser=Registration or logging in of non-registered users is currently blocked. Please contact the administrator.
login.oauth2RequestNotFound=Authorization request not found login.oauth2RequestNotFound=Authorization request not found
@ -876,6 +876,7 @@ login.alreadyLoggedIn=You are already logged in to
login.alreadyLoggedIn2=devices. Please log out of the devices and try again. login.alreadyLoggedIn2=devices. Please log out of the devices and try again.
login.toManySessions=You have too many active sessions login.toManySessions=You have too many active sessions
login.logoutMessage=You have been logged out. login.logoutMessage=You have been logged out.
login.invalidInResponseTo=The requested SAML response is invalid or has expired. Please contact the administrator.
#auto-redact #auto-redact
autoRedact.title=Auto Redact autoRedact.title=Auto Redact

View File

@ -19,7 +19,7 @@ import stirling.software.proprietary.audit.AuditEventType;
import stirling.software.proprietary.audit.AuditLevel; import stirling.software.proprietary.audit.AuditLevel;
import stirling.software.proprietary.audit.Audited; import stirling.software.proprietary.audit.Audited;
import stirling.software.proprietary.security.model.AuthenticationType; import stirling.software.proprietary.security.model.AuthenticationType;
import stirling.software.proprietary.security.service.JWTServiceInterface; import stirling.software.proprietary.security.service.JwtServiceInterface;
import stirling.software.proprietary.security.service.LoginAttemptService; import stirling.software.proprietary.security.service.LoginAttemptService;
import stirling.software.proprietary.security.service.UserService; import stirling.software.proprietary.security.service.UserService;
@ -29,12 +29,12 @@ public class CustomAuthenticationSuccessHandler
private final LoginAttemptService loginAttemptService; private final LoginAttemptService loginAttemptService;
private final UserService userService; private final UserService userService;
private final JWTServiceInterface jwtService; private final JwtServiceInterface jwtService;
public CustomAuthenticationSuccessHandler( public CustomAuthenticationSuccessHandler(
LoginAttemptService loginAttemptService, LoginAttemptService loginAttemptService,
UserService userService, UserService userService,
JWTServiceInterface jwtService) { JwtServiceInterface jwtService) {
this.loginAttemptService = loginAttemptService; this.loginAttemptService = loginAttemptService;
this.userService = userService; this.userService = userService;
this.jwtService = jwtService; this.jwtService = jwtService;

View File

@ -7,6 +7,7 @@ import java.util.ArrayList;
import java.util.List; import java.util.List;
import org.springframework.core.io.Resource; import org.springframework.core.io.Resource;
import org.springframework.http.HttpStatus;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
@ -33,7 +34,7 @@ import stirling.software.proprietary.audit.AuditLevel;
import stirling.software.proprietary.audit.Audited; import stirling.software.proprietary.audit.Audited;
import stirling.software.proprietary.security.saml2.CertificateUtils; import stirling.software.proprietary.security.saml2.CertificateUtils;
import stirling.software.proprietary.security.saml2.CustomSaml2AuthenticatedPrincipal; import stirling.software.proprietary.security.saml2.CustomSaml2AuthenticatedPrincipal;
import stirling.software.proprietary.security.service.JWTServiceInterface; import stirling.software.proprietary.security.service.JwtServiceInterface;
@Slf4j @Slf4j
@RequiredArgsConstructor @RequiredArgsConstructor
@ -45,7 +46,7 @@ public class CustomLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler {
private final AppConfig appConfig; private final AppConfig appConfig;
private final JWTServiceInterface jwtService; private final JwtServiceInterface jwtService;
@Override @Override
@Audited(type = AuditEventType.USER_LOGOUT, level = AuditLevel.BASIC) @Audited(type = AuditEventType.USER_LOGOUT, level = AuditLevel.BASIC)
@ -116,7 +117,10 @@ public class CustomLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler {
samlClient.setSPKeys(certificate, privateKey); samlClient.setSPKeys(certificate, privateKey);
// Redirect to identity provider for logout. todo: add relay state // Redirect to identity provider for logout. todo: add relay state
samlClient.redirectToIdentityProvider(response, null, nameIdValue); // samlClient.redirectToIdentityProvider(response, null, nameIdValue);
samlClient.processLogoutRequestPostFromIdentityProvider(request, nameIdValue);
samlClient.redirectToIdentityProviderLogout(
response, HttpStatus.OK.name(), nameIdValue);
} catch (Exception e) { } catch (Exception e) {
log.error( log.error(
"Error retrieving logout URL from Provider {} for user {}", "Error retrieving logout URL from Provider {} for user {}",

View File

@ -0,0 +1,22 @@
package stirling.software.proprietary.security;
import java.io.IOException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(
HttpServletRequest request,
HttpServletResponse response,
AuthenticationException authException)
throws IOException {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, authException.getMessage());
}
}

View File

@ -184,7 +184,7 @@ public class AccountWebController {
errorOAuth = "login.relyingPartyRegistrationNotFound"; errorOAuth = "login.relyingPartyRegistrationNotFound";
// Valid InResponseTo was not available from the validation context, unable to // Valid InResponseTo was not available from the validation context, unable to
// evaluate // evaluate
case "invalid_in_response_to" -> errorOAuth = "login.invalid_in_response_to"; case "invalid_in_response_to" -> errorOAuth = "login.invalidInResponseTo";
case "not_authentication_provider_found" -> case "not_authentication_provider_found" ->
errorOAuth = "login.not_authentication_provider_found"; errorOAuth = "login.not_authentication_provider_found";
} }

View File

@ -38,7 +38,7 @@ import stirling.software.common.model.ApplicationProperties;
import stirling.software.proprietary.security.CustomAuthenticationFailureHandler; import stirling.software.proprietary.security.CustomAuthenticationFailureHandler;
import stirling.software.proprietary.security.CustomAuthenticationSuccessHandler; import stirling.software.proprietary.security.CustomAuthenticationSuccessHandler;
import stirling.software.proprietary.security.CustomLogoutSuccessHandler; import stirling.software.proprietary.security.CustomLogoutSuccessHandler;
import stirling.software.proprietary.security.JWTAuthenticationEntryPoint; import stirling.software.proprietary.security.JwtAuthenticationEntryPoint;
import stirling.software.proprietary.security.database.repository.JPATokenRepositoryImpl; import stirling.software.proprietary.security.database.repository.JPATokenRepositoryImpl;
import stirling.software.proprietary.security.database.repository.PersistentLoginRepository; import stirling.software.proprietary.security.database.repository.PersistentLoginRepository;
import stirling.software.proprietary.security.filter.FirstLoginFilter; import stirling.software.proprietary.security.filter.FirstLoginFilter;
@ -53,7 +53,7 @@ import stirling.software.proprietary.security.saml2.CustomSaml2AuthenticationSuc
import stirling.software.proprietary.security.saml2.CustomSaml2ResponseAuthenticationConverter; import stirling.software.proprietary.security.saml2.CustomSaml2ResponseAuthenticationConverter;
import stirling.software.proprietary.security.service.CustomOAuth2UserService; import stirling.software.proprietary.security.service.CustomOAuth2UserService;
import stirling.software.proprietary.security.service.CustomUserDetailsService; import stirling.software.proprietary.security.service.CustomUserDetailsService;
import stirling.software.proprietary.security.service.JWTServiceInterface; import stirling.software.proprietary.security.service.JwtServiceInterface;
import stirling.software.proprietary.security.service.LoginAttemptService; import stirling.software.proprietary.security.service.LoginAttemptService;
import stirling.software.proprietary.security.service.UserService; import stirling.software.proprietary.security.service.UserService;
import stirling.software.proprietary.security.session.SessionPersistentRegistry; import stirling.software.proprietary.security.session.SessionPersistentRegistry;
@ -73,8 +73,8 @@ public class SecurityConfiguration {
private final ApplicationProperties.Security securityProperties; private final ApplicationProperties.Security securityProperties;
private final AppConfig appConfig; private final AppConfig appConfig;
private final UserAuthenticationFilter userAuthenticationFilter; private final UserAuthenticationFilter userAuthenticationFilter;
private final JWTServiceInterface jwtService; private final JwtServiceInterface jwtService;
private final JWTAuthenticationEntryPoint jwtAuthenticationEntryPoint; private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
private final LoginAttemptService loginAttemptService; private final LoginAttemptService loginAttemptService;
private final FirstLoginFilter firstLoginFilter; private final FirstLoginFilter firstLoginFilter;
private final SessionPersistentRegistry sessionRegistry; private final SessionPersistentRegistry sessionRegistry;
@ -92,8 +92,8 @@ public class SecurityConfiguration {
AppConfig appConfig, AppConfig appConfig,
ApplicationProperties.Security securityProperties, ApplicationProperties.Security securityProperties,
UserAuthenticationFilter userAuthenticationFilter, UserAuthenticationFilter userAuthenticationFilter,
JWTServiceInterface jwtService, JwtServiceInterface jwtService,
JWTAuthenticationEntryPoint jwtAuthenticationEntryPoint, JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint,
LoginAttemptService loginAttemptService, LoginAttemptService loginAttemptService,
FirstLoginFilter firstLoginFilter, FirstLoginFilter firstLoginFilter,
SessionPersistentRegistry sessionRegistry, SessionPersistentRegistry sessionRegistry,
@ -185,7 +185,7 @@ public class SecurityConfiguration {
// Configure session management based on JWT setting // Configure session management based on JWT setting
http.sessionManagement( http.sessionManagement(
sessionManagement -> { sessionManagement -> {
if (v2Enabled) { if (v2Enabled && !securityProperties.isSaml2Active()) {
sessionManagement.sessionCreationPolicy( sessionManagement.sessionCreationPolicy(
SessionCreationPolicy.STATELESS); SessionCreationPolicy.STATELESS);
} else { } else {
@ -306,7 +306,6 @@ public class SecurityConfiguration {
} }
// Handle SAML // Handle SAML
if (securityProperties.isSaml2Active() && runningProOrHigher) { if (securityProperties.isSaml2Active() && runningProOrHigher) {
// Configure the authentication provider
OpenSaml4AuthenticationProvider authenticationProvider = OpenSaml4AuthenticationProvider authenticationProvider =
new OpenSaml4AuthenticationProvider(); new OpenSaml4AuthenticationProvider();
authenticationProvider.setResponseAuthenticationConverter( authenticationProvider.setResponseAuthenticationConverter(

View File

@ -25,7 +25,7 @@ import stirling.software.common.model.ApplicationProperties;
import stirling.software.common.model.exception.UnsupportedProviderException; import stirling.software.common.model.exception.UnsupportedProviderException;
import stirling.software.common.util.RequestUriUtils; import stirling.software.common.util.RequestUriUtils;
import stirling.software.proprietary.security.model.AuthenticationType; import stirling.software.proprietary.security.model.AuthenticationType;
import stirling.software.proprietary.security.service.JWTServiceInterface; import stirling.software.proprietary.security.service.JwtServiceInterface;
import stirling.software.proprietary.security.service.LoginAttemptService; import stirling.software.proprietary.security.service.LoginAttemptService;
import stirling.software.proprietary.security.service.UserService; import stirling.software.proprietary.security.service.UserService;
@ -36,7 +36,7 @@ public class CustomOAuth2AuthenticationSuccessHandler
private final LoginAttemptService loginAttemptService; private final LoginAttemptService loginAttemptService;
private final ApplicationProperties.Security.OAUTH2 oauth2Properties; private final ApplicationProperties.Security.OAUTH2 oauth2Properties;
private final UserService userService; private final UserService userService;
private final JWTServiceInterface jwtService; private final JwtServiceInterface jwtService;
@Override @Override
public void onAuthenticationSuccess( public void onAuthenticationSuccess(

View File

@ -24,7 +24,7 @@ import stirling.software.common.model.ApplicationProperties;
import stirling.software.common.model.exception.UnsupportedProviderException; import stirling.software.common.model.exception.UnsupportedProviderException;
import stirling.software.common.util.RequestUriUtils; import stirling.software.common.util.RequestUriUtils;
import stirling.software.proprietary.security.model.AuthenticationType; import stirling.software.proprietary.security.model.AuthenticationType;
import stirling.software.proprietary.security.service.JWTServiceInterface; import stirling.software.proprietary.security.service.JwtServiceInterface;
import stirling.software.proprietary.security.service.LoginAttemptService; import stirling.software.proprietary.security.service.LoginAttemptService;
import stirling.software.proprietary.security.service.UserService; import stirling.software.proprietary.security.service.UserService;
@ -36,7 +36,7 @@ public class CustomSaml2AuthenticationSuccessHandler
private LoginAttemptService loginAttemptService; private LoginAttemptService loginAttemptService;
private ApplicationProperties.Security.SAML2 saml2Properties; private ApplicationProperties.Security.SAML2 saml2Properties;
private UserService userService; private UserService userService;
private final JWTServiceInterface jwtService; private final JwtServiceInterface jwtService;
@Override @Override
public void onAuthenticationSuccess( public void onAuthenticationSuccess(
@ -70,17 +70,6 @@ public class CustomSaml2AuthenticationSuccessHandler
savedRequest.getRedirectUrl()); savedRequest.getRedirectUrl());
super.onAuthenticationSuccess(request, response, authentication); super.onAuthenticationSuccess(request, response, authentication);
} else { } else {
if (jwtService.isJwtEnabled()) {
String jwt =
jwtService.generateToken(
authentication, Map.of("authType", AuthenticationType.SAML2));
jwtService.addTokenToResponse(response, jwt);
super.onAuthenticationSuccess(request, response, authentication);
// getRedirectStrategy().sendRedirect(request, response,
// "/");
// return;
}
log.debug( log.debug(
"Processing SAML2 authentication with autoCreateUser: {}", "Processing SAML2 authentication with autoCreateUser: {}",
saml2Properties.getAutoCreateUser()); saml2Properties.getAutoCreateUser());
@ -121,7 +110,7 @@ public class CustomSaml2AuthenticationSuccessHandler
} }
try { try {
if (saml2Properties.getBlockRegistration() && !userExists) { if (!userExists || saml2Properties.getBlockRegistration()) {
log.debug("Registration blocked for new user: {}", username); log.debug("Registration blocked for new user: {}", username);
response.sendRedirect( response.sendRedirect(
contextPath + "/login?errorOAuth=oAuth2AdminBlockedUser"); contextPath + "/login?errorOAuth=oAuth2AdminBlockedUser");
@ -131,6 +120,8 @@ public class CustomSaml2AuthenticationSuccessHandler
userService.processSSOPostLogin( userService.processSSOPostLogin(
username, saml2Properties.getAutoCreateUser(), SAML2); username, saml2Properties.getAutoCreateUser(), SAML2);
log.debug("Successfully processed authentication for user: {}", username); log.debug("Successfully processed authentication for user: {}", username);
generateJWT(response, authentication);
response.sendRedirect(contextPath + "/"); response.sendRedirect(contextPath + "/");
} catch (IllegalArgumentException | SQLException | UnsupportedProviderException e) { } catch (IllegalArgumentException | SQLException | UnsupportedProviderException e) {
log.debug( log.debug(
@ -144,4 +135,13 @@ public class CustomSaml2AuthenticationSuccessHandler
super.onAuthenticationSuccess(request, response, authentication); super.onAuthenticationSuccess(request, response, authentication);
} }
} }
private void generateJWT(HttpServletResponse response, Authentication authentication) {
if (jwtService.isJwtEnabled()) {
String jwt =
jwtService.generateToken(
authentication, Map.of("authType", AuthenticationType.SAML2));
jwtService.addTokenToResponse(response, jwt);
}
}
} }

View File

@ -1,179 +0,0 @@
package stirling.software.proprietary.security.saml2;
import java.security.cert.X509Certificate;
import java.util.Collections;
import java.util.UUID;
import org.opensaml.saml.saml2.core.AuthnRequest;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.Resource;
import org.springframework.security.saml2.core.Saml2X509Credential;
import org.springframework.security.saml2.core.Saml2X509Credential.Saml2X509CredentialType;
import org.springframework.security.saml2.provider.service.authentication.AbstractSaml2AuthenticationRequest;
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.Saml2MessageBinding;
import org.springframework.security.saml2.provider.service.web.HttpSessionSaml2AuthenticationRequestRepository;
import org.springframework.security.saml2.provider.service.web.authentication.OpenSaml4AuthenticationRequestResolver;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import stirling.software.common.model.ApplicationProperties;
import stirling.software.common.model.ApplicationProperties.Security.SAML2;
@Configuration
@Slf4j
@ConditionalOnProperty(value = "security.saml2.enabled", havingValue = "true")
@RequiredArgsConstructor
public class SAML2Configuration {
private final ApplicationProperties applicationProperties;
@Bean
@ConditionalOnProperty(name = "security.saml2.enabled", havingValue = "true")
public RelyingPartyRegistrationRepository relyingPartyRegistrations() throws Exception {
SAML2 samlConf = applicationProperties.getSecurity().getSaml2();
X509Certificate idpCert = CertificateUtils.readCertificate(samlConf.getIdpCert());
Saml2X509Credential verificationCredential = Saml2X509Credential.verification(idpCert);
Resource privateKeyResource = samlConf.getPrivateKey();
Resource certificateResource = samlConf.getSpCert();
Saml2X509Credential signingCredential =
new Saml2X509Credential(
CertificateUtils.readPrivateKey(privateKeyResource),
CertificateUtils.readCertificate(certificateResource),
Saml2X509CredentialType.SIGNING);
RelyingPartyRegistration rp =
RelyingPartyRegistration.withRegistrationId(samlConf.getRegistrationId())
.signingX509Credentials(c -> c.add(signingCredential))
.entityId(samlConf.getIdpIssuer())
.singleLogoutServiceBinding(Saml2MessageBinding.POST)
.singleLogoutServiceLocation(samlConf.getIdpSingleLogoutUrl())
.singleLogoutServiceResponseLocation("{baseUrl}:{basePort}/login")
.assertionConsumerServiceBinding(Saml2MessageBinding.POST)
.assertionConsumerServiceLocation(
"{baseUrl}/login/saml2/sso/{registrationId}")
.assertingPartyMetadata(
metadata ->
metadata.entityId(samlConf.getIdpIssuer())
.verificationX509Credentials(
c -> c.add(verificationCredential))
.singleSignOnServiceBinding(
Saml2MessageBinding.POST)
.singleSignOnServiceLocation(
samlConf.getIdpSingleLoginUrl())
.singleLogoutServiceBinding(
Saml2MessageBinding.POST)
.singleLogoutServiceLocation(
samlConf.getIdpSingleLogoutUrl())
.wantAuthnRequestsSigned(true))
.build();
return new InMemoryRelyingPartyRegistrationRepository(rp);
}
@Bean
@ConditionalOnProperty(name = "security.saml2.enabled", havingValue = "true")
public HttpSessionSaml2AuthenticationRequestRepository saml2AuthenticationRequestRepository() {
return new HttpSessionSaml2AuthenticationRequestRepository();
}
@Bean
@ConditionalOnProperty(name = "security.saml2.enabled", havingValue = "true")
public OpenSaml4AuthenticationRequestResolver authenticationRequestResolver(
RelyingPartyRegistrationRepository relyingPartyRegistrationRepository,
HttpSessionSaml2AuthenticationRequestRepository saml2AuthenticationRequestRepository) {
OpenSaml4AuthenticationRequestResolver resolver =
new OpenSaml4AuthenticationRequestResolver(relyingPartyRegistrationRepository);
resolver.setAuthnRequestCustomizer(
customizer -> {
HttpServletRequest request = customizer.getRequest();
AuthnRequest authnRequest = customizer.getAuthnRequest();
AbstractSaml2AuthenticationRequest saml2AuthenticationRequest =
saml2AuthenticationRequestRepository.loadAuthenticationRequest(request);
if (saml2AuthenticationRequest != null) {
String sessionId = request.getSession(false).getId();
log.debug(
"Retrieving SAML 2 authentication request ID from the current HTTP session {}",
sessionId);
String authenticationRequestId = saml2AuthenticationRequest.getId();
if (!authenticationRequestId.isBlank()) {
authnRequest.setID(authenticationRequestId);
} else {
log.warn(
"No authentication request found for HTTP session {}. Generating new ID",
sessionId);
authnRequest.setID("ARQ" + UUID.randomUUID().toString().substring(1));
}
} else {
log.debug("Generating new authentication request ID");
authnRequest.setID("ARQ" + UUID.randomUUID().toString().substring(1));
}
logAuthnRequestDetails(authnRequest);
logHttpRequestDetails(request);
});
return resolver;
}
private static void logAuthnRequestDetails(AuthnRequest authnRequest) {
String message =
"""
AuthnRequest:
ID: {}
Issuer: {}
IssueInstant: {}
AssertionConsumerService (ACS) URL: {}
""";
log.debug(
message,
authnRequest.getID(),
authnRequest.getIssuer() != null ? authnRequest.getIssuer().getValue() : null,
authnRequest.getIssueInstant(),
authnRequest.getAssertionConsumerServiceURL());
if (authnRequest.getNameIDPolicy() != null) {
log.debug("NameIDPolicy Format: {}", authnRequest.getNameIDPolicy().getFormat());
}
}
private static void logHttpRequestDetails(HttpServletRequest request) {
log.debug("HTTP Headers: ");
Collections.list(request.getHeaderNames())
.forEach(
headerName ->
log.debug("{}: {}", headerName, request.getHeader(headerName)));
String message =
"""
HTTP Request Method: {}
Session ID: {}
Request Path: {}
Query String: {}
Remote Address: {}
SAML Request Parameters:
SAMLRequest: {}
RelayState: {}
""";
log.debug(
message,
request.getMethod(),
request.getSession().getId(),
request.getRequestURI(),
request.getQueryString(),
request.getRemoteAddr(),
request.getParameter("SAMLRequest"),
request.getParameter("RelayState"));
}
}

View File

@ -11,7 +11,7 @@ import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
import stirling.software.common.configuration.AppConfig; import stirling.software.common.configuration.AppConfig;
import stirling.software.common.model.ApplicationProperties; import stirling.software.common.model.ApplicationProperties;
import stirling.software.proprietary.security.service.JWTServiceInterface; import stirling.software.proprietary.security.service.JwtServiceInterface;
import static org.mockito.Mockito.*; import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class) @ExtendWith(MockitoExtension.class)
@ -21,7 +21,7 @@ class CustomLogoutSuccessHandlerTest {
@Mock private AppConfig appConfig; @Mock private AppConfig appConfig;
@Mock private JWTServiceInterface jwtService; @Mock private JwtServiceInterface jwtService;
@InjectMocks private CustomLogoutSuccessHandler customLogoutSuccessHandler; @InjectMocks private CustomLogoutSuccessHandler customLogoutSuccessHandler;

View File

@ -0,0 +1,40 @@
package stirling.software.proprietary.security;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.io.IOException;
import stirling.software.proprietary.security.model.exception.AuthenticationFailureException;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class JwtAuthenticationEntryPointTest {
@Mock
private HttpServletRequest request;
@Mock
private HttpServletResponse response;
@Mock
private AuthenticationFailureException authException;
@InjectMocks
private JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
@Test
void testCommence() throws IOException {
String errorMessage = "Authentication failed";
when(authException.getMessage()).thenReturn(errorMessage);
jwtAuthenticationEntryPoint.commence(request, response, authException);
verify(response).sendError(HttpServletResponse.SC_UNAUTHORIZED, errorMessage);
}
}

View File

@ -0,0 +1,269 @@
package stirling.software.proprietary.security.service;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.util.Collections;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.security.core.Authentication;
import stirling.software.common.model.ApplicationProperties;
import stirling.software.proprietary.security.model.User;
import stirling.software.proprietary.security.model.exception.AuthenticationFailureException;
import java.util.HashMap;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
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.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.contains;
import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class JwtServiceTest {
@Mock
private ApplicationProperties.Security securityProperties;
@Mock
private Authentication authentication;
@Mock
private User userDetails;
@Mock
private HttpServletRequest request;
@Mock
private HttpServletResponse response;
private JwtService jwtService;
@BeforeEach
void setUp() {
jwtService = new JwtService(true);
}
@Test
void testGenerateTokenWithAuthentication() {
String username = "testuser";
when(authentication.getPrincipal()).thenReturn(userDetails);
when(userDetails.getUsername()).thenReturn(username);
when(authentication.getPrincipal()).thenReturn(userDetails);
when(userDetails.getUsername()).thenReturn(username);
String token = jwtService.generateToken(authentication, Collections.emptyMap());
assertNotNull(token);
assertTrue(!token.isEmpty());
assertEquals(username, jwtService.extractUsername(token));
}
@Test
void testGenerateTokenWithUsernameAndClaims() {
String username = "testuser";
Map<String, Object> claims = new HashMap<>();
claims.put("role", "admin");
claims.put("department", "IT");
when(authentication.getPrincipal()).thenReturn(userDetails);
when(userDetails.getUsername()).thenReturn(username);
String token = jwtService.generateToken(authentication, claims);
assertNotNull(token);
assertFalse(token.isEmpty());
assertEquals(username, jwtService.extractUsername(token));
Map<String, Object> extractedClaims = jwtService.extractAllClaims(token);
assertEquals("admin", extractedClaims.get("role"));
assertEquals("IT", extractedClaims.get("department"));
}
@Test
void testValidateTokenSuccess() {
String token = jwtService.generateToken(authentication, new HashMap<>());
assertDoesNotThrow(() -> jwtService.validateToken(token));
}
@Test
void testValidateTokenWithInvalidToken() {
assertThrows(AuthenticationFailureException.class, () -> {
jwtService.validateToken("invalid-token");
});
}
// fixme
// @Test
// void testValidateTokenWithExpiredToken() {
// // Create a token that expires immediately
// JWTService shortLivedJwtService = new JWTService(true);
// String token = shortLivedJwtService.generateToken("testuser", new HashMap<>());
//
// // Wait a bit to ensure expiration
// try {
// Thread.sleep(10);
// } catch (InterruptedException e) {
// Thread.currentThread().interrupt();
// }
//
// assertThrows(AuthenticationFailureException.class, () -> {
// shortLivedJwtService.validateToken(token);
// });
// }
@Test
void testValidateTokenWithMalformedToken() {
AuthenticationFailureException exception = assertThrows(AuthenticationFailureException.class, () -> {
jwtService.validateToken("malformed.token");
});
assertTrue(exception.getMessage().contains("Invalid"));
}
@Test
void testValidateTokenWithEmptyToken() {
AuthenticationFailureException exception = assertThrows(AuthenticationFailureException.class, () -> {
jwtService.validateToken("");
});
assertTrue(exception.getMessage().contains("Claims are empty") || exception.getMessage().contains("Invalid"));
}
@Test
void testExtractUsername() {
String username = "testuser";
User user = mock(User.class);
Map<String, Object> claims = Map.of("sub", "testuser", "authType", "WEB");
when(authentication.getPrincipal()).thenReturn(user);
when(user.getUsername()).thenReturn(username);
String token = jwtService.generateToken(authentication, claims);
assertEquals(username, jwtService.extractUsername(token));
}
@Test
void testExtractUsernameWithInvalidToken() {
assertThrows(AuthenticationFailureException.class, () -> jwtService.extractUsername("invalid-token"));
}
@Test
void testExtractAllClaims() {
String username = "testuser";
Map<String, Object> claims = Map.of("role", "admin", "department", "IT");
when(authentication.getPrincipal()).thenReturn(userDetails);
when(userDetails.getUsername()).thenReturn(username);
String token = jwtService.generateToken(authentication, claims);
Map<String, Object> extractedClaims = jwtService.extractAllClaims(token);
assertEquals("admin", extractedClaims.get("role"));
assertEquals("IT", extractedClaims.get("department"));
assertEquals(username, extractedClaims.get("sub"));
assertEquals("Stirling PDF", extractedClaims.get("iss"));
}
@Test
void testExtractAllClaimsWithInvalidToken() {
assertThrows(AuthenticationFailureException.class, () -> jwtService.extractAllClaims("invalid-token"));
}
// fixme
// @Test
// void testIsTokenExpired() {
// String token = jwtService.generateToken("testuser", new HashMap<>());
// assertFalse(jwtService.isTokenExpired(token));
//
// JWTService shortLivedJwtService = new JWTService();
// String expiredToken = shortLivedJwtService.generateToken("testuser", new HashMap<>());
//
// try {
// Thread.sleep(10);
// } catch (InterruptedException e) {
// Thread.currentThread().interrupt();
// }
//
// assertThrows(AuthenticationFailureException.class, () -> shortLivedJwtService.isTokenExpired(expiredToken));
// }
@Test
void testExtractTokenFromRequestWithAuthorizationHeader() {
String token = "test-token";
when(request.getHeader("Authorization")).thenReturn("Bearer " + token);
assertEquals(token, jwtService.extractTokenFromRequest(request));
}
@Test
void testExtractTokenFromRequestWithCookie() {
String token = "test-token";
Cookie[] cookies = { new Cookie("STIRLING_JWT", token) };
when(request.getHeader("Authorization")).thenReturn(null);
when(request.getCookies()).thenReturn(cookies);
assertEquals(token, jwtService.extractTokenFromRequest(request));
}
@Test
void testExtractTokenFromRequestWithNoCookies() {
when(request.getHeader("Authorization")).thenReturn(null);
when(request.getCookies()).thenReturn(null);
assertNull(jwtService.extractTokenFromRequest(request));
}
@Test
void testExtractTokenFromRequestWithWrongCookie() {
Cookie[] cookies = {new Cookie("OTHER_COOKIE", "value")};
when(request.getHeader("Authorization")).thenReturn(null);
when(request.getCookies()).thenReturn(cookies);
assertNull(jwtService.extractTokenFromRequest(request));
}
@Test
void testExtractTokenFromRequestWithInvalidAuthorizationHeader() {
when(request.getHeader("Authorization")).thenReturn("Basic token");
when(request.getCookies()).thenReturn(null);
assertNull(jwtService.extractTokenFromRequest(request));
}
@Test
void testAddTokenToResponse() {
String token = "test-token";
jwtService.addTokenToResponse(response, token);
verify(response).setHeader("Authorization", "Bearer " + token);
verify(response).addHeader(eq("Set-Cookie"), contains("STIRLING_JWT=" + token));
verify(response).addHeader(eq("Set-Cookie"), contains("HttpOnly"));
verify(response).addHeader(eq("Set-Cookie"), contains("Secure"));
// verify(response).addHeader(eq("Set-Cookie"), contains("SameSite=Strict"));
}
@Test
void testClearTokenFromResponse() {
jwtService.clearTokenFromResponse(response);
verify(response).setHeader("Authorization", "");
verify(response).addHeader(eq("Set-Cookie"), contains("STIRLING_JWT="));
verify(response).addHeader(eq("Set-Cookie"), contains("Max-Age=0"));
}
}