Skip to content

Commit

Permalink
Merge pull request #87 from kbss-cvut/feature/support-oauth2-rebase
Browse files Browse the repository at this point in the history
Feature/support oauth2 rebase
  • Loading branch information
blcham authored May 14, 2024
2 parents 185e6c6 + 9302e8d commit 4f13178
Show file tree
Hide file tree
Showing 36 changed files with 1,020 additions and 301 deletions.
4 changes: 3 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ buildscript {
}

plugins {
id 'org.springframework.boot' version '3.1.5'
id 'org.springframework.boot' version '3.2.2'
id 'io.spring.dependency-management' version '1.1.3'
id 'java'
id 'war'
Expand Down Expand Up @@ -44,6 +44,8 @@ dependencies {

implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.security:spring-security-oauth2-core'
implementation 'org.springframework.boot:spring-boot-starter-oauth2-resource-server'
implementation 'org.springframework.boot:spring-boot-starter-validation'

implementation 'io.jsonwebtoken:jjwt:0.9.1'
Expand Down
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()
);
}

}
20 changes: 0 additions & 20 deletions src/main/java/cz/cvut/kbss/analysis/config/conf/JwtConf.java

This file was deleted.

30 changes: 30 additions & 0 deletions src/main/java/cz/cvut/kbss/analysis/config/conf/SecurityConf.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package cz.cvut.kbss.analysis.config.conf;

import cz.cvut.kbss.analysis.util.ConfigParam;
import lombok.Getter;
import lombok.Setter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;

@Setter
@Getter
@Configuration
@EnableConfigurationProperties
@ConfigurationProperties("security")
public class SecurityConf {
private String allowedOrigins;

private String appContext;

private String roleClaim;

@Autowired
public SecurityConf(Environment env) {
allowedOrigins = env.getProperty(ConfigParam.CORS_ALLOWED_ORIGINS.toString());
appContext = env.getProperty(ConfigParam.APP_CONTEXT.toString());
roleClaim = env.getProperty(ConfigParam.OIDC_ROLE_CLAIM.toString());
}
}
53 changes: 0 additions & 53 deletions src/main/java/cz/cvut/kbss/analysis/controller/AuthController.java

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package cz.cvut.kbss.analysis.controller;

import cz.cvut.kbss.analysis.exception.EntityNotFoundException;
import cz.cvut.kbss.analysis.model.User;
import cz.cvut.kbss.analysis.security.SecurityConstants;
import cz.cvut.kbss.analysis.service.UserRepositoryService;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.http.MediaType;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
* API for getting basic user info.
* <p>
* Enabled when OIDC security is used.
*/
@ConditionalOnProperty(prefix = "security", name = "provider", havingValue = "oidc")
@RestController
@RequestMapping("/users")
public class OidcUserController {

private final UserRepositoryService userService;

public OidcUserController(UserRepositoryService userService) {
this.userService = userService;
}

@PreAuthorize("hasRole('" + SecurityConstants.ROLE_USER + "')")
@GetMapping(value = "/current", produces = MediaType.APPLICATION_JSON_VALUE)
public User getCurrent() {
return userService.getCurrentUser();
}

@PreAuthorize("hasRole('" + SecurityConstants.ROLE_ADMIN + "') or #username == authentication.name or " +
"hasRole('" + SecurityConstants.ROLE_USER + "')")
@GetMapping(value = "/{username}", produces = MediaType.APPLICATION_JSON_VALUE)
public User getByUsername(@PathVariable("username") String username) {
return userService.findByUsername(username).orElseThrow(() ->
EntityNotFoundException.create("User", username));
}

}
30 changes: 30 additions & 0 deletions src/main/java/cz/cvut/kbss/analysis/controller/UserController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package cz.cvut.kbss.analysis.controller;


import cz.cvut.kbss.analysis.model.User;
import cz.cvut.kbss.analysis.security.SecurityConstants;
import cz.cvut.kbss.analysis.service.UserRepositoryService;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.http.MediaType;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@ConditionalOnProperty(prefix = "security", name = "provider", havingValue = "internal", matchIfMissing = true)
@RestController
@RequestMapping("/users")
public class UserController {
private final UserRepositoryService userService;

public UserController(UserRepositoryService userService) {
this.userService = userService;
}

@PreAuthorize("hasRole('" + SecurityConstants.ROLE_USER + "')")
@GetMapping(value = "/current", produces = MediaType.APPLICATION_JSON_VALUE)
public User getCurrent() {
return userService.getCurrentUser();
}

}
Loading

0 comments on commit 4f13178

Please sign in to comment.