mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-08-02 18:45: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.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
|
||||||
|
@ -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;
|
||||||
|
@ -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 {}",
|
||||||
|
@ -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";
|
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";
|
||||||
}
|
}
|
||||||
|
@ -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(
|
||||||
|
@ -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(
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 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;
|
||||||
|
|
||||||
|
@ -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