This commit is contained in:
Anthony Stirling 2025-08-15 16:32:11 +01:00
parent 03a311e78d
commit a07f2cbe05
8 changed files with 333 additions and 357 deletions

View File

@ -3,9 +3,9 @@ logging.level.org.springframework=WARN
logging.level.org.hibernate=WARN
logging.level.org.eclipse.jetty=WARN
#logging.level.org.springframework.security.saml2=TRACE
#logging.level.org.springframework.security=DEBUG
logging.level.org.springframework.security=DEBUG
#logging.level.org.opensaml=DEBUG
#logging.level.stirling.software.SPDF.config.security: DEBUG
logging.level.stirling.software: DEBUG
logging.level.com.zaxxer.hikari=WARN
spring.jpa.open-in-view=false
server.forward-headers-strategy=NATIVE
@ -55,4 +55,14 @@ 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}
java.io.tmpdir=${stirling.tempfiles.directory:${java.io.tmpdir}/stirling-pdf}
#spring.security.oauth2.resourceserver.jwt.issuer-uri=https://nrlkjfznsavsbmweiyqu.supabase.co/auth/v1
spring.security.oauth2.resourceserver.jwk-set-uri=https://nrlkjfznsavsbmweiyqu.supabase.co/auth/v1/.well-known/jwks.json
spring.security.oauth2.resourceserver.audience=authenticated
logging:
level:
org.springframework.security: DEBUG
your.project.pkg.security: DEBUG

View File

