From 98fb8012476e36465bbaf381066efd20e97ec8f5 Mon Sep 17 00:00:00 2001 From: DarioGii Date: Fri, 4 Jul 2025 10:38:35 +0100 Subject: [PATCH] Adding JWTService and filter --- .claude/settings.local.json | 6 +- CLAUDE.md | 120 +++++++++++++++++ .../common/model/ApplicationProperties.java | 27 +++- .../src/main/resources/application.properties | 4 +- .../src/main/resources/settings.yml.template | 8 ++ app/proprietary/build.gradle | 11 ++ .../CustomAuthenticationSuccessHandler.java | 55 +++++--- .../security/CustomLogoutSuccessHandler.java | 21 ++- .../configuration/SecurityConfiguration.java | 122 ++++++++++++------ ...tomOAuth2AuthenticationSuccessHandler.java | 4 +- ...stomSaml2AuthenticationSuccessHandler.java | 4 +- .../service/CustomOAuth2UserService.java | 8 +- .../CustomLogoutSuccessHandlerTest.java | 35 ++--- 13 files changed, 326 insertions(+), 99 deletions(-) create mode 100644 CLAUDE.md diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 6e006423a..13d8d8350 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -5,7 +5,11 @@ "Bash(mkdir:*)", "Bash(./gradlew:*)", "Bash(grep:*)", - "Bash(cat:*)" + "Bash(cat:*)", + "Bash(find:*)", + "Bash(grep:*)", + "Bash(rg:*)", + "Bash(strings:*)" ], "deny": [] } diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 000000000..880e2bf40 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,120 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Essential Development Commands + +### Build and Run +```bash +# Build the project +./gradlew clean build + +# Run locally (includes JWT authentication work-in-progress) +./gradlew bootRun + +# Run specific module +./gradlew :stirling-pdf:bootRun + +# Build with security features enabled/disabled +DISABLE_ADDITIONAL_FEATURES=false ./gradlew clean build # enable security +DISABLE_ADDITIONAL_FEATURES=true ./gradlew clean build # disable security +``` + +### Testing +```bash +# Run unit tests +./gradlew test + +# Run comprehensive integration tests (builds all Docker versions and runs Cucumber tests) +./testing/test.sh + +# Run Cucumber/BDD tests specifically +cd testing/cucumber && python -m behave + +# Test web pages +cd testing && ./test_webpages.sh -f webpage_urls.txt -b http://localhost:8080 +``` + +### Code Quality and Formatting +```bash +# Apply Java code formatting (required before commits) +./gradlew spotlessApply + +# Check formatting compliance +./gradlew spotlessCheck + +# Generate license report +./gradlew generateLicenseReport +``` + +### Docker Development +```bash +# Build different Docker variants +docker build --build-arg VERSION_TAG=alpha -t stirlingtools/stirling-pdf:latest -f ./Dockerfile . +docker build --build-arg VERSION_TAG=alpha -t stirlingtools/stirling-pdf:latest-ultra-lite -f ./Dockerfile.ultra-lite . +DISABLE_ADDITIONAL_FEATURES=false docker build --build-arg VERSION_TAG=alpha -t stirlingtools/stirling-pdf:latest-fat -f ./Dockerfile.fat . + +# Use example Docker Compose configs +docker-compose -f exampleYmlFiles/docker-compose-latest-security.yml up -d +``` + +## Architecture Overview + +Stirling-PDF is a Spring Boot web application for PDF manipulation with the following key architectural components: + +### Multi-Module Structure +- **stirling-pdf/**: Main application module with web UI and REST APIs +- **common/**: Shared utilities and common functionality +- **proprietary/**: Enterprise/security features (JWT authentication, audit, teams) + +### Technology Stack +- **Backend**: Spring Boot 3.5, Spring Security, Spring Data JPA +- **Frontend**: Thymeleaf templates, Bootstrap, vanilla JavaScript +- **PDF Processing**: Apache PDFBox 3.0, qpdf, LibreOffice +- **Authentication**: JWT-based stateless sessions (in development) +- **Database**: H2 (default), supports PostgreSQL/MySQL +- **Build**: Gradle with multi-project setup + +### Current Development Context +The repository is on the `jwt-authentication` branch with work-in-progress changes to: +- JWT-based authentication system (`JWTService`, `JWTServiceInterface`) +- Stateless session management +- User model updates for JWT support + +### Key Directories +- `stirling-pdf/src/main/java/stirling/software/SPDF/`: Main application code + - `controller/`: REST API endpoints and UI controllers + - `service/`: Business logic layer + - `config/`: Spring configuration classes + - `security/`: Authentication and authorization +- `stirling-pdf/src/main/resources/templates/`: Thymeleaf HTML templates +- `stirling-pdf/src/main/resources/static/`: CSS, JavaScript, and assets +- `proprietary/src/main/java/stirling/software/proprietary/`: Enterprise features +- `testing/`: Integration tests and Cucumber features + +### Configuration Management +- Environment variables or `settings.yml` for runtime configuration +- Conditional feature compilation based on `DISABLE_ADDITIONAL_FEATURES` +- Multi-environment Docker configurations in `exampleYmlFiles/` + +### API Design Patterns +- RESTful endpoints under `/api/v1/` +- OpenAPI/Swagger documentation available at `/swagger-ui/index.html` +- File upload/download handling with multipart form data +- Consistent error handling and response formats + +## Development Workflow + +1. **Environment Setup**: Set `DISABLE_ADDITIONAL_FEATURES=false` for full feature development +2. **Code Formatting**: Always run `./gradlew spotlessApply` before committing +3. **Testing Strategy**: Use `./testing/test.sh` for comprehensive testing before PRs +4. **Feature Development**: Follow the controller -> service -> template pattern +5. **Security**: JWT authentication is currently in development on this branch + +## Important Notes + +- The application supports conditional compilation of security features +- Translation files are in `messages_*.properties` format +- PDF processing operations are primarily stateless +- Docker is the recommended deployment method +- All text should be internationalized using translation keys \ No newline at end of file diff --git a/app/common/src/main/java/stirling/software/common/model/ApplicationProperties.java b/app/common/src/main/java/stirling/software/common/model/ApplicationProperties.java index 802a55831..9e2705b3e 100644 --- a/app/common/src/main/java/stirling/software/common/model/ApplicationProperties.java +++ b/app/common/src/main/java/stirling/software/common/model/ApplicationProperties.java @@ -115,13 +115,14 @@ public class ApplicationProperties { private InitialLogin initialLogin = new InitialLogin(); private OAUTH2 oauth2 = new OAUTH2(); private SAML2 saml2 = new SAML2(); + private JWT jwt = new JWT(); private int loginAttemptCount; private long loginResetTimeMinutes; private String loginMethod = "all"; private String customGlobalAPIKey; public Boolean isAltLogin() { - return saml2.getEnabled() || oauth2.getEnabled(); + return saml2.getEnabled() || oauth2.getEnabled() || jwt.getEnabled(); } public enum LoginMethods { @@ -159,6 +160,10 @@ public class ApplicationProperties { && !loginMethod.equalsIgnoreCase(LoginMethods.NORMAL.toString())); } + public boolean isJwtActive() { + return (jwt != null && jwt.getEnabled()); + } + @Data public static class InitialLogin { private String username; @@ -297,6 +302,26 @@ public class ApplicationProperties { } } } + + @Data + public static class JWT { + private Boolean enabled = false; + @ToString.Exclude private String secretKey; + private Long expiration = 3600000L; // Default 1 hour in milliseconds + private String algorithm = "HS256"; // Default HMAC algorithm + private String issuer = "Stirling-PDF"; // Default issuer + private Boolean enableRefreshToken = false; + private Long refreshTokenExpiration = 86400000L; // Default 24 hours + + public boolean isSettingsValid() { + return enabled != null + && enabled + && secretKey != null + && !secretKey.trim().isEmpty() + && expiration != null + && expiration > 0; + } + } } @Data diff --git a/app/core/src/main/resources/application.properties b/app/core/src/main/resources/application.properties index ea30bf78e..fecfd1c21 100644 --- a/app/core/src/main/resources/application.properties +++ b/app/core/src/main/resources/application.properties @@ -5,7 +5,7 @@ logging.level.org.eclipse.jetty=WARN #logging.level.org.springframework.security.saml2=TRACE #logging.level.org.springframework.security=DEBUG #logging.level.org.opensaml=DEBUG -#logging.level.stirling.software.SPDF.config.security: DEBUG +#logging.level.stirling.software.proprietary.security: DEBUG logging.level.com.zaxxer.hikari=WARN spring.jpa.open-in-view=false server.forward-headers-strategy=NATIVE @@ -47,4 +47,4 @@ posthog.host=https://eu.i.posthog.com spring.main.allow-bean-definition-overriding=true # Set up a consistent temporary directory location -java.io.tmpdir=${stirling.tempfiles.directory:${java.io.tmpdir}/stirling-pdf} \ No newline at end of file +java.io.tmpdir=${stirling.tempfiles.directory:${java.io.tmpdir}/stirling-pdf} diff --git a/app/core/src/main/resources/settings.yml.template b/app/core/src/main/resources/settings.yml.template index cf22262e4..91a6e4f4f 100644 --- a/app/core/src/main/resources/settings.yml.template +++ b/app/core/src/main/resources/settings.yml.template @@ -59,6 +59,14 @@ security: idpCert: classpath:okta.cert # The certificate your Provider will use to authenticate your app's SAML authentication requests. Provided by your Provider privateKey: classpath:saml-private-key.key # Your private key. Generated from your keypair spCert: classpath:saml-public-cert.crt # Your signing certificate. Generated from your keypair + jwt: + enabled: true # set to 'true' to enable JWT authentication + secretKey: 'Uz4BgfMySCz2Uplhp1x9ij19vVV2bXYktROtrlw3CC0=' # secret + expiration: 3600000 # Expiration time in milliseconds. Default is 1 hour (3600000 ms) + algorithm: HS256 # JWT signing algorithm. Default is HS256 + 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: key: 00000000-0000-0000-0000-000000000000 diff --git a/app/proprietary/build.gradle b/app/proprietary/build.gradle index 2a72f8a65..197e5439e 100644 --- a/app/proprietary/build.gradle +++ b/app/proprietary/build.gradle @@ -1,9 +1,15 @@ repositories { maven { url = "https://build.shibboleth.net/maven/releases" } } + +ext { + jwtVersion = '0.12.6' +} + bootRun { enabled = false } + spotless { java { target sourceSets.main.allJava @@ -38,6 +44,11 @@ dependencies { implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity5:3.1.3.RELEASE' api 'io.micrometer:micrometer-registry-prometheus' implementation 'com.unboundid.product.scim2:scim2-sdk-client:4.0.0' + + // JWT dependencies + api "io.jsonwebtoken:jjwt-api:$jwtVersion" + runtimeOnly "io.jsonwebtoken:jjwt-impl:$jwtVersion" + runtimeOnly "io.jsonwebtoken:jjwt-jackson:$jwtVersion" runtimeOnly 'com.h2database:h2:2.3.232' // Don't upgrade h2database runtimeOnly 'org.postgresql:postgresql:42.7.7' constraints { diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/security/CustomAuthenticationSuccessHandler.java b/app/proprietary/src/main/java/stirling/software/proprietary/security/CustomAuthenticationSuccessHandler.java index d5180c321..ea115c4ef 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/security/CustomAuthenticationSuccessHandler.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/security/CustomAuthenticationSuccessHandler.java @@ -17,6 +17,7 @@ import stirling.software.common.util.RequestUriUtils; import stirling.software.proprietary.audit.AuditEventType; import stirling.software.proprietary.audit.AuditLevel; import stirling.software.proprietary.audit.Audited; +import stirling.software.proprietary.security.service.JWTServiceInterface; import stirling.software.proprietary.security.service.LoginAttemptService; import stirling.software.proprietary.security.service.UserService; @@ -24,13 +25,17 @@ import stirling.software.proprietary.security.service.UserService; public class CustomAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler { - private LoginAttemptService loginAttemptService; - private UserService userService; + private final LoginAttemptService loginAttemptService; + private final UserService userService; + private final JWTServiceInterface jwtService; public CustomAuthenticationSuccessHandler( - LoginAttemptService loginAttemptService, UserService userService) { + LoginAttemptService loginAttemptService, + UserService userService, + JWTServiceInterface jwtService) { this.loginAttemptService = loginAttemptService; this.userService = userService; + this.jwtService = jwtService; } @Override @@ -46,23 +51,35 @@ public class CustomAuthenticationSuccessHandler } loginAttemptService.loginSucceeded(userName); - // Get the saved request - HttpSession session = request.getSession(false); - SavedRequest savedRequest = - (session != null) - ? (SavedRequest) session.getAttribute("SPRING_SECURITY_SAVED_REQUEST") - : null; - - if (savedRequest != null - && !RequestUriUtils.isStaticResource( - request.getContextPath(), savedRequest.getRedirectUrl())) { - // Redirect to the original destination - super.onAuthenticationSuccess(request, response, authentication); - } else { - // Redirect to the root URL (considering context path) - getRedirectStrategy().sendRedirect(request, response, "/"); + // Generate JWT token if JWT authentication is enabled + boolean jwtEnabled = jwtService.isJwtEnabled(); + if (jwtService != null && jwtEnabled) { + try { + String jwt = jwtService.generateToken(authentication); + jwtService.addTokenToResponse(response, jwt); + log.debug("JWT token generated and added to response for user: {}", userName); + } catch (Exception e) { + log.error("Failed to generate JWT token for user: {}", userName, e); + } } - // super.onAuthenticationSuccess(request, response, authentication); + if (jwtEnabled) { + // JWT mode: stateless authentication, redirect after setting token + getRedirectStrategy().sendRedirect(request, response, "/"); + } else { + // Get the saved request + HttpSession session = request.getSession(false); + SavedRequest savedRequest = + (session != null) + ? (SavedRequest) session.getAttribute("SPRING_SECURITY_SAVED_REQUEST") + : null; + + if (savedRequest != null + && !RequestUriUtils.isStaticResource( + request.getContextPath(), savedRequest.getRedirectUrl())) { + // Redirect to the original destination + super.onAuthenticationSuccess(request, response, authentication); + } + } } } diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/security/CustomLogoutSuccessHandler.java b/app/proprietary/src/main/java/stirling/software/proprietary/security/CustomLogoutSuccessHandler.java index 033ea913c..2f19fedca 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/security/CustomLogoutSuccessHandler.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/security/CustomLogoutSuccessHandler.java @@ -33,6 +33,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; @Slf4j @RequiredArgsConstructor @@ -40,15 +41,29 @@ public class CustomLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler { public static final String LOGOUT_PATH = "/login?logout=true"; - private final ApplicationProperties applicationProperties; + private final ApplicationProperties.Security securityProperties; private final AppConfig appConfig; + private final JWTServiceInterface jwtService; + @Override @Audited(type = AuditEventType.USER_LOGOUT, level = AuditLevel.BASIC) public void onLogoutSuccess( HttpServletRequest request, HttpServletResponse response, Authentication authentication) 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 (authentication != null) { if (authentication instanceof Saml2Authentication samlAuthentication) { @@ -82,7 +97,7 @@ public class CustomLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler { Saml2Authentication samlAuthentication) throws IOException { - SAML2 samlConf = applicationProperties.getSecurity().getSaml2(); + SAML2 samlConf = securityProperties.getSaml2(); String registrationId = samlConf.getRegistrationId(); CustomSaml2AuthenticatedPrincipal principal = @@ -127,7 +142,7 @@ public class CustomLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler { OAuth2AuthenticationToken oAuthToken) throws IOException { String registrationId; - OAUTH2 oauth = applicationProperties.getSecurity().getOauth2(); + OAUTH2 oauth = securityProperties.getOauth2(); String path = checkForErrors(request); String redirectUrl = UrlUtils.getOrigin(request) + "/login?" + path; diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/security/configuration/SecurityConfiguration.java b/app/proprietary/src/main/java/stirling/software/proprietary/security/configuration/SecurityConfiguration.java index ab809a037..5185ac1ab 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/security/configuration/SecurityConfiguration.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/security/configuration/SecurityConfiguration.java @@ -8,11 +8,14 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.DependsOn; import org.springframework.context.annotation.Lazy; +import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.ProviderManager; import org.springframework.security.authentication.dao.DaoAuthenticationProvider; +import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configurers.CsrfConfigurer; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; @@ -39,6 +42,7 @@ import stirling.software.proprietary.security.database.repository.JPATokenReposi import stirling.software.proprietary.security.database.repository.PersistentLoginRepository; import stirling.software.proprietary.security.filter.FirstLoginFilter; import stirling.software.proprietary.security.filter.IPRateLimitingFilter; +import stirling.software.proprietary.security.filter.JWTAuthenticationFilter; import stirling.software.proprietary.security.filter.UserAuthenticationFilter; import stirling.software.proprietary.security.model.User; import stirling.software.proprietary.security.oauth2.CustomOAuth2AuthenticationFailureHandler; @@ -48,6 +52,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.LoginAttemptService; import stirling.software.proprietary.security.service.UserService; import stirling.software.proprietary.security.session.SessionPersistentRegistry; @@ -64,9 +69,11 @@ public class SecurityConfiguration { private final boolean loginEnabledValue; private final boolean runningProOrHigher; - private final ApplicationProperties applicationProperties; + private final ApplicationProperties.Security securityProperties; private final AppConfig appConfig; private final UserAuthenticationFilter userAuthenticationFilter; + private final JWTAuthenticationFilter jwtAuthenticationFilter; + private final JWTServiceInterface jwtService; private final LoginAttemptService loginAttemptService; private final FirstLoginFilter firstLoginFilter; private final SessionPersistentRegistry sessionRegistry; @@ -82,8 +89,10 @@ public class SecurityConfiguration { @Qualifier("loginEnabled") boolean loginEnabledValue, @Qualifier("runningProOrHigher") boolean runningProOrHigher, AppConfig appConfig, - ApplicationProperties applicationProperties, + ApplicationProperties.Security securityProperties, UserAuthenticationFilter userAuthenticationFilter, + JWTAuthenticationFilter jwtAuthenticationFilter, + JWTServiceInterface jwtService, LoginAttemptService loginAttemptService, FirstLoginFilter firstLoginFilter, SessionPersistentRegistry sessionRegistry, @@ -97,8 +106,10 @@ public class SecurityConfiguration { this.loginEnabledValue = loginEnabledValue; this.runningProOrHigher = runningProOrHigher; this.appConfig = appConfig; - this.applicationProperties = applicationProperties; + this.securityProperties = securityProperties; this.userAuthenticationFilter = userAuthenticationFilter; + this.jwtAuthenticationFilter = jwtAuthenticationFilter; + this.jwtService = jwtService; this.loginAttemptService = loginAttemptService; this.firstLoginFilter = firstLoginFilter; this.sessionRegistry = sessionRegistry; @@ -115,14 +126,27 @@ public class SecurityConfiguration { @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { - if (applicationProperties.getSecurity().getCsrfDisabled() || !loginEnabledValue) { - http.csrf(csrf -> csrf.disable()); + boolean jwtEnabled = securityProperties.isJwtActive(); + + // Disable CSRF if explicitly disabled, login is disabled, or JWT is enabled (stateless) + if (securityProperties.getCsrfDisabled() || !loginEnabledValue || jwtEnabled) { + http.csrf(CsrfConfigurer::disable); } if (loginEnabledValue) { - http.addFilterBefore( - userAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); - if (!applicationProperties.getSecurity().getCsrfDisabled()) { + if (jwtEnabled && jwtAuthenticationFilter != null) { + http.addFilterBefore( + jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); + // .addFilterAfter( + // jwtAuthenticationFilter, + // userAuthenticationFilter.getClass()); + } else { + http.addFilterBefore( + userAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); + } + http.addFilterAfter(firstLoginFilter, UsernamePasswordAuthenticationFilter.class) + .addFilterAfter(rateLimitingFilter(), firstLoginFilter.getClass()); + if (!securityProperties.getCsrfDisabled()) { CookieCsrfTokenRepository cookieRepo = CookieCsrfTokenRepository.withHttpOnlyFalse(); CsrfTokenRequestAttributeHandler requestHandler = @@ -156,18 +180,25 @@ public class SecurityConfiguration { .csrfTokenRepository(cookieRepo) .csrfTokenRequestHandler(requestHandler)); } - http.addFilterBefore(rateLimitingFilter(), UsernamePasswordAuthenticationFilter.class); - http.addFilterAfter(firstLoginFilter, UsernamePasswordAuthenticationFilter.class); + + // Configure session management based on JWT setting http.sessionManagement( - sessionManagement -> + sessionManagement -> { + if (jwtEnabled) { + sessionManagement.sessionCreationPolicy( + SessionCreationPolicy.STATELESS); + } else { sessionManagement .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED) .maximumSessions(10) .maxSessionsPreventsLogin(false) .sessionRegistry(sessionRegistry) - .expiredUrl("/login?logout=true")); + .expiredUrl("/login?logout=true"); + } + }); http.authenticationProvider(daoAuthenticationProvider()); http.requestCache(requestCache -> requestCache.requestCache(new NullRequestCache())); + // Configure logout behavior based on JWT setting http.logout( logout -> logout.logoutRequestMatcher( @@ -175,31 +206,36 @@ public class SecurityConfiguration { .matcher("/logout")) .logoutSuccessHandler( new CustomLogoutSuccessHandler( - applicationProperties, appConfig)) + securityProperties, appConfig, jwtService)) .clearAuthentication(true) .invalidateHttpSession(true) - .deleteCookies("JSESSIONID", "remember-me")); - http.rememberMe( - rememberMeConfigurer -> // Use the configurator directly - rememberMeConfigurer - .tokenRepository(persistentTokenRepository()) - .tokenValiditySeconds( // 14 days - 14 * 24 * 60 * 60) - .userDetailsService( // Your existing UserDetailsService - userDetailsService) - .useSecureCookie( // Enable secure cookie - true) - .rememberMeParameter( // Form parameter name - "remember-me") - .rememberMeCookieName( // Cookie name - "remember-me") - .alwaysRemember(false)); + .deleteCookies( + "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 + if (!jwtEnabled) { + http.rememberMe( + rememberMeConfigurer -> // Use the configurator directly + rememberMeConfigurer + .tokenRepository(persistentTokenRepository()) + .tokenValiditySeconds( // 14 days + 14 * 24 * 60 * 60) + .userDetailsService( // Your existing UserDetailsService + userDetailsService) + .useSecureCookie( // Enable secure cookie + true) + .rememberMeParameter( // Form parameter name + "remember-me") + .rememberMeCookieName( // Cookie name + "remember-me") + .alwaysRemember(false)); + } http.authorizeHttpRequests( authz -> authz.requestMatchers( req -> { String uri = req.getRequestURI(); String contextPath = req.getContextPath(); + // Remove the context path from the URI String trimmedUri = uri.startsWith(contextPath) @@ -224,22 +260,23 @@ public class SecurityConfiguration { .anyRequest() .authenticated()); // Handle User/Password Logins - if (applicationProperties.getSecurity().isUserPass()) { + if (securityProperties.isUserPass()) { http.formLogin( formLogin -> formLogin .loginPage("/login") .successHandler( new CustomAuthenticationSuccessHandler( - loginAttemptService, userService)) + loginAttemptService, + userService, + jwtService)) .failureHandler( new CustomAuthenticationFailureHandler( loginAttemptService, userService)) - .defaultSuccessUrl("/") .permitAll()); } // Handle OAUTH2 Logins - if (applicationProperties.getSecurity().isOauth2Active()) { + if (securityProperties.isOauth2Active()) { http.oauth2Login( oauth2 -> oauth2.loginPage("/oauth2") @@ -251,17 +288,17 @@ public class SecurityConfiguration { .successHandler( new CustomOAuth2AuthenticationSuccessHandler( loginAttemptService, - applicationProperties, + securityProperties, userService)) .failureHandler( new CustomOAuth2AuthenticationFailureHandler()) - . // Add existing Authorities from the database - userInfoEndpoint( + // Add existing Authorities from the database + .userInfoEndpoint( userInfoEndpoint -> userInfoEndpoint .oidcUserService( new CustomOAuth2UserService( - applicationProperties, + securityProperties, userService, loginAttemptService)) .userAuthoritiesMapper( @@ -269,7 +306,7 @@ public class SecurityConfiguration { .permitAll()); } // Handle SAML - if (applicationProperties.getSecurity().isSaml2Active() && runningProOrHigher) { + if (securityProperties.isSaml2Active() && runningProOrHigher) { // Configure the authentication provider OpenSaml4AuthenticationProvider authenticationProvider = new OpenSaml4AuthenticationProvider(); @@ -287,7 +324,7 @@ public class SecurityConfiguration { .successHandler( new CustomSaml2AuthenticationSuccessHandler( loginAttemptService, - applicationProperties, + securityProperties, userService)) .failureHandler( new CustomSaml2AuthenticationFailureHandler()) @@ -306,6 +343,13 @@ public class SecurityConfiguration { return http.build(); } + // todo: check if this is needed + @Bean + public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) + throws Exception { + return configuration.getAuthenticationManager(); + } + public DaoAuthenticationProvider daoAuthenticationProvider() { DaoAuthenticationProvider provider = new DaoAuthenticationProvider(userDetailsService); provider.setPasswordEncoder(passwordEncoder()); diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/security/oauth2/CustomOAuth2AuthenticationSuccessHandler.java b/app/proprietary/src/main/java/stirling/software/proprietary/security/oauth2/CustomOAuth2AuthenticationSuccessHandler.java index 71bd42a85..9b64bd68b 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/security/oauth2/CustomOAuth2AuthenticationSuccessHandler.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/security/oauth2/CustomOAuth2AuthenticationSuccessHandler.java @@ -30,7 +30,7 @@ public class CustomOAuth2AuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler { private final LoginAttemptService loginAttemptService; - private final ApplicationProperties applicationProperties; + private final ApplicationProperties.Security securityProperties; private final UserService userService; @Override @@ -60,7 +60,7 @@ public class CustomOAuth2AuthenticationSuccessHandler // Redirect to the original destination super.onAuthenticationSuccess(request, response, authentication); } else { - OAUTH2 oAuth = applicationProperties.getSecurity().getOauth2(); + OAUTH2 oAuth = securityProperties.getOauth2(); if (loginAttemptService.isBlocked(username)) { if (session != null) { diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/security/saml2/CustomSaml2AuthenticationSuccessHandler.java b/app/proprietary/src/main/java/stirling/software/proprietary/security/saml2/CustomSaml2AuthenticationSuccessHandler.java index 2170a9632..eeb73ef7e 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/security/saml2/CustomSaml2AuthenticationSuccessHandler.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/security/saml2/CustomSaml2AuthenticationSuccessHandler.java @@ -30,7 +30,7 @@ public class CustomSaml2AuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler { private LoginAttemptService loginAttemptService; - private ApplicationProperties applicationProperties; + private ApplicationProperties.Security securityProperties; private UserService userService; @Override @@ -65,7 +65,7 @@ public class CustomSaml2AuthenticationSuccessHandler savedRequest.getRedirectUrl()); super.onAuthenticationSuccess(request, response, authentication); } else { - SAML2 saml2 = applicationProperties.getSecurity().getSaml2(); + SAML2 saml2 = securityProperties.getSaml2(); log.debug( "Processing SAML2 authentication with autoCreateUser: {}", saml2.getAutoCreateUser()); diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/security/service/CustomOAuth2UserService.java b/app/proprietary/src/main/java/stirling/software/proprietary/security/service/CustomOAuth2UserService.java index 0b286e894..8f9afbe3d 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/security/service/CustomOAuth2UserService.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/security/service/CustomOAuth2UserService.java @@ -27,13 +27,13 @@ public class CustomOAuth2UserService implements OAuth2UserService