Skip to content

Commit

Permalink
Update security configuration and security related spring beans
Browse files Browse the repository at this point in the history
- remove old Jwt implementation
  • Loading branch information
kostobog committed May 3, 2024
1 parent faca007 commit f077962
Show file tree
Hide file tree
Showing 16 changed files with 604 additions and 189 deletions.
151 changes: 121 additions & 30 deletions src/main/java/cz/cvut/kbss/analysis/config/SecurityConfig.java
Original file line number Diff line number Diff line change
@@ -1,75 +1,166 @@
package cz.cvut.kbss.analysis.config;

import cz.cvut.kbss.analysis.security.JwtConfigurer;
import cz.cvut.kbss.analysis.service.JwtTokenProvider;
import cz.cvut.kbss.analysis.service.security.SecurityUtils;
import cz.cvut.kbss.analysis.config.conf.SecurityConf;
import cz.cvut.kbss.analysis.exception.FtaFmeaException;
import cz.cvut.kbss.analysis.security.CsrfHeaderFilter;
import cz.cvut.kbss.analysis.security.CustomSwitchUserFilter;
import cz.cvut.kbss.analysis.security.SecurityConstants;
import cz.cvut.kbss.analysis.util.ConfigParam;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.access.intercept.AuthorizationFilter;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.HttpStatusEntryPoint;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import org.springframework.security.web.authentication.switchuser.SwitchUserFilter;
import org.springframework.security.web.csrf.CsrfFilter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;

import java.util.Arrays;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.*;