@ -37,6 +37,8 @@ dependencies {
api "org.springframework.security:spring-security-core:$springSecuritySamlVersion"
api "org.springframework.security:spring-security-web:$springSecuritySamlVersion"
api "org.springframework.security:spring-security-config:$springSecuritySamlVersion"
api("org.springframework.boot:spring-boot-starter-oauth2-resource-server:3.5.4")
api "org.springframework.security:spring-security-oauth2-jose:$springSecuritySamlVersion"
api "org.springframework.security:spring-security-saml2-service-provider:$springSecuritySamlVersion"
api 'org.springframework.boot:spring-boot-starter-jetty'
api 'org.springframework.boot:spring-boot-starter-security'

View File

@ -7,14 +7,3 @@ import lombok.RequiredArgsConstructor;
import stirling.software.proprietary.security.filter.IPRateLimitingFilter;
@Component
@RequiredArgsConstructor
public class RateLimitResetScheduler {
private final IPRateLimitingFilter rateLimitingFilter;
@Scheduled(cron = "0 0 0 * * MON") // At 00:00 every Monday TODO: configurable
public void resetRateLimit() {
rateLimitingFilter.resetRequestCounts();
}
}

View File

@ -0,0 +1,311 @@
package stirling.software.proprietary.security.configuration;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ApplicationListener;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.lang.NonNull;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.authentication.event.AbstractAuthenticationEvent;
import org.springframework.security.config.Customizer;
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.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.oauth2.core.DelegatingOAuth2TokenValidator;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
import org.springframework.security.oauth2.server.resource.InvalidBearerTokenException;
import org.springframework.security.oauth2.server.resource.authentication.BearerTokenAuthenticationToken;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
import org.springframework.security.oauth2.server.resource.web.BearerTokenAuthenticationEntryPoint;
import org.springframework.security.oauth2.server.resource.web.access.BearerTokenAccessDeniedHandler;
import org.springframework.security.oauth2.server.resource.web.authentication.BearerTokenAuthenticationFilter;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
import java.time.Duration;
import java.time.Instant;
import java.util.*;
import java.util.stream.Collectors;
/**
* Security configuration for Supabase-issued JWTs only.
*
* Requires:
*
* spring:
* security:
* oauth2:
* resourceserver:
* jwt:
* issuer-uri: https://<project-id>.supabase.co/auth/v1
*
* Optional logging (application.yml):
* logging:
* level:
* org.springframework.security: DEBUG
* your.project.pkg.security: DEBUG
*/
@Configuration
@EnableWebSecurity
public class SecurityConfig {
private static final Logger log = LoggerFactory.getLogger(SecurityConfig.class);
/** Your Supabase project ref, e.g. abcd1234efgh5678ijkl */
@Value("${app.supabase.project-ref:nrlkjfznsavsbmweiyqu}")
private String projectRef;
/** Optional audience to enforce (leave empty to skip) */
@Value("${app.jwt.expected-aud:}")
private String expectedAud;
/** Clock skew in seconds for exp validation */
@Value("${app.jwt.clock-skew-seconds:120}")
private long clockSkewSeconds;
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http, JwtDecoder jwtDecoder) throws Exception {
http
.csrf(AbstractHttpConfigurer::disable)
.cors(Customizer.withDefaults())
.sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
// allow CORS preflight only
.requestMatchers(HttpMethod.OPTIONS, "/**").permitAll()
// public endpoints
.requestMatchers(
new AntPathRequestMatcher("/actuator/health"),
new AntPathRequestMatcher("/public/**"),
new AntPathRequestMatcher("/images/**"),
new AntPathRequestMatcher("/css/**"),
new AntPathRequestMatcher("/js/**")
).permitAll()
// everything else requires auth
.anyRequest().authenticated()
)
.addFilterBefore(new VerboseAuthLoggingFilter(), BearerTokenAuthenticationFilter.class)
.exceptionHandling(ex -> ex
.authenticationEntryPoint(new BearerTokenAuthenticationEntryPoint())
.accessDeniedHandler(new BearerTokenAccessDeniedHandler())
)
.oauth2ResourceServer(oauth -> oauth
.jwt(jwt -> jwt
.decoder(jwtDecoder)
.jwtAuthenticationConverter(this::toAuthentication)
)
);
return http.build();
}
/** CORS config so browser can send Authorization on real requests. */
@Bean
CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration cfg = new CorsConfiguration();
cfg.setAllowedOrigins(List.of(
"http://localhost:3000", // dev
"http://localhost:5173",
"http://localhost:8080",
"https://your-frontend.example"// prod
));
cfg.setAllowedMethods(List.of("GET","POST","PUT","PATCH","DELETE","OPTIONS"));
cfg.setAllowedHeaders(List.of("Authorization","Content-Type","X-Requested-With","Accept","Origin"));
cfg.setExposedHeaders(List.of("WWW-Authenticate"));
cfg.setAllowCredentials(true);
cfg.setMaxAge(3600L);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", cfg);
return source;
}
/** JWKS-based decoder + custom validator (issuer/exp/aud). */
@Bean
JwtDecoder jwtDecoder() {
String issuer = "https://" + projectRef + ".supabase.co/auth/v1"; // no trailing slash
String jwks = issuer + "/.well-known/jwks.json";
log.info("Configuring JWT decoder with JWKS: {}", jwks);
NimbusJwtDecoder decoder = NimbusJwtDecoder.withJwkSetUri(jwks).build();
decoder.setJwtValidator(new CompositeValidator(issuer, expectedAud, Duration.ofSeconds(clockSkewSeconds)));
if (expectedAud == null || expectedAud.isBlank()) {
log.info("JWT validation: enforcing issuer='{}' and exp (skew {}s)", issuer, clockSkewSeconds);
} else {
log.info("JWT validation: enforcing issuer='{}', audience='{}', and exp (skew {}s)",
issuer, expectedAud, clockSkewSeconds);
}
return decoder;
}
/** Map claims -> authorities. DEBUG: hotmail.com => ROLE_ADMIN. */
private AbstractAuthenticationToken toAuthentication(Jwt jwt) {
List<GrantedAuthority> authorities = new ArrayList<>();
// Supabase default role -> ROLE_authenticated (optional but handy)
String supabaseRole = jwt.getClaimAsString("role"); // often "authenticated"
if (supabaseRole != null && !supabaseRole.isBlank()) {
authorities.add(new SimpleGrantedAuthority("ROLE_" + supabaseRole));
}
// Your custom app_role (if you add it via Access Token Hook) -> ROLE_*
String appRole = jwt.getClaimAsString("app_role");
if (appRole != null && !appRole.isBlank()) {
authorities.add(new SimpleGrantedAuthority("ROLE_" + appRole.toUpperCase()));
}
// DEBUG RULE: email domain admin
String email = jwt.getClaimAsString("email");
if (email != null && email.toLowerCase(Locale.ROOT).endsWith("@hotmail.com")) {
authorities.add(new SimpleGrantedAuthority("ROLE_ADMIN"));
} else {
// Give a basic user role for convenience while debugging (optional)
authorities.add(new SimpleGrantedAuthority("ROLE_USER"));
}
// Optional permissions array -> PERM_*
List<String> perms = jwt.getClaimAsStringList("permissions");
if (perms != null) {
perms.stream()
.filter(p -> p != null && !p.isBlank())
.map(p -> new SimpleGrantedAuthority("PERM_" + p))
.forEach(authorities::add);
}
String principalName = (email != null && !email.isBlank()) ? email : jwt.getSubject();
if (log.isDebugEnabled()) {
log.debug("JWT accepted: sub='{}', email='{}', supabase.role='{}', app_role='{}', permissions={}",
jwt.getSubject(), email, supabaseRole, appRole, perms);
log.debug("Granted authorities: {}", authorities.stream()
.map(GrantedAuthority::getAuthority).collect(Collectors.toList()));
}
return new JwtAuthenticationToken(jwt, authorities, principalName);
}
/** Logs authentication lifecycle events (success/failure). */
@Bean
ApplicationListener<AbstractAuthenticationEvent> authenticationEventsLogger() {
return event -> {
try {
if (event.getSource() instanceof AbstractAuthenticationToken auth) {
String type = event.getClass().getSimpleName();
String name = auth.getName();
String authorities = auth.getAuthorities().stream()
.map(GrantedAuthority::getAuthority).collect(Collectors.joining(","));
log.debug("[AuthEvent] {} principal='{}' authorities='{}' details={}",
type, name, authorities, auth.getDetails());
} else {
log.debug("[AuthEvent] {} source={}", event.getClass().getSimpleName(), event.getSource());
}
} catch (Exception e) {
log.warn("Failed to log authentication event", e);
}
};
}
/** Super-chatty per-request logger around bearer processing; never logs raw token. */
static class VerboseAuthLoggingFilter extends OncePerRequestFilter {
private static final Logger flog = LoggerFactory.getLogger(VerboseAuthLoggingFilter.class);
@Override
protected void doFilterInternal(
@NonNull HttpServletRequest request,
@NonNull HttpServletResponse response,
@NonNull FilterChain filterChain
) throws ServletException, IOException {
String authHeader = request.getHeader("Authorization");
boolean hasBearer = authHeader != null && authHeader.startsWith("Bearer ");
if (flog.isDebugEnabled()) {
flog.debug("[REQ] {} {} AuthorizationHeaderPresent={} (token hidden)",
request.getMethod(), request.getRequestURI(), hasBearer);
}
try {
filterChain.doFilter(request, response);
} catch (InvalidBearerTokenException ibte) {
flog.warn("[AUTH] Invalid bearer token: {}", ibte.getMessage());
throw ibte;
} catch (Exception ex) {
flog.error("[AUTH] Unexpected auth error: {}", ex.toString(), ex);
throw ex;
}
var auth = org.springframework.security.core.context.SecurityContextHolder.getContext().getAuthentication();
if (auth instanceof JwtAuthenticationToken jwtAuth) {
String authorities = jwtAuth.getAuthorities().stream()
.map(GrantedAuthority::getAuthority).collect(Collectors.joining(","));
flog.debug("[AUTH] OK principal='{}' authorities='{}'", jwtAuth.getName(), authorities);
} else {
flog.debug("[AUTH] No authentication established");
}
}
}
/** Validator: issuer == expected, not expired (with skew), optional audience. */
static final class CompositeValidator implements OAuth2TokenValidator<Jwt> {
private final String expectedIssuer; // not null
private final String expectedAudienceOrNull; // may be null/blank
private final Duration skew;
CompositeValidator(String expectedIssuer, String expectedAudienceOrNull, Duration skew) {
this.expectedIssuer = Objects.requireNonNull(expectedIssuer);
this.expectedAudienceOrNull = (expectedAudienceOrNull != null && !expectedAudienceOrNull.isBlank())
? expectedAudienceOrNull : null;
this.skew = Objects.requireNonNull(skew);
}
@Override
public OAuth2TokenValidatorResult validate(Jwt token) {
List<OAuth2Error> errors = new ArrayList<>();
String iss = token.getIssuer() != null ? token.getIssuer().toString() : null;
if (iss == null || !iss.equals(expectedIssuer)) {
errors.add(new OAuth2Error("invalid_token", "Invalid issuer: " + iss, null));
}
Instant exp = token.getExpiresAt();
if (exp == null) {
errors.add(new OAuth2Error("invalid_token", "Missing exp claim", null));
} else if (exp.isBefore(Instant.now().minus(skew))) {
errors.add(new OAuth2Error("invalid_token", "Token expired at " + exp, null));
}
if (expectedAudienceOrNull != null) {
List<String> aud = token.getAudience();
if (aud == null || !aud.contains(expectedAudienceOrNull)) {
errors.add(new OAuth2Error("invalid_token", "Missing/invalid audience: " + expectedAudienceOrNull, null));
}
}
if (!errors.isEmpty()) {
errors.forEach(e -> log.warn("JWT validation error: {} - {}", e.getErrorCode(), e.getDescription()));
return OAuth2TokenValidatorResult.failure(errors);
}
return OAuth2TokenValidatorResult.success();
}
}
}

