2024-01-03 17:59:04 +00:00
|
|
|
package stirling.software.SPDF.config.security;
|
|
|
|
|
2024-04-29 15:01:22 -06:00
|
|
|
import jakarta.servlet.ServletException;
|
|
|
|
import jakarta.servlet.http.HttpServletRequest;
|
|
|
|
import jakarta.servlet.http.HttpServletResponse;
|
2024-01-03 17:59:04 +00:00
|
|
|
import org.springframework.beans.factory.annotation.Autowired;
|
|
|
|
import org.springframework.beans.factory.annotation.Qualifier;
|
2024-04-29 15:01:22 -06:00
|
|
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
2024-01-03 17:59:04 +00:00
|
|
|
import org.springframework.context.annotation.Bean;
|
|
|
|
import org.springframework.context.annotation.Configuration;
|
|
|
|
import org.springframework.context.annotation.Lazy;
|
|
|
|
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;
|
2024-02-18 15:47:19 +00:00
|
|
|
import org.springframework.security.config.http.SessionCreationPolicy;
|
2024-04-29 15:01:22 -06:00
|
|
|
import org.springframework.security.core.Authentication;
|
2024-05-02 14:52:50 -06:00
|
|
|
import org.springframework.security.core.GrantedAuthority;
|
|
|
|
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
|
|
|
import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;
|
2024-02-18 15:47:19 +00:00
|
|
|
import org.springframework.security.core.session.SessionRegistry;
|
|
|
|
import org.springframework.security.core.session.SessionRegistryImpl;
|
2024-01-03 17:59:04 +00:00
|
|
|
import org.springframework.security.core.userdetails.UserDetailsService;
|
|
|
|
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
|
|
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
2024-04-29 15:01:22 -06:00
|
|
|
import org.springframework.security.oauth2.client.registration.ClientRegistration;
|
|
|
|
import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository;
|
|
|
|
import org.springframework.security.oauth2.core.user.OAuth2User;
|
2024-05-02 14:52:50 -06:00
|
|
|
import org.springframework.security.oauth2.core.user.OAuth2UserAuthority;
|
2024-01-03 17:59:04 +00:00
|
|
|
import org.springframework.security.web.SecurityFilterChain;
|
2024-04-29 15:01:22 -06:00
|
|
|
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
|
2024-01-03 17:59:04 +00:00
|
|
|
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
|
|
|
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
|
|
|
|
import org.springframework.security.web.savedrequest.NullRequestCache;
|
|
|
|
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
2024-04-29 15:01:22 -06:00
|
|
|
import org.springframework.security.oauth2.client.registration.ClientRegistrations;
|
|
|
|
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
|
2024-01-03 17:59:04 +00:00
|
|
|
|
2024-03-08 18:06:40 +00:00
|
|
|
import jakarta.servlet.http.HttpSession;
|
2024-04-29 15:01:22 -06:00
|
|
|
import stirling.software.SPDF.model.ApplicationProperties;
|
2024-05-02 14:52:50 -06:00
|
|
|
import stirling.software.SPDF.model.User;
|
2024-01-03 17:59:04 +00:00
|
|
|
import stirling.software.SPDF.repository.JPATokenRepositoryImpl;
|
|
|
|
|
2024-04-29 15:01:22 -06:00
|
|
|
import java.io.IOException;
|
2024-05-02 14:52:50 -06:00
|
|
|
import java.util.*;
|
2024-04-29 15:01:22 -06:00
|
|
|
|
2024-01-03 17:59:04 +00:00
|
|
|
@Configuration
|
|
|
|
@EnableWebSecurity()
|
|
|
|
@EnableMethodSecurity
|
|
|
|
public class SecurityConfiguration {
|
|
|
|
|
|
|
|
@Autowired private UserDetailsService userDetailsService;
|
|
|
|
|
|
|
|
@Bean
|
|
|
|
public PasswordEncoder passwordEncoder() {
|
|
|
|
return new BCryptPasswordEncoder();
|
|
|
|
}
|
|
|
|
|
|
|
|
@Autowired @Lazy private UserService userService;
|
|
|
|
|
|
|
|
@Autowired
|
|
|
|
@Qualifier("loginEnabled")
|
|
|
|
public boolean loginEnabledValue;
|
|
|
|
|
2024-04-29 15:01:22 -06:00
|
|
|
@Autowired ApplicationProperties applicationProperties;
|
|
|
|
|
2024-01-03 17:59:04 +00:00
|
|
|
@Autowired private UserAuthenticationFilter userAuthenticationFilter;
|
|
|
|
|
|
|
|
@Autowired private LoginAttemptService loginAttemptService;
|
|
|
|
|
|
|
|
@Autowired private FirstLoginFilter firstLoginFilter;
|
|
|
|
|
2024-02-18 15:47:19 +00:00
|
|
|
@Bean
|
|
|
|
public SessionRegistry sessionRegistry() {
|
|
|
|
return new SessionRegistryImpl();
|
|
|
|
}
|
|
|
|
|
2024-01-03 17:59:04 +00:00
|
|
|
@Bean
|
|
|
|
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
|
|
|
http.addFilterBefore(userAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
|
|
|
|
|
|
|
|
if (loginEnabledValue) {
|
|
|
|
|
|
|
|
http.csrf(csrf -> csrf.disable());
|
|
|
|
http.addFilterBefore(rateLimitingFilter(), UsernamePasswordAuthenticationFilter.class);
|
|
|
|
http.addFilterAfter(firstLoginFilter, UsernamePasswordAuthenticationFilter.class);
|
2024-02-18 15:47:19 +00:00
|
|
|
http.sessionManagement(
|
|
|
|
sessionManagement ->
|
|
|
|
sessionManagement
|
|
|
|
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
|
2024-03-09 14:03:46 +00:00
|
|
|
.maximumSessions(10)
|
|
|
|
.maxSessionsPreventsLogin(false)
|
2024-02-18 15:47:19 +00:00
|
|
|
.sessionRegistry(sessionRegistry())
|
|
|
|
.expiredUrl("/login?logout=true"));
|
2024-03-09 14:03:46 +00:00
|
|
|
|
2024-01-03 17:59:04 +00:00
|
|
|
http.formLogin(
|
|
|
|
formLogin ->
|
|
|
|
formLogin
|
|
|
|
.loginPage("/login")
|
|
|
|
.successHandler(
|
|
|
|
new CustomAuthenticationSuccessHandler())
|
|
|
|
.defaultSuccessUrl("/")
|
|
|
|
.failureHandler(
|
|
|
|
new CustomAuthenticationFailureHandler(
|
2024-03-08 18:06:40 +00:00
|
|
|
loginAttemptService, userService))
|
2024-01-03 17:59:04 +00:00
|
|
|
.permitAll())
|
|
|
|
.requestCache(requestCache -> requestCache.requestCache(new NullRequestCache()))
|
|
|
|
.logout(
|
|
|
|
logout ->
|
|
|
|
logout.logoutRequestMatcher(
|
|
|
|
new AntPathRequestMatcher("/logout"))
|
2024-04-29 15:01:22 -06:00
|
|
|
.logoutSuccessHandler(new CustomLogoutSuccessHandler()) // Use a Custom Logout Handler to handle custom error message if OAUTH2 Auto Create is disabled
|
2024-01-03 17:59:04 +00:00
|
|
|
.invalidateHttpSession(true) // Invalidate session
|
2024-03-08 18:06:40 +00:00
|
|
|
.deleteCookies("JSESSIONID", "remember-me")
|
|
|
|
.addLogoutHandler(
|
|
|
|
(request, response, authentication) -> {
|
|
|
|
HttpSession session =
|
2024-03-09 14:03:46 +00:00
|
|
|
request.getSession(false);
|
2024-03-08 18:06:40 +00:00
|
|
|
if (session != null) {
|
|
|
|
String sessionId = session.getId();
|
|
|
|
sessionRegistry()
|
|
|
|
.removeSessionInformation(
|
|
|
|
sessionId);
|
|
|
|
}
|
|
|
|
}))
|
2024-01-03 17:59:04 +00:00
|
|
|
.rememberMe(
|
|
|
|
rememberMeConfigurer ->
|
|
|
|
rememberMeConfigurer // Use the configurator directly
|
|
|
|
.key("uniqueAndSecret")
|
|
|
|
.tokenRepository(persistentTokenRepository())
|
|
|
|
.tokenValiditySeconds(1209600) // 2 weeks
|
|
|
|
)
|
|
|
|
.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")
|
2024-04-29 15:01:22 -06:00
|
|
|
|| trimmedUri.startsWith("/oauth")
|
2024-01-03 17:59:04 +00:00
|
|
|
|| trimmedUri.endsWith(".svg")
|
|
|
|
|| trimmedUri.startsWith(
|
|
|
|
"/register")
|
|
|
|
|| trimmedUri.startsWith("/error")
|
|
|
|
|| trimmedUri.startsWith("/images/")
|
|
|
|
|| trimmedUri.startsWith("/public/")
|
|
|
|
|| trimmedUri.startsWith("/css/")
|
|
|
|
|| trimmedUri.startsWith("/js/")
|
|
|
|
|| trimmedUri.startsWith(
|
|
|
|
"/api/v1/info/status");
|
|
|
|
})
|
|
|
|
.permitAll()
|
|
|
|
.anyRequest()
|
|
|
|
.authenticated())
|
|
|
|
.userDetailsService(userDetailsService)
|
|
|
|
.authenticationProvider(authenticationProvider());
|
2024-04-29 15:01:22 -06:00
|
|
|
|
|
|
|
// Handle OAUTH2 Logins
|
|
|
|
if (applicationProperties.getSecurity().getOAUTH2().getEnabled()) {
|
|
|
|
|
|
|
|
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 autocreated but only if 'OAUTH2AutoCreateUser'
|
|
|
|
is set as true, else login fails with an error message advising the same.
|
|
|
|
*/
|
|
|
|
.successHandler(new AuthenticationSuccessHandler() {
|
|
|
|
@Override
|
|
|
|
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
|
|
|
|
Authentication authentication) throws ServletException , IOException{
|
|
|
|
OAuth2User oauthUser = (OAuth2User) authentication.getPrincipal();
|
|
|
|
if (userService.processOAuth2PostLogin(oauthUser.getAttribute("email"), applicationProperties.getSecurity().getOAUTH2().getAutoCreateUser())) {
|
|
|
|
response.sendRedirect("/");
|
|
|
|
}
|
|
|
|
else{
|
|
|
|
response.sendRedirect("/logout?oauth2AutoCreateDisabled=true");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
)
|
2024-05-02 14:52:50 -06:00
|
|
|
// Add existing Authorities from the database
|
|
|
|
.userInfoEndpoint( userInfoEndpoint ->
|
|
|
|
userInfoEndpoint.userAuthoritiesMapper(userAuthoritiesMapper())
|
|
|
|
)
|
2024-04-29 15:01:22 -06:00
|
|
|
);
|
|
|
|
}
|
2024-01-03 17:59:04 +00:00
|
|
|
} else {
|
|
|
|
http.csrf(csrf -> csrf.disable())
|
|
|
|
.authorizeHttpRequests(authz -> authz.anyRequest().permitAll());
|
|
|
|
}
|
|
|
|
|
|
|
|
return http.build();
|
|
|
|
}
|
|
|
|
|
2024-04-29 15:01:22 -06:00
|
|
|
// Client Registration Repository for OAUTH2 OIDC Login
|
|
|
|
@Bean
|
|
|
|
@ConditionalOnProperty(value = "security.oauth2.enabled" , havingValue = "true", matchIfMissing = false)
|
|
|
|
public ClientRegistrationRepository clientRegistrationRepository() {
|
|
|
|
return new InMemoryClientRegistrationRepository(this.oidcClientRegistration());
|
|
|
|
}
|
|
|
|
|
|
|
|
private ClientRegistration oidcClientRegistration() {
|
|
|
|
return ClientRegistrations.fromOidcIssuerLocation(applicationProperties.getSecurity().getOAUTH2().getIssuer())
|
|
|
|
.registrationId("oidc")
|
|
|
|
.clientId(applicationProperties.getSecurity().getOAUTH2().getClientId())
|
|
|
|
.clientSecret(applicationProperties.getSecurity().getOAUTH2().getClientSecret())
|
|
|
|
.scope("openid", "profile", "email")
|
|
|
|
.userNameAttributeName("email")
|
|
|
|
.clientName("OIDC")
|
|
|
|
.build();
|
|
|
|
}
|
|
|
|
|
2024-05-02 14:52:50 -06:00
|
|
|
/*
|
|
|
|
This following function is to grant Authorities to the OAUTH2 user from the values stored in the database.
|
|
|
|
This is required for the internal; 'hasRole()' function to give out the correct role.
|
|
|
|
*/
|
|
|
|
@Bean
|
|
|
|
@ConditionalOnProperty(value = "security.oauth2.enabled" , havingValue = "true", matchIfMissing = false)
|
|
|
|
GrantedAuthoritiesMapper userAuthoritiesMapper() {
|
|
|
|
return (authorities) -> {
|
|
|
|
Set<GrantedAuthority> mappedAuthorities = new HashSet<>();
|
|
|
|
|
|
|
|
authorities.forEach(authority -> {
|
|
|
|
// Add existing OAUTH2 Authorities
|
|
|
|
mappedAuthorities.add(new SimpleGrantedAuthority(authority.getAuthority()));
|
|
|
|
|
|
|
|
// Add Authorities from database for existing user, if user is present.
|
|
|
|
if (authority instanceof OAuth2UserAuthority oauth2Auth) {
|
|
|
|
Optional<User> userOpt = userService.findByUsernameIgnoreCase((String)oauth2Auth.getAttributes().get("email"));
|
|
|
|
if (userOpt.isPresent()) {
|
|
|
|
User user = userOpt.get();
|
|
|
|
if (user != null){
|
|
|
|
mappedAuthorities.add(new SimpleGrantedAuthority(
|
|
|
|
userService
|
|
|
|
.findRole(user)
|
|
|
|
.getAuthority()));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
return mappedAuthorities;
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2024-01-03 17:59:04 +00:00
|
|
|
@Bean
|
|
|
|
public IPRateLimitingFilter rateLimitingFilter() {
|
|
|
|
int maxRequestsPerIp = 1000000; // Example limit TODO add config level
|
|
|
|
return new IPRateLimitingFilter(maxRequestsPerIp, maxRequestsPerIp);
|
|
|
|
}
|
|
|
|
|
|
|
|
@Bean
|
|
|
|
public DaoAuthenticationProvider authenticationProvider() {
|
|
|
|
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
|
|
|
|
authProvider.setUserDetailsService(userDetailsService);
|
|
|
|
authProvider.setPasswordEncoder(passwordEncoder());
|
|
|
|
return authProvider;
|
|
|
|
}
|
|
|
|
|
|
|
|
@Bean
|
|
|
|
public PersistentTokenRepository persistentTokenRepository() {
|
|
|
|
return new JPATokenRepositoryImpl();
|
|
|
|
}
|
2024-04-21 22:16:39 +02:00
|
|
|
|
|
|
|
@Bean
|
|
|
|
public boolean activSecurity() {
|
|
|
|
return true;
|
|
|
|
}
|
2024-01-03 17:59:04 +00:00
|
|
|
}
|