mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-08-27 06:39:24 +00:00
Fixed JWT auth flow
Adding tests
This commit is contained in:
parent
65856e283f
commit
13e985657f
3
.gitignore
vendored
3
.gitignore
vendored
@ -197,3 +197,6 @@ id_ed25519.pub
|
|||||||
|
|
||||||
# node_modules
|
# node_modules
|
||||||
node_modules/
|
node_modules/
|
||||||
|
|
||||||
|
# Claude
|
||||||
|
CLAUDE.md
|
||||||
|
@ -284,7 +284,6 @@ public class ApplicationProperties {
|
|||||||
@Data
|
@Data
|
||||||
public static class JWT {
|
public static class JWT {
|
||||||
private Boolean enabled = false;
|
private Boolean enabled = false;
|
||||||
@ToString.Exclude private String secretKey;
|
|
||||||
private Long expiration = 3600000L; // Default 1 hour in milliseconds
|
private Long expiration = 3600000L; // Default 1 hour in milliseconds
|
||||||
private String algorithm = "HS256"; // Default HMAC algorithm
|
private String algorithm = "HS256"; // Default HMAC algorithm
|
||||||
private String issuer = "Stirling-PDF"; // Default issuer
|
private String issuer = "Stirling-PDF"; // Default issuer
|
||||||
@ -292,12 +291,7 @@ public class ApplicationProperties {
|
|||||||
private Long refreshTokenExpiration = 86400000L; // Default 24 hours
|
private Long refreshTokenExpiration = 86400000L; // Default 24 hours
|
||||||
|
|
||||||
public boolean isSettingsValid() {
|
public boolean isSettingsValid() {
|
||||||
return enabled != null
|
return enabled != null && enabled && expiration != null && expiration > 0;
|
||||||
&& enabled
|
|
||||||
&& secretKey != null
|
|
||||||
&& !secretKey.trim().isEmpty()
|
|
||||||
&& expiration != null
|
|
||||||
&& expiration > 0;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -53,11 +53,11 @@ public class CustomAuthenticationSuccessHandler
|
|||||||
|
|
||||||
// Generate JWT token if JWT authentication is enabled
|
// Generate JWT token if JWT authentication is enabled
|
||||||
boolean jwtEnabled = jwtService.isJwtEnabled();
|
boolean jwtEnabled = jwtService.isJwtEnabled();
|
||||||
if (jwtService != null && jwtEnabled) {
|
if (jwtEnabled) {
|
||||||
try {
|
try {
|
||||||
String jwt = jwtService.generateToken(authentication);
|
String jwt = jwtService.generateToken(authentication);
|
||||||
jwtService.addTokenToResponse(response, jwt);
|
jwtService.addTokenToResponse(response, jwt);
|
||||||
log.debug("JWT token generated and added to response for user: {}", userName);
|
log.debug("JWT generated for user: {}", userName);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("Failed to generate JWT token for user: {}", userName, e);
|
log.error("Failed to generate JWT token for user: {}", userName, e);
|
||||||
}
|
}
|
||||||
|
@ -53,17 +53,6 @@ public class CustomLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler {
|
|||||||
HttpServletRequest request, HttpServletResponse response, Authentication authentication)
|
HttpServletRequest request, HttpServletResponse response, Authentication authentication)
|
||||||
throws IOException {
|
throws IOException {
|
||||||
|
|
||||||
// Clear JWT token if JWT authentication is enabled
|
|
||||||
if (jwtService != null && jwtService.isJwtEnabled()) {
|
|
||||||
try {
|
|
||||||
jwtService.clearTokenFromResponse(response);
|
|
||||||
log.debug("JWT token cleared from response during logout");
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("Failed to clear JWT token during logout", e);
|
|
||||||
// Continue with normal logout flow even if JWT clearing fails
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!response.isCommitted()) {
|
if (!response.isCommitted()) {
|
||||||
if (authentication != null) {
|
if (authentication != null) {
|
||||||
if (authentication instanceof Saml2Authentication samlAuthentication) {
|
if (authentication instanceof Saml2Authentication samlAuthentication) {
|
||||||
@ -82,6 +71,11 @@ public class CustomLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler {
|
|||||||
authentication.getClass().getSimpleName());
|
authentication.getClass().getSimpleName());
|
||||||
getRedirectStrategy().sendRedirect(request, response, LOGOUT_PATH);
|
getRedirectStrategy().sendRedirect(request, response, LOGOUT_PATH);
|
||||||
}
|
}
|
||||||
|
} else if (jwtService.isJwtEnabled()) {
|
||||||
|
// Clear JWT token if JWT authentication is enabled
|
||||||
|
jwtService.clearTokenFromResponse(response);
|
||||||
|
log.debug("Cleared JWT from response");
|
||||||
|
getRedirectStrategy().sendRedirect(request, response, LOGOUT_PATH);
|
||||||
} else {
|
} else {
|
||||||
// Redirect to login page after logout
|
// Redirect to login page after logout
|
||||||
String path = checkForErrors(request);
|
String path = checkForErrors(request);
|
||||||
|
@ -0,0 +1,23 @@
|
|||||||
|
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, "Unauthorized: " + authException.getMessage());
|
||||||
|
}
|
||||||
|
}
|
@ -1,153 +0,0 @@
|
|||||||
package stirling.software.proprietary.security.configuration;
|
|
||||||
|
|
||||||
import java.security.SecureRandom;
|
|
||||||
import java.util.Base64;
|
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.boot.context.event.ApplicationReadyEvent;
|
|
||||||
import org.springframework.context.event.EventListener;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
|
|
||||||
import stirling.software.common.model.ApplicationProperties;
|
|
||||||
|
|
||||||
@Slf4j
|
|
||||||
@Component
|
|
||||||
public class JWTConfigurationValidator {
|
|
||||||
|
|
||||||
@Autowired private ApplicationProperties applicationProperties;
|
|
||||||
|
|
||||||
@EventListener(ApplicationReadyEvent.class)
|
|
||||||
public void validateJWTConfiguration() {
|
|
||||||
if (!isJwtEnabled()) {
|
|
||||||
log.debug("JWT authentication is disabled");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
log.info("Validating JWT configuration...");
|
|
||||||
|
|
||||||
ApplicationProperties.Security.JWT jwtConfig = applicationProperties.getSecurity().getJwt();
|
|
||||||
|
|
||||||
// Validate basic configuration
|
|
||||||
if (!jwtConfig.isSettingsValid()) {
|
|
||||||
log.error("JWT configuration is invalid. Please check your settings.yml file.");
|
|
||||||
log.error("Required fields: enabled=true, secretKey (Base64 encoded), expiration > 0");
|
|
||||||
throw new IllegalStateException("Invalid JWT configuration");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate secret key length and format
|
|
||||||
validateSecretKey(jwtConfig.getSecretKey());
|
|
||||||
|
|
||||||
// Validate expiration time
|
|
||||||
validateExpiration(jwtConfig.getExpiration());
|
|
||||||
|
|
||||||
// Validate algorithm
|
|
||||||
validateAlgorithm(jwtConfig.getAlgorithm());
|
|
||||||
|
|
||||||
log.info("JWT configuration validated successfully");
|
|
||||||
log.info("JWT algorithm: {}", jwtConfig.getAlgorithm());
|
|
||||||
log.info(
|
|
||||||
"JWT expiration: {} ms ({} minutes)",
|
|
||||||
jwtConfig.getExpiration(),
|
|
||||||
jwtConfig.getExpiration() / 60000);
|
|
||||||
log.info("JWT issuer: {}", jwtConfig.getIssuer());
|
|
||||||
}
|
|
||||||
|
|
||||||
private void validateSecretKey(String secretKey) {
|
|
||||||
if (secretKey == null || secretKey.trim().isEmpty()) {
|
|
||||||
log.error("JWT secret key is not configured");
|
|
||||||
throw new IllegalStateException("JWT secret key is required when JWT is enabled");
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
byte[] decodedKey = Base64.getDecoder().decode(secretKey);
|
|
||||||
|
|
||||||
// For HMAC-SHA256, minimum key length should be 32 bytes (256 bits)
|
|
||||||
if (decodedKey.length < 32) {
|
|
||||||
log.warn(
|
|
||||||
"JWT secret key is shorter than recommended 256 bits. Current length: {} bits",
|
|
||||||
decodedKey.length * 8);
|
|
||||||
log.warn("Consider using a longer key for better security");
|
|
||||||
} else {
|
|
||||||
log.debug("JWT secret key length: {} bits", decodedKey.length * 8);
|
|
||||||
}
|
|
||||||
} catch (IllegalArgumentException e) {
|
|
||||||
log.error("JWT secret key is not a valid Base64 encoded string");
|
|
||||||
log.error("Generate a valid key using: openssl rand -base64 32");
|
|
||||||
throw new IllegalStateException("Invalid JWT secret key format", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void validateExpiration(Long expiration) {
|
|
||||||
if (expiration == null || expiration <= 0) {
|
|
||||||
log.error("JWT expiration time must be positive. Current value: {}", expiration);
|
|
||||||
throw new IllegalStateException("Invalid JWT expiration time");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Warn if expiration is too short (less than 5 minutes)
|
|
||||||
if (expiration < 300000) { // 5 minutes in milliseconds
|
|
||||||
log.warn(
|
|
||||||
"JWT expiration time is very short: {} ms. Consider using a longer expiration time for better user experience.",
|
|
||||||
expiration);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Warn if expiration is too long (more than 24 hours)
|
|
||||||
if (expiration > 86400000) { // 24 hours in milliseconds
|
|
||||||
log.warn(
|
|
||||||
"JWT expiration time is very long: {} ms. Consider using a shorter expiration time for better security.",
|
|
||||||
expiration);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void validateAlgorithm(String algorithm) {
|
|
||||||
if (algorithm == null || algorithm.trim().isEmpty()) {
|
|
||||||
log.warn("JWT algorithm is not specified, defaulting to HS256");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (algorithm.toUpperCase()) {
|
|
||||||
case "HS256", "HS384", "HS512" -> {
|
|
||||||
log.debug("Using HMAC algorithm: {}", algorithm);
|
|
||||||
}
|
|
||||||
case "RS256", "RS384", "RS512" -> {
|
|
||||||
log.debug("Using RSA algorithm: {}", algorithm);
|
|
||||||
log.warn(
|
|
||||||
"RSA algorithms are configured but current implementation uses HMAC. Consider implementing RSA support for production use.");
|
|
||||||
}
|
|
||||||
default -> {
|
|
||||||
log.warn("Unsupported JWT algorithm: {}. Falling back to HS256", algorithm);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate a secure random Base64 encoded secret key for JWT This method is useful for
|
|
||||||
* generating initial secret keys
|
|
||||||
*/
|
|
||||||
public static String generateSecretKey() {
|
|
||||||
SecureRandom secureRandom = new SecureRandom();
|
|
||||||
byte[] key = new byte[32]; // 256 bits
|
|
||||||
secureRandom.nextBytes(key);
|
|
||||||
return Base64.getEncoder().encodeToString(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isJwtEnabled() {
|
|
||||||
return applicationProperties != null
|
|
||||||
&& applicationProperties.getSecurity() != null
|
|
||||||
&& applicationProperties.getSecurity().isJwtActive();
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Provides helpful information for JWT configuration troubleshooting */
|
|
||||||
public void logConfigurationHelp() {
|
|
||||||
log.info("JWT Configuration Help:");
|
|
||||||
log.info("1. Enable JWT: Set jwt.enabled=true in settings.yml");
|
|
||||||
log.info("2. Generate secret key: openssl rand -base64 32");
|
|
||||||
log.info("3. Set expiration: jwt.expiration=3600000 (1 hour in milliseconds)");
|
|
||||||
log.info("4. Example generated secret key: {}", generateSecretKey());
|
|
||||||
log.info("5. Recommended expiration times:");
|
|
||||||
log.info(" - Short sessions: 900000 (15 minutes)");
|
|
||||||
log.info(" - Medium sessions: 3600000 (1 hour)");
|
|
||||||
log.info(" - Long sessions: 14400000 (4 hours)");
|
|
||||||
}
|
|
||||||
}
|
|
@ -38,6 +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.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;
|
||||||
@ -74,6 +75,7 @@ public class SecurityConfiguration {
|
|||||||
private final UserAuthenticationFilter userAuthenticationFilter;
|
private final UserAuthenticationFilter userAuthenticationFilter;
|
||||||
private final JWTAuthenticationFilter jwtAuthenticationFilter;
|
private final JWTAuthenticationFilter jwtAuthenticationFilter;
|
||||||
private final JWTServiceInterface jwtService;
|
private final JWTServiceInterface jwtService;
|
||||||
|
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;
|
||||||
@ -93,6 +95,7 @@ public class SecurityConfiguration {
|
|||||||
UserAuthenticationFilter userAuthenticationFilter,
|
UserAuthenticationFilter userAuthenticationFilter,
|
||||||
JWTAuthenticationFilter jwtAuthenticationFilter,
|
JWTAuthenticationFilter jwtAuthenticationFilter,
|
||||||
JWTServiceInterface jwtService,
|
JWTServiceInterface jwtService,
|
||||||
|
JWTAuthenticationEntryPoint jwtAuthenticationEntryPoint,
|
||||||
LoginAttemptService loginAttemptService,
|
LoginAttemptService loginAttemptService,
|
||||||
FirstLoginFilter firstLoginFilter,
|
FirstLoginFilter firstLoginFilter,
|
||||||
SessionPersistentRegistry sessionRegistry,
|
SessionPersistentRegistry sessionRegistry,
|
||||||
@ -110,6 +113,7 @@ public class SecurityConfiguration {
|
|||||||
this.userAuthenticationFilter = userAuthenticationFilter;
|
this.userAuthenticationFilter = userAuthenticationFilter;
|
||||||
this.jwtAuthenticationFilter = jwtAuthenticationFilter;
|
this.jwtAuthenticationFilter = jwtAuthenticationFilter;
|
||||||
this.jwtService = jwtService;
|
this.jwtService = jwtService;
|
||||||
|
this.jwtAuthenticationEntryPoint = jwtAuthenticationEntryPoint;
|
||||||
this.loginAttemptService = loginAttemptService;
|
this.loginAttemptService = loginAttemptService;
|
||||||
this.firstLoginFilter = firstLoginFilter;
|
this.firstLoginFilter = firstLoginFilter;
|
||||||
this.sessionRegistry = sessionRegistry;
|
this.sessionRegistry = sessionRegistry;
|
||||||
@ -136,16 +140,19 @@ public class SecurityConfiguration {
|
|||||||
if (loginEnabledValue) {
|
if (loginEnabledValue) {
|
||||||
if (jwtEnabled && jwtAuthenticationFilter != null) {
|
if (jwtEnabled && jwtAuthenticationFilter != null) {
|
||||||
http.addFilterBefore(
|
http.addFilterBefore(
|
||||||
jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
|
jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
|
||||||
// .addFilterAfter(
|
.exceptionHandling(
|
||||||
// jwtAuthenticationFilter,
|
exceptionHandling ->
|
||||||
// userAuthenticationFilter.getClass());
|
exceptionHandling.authenticationEntryPoint(
|
||||||
|
jwtAuthenticationEntryPoint));
|
||||||
} else {
|
} else {
|
||||||
http.addFilterBefore(
|
http.addFilterBefore(
|
||||||
userAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
|
userAuthenticationFilter,
|
||||||
|
UsernamePasswordAuthenticationFilter.class)
|
||||||
|
.addFilterBefore(userAuthenticationFilter, firstLoginFilter.getClass());
|
||||||
}
|
}
|
||||||
http.addFilterAfter(firstLoginFilter, UsernamePasswordAuthenticationFilter.class)
|
http.addFilterAfter(rateLimitingFilter(), UsernamePasswordAuthenticationFilter.class);
|
||||||
.addFilterAfter(rateLimitingFilter(), firstLoginFilter.getClass());
|
|
||||||
if (!securityProperties.getCsrfDisabled()) {
|
if (!securityProperties.getCsrfDisabled()) {
|
||||||
CookieCsrfTokenRepository cookieRepo =
|
CookieCsrfTokenRepository cookieRepo =
|
||||||
CookieCsrfTokenRepository.withHttpOnlyFalse();
|
CookieCsrfTokenRepository.withHttpOnlyFalse();
|
||||||
@ -198,7 +205,6 @@ public class SecurityConfiguration {
|
|||||||
});
|
});
|
||||||
http.authenticationProvider(daoAuthenticationProvider());
|
http.authenticationProvider(daoAuthenticationProvider());
|
||||||
http.requestCache(requestCache -> requestCache.requestCache(new NullRequestCache()));
|
http.requestCache(requestCache -> requestCache.requestCache(new NullRequestCache()));
|
||||||
// Configure logout behavior based on JWT setting
|
|
||||||
http.logout(
|
http.logout(
|
||||||
logout ->
|
logout ->
|
||||||
logout.logoutRequestMatcher(
|
logout.logoutRequestMatcher(
|
||||||
@ -211,24 +217,21 @@ public class SecurityConfiguration {
|
|||||||
.invalidateHttpSession(true)
|
.invalidateHttpSession(true)
|
||||||
.deleteCookies(
|
.deleteCookies(
|
||||||
"JSESSIONID", "remember-me", "STIRLING_JWT_TOKEN"));
|
"JSESSIONID", "remember-me", "STIRLING_JWT_TOKEN"));
|
||||||
// Only configure remember-me if JWT is not enabled (stateless) todo: check if remember-me can be used with JWT
|
http.rememberMe(
|
||||||
if (!jwtEnabled) {
|
rememberMeConfigurer -> // Use the configurator directly
|
||||||
http.rememberMe(
|
rememberMeConfigurer
|
||||||
rememberMeConfigurer -> // Use the configurator directly
|
.tokenRepository(persistentTokenRepository())
|
||||||
rememberMeConfigurer
|
.tokenValiditySeconds( // 14 days
|
||||||
.tokenRepository(persistentTokenRepository())
|
14 * 24 * 60 * 60)
|
||||||
.tokenValiditySeconds( // 14 days
|
.userDetailsService( // Your existing UserDetailsService
|
||||||
14 * 24 * 60 * 60)
|
userDetailsService)
|
||||||
.userDetailsService( // Your existing UserDetailsService
|
.useSecureCookie( // Enable secure cookie
|
||||||
userDetailsService)
|
true)
|
||||||
.useSecureCookie( // Enable secure cookie
|
.rememberMeParameter( // Form parameter name
|
||||||
true)
|
"remember-me")
|
||||||
.rememberMeParameter( // Form parameter name
|
.rememberMeCookieName( // Cookie name
|
||||||
"remember-me")
|
"remember-me")
|
||||||
.rememberMeCookieName( // Cookie name
|
.alwaysRemember(false));
|
||||||
"remember-me")
|
|
||||||
.alwaysRemember(false));
|
|
||||||
}
|
|
||||||
http.authorizeHttpRequests(
|
http.authorizeHttpRequests(
|
||||||
authz ->
|
authz ->
|
||||||
authz.requestMatchers(
|
authz.requestMatchers(
|
||||||
@ -253,6 +256,7 @@ public class SecurityConfiguration {
|
|||||||
|| trimmedUri.startsWith("/css/")
|
|| trimmedUri.startsWith("/css/")
|
||||||
|| trimmedUri.startsWith("/fonts/")
|
|| trimmedUri.startsWith("/fonts/")
|
||||||
|| trimmedUri.startsWith("/js/")
|
|| trimmedUri.startsWith("/js/")
|
||||||
|
|| trimmedUri.startsWith("/favicon")
|
||||||
|| trimmedUri.startsWith(
|
|| trimmedUri.startsWith(
|
||||||
"/api/v1/info/status");
|
"/api/v1/info/status");
|
||||||
})
|
})
|
||||||
@ -343,7 +347,6 @@ public class SecurityConfiguration {
|
|||||||
return http.build();
|
return http.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
// todo: check if this is needed
|
|
||||||
@Bean
|
@Bean
|
||||||
public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration)
|
public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration)
|
||||||
throws Exception {
|
throws Exception {
|
||||||
|
@ -7,6 +7,7 @@ import org.springframework.security.authentication.UsernamePasswordAuthenticatio
|
|||||||
import org.springframework.security.core.Authentication;
|
import org.springframework.security.core.Authentication;
|
||||||
import org.springframework.security.core.context.SecurityContextHolder;
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
import org.springframework.security.core.userdetails.UserDetails;
|
import org.springframework.security.core.userdetails.UserDetails;
|
||||||
|
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
||||||
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
|
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import org.springframework.web.filter.OncePerRequestFilter;
|
import org.springframework.web.filter.OncePerRequestFilter;
|
||||||
@ -18,6 +19,7 @@ import jakarta.servlet.http.HttpServletResponse;
|
|||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
import stirling.software.proprietary.security.model.exception.AuthenticationFailureException;
|
||||||
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;
|
||||||
|
|
||||||
@ -43,51 +45,33 @@ public class JWTAuthenticationFilter extends OncePerRequestFilter {
|
|||||||
filterChain.doFilter(request, response);
|
filterChain.doFilter(request, response);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (shouldNotFilter(request)) {
|
||||||
try {
|
filterChain.doFilter(request, response);
|
||||||
if (shouldNotFilter(request)) {
|
|
||||||
filterChain.doFilter(request, response);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
String jwtToken = jwtService.extractTokenFromRequest(request);
|
|
||||||
|
|
||||||
if (jwtToken == null) {
|
|
||||||
sendUnauthorizedResponse(response, "JWT token is missing");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!jwtService.validateToken(jwtToken)) {
|
|
||||||
sendUnauthorizedResponse(response, "JWT token is invalid or expired");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
String username = jwtService.extractUsername(jwtToken);
|
|
||||||
Authentication authentication = createAuthToken(request, username);
|
|
||||||
String jwt = jwtService.generateToken(authentication);
|
|
||||||
|
|
||||||
jwtService.addTokenToResponse(response, jwt);
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error(
|
|
||||||
"JWT authentication failed for request: {} {}",
|
|
||||||
request.getMethod(),
|
|
||||||
request.getRequestURI(),
|
|
||||||
e);
|
|
||||||
|
|
||||||
// Determine specific error message based on exception type
|
|
||||||
String errorMessage = "JWT authentication failed";
|
|
||||||
if (e.getMessage() != null && e.getMessage().contains("expired")) {
|
|
||||||
errorMessage = "JWT token has expired";
|
|
||||||
} else if (e.getMessage() != null && e.getMessage().contains("signature")) {
|
|
||||||
errorMessage = "JWT token signature is invalid";
|
|
||||||
} else if (e.getMessage() != null && e.getMessage().contains("malformed")) {
|
|
||||||
errorMessage = "JWT token is malformed";
|
|
||||||
}
|
|
||||||
|
|
||||||
sendUnauthorizedResponse(response, errorMessage);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String jwtToken = jwtService.extractTokenFromRequest(request);
|
||||||
|
|
||||||
|
if (jwtToken == null) {
|
||||||
|
// Special handling for root path - redirect to login instead of 401
|
||||||
|
if ("/".equals(request.getRequestURI())
|
||||||
|
&& "GET".equalsIgnoreCase(request.getMethod())) {
|
||||||
|
response.sendRedirect("/login");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw new AuthenticationFailureException("JWT is missing from request");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!jwtService.validateToken(jwtToken)) {
|
||||||
|
throw new AuthenticationFailureException("JWT is invalid or expired");
|
||||||
|
}
|
||||||
|
|
||||||
|
String tokenUsername = jwtService.extractUsername(jwtToken);
|
||||||
|
Authentication authentication = createAuthToken(request, tokenUsername);
|
||||||
|
String jwt = jwtService.generateToken(authentication);
|
||||||
|
|
||||||
|
jwtService.addTokenToResponse(response, jwt);
|
||||||
|
|
||||||
filterChain.doFilter(request, response);
|
filterChain.doFilter(request, response);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -101,25 +85,29 @@ public class JWTAuthenticationFilter extends OncePerRequestFilter {
|
|||||||
userDetails, null, userDetails.getAuthorities());
|
userDetails, null, userDetails.getAuthorities());
|
||||||
|
|
||||||
authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
|
authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
|
||||||
|
|
||||||
// Set authentication in SecurityContext
|
|
||||||
SecurityContextHolder.getContext().setAuthentication(authToken);
|
SecurityContextHolder.getContext().setAuthentication(authToken);
|
||||||
|
|
||||||
log.debug("JWT authentication successful for user: {}", username);
|
log.debug("JWT authentication successful for user: {}", username);
|
||||||
|
|
||||||
return SecurityContextHolder.getContext().getAuthentication();
|
} else {
|
||||||
|
throw new UsernameNotFoundException("User not found: " + username);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return SecurityContextHolder.getContext().getAuthentication();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected boolean shouldNotFilter(HttpServletRequest request) {
|
protected boolean shouldNotFilter(HttpServletRequest request) {
|
||||||
String uri = request.getRequestURI();
|
String uri = request.getRequestURI();
|
||||||
String contextPath = request.getContextPath();
|
String method = request.getMethod();
|
||||||
|
|
||||||
|
// Always allow login POST requests to be processed
|
||||||
|
if ("/login".equals(uri) && "POST".equalsIgnoreCase(method)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
String[] permitAllPatterns = {
|
String[] permitAllPatterns = {
|
||||||
"/",
|
|
||||||
"/login",
|
"/login",
|
||||||
"/register",
|
"/register",
|
||||||
"/error",
|
"/error",
|
||||||
@ -131,7 +119,8 @@ public class JWTAuthenticationFilter extends OncePerRequestFilter {
|
|||||||
"/pdfjs/",
|
"/pdfjs/",
|
||||||
"/pdfjs-legacy/",
|
"/pdfjs-legacy/",
|
||||||
"/api/v1/info/status",
|
"/api/v1/info/status",
|
||||||
"/site.webmanifest"
|
"/site.webmanifest",
|
||||||
|
"/favicon"
|
||||||
};
|
};
|
||||||
|
|
||||||
for (String pattern : permitAllPatterns) {
|
for (String pattern : permitAllPatterns) {
|
||||||
@ -145,25 +134,4 @@ public class JWTAuthenticationFilter extends OncePerRequestFilter {
|
|||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void sendUnauthorizedResponse(HttpServletResponse response, String message)
|
|
||||||
throws IOException {
|
|
||||||
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
|
|
||||||
response.setContentType("application/json");
|
|
||||||
response.setCharacterEncoding("UTF-8");
|
|
||||||
|
|
||||||
String jsonResponse =
|
|
||||||
String.format(
|
|
||||||
"""
|
|
||||||
{
|
|
||||||
"error": "Unauthorized",
|
|
||||||
"mesaage": %s,
|
|
||||||
"status": 401
|
|
||||||
}
|
|
||||||
""",
|
|
||||||
message);
|
|
||||||
|
|
||||||
response.getWriter().write(jsonResponse);
|
|
||||||
response.getWriter().flush();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -117,18 +117,18 @@ public class UserAuthenticationFilter extends OncePerRequestFilter {
|
|||||||
|
|
||||||
if ("GET".equalsIgnoreCase(method) && !(contextPath + "/login").equals(requestURI)) {
|
if ("GET".equalsIgnoreCase(method) && !(contextPath + "/login").equals(requestURI)) {
|
||||||
response.sendRedirect(contextPath + "/login"); // redirect to the login page
|
response.sendRedirect(contextPath + "/login"); // redirect to the login page
|
||||||
return;
|
|
||||||
} else {
|
} else {
|
||||||
response.setStatus(HttpStatus.UNAUTHORIZED.value());
|
response.setStatus(HttpStatus.UNAUTHORIZED.value());
|
||||||
response.getWriter()
|
response.getWriter()
|
||||||
.write(
|
.write(
|
||||||
"Authentication required. Please provide a X-API-KEY in request"
|
"""
|
||||||
+ " header.\n"
|
Authentication required. Please provide a X-API-KEY in request\
|
||||||
+ "This is found in Settings -> Account Settings -> API Key\n"
|
header.
|
||||||
+ "Alternatively you can disable authentication if this is"
|
This is found in Settings -> Account Settings -> API Key
|
||||||
+ " unexpected");
|
Alternatively you can disable authentication if this is\
|
||||||
return;
|
unexpected""");
|
||||||
}
|
}
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if the authenticated user is disabled and invalidate their session if so
|
// Check if the authenticated user is disabled and invalidate their session if so
|
||||||
|
@ -0,0 +1,9 @@
|
|||||||
|
package stirling.software.proprietary.security.model.exception;
|
||||||
|
|
||||||
|
import org.springframework.security.core.AuthenticationException;
|
||||||
|
|
||||||
|
public class AuthenticationFailureException extends AuthenticationException {
|
||||||
|
public AuthenticationFailureException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
}
|
@ -1,16 +1,11 @@
|
|||||||
package stirling.software.proprietary.security.service;
|
package stirling.software.proprietary.security.service;
|
||||||
|
|
||||||
import static org.apache.commons.lang3.StringUtils.*;
|
import java.security.KeyPair;
|
||||||
|
|
||||||
import java.security.SecureRandom;
|
|
||||||
import java.util.Base64;
|
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
|
|
||||||
import javax.crypto.SecretKey;
|
|
||||||
|
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBooleanProperty;
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnBooleanProperty;
|
||||||
import org.springframework.http.ResponseCookie;
|
import org.springframework.http.ResponseCookie;
|
||||||
import org.springframework.security.core.Authentication;
|
import org.springframework.security.core.Authentication;
|
||||||
@ -22,10 +17,8 @@ import io.jsonwebtoken.ExpiredJwtException;
|
|||||||
import io.jsonwebtoken.Jwts;
|
import io.jsonwebtoken.Jwts;
|
||||||
import io.jsonwebtoken.MalformedJwtException;
|
import io.jsonwebtoken.MalformedJwtException;
|
||||||
import io.jsonwebtoken.UnsupportedJwtException;
|
import io.jsonwebtoken.UnsupportedJwtException;
|
||||||
import io.jsonwebtoken.security.Keys;
|
|
||||||
import io.jsonwebtoken.security.SignatureException;
|
import io.jsonwebtoken.security.SignatureException;
|
||||||
|
|
||||||
import jakarta.annotation.PostConstruct;
|
|
||||||
import jakarta.servlet.http.Cookie;
|
import jakarta.servlet.http.Cookie;
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
@ -39,79 +32,18 @@ import stirling.software.common.model.ApplicationProperties;
|
|||||||
@ConditionalOnBooleanProperty("security.jwt.enabled")
|
@ConditionalOnBooleanProperty("security.jwt.enabled")
|
||||||
public class JWTService implements JWTServiceInterface {
|
public class JWTService implements JWTServiceInterface {
|
||||||
|
|
||||||
private static final String JWT_COOKIE_NAME = "STIRLING_JWT_TOKEN";
|
private static final String JWT_COOKIE_NAME = "STIRLING_JWT";
|
||||||
private static final String AUTHORIZATION_HEADER = "Authorization";
|
private static final String AUTHORIZATION_HEADER = "Authorization";
|
||||||
private static final String BEARER_PREFIX = "Bearer ";
|
private static final String BEARER_PREFIX = "Bearer ";
|
||||||
|
|
||||||
private final ApplicationProperties.Security securityProperties;
|
private final ApplicationProperties.Security securityProperties;
|
||||||
|
private final ApplicationProperties.Security.JWT jwtProperties;
|
||||||
|
private final KeyPair keyPair;
|
||||||
|
|
||||||
private SecretKey signingKey;
|
public JWTService(ApplicationProperties.Security securityProperties) {
|
||||||
|
this.securityProperties = securityProperties;
|
||||||
public JWTService(ApplicationProperties applicationProperties) {
|
this.jwtProperties = securityProperties.getJwt();
|
||||||
this.securityProperties = applicationProperties.getSecurity();
|
keyPair = Jwts.SIG.RS256.keyPair().build();
|
||||||
}
|
|
||||||
|
|
||||||
@PostConstruct
|
|
||||||
public void init() {
|
|
||||||
if (isJwtEnabled()) {
|
|
||||||
try {
|
|
||||||
initializeSigningKey();
|
|
||||||
log.info("JWT service initialized successfully");
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error(
|
|
||||||
"Failed to initialize JWT service. JWT authentication will be disabled.",
|
|
||||||
e);
|
|
||||||
throw new RuntimeException("JWT service initialization failed", e);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
log.debug("JWT authentication is disabled");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void initializeSigningKey() {
|
|
||||||
try {
|
|
||||||
ApplicationProperties.Security.JWT jwtProperties = securityProperties.getJwt();
|
|
||||||
String secretKey = jwtProperties.getSecretKey();
|
|
||||||
|
|
||||||
if (isBlank(secretKey)) {
|
|
||||||
log.warn(
|
|
||||||
"JWT secret key is not configured. Generating a temporary key for this session.");
|
|
||||||
secretKey = generateTemporaryKey();
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (jwtProperties.getAlgorithm()) {
|
|
||||||
case "HS256" ->
|
|
||||||
this.signingKey = Keys.hmacShaKeyFor(Base64.getDecoder().decode(secretKey));
|
|
||||||
case "RS256" -> // RSA256 algorithm requires a 2048-bit key. Should load RSA key
|
|
||||||
// pairs configuration
|
|
||||||
// this.signingKey =
|
|
||||||
// Jwts.SIG.RS256.keyPair().build().getPrivate()
|
|
||||||
log.info("Using RSA algorithm: RS256");
|
|
||||||
default -> {
|
|
||||||
log.warn(
|
|
||||||
"Unsupported JWT algorithm: {}. Using default algorithm.",
|
|
||||||
jwtProperties.getAlgorithm());
|
|
||||||
this.signingKey = Keys.hmacShaKeyFor(Base64.getDecoder().decode(secretKey));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
log.info("JWT service initialized with algorithm: {}", jwtProperties.getAlgorithm());
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("Failed to initialize JWT signing key", e);
|
|
||||||
throw new RuntimeException("JWT service initialization failed", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private String generateTemporaryKey() {
|
|
||||||
try {
|
|
||||||
// Generate a secure random key for HMAC-SHA256
|
|
||||||
SecureRandom secureRandom = new SecureRandom();
|
|
||||||
byte[] key = new byte[32]; // 256 bits
|
|
||||||
secureRandom.nextBytes(key);
|
|
||||||
return Base64.getEncoder().encodeToString(key);
|
|
||||||
} catch (Exception e) {
|
|
||||||
throw new RuntimeException("Failed to generate temporary JWT key", e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -126,15 +58,13 @@ public class JWTService implements JWTServiceInterface {
|
|||||||
throw new IllegalStateException("JWT is not enabled");
|
throw new IllegalStateException("JWT is not enabled");
|
||||||
}
|
}
|
||||||
|
|
||||||
ApplicationProperties.Security.JWT jwtProperties = securityProperties.getJwt();
|
|
||||||
|
|
||||||
return Jwts.builder()
|
return Jwts.builder()
|
||||||
.claims(claims)
|
.claims(claims)
|
||||||
.subject(username)
|
.subject(username)
|
||||||
.issuer(jwtProperties.getIssuer())
|
.issuer(jwtProperties.getIssuer())
|
||||||
.issuedAt(new Date())
|
.issuedAt(new Date())
|
||||||
.expiration(new Date(System.currentTimeMillis() + jwtProperties.getExpiration()))
|
.expiration(new Date(System.currentTimeMillis() + jwtProperties.getExpiration()))
|
||||||
.signWith(signingKey)
|
.signWith(keyPair.getPrivate(), Jwts.SIG.RS256)
|
||||||
.compact();
|
.compact();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -145,19 +75,20 @@ public class JWTService implements JWTServiceInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Jwts.parser().verifyWith(signingKey).build().parseSignedClaims(token);
|
Jwts.parser().verifyWith(keyPair.getPublic()).build().parseSignedClaims(token);
|
||||||
return true;
|
return true;
|
||||||
} catch (SignatureException e) {
|
} catch (SignatureException e) {
|
||||||
log.debug("Invalid JWT signature: {}", e.getMessage());
|
log.warn("Invalid JWT signature: {}", e.getMessage());
|
||||||
} catch (MalformedJwtException e) {
|
} catch (MalformedJwtException e) {
|
||||||
log.debug("Invalid JWT token: {}", e.getMessage());
|
log.warn("Invalid JWT token: {}", e.getMessage());
|
||||||
} catch (ExpiredJwtException e) {
|
} catch (ExpiredJwtException e) {
|
||||||
log.debug("JWT token is expired: {}", e.getMessage());
|
log.warn("JWT token is expired: {}", e.getMessage());
|
||||||
} catch (UnsupportedJwtException e) {
|
} catch (UnsupportedJwtException e) {
|
||||||
log.debug("JWT token is unsupported: {}", e.getMessage());
|
log.warn("JWT token is unsupported: {}", e.getMessage());
|
||||||
} catch (IllegalArgumentException e) {
|
} catch (IllegalArgumentException e) {
|
||||||
log.debug("JWT claims string is empty: {}", e.getMessage());
|
log.warn("JWT claims string is empty: {}", e.getMessage());
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -191,18 +122,21 @@ public class JWTService implements JWTServiceInterface {
|
|||||||
throw new IllegalStateException("JWT is not enabled");
|
throw new IllegalStateException("JWT is not enabled");
|
||||||
}
|
}
|
||||||
|
|
||||||
return Jwts.parser().verifyWith(signingKey).build().parseSignedClaims(token).getPayload();
|
return Jwts.parser()
|
||||||
|
.verifyWith(keyPair.getPublic())
|
||||||
|
.build()
|
||||||
|
.parseSignedClaims(token)
|
||||||
|
.getPayload();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String extractTokenFromRequest(HttpServletRequest request) {
|
public String extractTokenFromRequest(HttpServletRequest request) {
|
||||||
// First, try to get token from Authorization header
|
|
||||||
String authHeader = request.getHeader(AUTHORIZATION_HEADER);
|
String authHeader = request.getHeader(AUTHORIZATION_HEADER);
|
||||||
|
|
||||||
if (authHeader != null && authHeader.startsWith(BEARER_PREFIX)) {
|
if (authHeader != null && authHeader.startsWith(BEARER_PREFIX)) {
|
||||||
return authHeader.substring(BEARER_PREFIX.length());
|
return authHeader.substring(BEARER_PREFIX.length());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback to cookie
|
|
||||||
Cookie[] cookies = request.getCookies();
|
Cookie[] cookies = request.getCookies();
|
||||||
if (cookies != null) {
|
if (cookies != null) {
|
||||||
for (Cookie cookie : cookies) {
|
for (Cookie cookie : cookies) {
|
||||||
@ -217,11 +151,8 @@ public class JWTService implements JWTServiceInterface {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void addTokenToResponse(HttpServletResponse response, String token) {
|
public void addTokenToResponse(HttpServletResponse response, String token) {
|
||||||
ApplicationProperties.Security.JWT jwtProperties = securityProperties.getJwt();
|
|
||||||
// Add to Authorization header
|
|
||||||
response.setHeader(AUTHORIZATION_HEADER, BEARER_PREFIX + token);
|
response.setHeader(AUTHORIZATION_HEADER, BEARER_PREFIX + token);
|
||||||
|
|
||||||
// Add as HTTP-only secure cookie
|
|
||||||
ResponseCookie cookie =
|
ResponseCookie cookie =
|
||||||
ResponseCookie.from(JWT_COOKIE_NAME, token)
|
ResponseCookie.from(JWT_COOKIE_NAME, token)
|
||||||
.httpOnly(true)
|
.httpOnly(true)
|
||||||
@ -252,8 +183,6 @@ public class JWTService implements JWTServiceInterface {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isJwtEnabled() {
|
public boolean isJwtEnabled() {
|
||||||
ApplicationProperties.Security.JWT jwtProperties = securityProperties.getJwt();
|
|
||||||
|
|
||||||
return securityProperties.isJwtActive()
|
return securityProperties.isJwtActive()
|
||||||
&& jwtProperties != null
|
&& jwtProperties != null
|
||||||
&& jwtProperties.isSettingsValid();
|
&& jwtProperties.isSettingsValid();
|
||||||
|
@ -9,7 +9,9 @@ import org.mockito.InjectMocks;
|
|||||||
import org.mockito.Mock;
|
import org.mockito.Mock;
|
||||||
import org.mockito.junit.jupiter.MockitoExtension;
|
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.model.ApplicationProperties;
|
import stirling.software.common.model.ApplicationProperties;
|
||||||
|
import stirling.software.proprietary.security.service.JWTServiceInterface;
|
||||||
import static org.mockito.Mockito.*;
|
import static org.mockito.Mockito.*;
|
||||||
|
|
||||||
@ExtendWith(MockitoExtension.class)
|
@ExtendWith(MockitoExtension.class)
|
||||||
@ -17,6 +19,10 @@ class CustomLogoutSuccessHandlerTest {
|
|||||||
|
|
||||||
@Mock private ApplicationProperties.Security securityProperties;
|
@Mock private ApplicationProperties.Security securityProperties;
|
||||||
|
|
||||||
|
@Mock private AppConfig appConfig;
|
||||||
|
|
||||||
|
@Mock private JWTServiceInterface jwtService;
|
||||||
|
|
||||||
@InjectMocks private CustomLogoutSuccessHandler customLogoutSuccessHandler;
|
@InjectMocks private CustomLogoutSuccessHandler customLogoutSuccessHandler;
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -26,6 +32,7 @@ class CustomLogoutSuccessHandlerTest {
|
|||||||
String logoutPath = "logout=true";
|
String logoutPath = "logout=true";
|
||||||
|
|
||||||
when(response.isCommitted()).thenReturn(false);
|
when(response.isCommitted()).thenReturn(false);
|
||||||
|
when(jwtService.isJwtEnabled()).thenReturn(false);
|
||||||
when(request.getContextPath()).thenReturn("");
|
when(request.getContextPath()).thenReturn("");
|
||||||
when(response.encodeRedirectURL(logoutPath)).thenReturn(logoutPath);
|
when(response.encodeRedirectURL(logoutPath)).thenReturn(logoutPath);
|
||||||
|
|
||||||
@ -34,6 +41,23 @@ class CustomLogoutSuccessHandlerTest {
|
|||||||
verify(response).sendRedirect(logoutPath);
|
verify(response).sendRedirect(logoutPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testSuccessfulLogoutViaJWT() throws IOException {
|
||||||
|
HttpServletRequest request = mock(HttpServletRequest.class);
|
||||||
|
HttpServletResponse response = mock(HttpServletResponse.class);
|
||||||
|
String logoutPath = "/login?logout=true";
|
||||||
|
|
||||||
|
when(response.isCommitted()).thenReturn(false);
|
||||||
|
when(jwtService.isJwtEnabled()).thenReturn(true);
|
||||||
|
when(request.getContextPath()).thenReturn("");
|
||||||
|
when(response.encodeRedirectURL(logoutPath)).thenReturn(logoutPath);
|
||||||
|
|
||||||
|
customLogoutSuccessHandler.onLogoutSuccess(request, response, null);
|
||||||
|
|
||||||
|
verify(response).sendRedirect(logoutPath);
|
||||||
|
verify(jwtService).clearTokenFromResponse(response);
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testSuccessfulLogoutViaOAuth2() throws IOException {
|
void testSuccessfulLogoutViaOAuth2() throws IOException {
|
||||||
HttpServletRequest request = mock(HttpServletRequest.class);
|
HttpServletRequest request = mock(HttpServletRequest.class);
|
||||||
|
@ -11,14 +11,14 @@
|
|||||||
#############################################################################################################
|
#############################################################################################################
|
||||||
|
|
||||||
security:
|
security:
|
||||||
enableLogin: false # set to 'true' to enable login
|
enableLogin: true # set to 'true' to enable login
|
||||||
csrfDisabled: false # set to 'true' to disable CSRF protection (not recommended for production)
|
csrfDisabled: true # set to 'true' to disable CSRF protection (not recommended for production)
|
||||||
loginAttemptCount: 5 # lock user account after 5 tries; when using e.g. Fail2Ban you can deactivate the function with -1
|
loginAttemptCount: 5 # lock user account after 5 tries; when using e.g. Fail2Ban you can deactivate the function with -1
|
||||||
loginResetTimeMinutes: 120 # lock account for 2 hours after x attempts
|
loginResetTimeMinutes: 120 # lock account for 2 hours after x attempts
|
||||||
loginMethod: all # Accepts values like 'all' and 'normal'(only Login with Username/Password), 'oauth2'(only Login with OAuth2) or 'saml2'(only Login with SAML2)
|
loginMethod: all # Accepts values like 'all' and 'normal'(only Login with Username/Password), 'oauth2'(only Login with OAuth2) or 'saml2'(only Login with SAML2)
|
||||||
initialLogin:
|
initialLogin:
|
||||||
username: '' # initial username for the first login
|
username: 'admin' # initial username for the first login
|
||||||
password: '' # initial password for the first login
|
password: 'stirling' # initial password for the first login
|
||||||
oauth2:
|
oauth2:
|
||||||
enabled: false # set to 'true' to enable login (Note: enableLogin must also be 'true' for this to work)
|
enabled: false # set to 'true' to enable login (Note: enableLogin must also be 'true' for this to work)
|
||||||
client:
|
client:
|
||||||
@ -61,12 +61,9 @@ security:
|
|||||||
spCert: classpath:saml-public-cert.crt # Your signing certificate. Generated from your keypair
|
spCert: classpath:saml-public-cert.crt # Your signing certificate. Generated from your keypair
|
||||||
jwt:
|
jwt:
|
||||||
enabled: true # set to 'true' to enable JWT authentication
|
enabled: true # set to 'true' to enable JWT authentication
|
||||||
secretKey: 'Uz4BgfMySCz2Uplhp1x9ij19vVV2bXYktROtrlw3CC0=' # secret
|
|
||||||
expiration: 3600000 # Expiration time in milliseconds. Default is 1 hour (3600000 ms)
|
expiration: 3600000 # Expiration time in milliseconds. Default is 1 hour (3600000 ms)
|
||||||
algorithm: HS256 # JWT signing algorithm. Default is HS256
|
algorithm: HS256 # JWT signing algorithm. Default is HS256
|
||||||
issuer: Stirling-PDF # Issuer of the JWT token. Default is 'Stirling-PDF'
|
issuer: Stirling-PDF # Issuer of the JWT token. Default is 'Stirling-PDF'
|
||||||
refreshTokenEnabled: false # Set to 'true' to enable refresh tokens
|
|
||||||
refreshTokenExpiration: 86400000 # Expiration time for refresh tokens in milliseconds. Default is 1 day (86400000 ms)
|
|
||||||
|
|
||||||
premium:
|
premium:
|
||||||
key: 00000000-0000-0000-0000-000000000000
|
key: 00000000-0000-0000-0000-000000000000
|
||||||
|
Loading…
x
Reference in New Issue
Block a user