View File

@ -1,326 +0,0 @@
package stirling.software.proprietary.security.configuration;
import java.util.Optional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
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.ProviderManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
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.http.SessionCreationPolicy;
import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.saml2.provider.service.authentication.OpenSaml4AuthenticationProvider;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository;
import org.springframework.security.saml2.provider.service.web.authentication.OpenSaml4AuthenticationRequestResolver;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
import org.springframework.security.web.csrf.CsrfTokenRequestAttributeHandler;
import org.springframework.security.web.savedrequest.NullRequestCache;
import org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher;
import lombok.extern.slf4j.Slf4j;
import stirling.software.common.configuration.AppConfig;
import stirling.software.common.model.ApplicationProperties;
import stirling.software.proprietary.security.CustomAuthenticationFailureHandler;
import stirling.software.proprietary.security.CustomAuthenticationSuccessHandler;
import stirling.software.proprietary.security.CustomLogoutSuccessHandler;
import stirling.software.proprietary.security.database.repository.JPATokenRepositoryImpl;
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.UserAuthenticationFilter;
import stirling.software.proprietary.security.model.User;
import stirling.software.proprietary.security.oauth2.CustomOAuth2AuthenticationFailureHandler;
import stirling.software.proprietary.security.oauth2.CustomOAuth2AuthenticationSuccessHandler;
import stirling.software.proprietary.security.saml2.CustomSaml2AuthenticationFailureHandler;
import stirling.software.proprietary.security.saml2.CustomSaml2AuthenticationSuccessHandler;
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.LoginAttemptService;
import stirling.software.proprietary.security.service.UserService;
import stirling.software.proprietary.security.session.SessionPersistentRegistry;
@Slf4j
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
@DependsOn("runningProOrHigher")
public class SecurityConfiguration {
private final CustomUserDetailsService userDetailsService;
private final UserService userService;
private final boolean loginEnabledValue;
private final boolean runningProOrHigher;
private final ApplicationProperties applicationProperties;
private final AppConfig appConfig;
private final UserAuthenticationFilter userAuthenticationFilter;
private final LoginAttemptService loginAttemptService;
private final FirstLoginFilter firstLoginFilter;
private final SessionPersistentRegistry sessionRegistry;
private final PersistentLoginRepository persistentLoginRepository;
private final GrantedAuthoritiesMapper oAuth2userAuthoritiesMapper;
private final RelyingPartyRegistrationRepository saml2RelyingPartyRegistrations;
private final OpenSaml4AuthenticationRequestResolver saml2AuthenticationRequestResolver;
public SecurityConfiguration(
PersistentLoginRepository persistentLoginRepository,
CustomUserDetailsService userDetailsService,
@Lazy UserService userService,
@Qualifier("loginEnabled") boolean loginEnabledValue,
@Qualifier("runningProOrHigher") boolean runningProOrHigher,
AppConfig appConfig,
ApplicationProperties applicationProperties,
UserAuthenticationFilter userAuthenticationFilter,
LoginAttemptService loginAttemptService,
FirstLoginFilter firstLoginFilter,
SessionPersistentRegistry sessionRegistry,
@Autowired(required = false) GrantedAuthoritiesMapper oAuth2userAuthoritiesMapper,
@Autowired(required = false)
RelyingPartyRegistrationRepository saml2RelyingPartyRegistrations,
@Autowired(required = false)
OpenSaml4AuthenticationRequestResolver saml2AuthenticationRequestResolver) {
this.userDetailsService = userDetailsService;
this.userService = userService;
this.loginEnabledValue = loginEnabledValue;
this.runningProOrHigher = runningProOrHigher;
this.appConfig = appConfig;
this.applicationProperties = applicationProperties;
this.userAuthenticationFilter = userAuthenticationFilter;
this.loginAttemptService = loginAttemptService;
this.firstLoginFilter = firstLoginFilter;
this.sessionRegistry = sessionRegistry;
this.persistentLoginRepository = persistentLoginRepository;
this.oAuth2userAuthoritiesMapper = oAuth2userAuthoritiesMapper;
this.saml2RelyingPartyRegistrations = saml2RelyingPartyRegistrations;
this.saml2AuthenticationRequestResolver = saml2AuthenticationRequestResolver;
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
if (applicationProperties.getSecurity().getCsrfDisabled() || !loginEnabledValue) {
http.csrf(csrf -> csrf.disable());
}
if (loginEnabledValue) {
http.addFilterBefore(
userAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
if (!applicationProperties.getSecurity().getCsrfDisabled()) {
CookieCsrfTokenRepository cookieRepo =
CookieCsrfTokenRepository.withHttpOnlyFalse();
CsrfTokenRequestAttributeHandler requestHandler =
new CsrfTokenRequestAttributeHandler();
requestHandler.setCsrfRequestAttributeName(null);
http.csrf(
csrf ->
csrf.ignoringRequestMatchers(
request -> {
String apiKey = request.getHeader("X-API-KEY");
// If there's no API key, don't ignore CSRF
// (return false)
if (apiKey == null || apiKey.trim().isEmpty()) {
return false;
}
// Validate API key using existing UserService
try {
Optional<User> user =
userService.getUserByApiKey(apiKey);
// If API key is valid, ignore CSRF (return
// true)
// If API key is invalid, don't ignore CSRF
// (return false)
return user.isPresent();
} catch (Exception e) {
// If there's any error validating the API
// key, don't ignore CSRF
return false;
}
})
.csrfTokenRepository(cookieRepo)
.csrfTokenRequestHandler(requestHandler));
}
http.addFilterBefore(rateLimitingFilter(), UsernamePasswordAuthenticationFilter.class);
http.addFilterAfter(firstLoginFilter, UsernamePasswordAuthenticationFilter.class);
http.sessionManagement(
sessionManagement ->
sessionManagement
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
.maximumSessions(10)
.maxSessionsPreventsLogin(false)
.sessionRegistry(sessionRegistry)
.expiredUrl("/login?logout=true"));
http.authenticationProvider(daoAuthenticationProvider());
http.requestCache(requestCache -> requestCache.requestCache(new NullRequestCache()));
http.logout(
logout ->
logout.logoutRequestMatcher(
PathPatternRequestMatcher.withDefaults()
.matcher("/logout"))
.logoutSuccessHandler(
new CustomLogoutSuccessHandler(
applicationProperties, appConfig))
.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));
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)
? uri.substring(
contextPath.length())
: uri;
return trimmedUri.startsWith("/login")
|| trimmedUri.startsWith("/oauth")
|| trimmedUri.startsWith("/saml2")
|| trimmedUri.endsWith(".svg")
|| trimmedUri.startsWith("/register")
|| trimmedUri.startsWith("/error")
|| trimmedUri.startsWith("/images/")
|| trimmedUri.startsWith("/public/")
|| trimmedUri.startsWith("/css/")
|| trimmedUri.startsWith("/fonts/")
|| trimmedUri.startsWith("/js/")
|| trimmedUri.startsWith(
"/api/v1/info/status");
})
.permitAll()
.anyRequest()
.authenticated());
// Handle User/Password Logins
if (applicationProperties.getSecurity().isUserPass()) {
http.formLogin(
formLogin ->
formLogin
.loginPage("/login")
.successHandler(
new CustomAuthenticationSuccessHandler(
loginAttemptService, userService))
.failureHandler(
new CustomAuthenticationFailureHandler(
loginAttemptService, userService))
.defaultSuccessUrl("/")
.permitAll());
}
// Handle OAUTH2 Logins
if (applicationProperties.getSecurity().isOauth2Active()) {
http.oauth2Login(
oauth2 ->
oauth2.loginPage("/oauth2")
/*
This Custom handler is used to check if the OAUTH2 user trying to log in, already exists in the database.
If user exists, login proceeds as usual. If user does not exist, then it is auto-created but only if 'OAUTH2AutoCreateUser'
is set as true, else login fails with an error message advising the same.
*/
.successHandler(
new CustomOAuth2AuthenticationSuccessHandler(
loginAttemptService,
applicationProperties,
userService))
.failureHandler(
new CustomOAuth2AuthenticationFailureHandler())
. // Add existing Authorities from the database
userInfoEndpoint(
userInfoEndpoint ->
userInfoEndpoint
.oidcUserService(
new CustomOAuth2UserService(
applicationProperties,
userService,
loginAttemptService))
.userAuthoritiesMapper(
oAuth2userAuthoritiesMapper))
.permitAll());
}
// Handle SAML
if (applicationProperties.getSecurity().isSaml2Active() && runningProOrHigher) {
// Configure the authentication provider
OpenSaml4AuthenticationProvider authenticationProvider =
new OpenSaml4AuthenticationProvider();
authenticationProvider.setResponseAuthenticationConverter(
new CustomSaml2ResponseAuthenticationConverter(userService));
http.authenticationProvider(authenticationProvider)
.saml2Login(
saml2 -> {
try {
saml2.loginPage("/saml2")
.relyingPartyRegistrationRepository(
saml2RelyingPartyRegistrations)
.authenticationManager(
new ProviderManager(authenticationProvider))
.successHandler(
new CustomSaml2AuthenticationSuccessHandler(
loginAttemptService,
applicationProperties,
userService))
.failureHandler(
new CustomSaml2AuthenticationFailureHandler())
.authenticationRequestResolver(
saml2AuthenticationRequestResolver);
} catch (Exception e) {
log.error("Error configuring SAML 2 login", e);
throw new RuntimeException(e);
}
});
}
} else {
log.debug("Login is not enabled.");
http.authorizeHttpRequests(authz -> authz.anyRequest().permitAll());
}
return http.build();
}
public DaoAuthenticationProvider daoAuthenticationProvider() {
DaoAuthenticationProvider provider = new DaoAuthenticationProvider(userDetailsService);
provider.setPasswordEncoder(passwordEncoder());
return provider;
}
@Bean
public IPRateLimitingFilter rateLimitingFilter() {
// Example limit TODO add config level
int maxRequestsPerIp = 1000000;
return new IPRateLimitingFilter(maxRequestsPerIp, maxRequestsPerIp);
}
@Bean
public PersistentTokenRepository persistentTokenRepository() {
return new JPATokenRepositoryImpl(persistentLoginRepository);
}
}

