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.locked=Your account has been locked.
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.oAuth2AdminBlockedUser=Registration or logging in of non-registered users is currently blocked. Please contact the administrator.
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.toManySessions=You have too many active sessions
login.logoutMessage=You have been logged out.
login.invalidInResponseTo=The requested SAML response is invalid or has expired. Please contact the administrator.
#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.Audited;
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.UserService;
@ -29,12 +29,12 @@ public class CustomAuthenticationSuccessHandler
private final LoginAttemptService loginAttemptService;
private final UserService userService;
private final JWTServiceInterface jwtService;
private final JwtServiceInterface jwtService;
public CustomAuthenticationSuccessHandler(
LoginAttemptService loginAttemptService,
UserService userService,
JWTServiceInterface jwtService) {
JwtServiceInterface jwtService) {
this.loginAttemptService = loginAttemptService;
this.userService = userService;
this.jwtService = jwtService;

View File

@ -7,6 +7,7 @@ import java.util.ArrayList;
import java.util.List;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpStatus;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
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.security.saml2.CertificateUtils;
import stirling.software.proprietary.security.saml2.CustomSaml2AuthenticatedPrincipal;
import stirling.software.proprietary.security.service.JWTServiceInterface;
import stirling.software.proprietary.security.service.JwtServiceInterface;
@Slf4j
@RequiredArgsConstructor
@ -45,7 +46,7 @@ public class CustomLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler {
private final AppConfig appConfig;
private final JWTServiceInterface jwtService;
private final JwtServiceInterface jwtService;
@Override
@Audited(type = AuditEventType.USER_LOGOUT, level = AuditLevel.BASIC)
@ -116,7 +117,10 @@ public class CustomLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler {
samlClient.setSPKeys(certificate, privateKey);
// 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) {
log.error(
"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";
// Valid InResponseTo was not available from the validation context, unable to
// 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" ->
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.CustomAuthenticationSuccessHandler;
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.PersistentLoginRepository;
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.service.CustomOAuth2UserService;
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.UserService;
import stirling.software.proprietary.security.session.SessionPersistentRegistry;
@ -73,8 +73,8 @@ public class SecurityConfiguration {
private final ApplicationProperties.Security securityProperties;
private final AppConfig appConfig;
private final UserAuthenticationFilter userAuthenticationFilter;
private final JWTServiceInterface jwtService;
private final JWTAuthenticationEntryPoint jwtAuthenticationEntryPoint;
private final JwtServiceInterface jwtService;
private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
private final LoginAttemptService loginAttemptService;
private final FirstLoginFilter firstLoginFilter;
private final SessionPersistentRegistry sessionRegistry;
@ -92,8 +92,8 @@ public class SecurityConfiguration {
AppConfig appConfig,
ApplicationProperties.Security securityProperties,
UserAuthenticationFilter userAuthenticationFilter,
JWTServiceInterface jwtService,
JWTAuthenticationEntryPoint jwtAuthenticationEntryPoint,
JwtServiceInterface jwtService,
JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint,
LoginAttemptService loginAttemptService,
FirstLoginFilter firstLoginFilter,
SessionPersistentRegistry sessionRegistry,
@ -185,7 +185,7 @@ public class SecurityConfiguration {
// Configure session management based on JWT setting
http.sessionManagement(
sessionManagement -> {
if (v2Enabled) {
if (v2Enabled && !securityProperties.isSaml2Active()) {
sessionManagement.sessionCreationPolicy(
SessionCreationPolicy.STATELESS);
} else {
@ -306,7 +306,6 @@ public class SecurityConfiguration {
}
// Handle SAML
if (securityProperties.isSaml2Active() && runningProOrHigher) {
// Configure the authentication provider
OpenSaml4AuthenticationProvider authenticationProvider =
new OpenSaml4AuthenticationProvider();
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.util.RequestUriUtils;
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.UserService;
@ -36,7 +36,7 @@ public class CustomOAuth2AuthenticationSuccessHandler
private final LoginAttemptService loginAttemptService;
private final ApplicationProperties.Security.OAUTH2 oauth2Properties;
private final UserService userService;
private final JWTServiceInterface jwtService;
private final JwtServiceInterface jwtService;
@Override
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.util.RequestUriUtils;
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.UserService;
@ -36,7 +36,7 @@ public class CustomSaml2AuthenticationSuccessHandler
private LoginAttemptService loginAttemptService;
private ApplicationProperties.Security.SAML2 saml2Properties;
private UserService userService;
private final JWTServiceInterface jwtService;
private final JwtServiceInterface jwtService;
@Override
public void onAuthenticationSuccess(
@ -70,17 +70,6 @@ public class CustomSaml2AuthenticationSuccessHandler
savedRequest.getRedirectUrl());
super.onAuthenticationSuccess(request, response, authentication);
} 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(
"Processing SAML2 authentication with autoCreateUser: {}",
saml2Properties.getAutoCreateUser());
@ -121,7 +110,7 @@ public class CustomSaml2AuthenticationSuccessHandler
}
try {
if (saml2Properties.getBlockRegistration() && !userExists) {
if (!userExists || saml2Properties.getBlockRegistration()) {
log.debug("Registration blocked for new user: {}", username);
response.sendRedirect(
contextPath + "/login?errorOAuth=oAuth2AdminBlockedUser");
@ -131,6 +120,8 @@ public class CustomSaml2AuthenticationSuccessHandler
userService.processSSOPostLogin(
username, saml2Properties.getAutoCreateUser(), SAML2);
log.debug("Successfully processed authentication for user: {}", username);
generateJWT(response, authentication);
response.sendRedirect(contextPath + "/");
} catch (IllegalArgumentException | SQLException | UnsupportedProviderException e) {
log.debug(
@ -144,4 +135,13 @@ public class CustomSaml2AuthenticationSuccessHandler
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 stirling.software.common.configuration.AppConfig;
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.*;
@ExtendWith(MockitoExtension.class)
@ -21,7 +21,7 @@ class CustomLogoutSuccessHandlerTest {
@Mock private AppConfig appConfig;
@Mock private JWTServiceInterface jwtService;
@Mock private JwtServiceInterface jwtService;
@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"));
}
}