mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-08-26 06:09:23 +00:00
testing
This commit is contained in:
parent
03a311e78d
commit
a07f2cbe05
@ -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
|
@ -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'
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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 {
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user