mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-08-02 02:25:21 +00:00
Completed SAML2 JWT auth, fixed InResponseTo error
This commit is contained in:
parent
b53ac89541
commit
ae8980f656
@ -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
|
||||
|
@ -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;
|
||||
|
@ -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 {}",
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
@ -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";
|
||||
}
|
||||
|
@ -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(
|
||||
|
@ -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(
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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"));
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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"));
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user