@ConditionalOnProperty(prefix = "security", name = "provider", havingValue = "internal", matchIfMissing = true)
@Configuration
@EnableWebSecurity
@Slf4j
@EnableMethodSecurity
public class SecurityConfig {

private final JwtTokenProvider jwtTokenProvider;
private final SecurityUtils securityUtils;
private final AuthenticationProvider ontologyAuthenticationProvider;

private final AuthenticationSuccessHandler authenticationSuccessHandler;

private final AuthenticationFailureHandler authenticationFailureHandler;

private final LogoutSuccessHandler logoutSuccessHandler;


private static final String[] COOKIES_TO_DESTROY = {
SecurityConstants.SESSION_COOKIE_NAME,
SecurityConstants.REMEMBER_ME_COOKIE_NAME,
SecurityConstants.CSRF_COOKIE_NAME
};


@Autowired
public SecurityConfig(JwtTokenProvider jwtTokenProvider, SecurityUtils securityUtils) {
this.jwtTokenProvider = jwtTokenProvider;
this.securityUtils = securityUtils;
public SecurityConfig(AuthenticationProvider ontologyAuthenticationProvider, AuthenticationSuccessHandler authenticationSuccessHandler, AuthenticationFailureHandler authenticationFailureHandler, LogoutSuccessHandler logoutSuccessHandler) {
this.ontologyAuthenticationProvider = ontologyAuthenticationProvider;
this.authenticationSuccessHandler = authenticationSuccessHandler;
this.authenticationFailureHandler = authenticationFailureHandler;
this.logoutSuccessHandler = logoutSuccessHandler;
}

@Bean
public AuthenticationManager buildAuthenticationManager(HttpSecurity http) throws Exception {
final AuthenticationManagerBuilder ab = http.getSharedObject(AuthenticationManagerBuilder.class);
ab.authenticationProvider(ontologyAuthenticationProvider);
return ab.build();
}

protected CorsConfigurationSource corsConfigurationSource() {
@Bean
CorsConfigurationSource corsConfigurationSource(SecurityConf config) {
return createCorsConfiguration(config);
}

@Bean
public SecurityFilterChain filterChain(HttpSecurity http, SecurityConf config, UserDetailsService userDetailsService) throws Exception {
log.debug("Using internal security mechanisms.");
final AuthenticationManager authManager = buildAuthenticationManager(http);
http.authorizeHttpRequests(auth ->
auth.requestMatchers("/rest/users/impersonate").
hasAuthority(SecurityConstants.ROLE_ADMIN).
anyRequest().permitAll())
.cors(auth -> auth.configurationSource(corsConfigurationSource(config)))
.csrf(AbstractHttpConfigurer::disable)
.addFilterAfter(new CsrfHeaderFilter(), CsrfFilter.class)
.exceptionHandling(ehc -> ehc.authenticationEntryPoint(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED)))
.formLogin(form ->
form.loginProcessingUrl(SecurityConstants.SECURITY_CHECK_URI)
.successHandler(authenticationSuccessHandler)
.failureHandler(authenticationFailureHandler))
.logout(auth ->
auth.logoutUrl(SecurityConstants.LOGOUT_URI)
.logoutSuccessHandler(logoutSuccessHandler)
.invalidateHttpSession(true).deleteCookies(COOKIES_TO_DESTROY))
.sessionManagement(auth -> auth.maximumSessions(1))
.addFilterAfter(switchUserFilter(userDetailsService), AuthorizationFilter.class)
.authenticationManager(authManager);
return http.build();
}

UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
@Bean
public SwitchUserFilter switchUserFilter(UserDetailsService userDetailsService) {
final SwitchUserFilter filter = new CustomSwitchUserFilter();
filter.setUserDetailsService(userDetailsService);
filter.setUsernameParameter("username");
filter.setSwitchUserUrl("/rest/users/impersonate");
filter.setExitUserUrl("/rest/users/impersonate/logout");
filter.setSuccessHandler(authenticationSuccessHandler);
return filter;
}

CorsConfiguration corsConfiguration = new CorsConfiguration();
corsConfiguration.addExposedHeader("Location");
corsConfiguration.applyPermitDefaultValues();
public static CorsConfigurationSource createCorsConfiguration(SecurityConf configReader) {
final CorsConfiguration corsConfiguration = new CorsConfiguration().applyPermitDefaultValues();
corsConfiguration.setAllowedMethods(Arrays.asList("GET", "PUT", "POST", "PATCH", "DELETE", "OPTIONS"));
configureAllowedOrigins(corsConfiguration, configReader);
corsConfiguration.addExposedHeader(HttpHeaders.AUTHORIZATION);
corsConfiguration.addExposedHeader(HttpHeaders.LOCATION);
corsConfiguration.addExposedHeader(HttpHeaders.CONTENT_DISPOSITION);

final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", corsConfiguration);
return source;
}

private static Optional<String> getApplicationUrlOrigin(SecurityConf configReader) {
String appUrlConfig = configReader.getAppContext();

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
log.debug("Using internal security mechanisms.");
http
.cors(auth -> auth.configurationSource(corsConfigurationSource()))
.httpBasic(AbstractHttpConfigurer::disable)
.csrf(AbstractHttpConfigurer::disable)
.sessionManagement(sessionManagement -> sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers(AntPathRequestMatcher.antMatcher("/auth/register")).permitAll()
.requestMatchers(AntPathRequestMatcher.antMatcher("/auth/signin")).permitAll()
.anyRequest().authenticated())
.apply(new JwtConfigurer(jwtTokenProvider, securityUtils));
return http.build();
if (appUrlConfig.isBlank()) {
return Optional.empty();
}
try {
final URL appUrl = new URL(appUrlConfig);
return Optional.of(appUrl.getProtocol() + "://" + appUrl.getAuthority());
} catch (MalformedURLException e) {
throw new FtaFmeaException("Invalid configuration parameter " + ConfigParam.APP_CONTEXT + ".", e);
}
}

private static void configureAllowedOrigins(CorsConfiguration corsConfig, SecurityConf config) {
final Optional<String> appUrlOrigin = getApplicationUrlOrigin(config);
final List<String> allowedOrigins = new ArrayList<>();
appUrlOrigin.ifPresent(allowedOrigins::add);
final String allowedOriginsConfig = config.getAllowedOrigins();
if (!allowedOrigins.isEmpty() && allowedOriginsConfig != null) {
Arrays.stream(allowedOriginsConfig.split(",")).filter(s -> !s.isBlank()).forEach(allowedOrigins::add);
corsConfig.setAllowedOrigins(allowedOrigins);
corsConfig.setAllowCredentials(true);
} else {
corsConfig.setAllowedOrigins(null);
}
log.debug(
"Using response header Access-Control-Allow-Origin with value {}.",
corsConfig.getAllowedOrigins()
);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package cz.cvut.kbss.analysis.exception;

import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;

/**
* Generic exception for bad requests.
*/
@ResponseStatus(HttpStatus.BAD_REQUEST)
public class BadRequestException extends RuntimeException {

public BadRequestException(String message) {
super(message);
}

public BadRequestException(String message, Throwable cause) {
super(message, cause);
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package cz.cvut.kbss.analysis.exception;

public class EntityNotFoundException extends RuntimeException {
public class EntityNotFoundException extends FtaFmeaException {

public EntityNotFoundException(String message) {
super(message);
Expand All @@ -10,4 +10,4 @@ public static EntityNotFoundException create(String resourceName, Object identif
return new EntityNotFoundException(resourceName + " identified by " + identifier + " not found.");
}

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package cz.cvut.kbss.analysis.exception;


/**
* Application-specific exception.
* <p>
* All exceptions related to the application should be subclasses of this one.
*/
public class FtaFmeaException extends RuntimeException {

protected FtaFmeaException() {
}

public FtaFmeaException(String message) {
super(message);
}

public FtaFmeaException(String message, Throwable cause) {
super(message, cause);
}

public FtaFmeaException(Throwable cause) {
super(cause);
}
}
27 changes: 27 additions & 0 deletions src/main/java/cz/cvut/kbss/analysis/model/User.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Objects;

import static java.util.stream.Collectors.toList;

Expand Down Expand Up @@ -67,4 +68,30 @@ public boolean isEnabled() {
public String toString() {
return "User <" + getUri() + "/>";
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
User that = (User) o;
return Objects.equals(getUri(), that.getUri());
}

@Override
public int hashCode() {
return Objects.hash(getUri());
}

/**
* @return A copy of this user.
*/
public User copy() {
final User copy = new User();
copy.setUri(getUri());
copy.setUsername(getUsername());
copy.setPassword(getPassword());
copy.setRoles(getRoles());
return copy;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package cz.cvut.kbss.analysis.security;

import com.fasterxml.jackson.databind.ObjectMapper;
import cz.cvut.kbss.analysis.security.model.LoginStatus;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.stereotype.Service;

import java.io.IOException;

@Service
@Slf4j
public class AuthenticationFailure implements AuthenticationFailureHandler {
private final ObjectMapper mapper;

public AuthenticationFailure(ObjectMapper mapper) {
this.mapper = mapper;
}

@Override
public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse,
AuthenticationException e) throws IOException {
log.atTrace().log("Login failed for user {}.", httpServletRequest.getParameter(SecurityConstants.USERNAME_PARAM));
final LoginStatus status = new LoginStatus(false, false, null, e.getMessage());
mapper.writeValue(httpServletResponse.getOutputStream(), status);
}
}
Loading

0 comments on commit f077962

Please sign in to comment.