Skip to content

Commit

Permalink
Admin panel - Enhanced User Management & Fix: #1630 (#1658)
Browse files Browse the repository at this point in the history
* Prevents SSO login due to faulty verification

* add translation & fix show error message

* Update settings.yml.template

---------

Co-authored-by: Anthony Stirling <[email protected]>
  • Loading branch information
Ludy87 and Frooodle authored Aug 16, 2024
1 parent 2cbe34e commit 29fcbf3
Show file tree
Hide file tree
Showing 61 changed files with 1,315 additions and 218 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,7 @@ security:
clientId: '' # Client ID from your provider
clientSecret: '' # Client Secret from your provider
autoCreateUser: false # set to 'true' to allow auto-creation of non-existing users
blockRegistration: false # set to 'true' to deny login with SSO without prior registration by an admin
useAsUsername: email # Default is 'email'; custom fields can be used as the username
scopes: openid, profile, email # Specify the scopes for which the application will request permissions
provider: google # Set this to your OAuth provider's name, e.g., 'google' or 'keycloak'
Expand Down
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ sourceSets {
exclude "stirling/software/SPDF/model/AttemptCounter.java"
exclude "stirling/software/SPDF/model/Authority.java"
exclude "stirling/software/SPDF/model/PersistentLogin.java"
exclude "stirling/software/SPDF/model/SessionEntity.java"
exclude "stirling/software/SPDF/model/User.java"
exclude "stirling/software/SPDF/repository/**"
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,8 @@
import java.io.IOException;
import java.util.Optional;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.DisabledException;
import org.springframework.security.authentication.InternalAuthenticationServiceException;
import org.springframework.security.authentication.LockedException;
import org.springframework.security.core.AuthenticationException;
Expand All @@ -15,17 +14,16 @@
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.model.User;

@Slf4j
public class CustomAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {

private LoginAttemptService loginAttemptService;

private UserService userService;

private static final Logger logger =
LoggerFactory.getLogger(CustomAuthenticationFailureHandler.class);

public CustomAuthenticationFailureHandler(
final LoginAttemptService loginAttemptService, UserService userService) {
this.loginAttemptService = loginAttemptService;
Expand All @@ -39,35 +37,43 @@ public void onAuthenticationFailure(
AuthenticationException exception)
throws IOException, ServletException {

String ip = request.getRemoteAddr();
logger.error("Failed login attempt from IP: {}", ip);
if (exception instanceof DisabledException) {
log.error("User is deactivated: ", exception);
getRedirectStrategy().sendRedirect(request, response, "/logout?userIsDisabled=true");
return;
}

String contextPath = request.getContextPath();
String ip = request.getRemoteAddr();
log.error("Failed login attempt from IP: {}", ip);

if (exception.getClass().isAssignableFrom(InternalAuthenticationServiceException.class)
|| "Password must not be null".equalsIgnoreCase(exception.getMessage())) {
response.sendRedirect(contextPath + "/login?error=oauth2AuthenticationError");
if (exception instanceof LockedException) {
getRedirectStrategy().sendRedirect(request, response, "/login?error=locked");
return;
}

String username = request.getParameter("username");
Optional<User> optUser = userService.findByUsernameIgnoreCase(username);

if (username != null && optUser.isPresent() && !isDemoUser(optUser)) {
logger.info(
log.info(
"Remaining attempts for user {}: {}",
optUser.get().getUsername(),
username,
loginAttemptService.getRemainingAttempts(username));
loginAttemptService.loginFailed(username);
if (loginAttemptService.isBlocked(username)
|| exception.getClass().isAssignableFrom(LockedException.class)) {
response.sendRedirect(contextPath + "/login?error=locked");
if (loginAttemptService.isBlocked(username) || exception instanceof LockedException) {
getRedirectStrategy().sendRedirect(request, response, "/login?error=locked");
return;
}
}
if (exception.getClass().isAssignableFrom(BadCredentialsException.class)
|| exception.getClass().isAssignableFrom(UsernameNotFoundException.class)) {
response.sendRedirect(contextPath + "/login?error=badcredentials");
if (exception instanceof BadCredentialsException
|| exception instanceof UsernameNotFoundException) {
getRedirectStrategy().sendRedirect(request, response, "/login?error=badcredentials");
return;
}
if (exception instanceof InternalAuthenticationServiceException
|| "Password must not be null".equalsIgnoreCase(exception.getMessage())) {
getRedirectStrategy()
.sendRedirect(request, response, "/login?error=oauth2AuthenticationError");
return;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,20 @@
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.utils.RequestUriUtils;

@Slf4j
public class CustomAuthenticationSuccessHandler
extends SavedRequestAwareAuthenticationSuccessHandler {

private LoginAttemptService loginAttemptService;
private UserService userService;

public CustomAuthenticationSuccessHandler(LoginAttemptService loginAttemptService) {
public CustomAuthenticationSuccessHandler(
LoginAttemptService loginAttemptService, UserService userService) {
this.loginAttemptService = loginAttemptService;
this.userService = userService;
}

@Override
Expand All @@ -27,6 +32,10 @@ public void onAuthenticationSuccess(
throws ServletException, IOException {

String userName = request.getParameter("username");
if (userService.isUserDisabled(userName)) {
getRedirectStrategy().sendRedirect(request, response, "/logout?userIsDisabled=true");
return;
}
loginAttemptService.loginSucceeded(userName);

// Get the saved request
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,32 +2,26 @@

import java.io.IOException;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.session.SessionRegistry;
import org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler;

import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;

public class CustomLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler {

@Autowired SessionRegistry sessionRegistry;

@Override
public void onLogoutSuccess(
HttpServletRequest request, HttpServletResponse response, Authentication authentication)
throws IOException, ServletException {
HttpSession session = request.getSession(false);
if (session != null) {
String sessionId = session.getId();
sessionRegistry.removeSessionInformation(sessionId);
session.invalidate();
logger.debug("Session invalidated: " + sessionId);

if (request.getParameter("userIsDisabled") != null) {
getRedirectStrategy()
.sendRedirect(request, response, "/login?erroroauth=userIsDisabled");
return;
}

response.sendRedirect(request.getContextPath() + "/login?logout=true");
getRedirectStrategy().sendRedirect(request, response, "/login?logout=true");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

Expand All @@ -17,8 +15,6 @@ public class LoginAttemptService {

@Autowired ApplicationProperties applicationProperties;

private static final Logger logger = LoggerFactory.getLogger(LoginAttemptService.class);

private int MAX_ATTEMPT;
private long ATTEMPT_INCREMENT_TIME;
private ConcurrentHashMap<String, AttemptCounter> attemptsCache;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,6 @@
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;
import org.springframework.security.core.session.SessionRegistry;
import org.springframework.security.core.session.SessionRegistryImpl;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
Expand All @@ -37,6 +35,7 @@
import stirling.software.SPDF.config.security.oauth2.CustomOAuth2AuthenticationSuccessHandler;
import stirling.software.SPDF.config.security.oauth2.CustomOAuth2LogoutSuccessHandler;
import stirling.software.SPDF.config.security.oauth2.CustomOAuth2UserService;
import stirling.software.SPDF.config.security.session.SessionPersistentRegistry;
import stirling.software.SPDF.model.ApplicationProperties;
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2;
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2.Client;
Expand All @@ -47,7 +46,7 @@
import stirling.software.SPDF.repository.JPATokenRepositoryImpl;

@Configuration
@EnableWebSecurity()
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfiguration {

Expand All @@ -73,11 +72,7 @@ public PasswordEncoder passwordEncoder() {
@Autowired private LoginAttemptService loginAttemptService;

@Autowired private FirstLoginFilter firstLoginFilter;

@Bean
public SessionRegistry sessionRegistry() {
return new SessionRegistryImpl();
}
@Autowired private SessionPersistentRegistry sessionRegistry;

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
Expand All @@ -94,7 +89,7 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
.maximumSessions(10)
.maxSessionsPreventsLogin(false)
.sessionRegistry(sessionRegistry())
.sessionRegistry(sessionRegistry)
.expiredUrl("/login?logout=true"));

http.formLogin(
Expand All @@ -103,7 +98,7 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
.loginPage("/login")
.successHandler(
new CustomAuthenticationSuccessHandler(
loginAttemptService))
loginAttemptService, userService))
.defaultSuccessUrl("/")
.failureHandler(
new CustomAuthenticationFailureHandler(
Expand Down Expand Up @@ -160,7 +155,11 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {

// Handle OAUTH2 Logins
if (applicationProperties.getSecurity().getOAUTH2() != null
&& applicationProperties.getSecurity().getOAUTH2().getEnabled()) {
&& applicationProperties.getSecurity().getOAUTH2().getEnabled()
&& !applicationProperties
.getSecurity()
.getLoginMethod()
.equalsIgnoreCase("normal")) {

http.oauth2Login(
oauth2 ->
Expand Down Expand Up @@ -191,10 +190,8 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
.logout(
logout ->
logout.logoutSuccessHandler(
new CustomOAuth2LogoutSuccessHandler(
this.applicationProperties,
sessionRegistry()))
.invalidateHttpSession(true));
new CustomOAuth2LogoutSuccessHandler(
applicationProperties)));
}
} else {
http.csrf(csrf -> csrf.disable())
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package stirling.software.SPDF.config.security;

import java.io.IOException;
import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
Expand All @@ -9,24 +10,26 @@
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.session.SessionInformation;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import stirling.software.SPDF.config.security.session.SessionPersistentRegistry;
import stirling.software.SPDF.model.ApiKeyAuthenticationToken;

@Component
public class UserAuthenticationFilter extends OncePerRequestFilter {

@Autowired private UserDetailsService userDetailsService;

@Autowired @Lazy private UserService userService;

@Autowired private SessionPersistentRegistry sessionPersistentRegistry;

@Autowired
@Qualifier("loginEnabled")
public boolean loginEnabledValue;
Expand Down Expand Up @@ -87,6 +90,43 @@ protected void doFilterInternal(
}
}

// Check if the authenticated user is disabled and invalidate their session if so
if (authentication != null && authentication.isAuthenticated()) {
Object principal = authentication.getPrincipal();
String username = null;
if (principal instanceof UserDetails) {
username = ((UserDetails) principal).getUsername();
} else if (principal instanceof OAuth2User) {
username = ((OAuth2User) principal).getName();
} else if (principal instanceof String) {
username = (String) principal;
}

List<SessionInformation> sessionsInformations =
sessionPersistentRegistry.getAllSessions(principal, false);

if (username != null) {
boolean isUserExists = userService.usernameExistsIgnoreCase(username);
boolean isUserDisabled = userService.isUserDisabled(username);

if (!isUserExists || isUserDisabled) {
for (SessionInformation sessionsInformation : sessionsInformations) {
sessionsInformation.expireNow();
sessionPersistentRegistry.expireSession(sessionsInformation.getSessionId());
}
}

if (!isUserExists) {
response.sendRedirect(request.getContextPath() + "/logout?badcredentials=true");
return;
}
if (isUserDisabled) {
response.sendRedirect(request.getContextPath() + "/logout?userIsDisabled=true");
return;
}
}
}

filterChain.doFilter(request, response);
}

Expand Down
Loading

0 comments on commit 29fcbf3

Please sign in to comment.