View File

@ -43,7 +43,7 @@ import stirling.software.proprietary.security.model.api.admin.UpdateSettingsRequ
@Tag(name = "Admin Settings", description = "Admin-only Settings Management APIs")
@RequestMapping("/api/v1/admin/settings")
@RequiredArgsConstructor
@PreAuthorize("hasRole('ROLE_ADMIN')")
@PreAuthorize("hasRole('ADMIN')")
@Slf4j
public class AdminSettingsController {

View File

@ -105,9 +105,7 @@ public class UserController {
if (user.getUsername().equals(newUsername)) {
return new RedirectView("/account?messageType=usernameExists", true);
}
if (!userService.isPasswordCorrect(user, currentPassword)) {
return new RedirectView("/account?messageType=incorrectPassword", true);
}
if (!user.getUsername().equals(newUsername) && userService.usernameExists(newUsername)) {
return new RedirectView("/account?messageType=usernameExists", true);
}
@ -141,9 +139,7 @@ public class UserController {
return new RedirectView("/change-creds?messageType=userNotFound", true);
}
User user = userOpt.get();
if (!userService.isPasswordCorrect(user, currentPassword)) {
return new RedirectView("/change-creds?messageType=incorrectPassword", true);
}
userService.changePassword(user, newPassword);
userService.changeFirstUse(user, false);
// Logout using Spring's utility
@ -169,9 +165,7 @@ public class UserController {
return new RedirectView("/account?messageType=userNotFound", true);
}
User user = userOpt.get();
if (!userService.isPasswordCorrect(user, currentPassword)) {
return new RedirectView("/account?messageType=incorrectPassword", true);
}
userService.changePassword(user, newPassword);
// Logout using Spring's utility
new SecurityContextLogoutHandler().logout(request, response, null);

View File

@ -51,7 +51,6 @@ public class UserService implements UserServiceInterface {
private final TeamRepository teamRepository;
private final AuthorityRepository authorityRepository;
private final PasswordEncoder passwordEncoder;
private final MessageSource messageSource;
@ -344,7 +343,7 @@ public class UserService implements UserServiceInterface {
public void changePassword(User user, String newPassword)
throws SQLException, UnsupportedProviderException {
user.setPassword(passwordEncoder.encode(newPassword));
//user.setPassword(passwordEncoder.encode(newPassword));
userRepository.save(user);
databaseService.exportDatabase();
}
@ -381,10 +380,7 @@ public class UserService implements UserServiceInterface {
databaseService.exportDatabase();
}
public boolean isPasswordCorrect(User user, String currentPassword) {
return passwordEncoder.matches(currentPassword, user.getPassword());
}
/**
* Resolves a team based on the provided information, with consistent error handling.
*
@ -456,7 +452,7 @@ public class UserService implements UserServiceInterface {
// Set password if provided
if (password != null && !password.isEmpty()) {
user.setPassword(passwordEncoder.encode(password));
// user.setPassword(passwordEncoder.encode(password));
}
// Set authentication type