From 7e5b3cad5e7f78b3b2e64192301881c8764bfa91 Mon Sep 17 00:00:00 2001 From: Bogdan Kostov Date: Fri, 3 May 2024 15:02:12 +0200 Subject: [PATCH 01/11] Fix wrong SCAN_PACKAGE in test Environment --- .../java/cz/cvut/kbss/analysis/environment/Environment.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/cz/cvut/kbss/analysis/environment/Environment.java b/src/test/java/cz/cvut/kbss/analysis/environment/Environment.java index 478779bb..7a50122d 100755 --- a/src/test/java/cz/cvut/kbss/analysis/environment/Environment.java +++ b/src/test/java/cz/cvut/kbss/analysis/environment/Environment.java @@ -93,7 +93,7 @@ public static ObjectMapper getJsonLdObjectMapper() { jsonLdObjectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); jsonLdObjectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); final JsonLdModule module = new JsonLdModule(); - module.configure(ConfigParam.SCAN_PACKAGE, "cz.cvut.kbss.termit"); + module.configure(ConfigParam.SCAN_PACKAGE, "cz.cvut.kbss.analysis"); jsonLdObjectMapper.registerModule(module); } return jsonLdObjectMapper; From 867b697134d7d20336978fdc08a4da74d76e10d2 Mon Sep 17 00:00:00 2001 From: Bogdan Kostov Date: Fri, 3 May 2024 15:05:49 +0200 Subject: [PATCH 02/11] Update dependencies, add oauth dependencies, upgrade spring boot version --- build.gradle | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index df0e55de..e08f573b 100644 --- a/build.gradle +++ b/build.gradle @@ -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' @@ -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' From 869f25562f6b928d3b913bc8ba79d7400a1d8078 Mon Sep 17 00:00:00 2001 From: Bogdan Kostov Date: Fri, 3 May 2024 17:53:13 +0200 Subject: [PATCH 03/11] Update security configuration - add SecurityConf and add oidc and oauth2 application.yml - add ConfigReader, ConfigParam - add SecurityConstants --- .../analysis/config/conf/SecurityConf.java | 26 ++++++++++++ .../analysis/security/SecurityConstants.java | 33 +++++++++++++++ .../kbss/analysis/service/ConfigReader.java | 42 +++++++++++++++++++ .../cvut/kbss/analysis/util/ConfigParam.java | 19 +++++++++ src/main/resources/application.yml | 18 +++++++- 5 files changed, 136 insertions(+), 2 deletions(-) create mode 100644 src/main/java/cz/cvut/kbss/analysis/config/conf/SecurityConf.java create mode 100644 src/main/java/cz/cvut/kbss/analysis/security/SecurityConstants.java create mode 100644 src/main/java/cz/cvut/kbss/analysis/service/ConfigReader.java create mode 100644 src/main/java/cz/cvut/kbss/analysis/util/ConfigParam.java diff --git a/src/main/java/cz/cvut/kbss/analysis/config/conf/SecurityConf.java b/src/main/java/cz/cvut/kbss/analysis/config/conf/SecurityConf.java new file mode 100644 index 00000000..2bae7cd5 --- /dev/null +++ b/src/main/java/cz/cvut/kbss/analysis/config/conf/SecurityConf.java @@ -0,0 +1,26 @@ +package cz.cvut.kbss.analysis.config.conf; + +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +@Setter +@Getter +@Configuration +@EnableConfigurationProperties +@ConfigurationProperties("security") +public class SecurityConf { + private String allowedOrigins; + + private String appContext; + + private String issuerUri; + + private String secretKey; + + private Long expiryMs; + + private String roleClaim; +} diff --git a/src/main/java/cz/cvut/kbss/analysis/security/SecurityConstants.java b/src/main/java/cz/cvut/kbss/analysis/security/SecurityConstants.java new file mode 100644 index 00000000..a7a52374 --- /dev/null +++ b/src/main/java/cz/cvut/kbss/analysis/security/SecurityConstants.java @@ -0,0 +1,33 @@ +package cz.cvut.kbss.analysis.security; + +public class SecurityConstants { + + private SecurityConstants() { + throw new AssertionError(); + } + + public static final String SESSION_COOKIE_NAME = "FSM_JSESSIONID"; + + public static final String REMEMBER_ME_COOKIE_NAME = "remember-me"; + + public static final String CSRF_COOKIE_NAME = "CSRF-TOKEN"; + + public static final String USERNAME_PARAM = "username"; + + public static final String PASSWORD_PARAM = "password"; + + public static final String SECURITY_CHECK_URI = "/j_spring_security_check"; + + public static final String LOGOUT_URI = "/j_spring_security_logout"; + + public static final String COOKIE_URI = "/"; + + /** + * Session timeout in seconds. + */ + public static final int SESSION_TIMEOUT = 12 * 60 * 60; + + public static final String ROLE_USER = "ROLE_USER"; + + public static final String ROLE_ADMIN = "ROLE_ADMIN"; +} \ No newline at end of file diff --git a/src/main/java/cz/cvut/kbss/analysis/service/ConfigReader.java b/src/main/java/cz/cvut/kbss/analysis/service/ConfigReader.java new file mode 100644 index 00000000..59c8d329 --- /dev/null +++ b/src/main/java/cz/cvut/kbss/analysis/service/ConfigReader.java @@ -0,0 +1,42 @@ +package cz.cvut.kbss.analysis.service; + +import cz.cvut.kbss.analysis.util.ConfigParam; +import org.springframework.core.env.Environment; +import org.springframework.stereotype.Component; + +import java.util.Map; + +@Component +public class ConfigReader { + + private final Environment environment; + + public ConfigReader(Environment environment) { + this.environment = environment; + } + + /** + * Gets value of the specified configuration parameter. + * + * @param param Configuration parameter + * @return Configuration parameter value, empty string if the parameter is not set + */ + public String getConfig(ConfigParam param) { + return getConfig(param, ""); + } + + public String getConfig(ConfigParam param, String defaultValue) { + if (environment.containsProperty(param.toString())) { + return environment.getProperty(param.toString()); + } + return defaultValue; + } + + public String getConfigWithParams(ConfigParam param, Map params) { + String str = environment.getProperty(param.toString()); + for ( String key : params.keySet() ) { + str = str.replace("{{" + key + "}}", params.get(key)); + } + return str; + } +} \ No newline at end of file diff --git a/src/main/java/cz/cvut/kbss/analysis/util/ConfigParam.java b/src/main/java/cz/cvut/kbss/analysis/util/ConfigParam.java new file mode 100644 index 00000000..4e408601 --- /dev/null +++ b/src/main/java/cz/cvut/kbss/analysis/util/ConfigParam.java @@ -0,0 +1,19 @@ +package cz.cvut.kbss.analysis.util; + +public enum ConfigParam { + + SECURITY_SAME_SITE("security.sameSite"), + + APP_CONTEXT("appContext"); + + private final String name; + + ConfigParam(String name) { + this.name = name; + } + + @Override + public String toString() { + return name; + } +} \ No newline at end of file diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index fab1ae42..a3271278 100755 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -2,7 +2,7 @@ server: port: 9999 repository: - url: http://localhost:1235/services/db-server/repositories/fta-fmea + url: http://localhost/ava/services/db-server/repositories/fta-fmea persistence: @@ -10,6 +10,11 @@ persistence: language: cs security: + provider: oidc + oidcRoleClaim: realm_access.roles + appContext: http://localhost:3000/fta-fmea + cors: + allowedOrigins: jwt: secretKey: lwrUj5PmCE6X8ekbLd9wDTRlBkEJA0HB expiryMs: 28800000 # 8 hours @@ -25,4 +30,13 @@ logging: annotator: list-documents-api: ${LIST_DOCUMENT_API:http://localhost:8282/s-pipes/service?_pId=list-documents} convert-document-api: ${CONVERT_DOCUMENT_API:http://localhost:8282/s-pipes/service?_pId=convert-document} - process-annotatoins-api: ${PROCESS_ANNOTATION_API:https://localhost:8090/annotator/process-annotation-service.sh} \ No newline at end of file + process-annotatoins-api: ${PROCESS_ANNOTATION_API:https://localhost:8090/annotator/process-annotation-service.sh} + +spring: + security: + oauth2: + resourceserver: + jwt: + issuer-uri: http://localhost/services/auth/realms/record-manager + jwk-set-uri: http://localhost/services/auth/realms/record-manager/protocol/openid-connect/certs + From 356b66af43d2162fea574d8529402682c5de0dfb Mon Sep 17 00:00:00 2001 From: Bogdan Kostov Date: Fri, 3 May 2024 18:07:52 +0200 Subject: [PATCH 04/11] Refactor SecurityUtils.getCurrentUser --- .../cvut/kbss/analysis/dao/FaultTreeDao.java | 5 +- .../kbss/analysis/dao/ManagedEntityDao.java | 7 +- .../cz/cvut/kbss/analysis/dao/SystemDao.java | 5 +- ...ComplexManagedEntityRepositoryService.java | 8 +- .../service/FaultTreeRepositoryService.java | 7 +- .../service/SystemRepositoryService.java | 6 +- .../service/UserRepositoryService.java | 10 ++- .../service/security/SecurityUtils.java | 75 +++++++++++++++---- 8 files changed, 91 insertions(+), 32 deletions(-) diff --git a/src/main/java/cz/cvut/kbss/analysis/dao/FaultTreeDao.java b/src/main/java/cz/cvut/kbss/analysis/dao/FaultTreeDao.java index af19e6b9..5556e37b 100755 --- a/src/main/java/cz/cvut/kbss/analysis/dao/FaultTreeDao.java +++ b/src/main/java/cz/cvut/kbss/analysis/dao/FaultTreeDao.java @@ -6,6 +6,7 @@ import cz.cvut.kbss.analysis.model.FaultTree; import cz.cvut.kbss.analysis.model.FaultTreeSummary; import cz.cvut.kbss.analysis.service.IdentifierService; +import cz.cvut.kbss.analysis.service.security.SecurityUtils; import cz.cvut.kbss.analysis.util.Vocabulary; import cz.cvut.kbss.jopa.model.EntityManager; import cz.cvut.kbss.jopa.model.descriptors.EntityDescriptor; @@ -22,8 +23,8 @@ public class FaultTreeDao extends ManagedEntityDao { @Autowired - protected FaultTreeDao(EntityManager em, PersistenceConf config, IdentifierService identifierService) { - super(FaultTree.class, em, config, identifierService); + protected FaultTreeDao(EntityManager em, PersistenceConf config, IdentifierService identifierService, SecurityUtils securityUtils) { + super(FaultTree.class, em, config, identifierService, securityUtils); } public boolean isRootEvent(URI faultEventIri) { diff --git a/src/main/java/cz/cvut/kbss/analysis/dao/ManagedEntityDao.java b/src/main/java/cz/cvut/kbss/analysis/dao/ManagedEntityDao.java index 0ba04767..4e876218 100644 --- a/src/main/java/cz/cvut/kbss/analysis/dao/ManagedEntityDao.java +++ b/src/main/java/cz/cvut/kbss/analysis/dao/ManagedEntityDao.java @@ -23,8 +23,11 @@ public class ManagedEntityDao extends NamedEntityDao public static URI P_CREATOR = URI.create( DC.Terms.CREATOR); public static URI P_LAST_EDITOR = URI.create(Vocabulary.s_c_editor); - protected ManagedEntityDao(Class type, EntityManager em, PersistenceConf config, IdentifierService identifierService) { + protected final SecurityUtils securityUtils; + + protected ManagedEntityDao(Class type, EntityManager em, PersistenceConf config, IdentifierService identifierService, SecurityUtils securityUtils) { super(type, em, config, identifierService); + this.securityUtils = securityUtils; } @@ -41,7 +44,7 @@ protected void setEntityDescriptor(EntityDescriptor descriptor) { } public void setChangedByContext(URI context, Date date){ - UserReference user = SecurityUtils.currentUserReference(); + UserReference user = securityUtils.getCurrentUserReference(); em.createNativeQuery(""" DELETE{ GRAPH ?context{ diff --git a/src/main/java/cz/cvut/kbss/analysis/dao/SystemDao.java b/src/main/java/cz/cvut/kbss/analysis/dao/SystemDao.java index 6b45a6cf..498c66ec 100755 --- a/src/main/java/cz/cvut/kbss/analysis/dao/SystemDao.java +++ b/src/main/java/cz/cvut/kbss/analysis/dao/SystemDao.java @@ -3,6 +3,7 @@ import cz.cvut.kbss.analysis.config.conf.PersistenceConf; import cz.cvut.kbss.analysis.model.System; import cz.cvut.kbss.analysis.service.IdentifierService; +import cz.cvut.kbss.analysis.service.security.SecurityUtils; import cz.cvut.kbss.jopa.model.EntityManager; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Repository; @@ -11,7 +12,7 @@ public class SystemDao extends ManagedEntityDao { @Autowired - protected SystemDao(EntityManager em, PersistenceConf config, IdentifierService identifierService) { - super(System.class, em, config, identifierService); + protected SystemDao(EntityManager em, PersistenceConf config, IdentifierService identifierService, SecurityUtils securityUtils) { + super(System.class, em, config, identifierService, securityUtils); } } diff --git a/src/main/java/cz/cvut/kbss/analysis/service/ComplexManagedEntityRepositoryService.java b/src/main/java/cz/cvut/kbss/analysis/service/ComplexManagedEntityRepositoryService.java index a5690903..5b173376 100644 --- a/src/main/java/cz/cvut/kbss/analysis/service/ComplexManagedEntityRepositoryService.java +++ b/src/main/java/cz/cvut/kbss/analysis/service/ComplexManagedEntityRepositoryService.java @@ -21,16 +21,18 @@ */ public abstract class ComplexManagedEntityRepositoryService extends BaseRepositoryService { protected final UserDao userDao; + protected final SecurityUtils securityUtils; - public ComplexManagedEntityRepositoryService(Validator validator, UserDao userDao) { + public ComplexManagedEntityRepositoryService(Validator validator, UserDao userDao, SecurityUtils securityUtils) { super(validator); this.userDao = userDao; + this.securityUtils = securityUtils; } @Override protected void preUpdate(@NonNull T instance) { super.preUpdate(instance); - UserReference user = SecurityUtils.currentUserReference(); + UserReference user = securityUtils.getCurrentUserReference(); instance.setLastEditor(user); instance.setModified(new Date()); } @@ -38,7 +40,7 @@ protected void preUpdate(@NonNull T instance) { @Override protected void prePersist(@NonNull T instance) { super.prePersist(instance); - UserReference user = SecurityUtils.currentUserReference(); + UserReference user = securityUtils.getCurrentUserReference(); instance.setCreator(user); instance.setCreated(new Date()); } diff --git a/src/main/java/cz/cvut/kbss/analysis/service/FaultTreeRepositoryService.java b/src/main/java/cz/cvut/kbss/analysis/service/FaultTreeRepositoryService.java index 42307e84..40531b41 100755 --- a/src/main/java/cz/cvut/kbss/analysis/service/FaultTreeRepositoryService.java +++ b/src/main/java/cz/cvut/kbss/analysis/service/FaultTreeRepositoryService.java @@ -11,6 +11,7 @@ import cz.cvut.kbss.analysis.model.fta.FTAMinimalCutSetEvaluation; import cz.cvut.kbss.analysis.model.fta.FtaEventType; import cz.cvut.kbss.analysis.model.fta.GateType; +import cz.cvut.kbss.analysis.service.security.SecurityUtils; import cz.cvut.kbss.analysis.service.util.FaultTreeTraversalUtils; import cz.cvut.kbss.analysis.service.util.Pair; import cz.cvut.kbss.analysis.util.Vocabulary; @@ -37,7 +38,6 @@ public class FaultTreeRepositoryService extends ComplexManagedEntityRepositorySe private final FunctionRepositoryService functionRepositoryService; private final IdentifierService identifierService; - private final ThreadLocal> visitedBehaviors = new ThreadLocal<>(); @Autowired @@ -47,9 +47,10 @@ public FaultTreeRepositoryService(@Qualifier("defaultValidator") Validator valid FaultEventRepositoryService faultEventRepositoryService, FunctionRepositoryService functionRepositoryService, IdentifierService identifierService, - UserDao userDao + UserDao userDao, + SecurityUtils securityUtils ) { - super(validator, userDao); + super(validator, userDao, securityUtils); this.faultTreeDao = faultTreeDao; this.faultEventScenarioDao = faultEventScenarioDao; this.faultEventRepositoryService = faultEventRepositoryService; diff --git a/src/main/java/cz/cvut/kbss/analysis/service/SystemRepositoryService.java b/src/main/java/cz/cvut/kbss/analysis/service/SystemRepositoryService.java index feb81289..911fd1d3 100755 --- a/src/main/java/cz/cvut/kbss/analysis/service/SystemRepositoryService.java +++ b/src/main/java/cz/cvut/kbss/analysis/service/SystemRepositoryService.java @@ -8,6 +8,7 @@ import cz.cvut.kbss.analysis.model.FailureMode; import cz.cvut.kbss.analysis.model.Item; import cz.cvut.kbss.analysis.model.System; +import cz.cvut.kbss.analysis.service.security.SecurityUtils; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; @@ -33,10 +34,11 @@ public SystemRepositoryService(@Qualifier("defaultValidator") Validator validato SystemDao systemDao, ComponentRepositoryService componentRepositoryService, ComponentDao componentDao, - UserDao userDao + UserDao userDao, + SecurityUtils securityUtils ) { - super(validator, userDao); + super(validator, userDao, securityUtils); this.systemDao = systemDao; this.componentRepositoryService = componentRepositoryService; this.componentDao = componentDao; diff --git a/src/main/java/cz/cvut/kbss/analysis/service/UserRepositoryService.java b/src/main/java/cz/cvut/kbss/analysis/service/UserRepositoryService.java index 9a4d96ae..269d2746 100755 --- a/src/main/java/cz/cvut/kbss/analysis/service/UserRepositoryService.java +++ b/src/main/java/cz/cvut/kbss/analysis/service/UserRepositoryService.java @@ -24,12 +24,18 @@ public class UserRepositoryService extends BaseRepositoryService { private final UserDao userDao; private final PasswordEncoder passwordEncoder; + private final SecurityUtils securityUtils; @Autowired - public UserRepositoryService(@Qualifier("defaultValidator") Validator validator, UserDao userDao, PasswordEncoder passwordEncoder) { + public UserRepositoryService(@Qualifier("defaultValidator") Validator validator, UserDao userDao, PasswordEncoder passwordEncoder, SecurityUtils securityUtils) { super(validator); this.userDao = userDao; this.passwordEncoder = passwordEncoder; + this.securityUtils = securityUtils; + } + + public User getCurrentUser() { + return securityUtils.getCurrentUser(); } @Override @@ -54,7 +60,7 @@ public URI register(User user) { public void updateCurrent(UserUpdateDTO userUpdate) { log.info("> updateCurrent - {}", userUpdate.getUsername()); - User currentUser = SecurityUtils.currentUser(); + User currentUser = getCurrentUser(); if (!currentUser.getUri().equals(userUpdate.getUri())) { log.warn("< updateCurrent - URIs do not match! {} != {}", currentUser.getUri(), userUpdate.getUri()); throw new LogicViolationException("User update uri does not match current user!"); diff --git a/src/main/java/cz/cvut/kbss/analysis/service/security/SecurityUtils.java b/src/main/java/cz/cvut/kbss/analysis/service/security/SecurityUtils.java index f76bb819..d67558be 100755 --- a/src/main/java/cz/cvut/kbss/analysis/service/security/SecurityUtils.java +++ b/src/main/java/cz/cvut/kbss/analysis/service/security/SecurityUtils.java @@ -1,11 +1,18 @@ package cz.cvut.kbss.analysis.service.security; +import cz.cvut.kbss.analysis.config.conf.SecurityConf; +import cz.cvut.kbss.analysis.dao.UserDao; +import cz.cvut.kbss.analysis.exception.EntityNotFoundException; import cz.cvut.kbss.analysis.model.User; import cz.cvut.kbss.analysis.model.UserReference; -import org.springframework.security.core.Authentication; +import org.springframework.security.authentication.AbstractAuthenticationToken; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.context.SecurityContextImpl; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.oauth2.core.oidc.OidcUserInfo; +import org.springframework.security.oauth2.jwt.Jwt; import org.springframework.stereotype.Service; /** @@ -13,37 +20,73 @@ */ @Service public class SecurityUtils { - - public SecurityUtils() { + + private final UserDao userDao; + + private final SecurityConf config; + + public SecurityUtils(UserDao userDao, SecurityConf config) { + this.userDao = userDao; + this.config = config; // Ensures security context is propagated to additionally spun threads, e.g., used // by @Async methods - SecurityContextHolder.setStrategyName(SecurityContextHolder.MODE_INHERITABLETHREADLOCAL); + SecurityContextHolder.setStrategyName(SecurityContextHolder.MODE_INHERITABLETHREADLOCAL); // TODO check what it does } /** + * Gets the currently authenticated user. + * If the user is impersonating another user, the impersonated user is returned. + * Otherwise, the currently authenticated user is returned. * - *

It allows to access the currently logged in user without injecting {@code SecurityUtils} - * as a bean. - * - * @return Currently logged in user + * @return */ - public static User currentUser() { - return (User) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); + public User getCurrentUser() { + final SecurityContext context = SecurityContextHolder.getContext(); + assert context != null; + final Object principal = context.getAuthentication().getPrincipal(); + if (principal instanceof Jwt) { + return resolveAccountFromOAuthPrincipal((Jwt) principal); + } else { + final String username = context.getAuthentication().getName(); + //TODO impersonalization? + return userDao.findByUsername(username).orElseThrow().copy(); + } + } + public UserReference getCurrentUserReference() { + return new UserReference(getCurrentUser()); } - public static UserReference currentUserReference(){ - return new UserReference(currentUser()); + // TODO map role, but I am not sure which changes in the model when be required if I add addRole method to User + private User resolveAccountFromOAuthPrincipal(Jwt principal) { + final OidcUserInfo userInfo = new OidcUserInfo(principal.getClaims()); +// final List roles = new OidcGrantedAuthoritiesExtractor(config).extractRoles(principal); +// var user = userDao.findByUsername(userInfo.getPreferredUsername()); +// roles.stream().map(r -> "ROLE_" + r).forEach(user::addRole); + return userDao.findByUsername(userInfo.getPreferredUsername()).orElseThrow(() -> EntityNotFoundException.create("User", userInfo.getPreferredUsername())); } + + /** - * Sets authentication to the current thread's security context. + * Sets the current security context to the user represented by the provided user details. + *

+ * Note that this method erases credentials from the provided user details for security reasons. + *

+ * This method should be used only when internal authentication is used. * - * @param authentication Currently logged-in user's authentication + * @param userDetails User details */ - public void setCurrentUser(Authentication authentication) { + public static AbstractAuthenticationToken setCurrentUser(UserDetails userDetails) { + final UsernamePasswordAuthenticationToken token = + UsernamePasswordAuthenticationToken.authenticated(userDetails, userDetails.getPassword(), + userDetails.getAuthorities()); + token.setDetails(userDetails); + token.eraseCredentials(); // Do not pass credentials around + final SecurityContext context = new SecurityContextImpl(); - context.setAuthentication(authentication); + context.setAuthentication(token); SecurityContextHolder.setContext(context); + return token; } } From 28d8fd636be78d2743a38a4fe569610406435006 Mon Sep 17 00:00:00 2001 From: Bogdan Kostov Date: Fri, 3 May 2024 18:31:33 +0200 Subject: [PATCH 05/11] Update security configuration and security related spring beans - remove old Jwt implementation --- .../kbss/analysis/config/SecurityConfig.java | 151 ++++++++++++++---- .../exception/BadRequestException.java | 19 +++ .../exception/EntityNotFoundException.java | 4 +- .../analysis/exception/FtaFmeaException.java | 25 +++ .../cz/cvut/kbss/analysis/model/User.java | 27 ++++ .../security/AuthenticationFailure.java | 30 ++++ .../security/AuthenticationSuccess.java | 120 ++++++++++++++ .../analysis/security/CsrfHeaderFilter.java | 31 ++++ .../security/CustomSwitchUserFilter.java | 23 +++ .../kbss/analysis/security/JwtConfigurer.java | 25 --- .../analysis/security/JwtTokenFilter.java | 60 ------- .../security/OAuth2SecurityConfig.java | 85 ++++++++++ .../OntologyAuthenticationProvider.java | 50 ++++++ .../analysis/security/model/LoginStatus.java | 19 +++ .../analysis/service/JwtTokenProvider.java | 72 --------- .../util/OidcGrantedAuthoritiesExtractor.java | 52 ++++++ 16 files changed, 604 insertions(+), 189 deletions(-) create mode 100644 src/main/java/cz/cvut/kbss/analysis/exception/BadRequestException.java create mode 100644 src/main/java/cz/cvut/kbss/analysis/exception/FtaFmeaException.java create mode 100644 src/main/java/cz/cvut/kbss/analysis/security/AuthenticationFailure.java create mode 100644 src/main/java/cz/cvut/kbss/analysis/security/AuthenticationSuccess.java create mode 100644 src/main/java/cz/cvut/kbss/analysis/security/CsrfHeaderFilter.java create mode 100644 src/main/java/cz/cvut/kbss/analysis/security/CustomSwitchUserFilter.java delete mode 100755 src/main/java/cz/cvut/kbss/analysis/security/JwtConfigurer.java delete mode 100755 src/main/java/cz/cvut/kbss/analysis/security/JwtTokenFilter.java create mode 100644 src/main/java/cz/cvut/kbss/analysis/security/OAuth2SecurityConfig.java create mode 100644 src/main/java/cz/cvut/kbss/analysis/security/OntologyAuthenticationProvider.java create mode 100644 src/main/java/cz/cvut/kbss/analysis/security/model/LoginStatus.java delete mode 100755 src/main/java/cz/cvut/kbss/analysis/service/JwtTokenProvider.java create mode 100644 src/main/java/cz/cvut/kbss/analysis/util/OidcGrantedAuthoritiesExtractor.java diff --git a/src/main/java/cz/cvut/kbss/analysis/config/SecurityConfig.java b/src/main/java/cz/cvut/kbss/analysis/config/SecurityConfig.java index cfd500db..b6817e8e 100755 --- a/src/main/java/cz/cvut/kbss/analysis/config/SecurityConfig.java +++ b/src/main/java/cz/cvut/kbss/analysis/config/SecurityConfig.java @@ -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 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 appUrlOrigin = getApplicationUrlOrigin(config); + final List 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() + ); + } + } \ No newline at end of file diff --git a/src/main/java/cz/cvut/kbss/analysis/exception/BadRequestException.java b/src/main/java/cz/cvut/kbss/analysis/exception/BadRequestException.java new file mode 100644 index 00000000..f6566200 --- /dev/null +++ b/src/main/java/cz/cvut/kbss/analysis/exception/BadRequestException.java @@ -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); + } +} diff --git a/src/main/java/cz/cvut/kbss/analysis/exception/EntityNotFoundException.java b/src/main/java/cz/cvut/kbss/analysis/exception/EntityNotFoundException.java index 1faf8eb2..b2d432ea 100755 --- a/src/main/java/cz/cvut/kbss/analysis/exception/EntityNotFoundException.java +++ b/src/main/java/cz/cvut/kbss/analysis/exception/EntityNotFoundException.java @@ -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); @@ -10,4 +10,4 @@ public static EntityNotFoundException create(String resourceName, Object identif return new EntityNotFoundException(resourceName + " identified by " + identifier + " not found."); } -} +} \ No newline at end of file diff --git a/src/main/java/cz/cvut/kbss/analysis/exception/FtaFmeaException.java b/src/main/java/cz/cvut/kbss/analysis/exception/FtaFmeaException.java new file mode 100644 index 00000000..876fbcc0 --- /dev/null +++ b/src/main/java/cz/cvut/kbss/analysis/exception/FtaFmeaException.java @@ -0,0 +1,25 @@ +package cz.cvut.kbss.analysis.exception; + + +/** + * Application-specific exception. + *

+ * 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); + } +} diff --git a/src/main/java/cz/cvut/kbss/analysis/model/User.java b/src/main/java/cz/cvut/kbss/analysis/model/User.java index 23d1dde0..737e4528 100755 --- a/src/main/java/cz/cvut/kbss/analysis/model/User.java +++ b/src/main/java/cz/cvut/kbss/analysis/model/User.java @@ -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; @@ -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; + } + } diff --git a/src/main/java/cz/cvut/kbss/analysis/security/AuthenticationFailure.java b/src/main/java/cz/cvut/kbss/analysis/security/AuthenticationFailure.java new file mode 100644 index 00000000..d800c838 --- /dev/null +++ b/src/main/java/cz/cvut/kbss/analysis/security/AuthenticationFailure.java @@ -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); + } +} diff --git a/src/main/java/cz/cvut/kbss/analysis/security/AuthenticationSuccess.java b/src/main/java/cz/cvut/kbss/analysis/security/AuthenticationSuccess.java new file mode 100644 index 00000000..24c97c39 --- /dev/null +++ b/src/main/java/cz/cvut/kbss/analysis/security/AuthenticationSuccess.java @@ -0,0 +1,120 @@ +package cz.cvut.kbss.analysis.security; + + +import com.fasterxml.jackson.databind.ObjectMapper; +import cz.cvut.kbss.analysis.exception.FtaFmeaException; +import cz.cvut.kbss.analysis.security.model.LoginStatus; +import cz.cvut.kbss.analysis.service.ConfigReader; +import cz.cvut.kbss.analysis.util.ConfigParam; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpHeaders; +import org.springframework.security.core.Authentication; +import org.springframework.security.web.authentication.AuthenticationSuccessHandler; +import org.springframework.security.web.authentication.logout.LogoutSuccessHandler; +import org.springframework.stereotype.Service; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Collection; +import java.util.Optional; + +/** + * Writes basic login/logout information into the response. + */ +@Service +@Slf4j +public class AuthenticationSuccess implements AuthenticationSuccessHandler, LogoutSuccessHandler { + + private final ObjectMapper mapper; + + private final ConfigReader config; + + public AuthenticationSuccess(ObjectMapper mapper, ConfigReader config) { + this.mapper = mapper; + this.config = config; + } + + @Override + public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, + Authentication authentication) throws IOException { + final String username = getUsername(authentication); + log.atTrace().log("Successfully authenticated user {}", username); + addSameSiteCookieAttribute(httpServletResponse); + final LoginStatus loginStatus = new LoginStatus(true, authentication.isAuthenticated(), username, null); + mapper.writeValue(httpServletResponse.getOutputStream(), loginStatus); + } + + private String getUsername(Authentication authentication) { + if (authentication == null) { + return ""; + } + return authentication.getName(); + } + + @Override + public void onLogoutSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, + Authentication authentication) throws IOException { + log.atTrace().log("Successfully logged out user {}", getUsername(authentication)); + final LoginStatus loginStatus = new LoginStatus(false, true, null, null); + mapper.writeValue(httpServletResponse.getOutputStream(), loginStatus); + } + + enum SameSiteValue { + STRICT("Strict"), + LAX("Lax"), + NONE("None"); + + private final String name; + + SameSiteValue(String name) { + this.name = name; + } + + public static Optional getValue(String value) { + return Arrays.stream(SameSiteValue.values()) + .filter(v -> v.name.equals(value)) + .findFirst(); + } + + @Override + public String toString() { + return name; + } + } + + private void addSameSiteCookieAttribute(HttpServletResponse response) { + String configValue = config.getConfig(ConfigParam.SECURITY_SAME_SITE, ""); + + log.debug("SameSite attribute for set-cookie header configured to {}.", configValue); + + SameSiteValue sameSiteValue = SameSiteValue.getValue(configValue) + .orElseThrow( + () -> new FtaFmeaException( + "Could not recognize " + ConfigParam.SECURITY_SAME_SITE + " parameter value '" + + configValue + "', as it is not one of the values " + + Arrays.toString(SameSiteValue.values()) + "." + ) + ); + + StringBuilder headerValues = new StringBuilder(); + if (sameSiteValue.equals(SameSiteValue.NONE)) { + headerValues.append("Secure; "); + } + headerValues.append("SameSite=").append(sameSiteValue); + + Collection headers = response.getHeaders(HttpHeaders.SET_COOKIE); + boolean firstHeader = true; + // there can be multiple Set-Cookie attributes + for (String header : headers) { + if (firstHeader) { + response.setHeader(HttpHeaders.SET_COOKIE, String.format("%s; %s", header, headerValues)); + firstHeader = false; + continue; + } + response.addHeader(HttpHeaders.SET_COOKIE, String.format("%s; %s", header, headerValues)); + } + } + +} \ No newline at end of file diff --git a/src/main/java/cz/cvut/kbss/analysis/security/CsrfHeaderFilter.java b/src/main/java/cz/cvut/kbss/analysis/security/CsrfHeaderFilter.java new file mode 100644 index 00000000..e8e69dfb --- /dev/null +++ b/src/main/java/cz/cvut/kbss/analysis/security/CsrfHeaderFilter.java @@ -0,0 +1,31 @@ +package cz.cvut.kbss.analysis.security; + +import org.springframework.security.web.csrf.CsrfToken; +import org.springframework.web.filter.OncePerRequestFilter; +import org.springframework.web.util.WebUtils; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; + +public class CsrfHeaderFilter extends OncePerRequestFilter { + + @Override + protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, + FilterChain filterChain) throws ServletException, IOException { + CsrfToken csrfToken = (CsrfToken) httpServletRequest.getAttribute(CsrfToken.class.getName()); + if (csrfToken != null) { + Cookie cookie = WebUtils.getCookie(httpServletRequest, SecurityConstants.CSRF_COOKIE_NAME); + String token = csrfToken.getToken(); + if (cookie == null || token != null && !token.equals(cookie.getValue())) { + cookie = new Cookie(SecurityConstants.CSRF_COOKIE_NAME, token); + cookie.setPath(SecurityConstants.COOKIE_URI); + httpServletResponse.addCookie(cookie); + } + } + filterChain.doFilter(httpServletRequest, httpServletResponse); + } +} \ No newline at end of file diff --git a/src/main/java/cz/cvut/kbss/analysis/security/CustomSwitchUserFilter.java b/src/main/java/cz/cvut/kbss/analysis/security/CustomSwitchUserFilter.java new file mode 100644 index 00000000..d28ac6f5 --- /dev/null +++ b/src/main/java/cz/cvut/kbss/analysis/security/CustomSwitchUserFilter.java @@ -0,0 +1,23 @@ +package cz.cvut.kbss.analysis.security; + +import cz.cvut.kbss.analysis.exception.BadRequestException; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.authentication.switchuser.SwitchUserFilter; + +import jakarta.servlet.http.HttpServletRequest; + +/** + * Extends default user switching logic by preventing switching to an admin account. + */ +public class CustomSwitchUserFilter extends SwitchUserFilter { + + @Override + protected Authentication attemptSwitchUser(HttpServletRequest request) throws AuthenticationException { + final Authentication switchTo = super.attemptSwitchUser(request); + if (switchTo.getAuthorities().stream().anyMatch(a -> SecurityConstants.ROLE_ADMIN.equals(a.getAuthority()))) { + throw new BadRequestException("Cannot impersonate admin."); + } + return switchTo; + } +} \ No newline at end of file diff --git a/src/main/java/cz/cvut/kbss/analysis/security/JwtConfigurer.java b/src/main/java/cz/cvut/kbss/analysis/security/JwtConfigurer.java deleted file mode 100755 index 59a2d7f4..00000000 --- a/src/main/java/cz/cvut/kbss/analysis/security/JwtConfigurer.java +++ /dev/null @@ -1,25 +0,0 @@ -package cz.cvut.kbss.analysis.security; - -import cz.cvut.kbss.analysis.service.JwtTokenProvider; -import cz.cvut.kbss.analysis.service.security.SecurityUtils; -import org.springframework.security.config.annotation.SecurityConfigurerAdapter; -import org.springframework.security.config.annotation.web.builders.HttpSecurity; -import org.springframework.security.web.DefaultSecurityFilterChain; -import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; - -public class JwtConfigurer extends SecurityConfigurerAdapter { - - private final JwtTokenProvider jwtTokenProvider; - private final SecurityUtils securityUtils; - - public JwtConfigurer(JwtTokenProvider jwtTokenProvider, SecurityUtils securityUtils) { - this.jwtTokenProvider = jwtTokenProvider; - this.securityUtils = securityUtils; - } - - @Override - public void configure(HttpSecurity http) { - JwtTokenFilter customFilter = new JwtTokenFilter(jwtTokenProvider, securityUtils); - http.addFilterBefore(customFilter, UsernamePasswordAuthenticationFilter.class); - } -} \ No newline at end of file diff --git a/src/main/java/cz/cvut/kbss/analysis/security/JwtTokenFilter.java b/src/main/java/cz/cvut/kbss/analysis/security/JwtTokenFilter.java deleted file mode 100755 index cc4b3cf3..00000000 --- a/src/main/java/cz/cvut/kbss/analysis/security/JwtTokenFilter.java +++ /dev/null @@ -1,60 +0,0 @@ -package cz.cvut.kbss.analysis.security; - -import cz.cvut.kbss.analysis.exception.InvalidJwtAuthenticationException; -import cz.cvut.kbss.analysis.service.JwtTokenProvider; -import cz.cvut.kbss.analysis.service.security.SecurityUtils; -import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.security.web.header.HeaderWriterFilter; -import org.springframework.web.filter.GenericFilterBean; - -import jakarta.servlet.FilterChain; -import jakarta.servlet.ServletException; -import jakarta.servlet.ServletRequest; -import jakarta.servlet.ServletResponse; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import java.io.IOException; - -@Slf4j -public class JwtTokenFilter extends GenericFilterBean { - - private final JwtTokenProvider jwtTokenProvider; - private final SecurityUtils securityUtils; - - public JwtTokenFilter(JwtTokenProvider jwtTokenProvider, SecurityUtils securityUtils) { - this.jwtTokenProvider = jwtTokenProvider; - this.securityUtils = securityUtils; - } - - @Override - public void doFilter(ServletRequest req, ServletResponse res, FilterChain filterChain) - throws IOException, ServletException { - - String REGISTER_PATH = "/auth/register"; - String LOGIN_PATH ="/auth/signin"; - - String path = ((HttpServletRequest) req).getRequestURI(); - String authHeader = ((HttpServletRequest) req).getHeader("Authorization"); - - if (path.endsWith(LOGIN_PATH) || (path.endsWith(REGISTER_PATH) && authHeader.startsWith("Bearer undefined"))) { - filterChain.doFilter(req, res); - return; - } - - try { - String token = jwtTokenProvider.resolveToken((HttpServletRequest) req); - if (token != null && jwtTokenProvider.validateToken(token)) { - Authentication auth = jwtTokenProvider.getAuthentication(token); - securityUtils.setCurrentUser(auth); - } - } catch (InvalidJwtAuthenticationException e) { - log.error("Unauthorized request", e); - ((HttpServletResponse) res).setStatus(HttpServletResponse.SC_UNAUTHORIZED); - return; - } - filterChain.doFilter(req, res); - } -} diff --git a/src/main/java/cz/cvut/kbss/analysis/security/OAuth2SecurityConfig.java b/src/main/java/cz/cvut/kbss/analysis/security/OAuth2SecurityConfig.java new file mode 100644 index 00000000..e8fd35c8 --- /dev/null +++ b/src/main/java/cz/cvut/kbss/analysis/security/OAuth2SecurityConfig.java @@ -0,0 +1,85 @@ +package cz.cvut.kbss.analysis.security; + + +import cz.cvut.kbss.analysis.config.SecurityConfig; +import cz.cvut.kbss.analysis.config.conf.SecurityConf; +import cz.cvut.kbss.analysis.service.ConfigReader; +import cz.cvut.kbss.analysis.util.OidcGrantedAuthoritiesExtractor; +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.core.convert.converter.Converter; +import org.springframework.http.HttpStatus; +import org.springframework.security.authentication.AbstractAuthenticationToken; +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.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.session.SessionRegistryImpl; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.HttpStatusEntryPoint; +import org.springframework.security.web.authentication.session.RegisterSessionAuthenticationStrategy; +import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy; +import org.springframework.web.cors.CorsConfigurationSource; + +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; + +@ConditionalOnProperty(prefix = "security", name = "provider", havingValue = "oidc") +@Configuration +@EnableWebSecurity +@EnableMethodSecurity +@Slf4j +public class OAuth2SecurityConfig { + + private final AuthenticationSuccess authenticationSuccess; + + private final SecurityConf config; + + @Autowired + public OAuth2SecurityConfig(AuthenticationSuccess authenticationSuccess, SecurityConf config) { + this.authenticationSuccess = authenticationSuccess; + this.config = config; + } + + @Bean + protected SessionAuthenticationStrategy sessionAuthenticationStrategy() { + return new RegisterSessionAuthenticationStrategy(new SessionRegistryImpl()); + } + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + log.debug("Using OAuth2/OIDC security."); + http.oauth2ResourceServer( + auth -> auth.jwt(jwt -> jwt.jwtAuthenticationConverter(grantedAuthoritiesExtractor()))) + .authorizeHttpRequests(auth -> auth.anyRequest().permitAll()) + .exceptionHandling(ehc -> ehc.authenticationEntryPoint(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED))) + .cors(auth -> auth.configurationSource(corsConfigurationSource())) + .csrf(AbstractHttpConfigurer::disable) + .logout(auth -> auth.logoutUrl(SecurityConstants.LOGOUT_URI) + .logoutSuccessHandler(authenticationSuccess)); + return http.build(); + } + + CorsConfigurationSource corsConfigurationSource() { + return SecurityConfig.createCorsConfiguration(config); + } + + private Converter grantedAuthoritiesExtractor() { + return source -> { + final Collection extractedRoles = + new OidcGrantedAuthoritiesExtractor(config).convert(source); + assert extractedRoles != null; + final Set authorities = new HashSet<>(extractedRoles); + // Add default role if it is not present + authorities.add(new SimpleGrantedAuthority(SecurityConstants.ROLE_USER)); + return new JwtAuthenticationToken(source, authorities); + }; + } +} diff --git a/src/main/java/cz/cvut/kbss/analysis/security/OntologyAuthenticationProvider.java b/src/main/java/cz/cvut/kbss/analysis/security/OntologyAuthenticationProvider.java new file mode 100644 index 00000000..1513075c --- /dev/null +++ b/src/main/java/cz/cvut/kbss/analysis/security/OntologyAuthenticationProvider.java @@ -0,0 +1,50 @@ +package cz.cvut.kbss.analysis.security; + +import cz.cvut.kbss.analysis.service.security.SecurityUtils; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; + +/** + * The class is used for local authentication instead of OAuth2. + */ +@Service +@Slf4j +public class OntologyAuthenticationProvider implements AuthenticationProvider { + + private final UserDetailsService userDetailsService; + + private final PasswordEncoder passwordEncoder; + + public OntologyAuthenticationProvider(UserDetailsService userDetailsService, + PasswordEncoder passwordEncoder) { + this.userDetailsService = userDetailsService; + this.passwordEncoder = passwordEncoder; + } + + @Override + public Authentication authenticate(Authentication authentication) throws AuthenticationException { + final String username = authentication.getPrincipal().toString(); + log.atDebug().log("Authenticating user {}", username); + + final UserDetails userDetails = userDetailsService.loadUserByUsername(username); + final String password = (String) authentication.getCredentials(); + if (!passwordEncoder.matches(password, userDetails.getPassword())) { + log.trace("Provided password for username '{}' doesn't match.", username); + throw new BadCredentialsException("Provided password for username '" + username + "' doesn't match."); + } + return SecurityUtils.setCurrentUser(userDetails); + } + + @Override + public boolean supports(Class aClass) { + return UsernamePasswordAuthenticationToken.class.isAssignableFrom(aClass); + } +} diff --git a/src/main/java/cz/cvut/kbss/analysis/security/model/LoginStatus.java b/src/main/java/cz/cvut/kbss/analysis/security/model/LoginStatus.java new file mode 100644 index 00000000..037f999f --- /dev/null +++ b/src/main/java/cz/cvut/kbss/analysis/security/model/LoginStatus.java @@ -0,0 +1,19 @@ +package cz.cvut.kbss.analysis.security.model; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@NoArgsConstructor +@AllArgsConstructor +@Getter +@Setter +public class LoginStatus { + + private boolean loggedIn; + private boolean success; + private String username; + private String errorMessage; + +} \ No newline at end of file diff --git a/src/main/java/cz/cvut/kbss/analysis/service/JwtTokenProvider.java b/src/main/java/cz/cvut/kbss/analysis/service/JwtTokenProvider.java deleted file mode 100755 index 79581121..00000000 --- a/src/main/java/cz/cvut/kbss/analysis/service/JwtTokenProvider.java +++ /dev/null @@ -1,72 +0,0 @@ -package cz.cvut.kbss.analysis.service; - -import cz.cvut.kbss.analysis.config.conf.JwtConf; -import cz.cvut.kbss.analysis.exception.InvalidJwtAuthenticationException; -import io.jsonwebtoken.*; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.stereotype.Component; - -import jakarta.servlet.http.HttpServletRequest; -import java.util.Date; -import java.util.List; - -@Component -@RequiredArgsConstructor(onConstructor = @__(@Autowired)) -@Slf4j -public class JwtTokenProvider { - - private static final String AUTHORIZATION_HEADER = "Authorization"; - private static final String BEARER_HEADER = "Bearer "; - - private final JwtConf jwtConf; - private final UserDetailsService userDetailsService; - - public String createToken(String username, List roles) { - Claims claims = Jwts.claims().setSubject(username); - claims.put("roles", roles); - Date now = new Date(); - Date validity = new Date(now.getTime() + jwtConf.getExpiryMs()); - - return Jwts.builder() - .setClaims(claims) - .setIssuedAt(now) - .setExpiration(validity) - .signWith(SignatureAlgorithm.HS512, jwtConf.getSecretKey()) - .compact(); - } - - public Authentication getAuthentication(String token) { - UserDetails userDetails = userDetailsService.loadUserByUsername(getUsername(token)); - return new UsernamePasswordAuthenticationToken(userDetails, "", userDetails.getAuthorities()); - } - - public String resolveToken(HttpServletRequest req) { - String bearerToken = req.getHeader(AUTHORIZATION_HEADER); - if (bearerToken != null && bearerToken.startsWith(BEARER_HEADER)) { - return bearerToken.substring(BEARER_HEADER.length()); - } - - log.warn("< resolveToken - failed to resolve token"); - return null; - } - - public boolean validateToken(String token) { - try { - Jws claims = Jwts.parser().setSigningKey(jwtConf.getSecretKey()).parseClaimsJws(token); - return !claims.getBody().getExpiration().before(new Date()); - } catch (JwtException | IllegalArgumentException e) { - throw new InvalidJwtAuthenticationException("Expired or invalid JWT token"); - } - } - - private String getUsername(String token) { - return Jwts.parser().setSigningKey(jwtConf.getSecretKey()).parseClaimsJws(token) - .getBody().getSubject(); - } -} \ No newline at end of file diff --git a/src/main/java/cz/cvut/kbss/analysis/util/OidcGrantedAuthoritiesExtractor.java b/src/main/java/cz/cvut/kbss/analysis/util/OidcGrantedAuthoritiesExtractor.java new file mode 100644 index 00000000..cba64947 --- /dev/null +++ b/src/main/java/cz/cvut/kbss/analysis/util/OidcGrantedAuthoritiesExtractor.java @@ -0,0 +1,52 @@ +package cz.cvut.kbss.analysis.util; + +import cz.cvut.kbss.analysis.config.conf.SecurityConf; +import org.springframework.core.convert.converter.Converter; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.oauth2.core.ClaimAccessor; +import org.springframework.security.oauth2.jwt.Jwt; + +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +public class OidcGrantedAuthoritiesExtractor implements Converter> { + + private final SecurityConf config; + + public OidcGrantedAuthoritiesExtractor(SecurityConf config) { + this.config = config; + } + + @Override + public Collection convert(Jwt source) { + return extractRoles(source).stream() + .map(SimpleGrantedAuthority::new) + .collect(Collectors.toList()); + } + + public List extractRoles(ClaimAccessor source) { + final String rolesClaim = config.getRoleClaim(); + final String[] parts = rolesClaim.split("\\."); + assert parts.length > 0; + final List roles; + if (parts.length == 1) { + roles = source.getClaimAsStringList(rolesClaim); + } else { + Map map = source.getClaimAsMap(parts[0]); + for (int i = 1; i < parts.length - 1; i++) { + if (map.containsKey(parts[i]) && !(map.get(parts[i]) instanceof Map)) { + throw new IllegalArgumentException("Access token does not contain roles under the expected claim '" + rolesClaim + "'."); + } + map = (Map) map.getOrDefault(parts[i], Collections.emptyMap()); + } + if (map.containsKey(parts[parts.length - 1]) && !(map.get(parts[parts.length - 1]) instanceof List)) { + throw new IllegalArgumentException("Roles claim does not contain a list."); + } + roles = (List) map.getOrDefault(parts[parts.length - 1], Collections.emptyList()); + } + return roles; + } +} From 2667d003daae600f479c920cbb6c1a10aef5f198 Mon Sep 17 00:00:00 2001 From: Bogdan Kostov Date: Fri, 3 May 2024 18:35:38 +0200 Subject: [PATCH 06/11] Update authentication controller - remove old authentication controller - add two new authentication controllers --- .../analysis/controller/AuthController.java | 53 ------------------- .../controller/OidcUserController.java | 45 ++++++++++++++++ .../analysis/controller/UserController.java | 30 +++++++++++ 3 files changed, 75 insertions(+), 53 deletions(-) delete mode 100755 src/main/java/cz/cvut/kbss/analysis/controller/AuthController.java create mode 100644 src/main/java/cz/cvut/kbss/analysis/controller/OidcUserController.java create mode 100644 src/main/java/cz/cvut/kbss/analysis/controller/UserController.java diff --git a/src/main/java/cz/cvut/kbss/analysis/controller/AuthController.java b/src/main/java/cz/cvut/kbss/analysis/controller/AuthController.java deleted file mode 100755 index c069d147..00000000 --- a/src/main/java/cz/cvut/kbss/analysis/controller/AuthController.java +++ /dev/null @@ -1,53 +0,0 @@ -package cz.cvut.kbss.analysis.controller; - -import cz.cvut.kbss.analysis.dto.UserUpdateDTO; -import cz.cvut.kbss.analysis.dto.authentication.AuthenticationRequest; -import cz.cvut.kbss.analysis.dto.authentication.AuthenticationResponse; -import cz.cvut.kbss.analysis.model.User; -import cz.cvut.kbss.analysis.service.JwtTokenProvider; -import cz.cvut.kbss.analysis.service.UserRepositoryService; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.HttpStatus; -import org.springframework.http.MediaType; -import org.springframework.security.authentication.AuthenticationManager; -import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; -import org.springframework.security.core.userdetails.UsernameNotFoundException; -import org.springframework.web.bind.annotation.*; - -@RestController -@RequestMapping("/auth") -@RequiredArgsConstructor(onConstructor = @__(@Autowired)) -@Slf4j -public class AuthController { - - private final AuthenticationManager authenticationManager; - private final JwtTokenProvider jwtTokenProvider; - private final UserRepositoryService userRepositoryService; - - - @PostMapping("/signin") - public AuthenticationResponse signIn(@RequestBody AuthenticationRequest data) { - log.info("> signIn - {}", data.getUsername()); - - String username = data.getUsername(); - authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, data.getPassword())); - - User user = userRepositoryService - .findByUsername(username) - .orElseThrow(() -> new UsernameNotFoundException("Username " + username + "not found")); - - String token = jwtTokenProvider.createToken(username, user.getRoles()); - - log.info("< signIn - {}", username); - return new AuthenticationResponse(user.getUri(), username, token); - } - - @ResponseStatus(HttpStatus.NO_CONTENT) - @PutMapping(value = "/current", consumes = {MediaType.APPLICATION_JSON_VALUE}) - public void updateCurrent(@RequestBody UserUpdateDTO userUpdate) { - userRepositoryService.updateCurrent(userUpdate); - log.info("< updateCurrent - user {} updated", userUpdate.getUri()); - } -} diff --git a/src/main/java/cz/cvut/kbss/analysis/controller/OidcUserController.java b/src/main/java/cz/cvut/kbss/analysis/controller/OidcUserController.java new file mode 100644 index 00000000..c5dd2c00 --- /dev/null +++ b/src/main/java/cz/cvut/kbss/analysis/controller/OidcUserController.java @@ -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. + *

+ * 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)); + } + +} \ No newline at end of file diff --git a/src/main/java/cz/cvut/kbss/analysis/controller/UserController.java b/src/main/java/cz/cvut/kbss/analysis/controller/UserController.java new file mode 100644 index 00000000..7f78b834 --- /dev/null +++ b/src/main/java/cz/cvut/kbss/analysis/controller/UserController.java @@ -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(); + } + +} From a4ebf93b9997a9f076cd77b6ab6e341456849497 Mon Sep 17 00:00:00 2001 From: Bogdan Kostov Date: Tue, 7 May 2024 14:41:59 +0200 Subject: [PATCH 07/11] Refactor security configuration - remove unused properties - renamed some properties according to record manager --- .../kbss/analysis/config/conf/JwtConf.java | 20 ------------------- .../analysis/config/conf/SecurityConf.java | 19 ++++++++++-------- .../security/OAuth2SecurityConfig.java | 1 - src/main/resources/application.yml | 15 ++++++-------- 4 files changed, 17 insertions(+), 38 deletions(-) delete mode 100755 src/main/java/cz/cvut/kbss/analysis/config/conf/JwtConf.java diff --git a/src/main/java/cz/cvut/kbss/analysis/config/conf/JwtConf.java b/src/main/java/cz/cvut/kbss/analysis/config/conf/JwtConf.java deleted file mode 100755 index be0e0660..00000000 --- a/src/main/java/cz/cvut/kbss/analysis/config/conf/JwtConf.java +++ /dev/null @@ -1,20 +0,0 @@ -package cz.cvut.kbss.analysis.config.conf; - -import lombok.Getter; -import lombok.Setter; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.context.annotation.Configuration; - -@Setter -@Getter -@Configuration -@EnableConfigurationProperties -@ConfigurationProperties("security.jwt") -public class JwtConf { - - private String secretKey; - - private Long expiryMs; - -} diff --git a/src/main/java/cz/cvut/kbss/analysis/config/conf/SecurityConf.java b/src/main/java/cz/cvut/kbss/analysis/config/conf/SecurityConf.java index 2bae7cd5..fe726397 100644 --- a/src/main/java/cz/cvut/kbss/analysis/config/conf/SecurityConf.java +++ b/src/main/java/cz/cvut/kbss/analysis/config/conf/SecurityConf.java @@ -2,9 +2,11 @@ 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 @@ -12,15 +14,16 @@ @EnableConfigurationProperties @ConfigurationProperties("security") public class SecurityConf { - private String allowedOrigins; + private String allowedOrigins; - private String appContext; + private String appContext; - private String issuerUri; + private String roleClaim; - private String secretKey; - - private Long expiryMs; - - private String roleClaim; + @Autowired + public SecurityConf(Environment env) { + allowedOrigins = env.getProperty("cors.allowedOrigins"); + appContext = env.getProperty("appContext"); + roleClaim = env.getProperty("oidc.RoleClaim"); + } } diff --git a/src/main/java/cz/cvut/kbss/analysis/security/OAuth2SecurityConfig.java b/src/main/java/cz/cvut/kbss/analysis/security/OAuth2SecurityConfig.java index e8fd35c8..db5939b2 100644 --- a/src/main/java/cz/cvut/kbss/analysis/security/OAuth2SecurityConfig.java +++ b/src/main/java/cz/cvut/kbss/analysis/security/OAuth2SecurityConfig.java @@ -3,7 +3,6 @@ import cz.cvut.kbss.analysis.config.SecurityConfig; import cz.cvut.kbss.analysis.config.conf.SecurityConf; -import cz.cvut.kbss.analysis.service.ConfigReader; import cz.cvut.kbss.analysis.util.OidcGrantedAuthoritiesExtractor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index a3271278..25c4008d 100755 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -11,13 +11,10 @@ persistence: security: provider: oidc - oidcRoleClaim: realm_access.roles - appContext: http://localhost:3000/fta-fmea - cors: - allowedOrigins: - jwt: - secretKey: lwrUj5PmCE6X8ekbLd9wDTRlBkEJA0HB - expiryMs: 28800000 # 8 hours + +appContext: http://localhost:5173 +oidc.RoleClaim: realm_access.roles +cors.allowedOrigins: logging: pattern: @@ -37,6 +34,6 @@ spring: oauth2: resourceserver: jwt: - issuer-uri: http://localhost/services/auth/realms/record-manager - jwk-set-uri: http://localhost/services/auth/realms/record-manager/protocol/openid-connect/certs + issuer-uri: http://localhost/ava/services/auth/realms/record-manager + jwt-set-uri: http://localhost/ava/services/auth/realms/record-manager/protocol/openid-connect/certs From 187d2d686534ae00aa2432e89a3d27cd4ec16e17 Mon Sep 17 00:00:00 2001 From: Bogdan Kostov Date: Tue, 7 May 2024 16:10:10 +0200 Subject: [PATCH 08/11] Fix internal security mode: - set signin uri to match frontend. --- .../cz/cvut/kbss/analysis/security/SecurityConstants.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/cz/cvut/kbss/analysis/security/SecurityConstants.java b/src/main/java/cz/cvut/kbss/analysis/security/SecurityConstants.java index a7a52374..82a6b7e1 100644 --- a/src/main/java/cz/cvut/kbss/analysis/security/SecurityConstants.java +++ b/src/main/java/cz/cvut/kbss/analysis/security/SecurityConstants.java @@ -16,9 +16,9 @@ private SecurityConstants() { public static final String PASSWORD_PARAM = "password"; - public static final String SECURITY_CHECK_URI = "/j_spring_security_check"; + public static final String SECURITY_CHECK_URI = "/auth/signin"; - public static final String LOGOUT_URI = "/j_spring_security_logout"; + public static final String LOGOUT_URI = "/auth/logout"; public static final String COOKIE_URI = "/"; From 856e64d0fa3292c4c3a12904488425988268cc87 Mon Sep 17 00:00:00 2001 From: Bogdan Kostov Date: Wed, 8 May 2024 13:44:16 +0200 Subject: [PATCH 09/11] Refactor configuration parameters - define and use enum values in ConfigParam instead of using strings --- .../cz/cvut/kbss/analysis/config/conf/SecurityConf.java | 7 ++++--- src/main/java/cz/cvut/kbss/analysis/util/ConfigParam.java | 6 +++++- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/main/java/cz/cvut/kbss/analysis/config/conf/SecurityConf.java b/src/main/java/cz/cvut/kbss/analysis/config/conf/SecurityConf.java index fe726397..78535478 100644 --- a/src/main/java/cz/cvut/kbss/analysis/config/conf/SecurityConf.java +++ b/src/main/java/cz/cvut/kbss/analysis/config/conf/SecurityConf.java @@ -1,5 +1,6 @@ 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; @@ -22,8 +23,8 @@ public class SecurityConf { @Autowired public SecurityConf(Environment env) { - allowedOrigins = env.getProperty("cors.allowedOrigins"); - appContext = env.getProperty("appContext"); - roleClaim = env.getProperty("oidc.RoleClaim"); + allowedOrigins = env.getProperty(ConfigParam.CORS_ALLOWED_ORIGINS.toString()); + appContext = env.getProperty(ConfigParam.APP_CONTEXT.toString()); + roleClaim = env.getProperty(ConfigParam.OIDC_ROLE_CLAIM.toString()); } } diff --git a/src/main/java/cz/cvut/kbss/analysis/util/ConfigParam.java b/src/main/java/cz/cvut/kbss/analysis/util/ConfigParam.java index 4e408601..2e2f0baa 100644 --- a/src/main/java/cz/cvut/kbss/analysis/util/ConfigParam.java +++ b/src/main/java/cz/cvut/kbss/analysis/util/ConfigParam.java @@ -4,7 +4,11 @@ public enum ConfigParam { SECURITY_SAME_SITE("security.sameSite"), - APP_CONTEXT("appContext"); + APP_CONTEXT("appContext"), + + OIDC_ROLE_CLAIM("oidc.RoleClaim"), + + CORS_ALLOWED_ORIGINS("cors.allowedOrigins"); private final String name; From 328c9d465765c15a8f615202cece0d4efc8b244f Mon Sep 17 00:00:00 2001 From: Bogdan Kostov Date: Wed, 8 May 2024 13:45:41 +0200 Subject: [PATCH 10/11] Add OidcGrantedAuthoritiesExtractorTest --- .../OidcGrantedAuthoritiesExtractorTest.java | 103 ++++++++++++++++++ 1 file changed, 103 insertions(+) create mode 100644 src/test/java/cz/cvut/kbss/analysis/util/OidcGrantedAuthoritiesExtractorTest.java diff --git a/src/test/java/cz/cvut/kbss/analysis/util/OidcGrantedAuthoritiesExtractorTest.java b/src/test/java/cz/cvut/kbss/analysis/util/OidcGrantedAuthoritiesExtractorTest.java new file mode 100644 index 00000000..6fad41fc --- /dev/null +++ b/src/test/java/cz/cvut/kbss/analysis/util/OidcGrantedAuthoritiesExtractorTest.java @@ -0,0 +1,103 @@ +package cz.cvut.kbss.analysis.util; + +import cz.cvut.kbss.analysis.config.conf.SecurityConf; +import cz.cvut.kbss.analysis.security.SecurityConstants; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.oauth2.jwt.Jwt; + +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.Collection; +import java.util.List; +import java.util.Map; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.hasItem; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class OidcGrantedAuthoritiesExtractorTest { + + @Mock + private SecurityConf config; + + @Test + void convertMapsTopLevelClaimWithRolesToGrantedAuthorities() { + when(config.getRoleClaim()).thenReturn("roles"); + final List roles = List.of(SecurityConstants.ROLE_ADMIN, SecurityConstants.ROLE_USER); + final Jwt token = Jwt.withTokenValue("abcdef12345") + .header("alg", "RS256") + .header("typ", "JWT") + .claim("roles", roles) + .issuer("http://localhost:8080/termit") + .subject("termit") + .expiresAt(Instant.now().truncatedTo(ChronoUnit.SECONDS).plusSeconds(300)) + .build(); + + final OidcGrantedAuthoritiesExtractor sut = new OidcGrantedAuthoritiesExtractor(config); + final Collection result = sut.convert(token); + assertNotNull(result); + for (String r : roles) { + assertThat(result, hasItem(new SimpleGrantedAuthority(r))); + } + } + + @Test + void convertSupportsNestedRolesClaim() { + when(config.getRoleClaim()).thenReturn("realm_access.roles"); + final List roles = List.of(SecurityConstants.ROLE_ADMIN, SecurityConstants.ROLE_USER); + final Jwt token = Jwt.withTokenValue("abcdef12345") + .header("alg", "RS256") + .header("typ", "JWT") + .claim("realm_access", Map.of("roles", roles)) + .issuer("http://localhost:8080/fta-fmea") + .subject("termit") + .expiresAt(Instant.now().truncatedTo(ChronoUnit.SECONDS).plusSeconds(300)) + .build(); + + final OidcGrantedAuthoritiesExtractor sut = new OidcGrantedAuthoritiesExtractor(config); + final Collection result = sut.convert(token); + assertNotNull(result); + for (String r : roles) { + assertThat(result, hasItem(new SimpleGrantedAuthority(r))); + } + } + + @Test + void convertThrowsIllegalArgumentExceptionWhenExpectedClaimPathIsNotTraversable() { + when(config.getRoleClaim()).thenReturn("realm_access.roles.list"); + final Jwt token = Jwt.withTokenValue("abcdef12345") + .header("alg", "RS256") + .header("typ", "JWT") + .claim("realm_access", Map.of("roles", 1235)) + .issuer("http://localhost:8080/fta-fmea") + .subject("termit") + .expiresAt(Instant.now().truncatedTo(ChronoUnit.SECONDS).plusSeconds(300)) + .build(); + + final OidcGrantedAuthoritiesExtractor sut = new OidcGrantedAuthoritiesExtractor(config); + assertThrows(IllegalArgumentException.class, () -> sut.convert(token)); + } + + @Test + void convertThrowsIllegalArgumentExceptionWhenNestedRolesClaimIsNotList() { + when(config.getRoleClaim()).thenReturn("realm_access.roles.notlist"); + final Jwt token = Jwt.withTokenValue("abcdef12345") + .header("alg", "RS256") + .header("typ", "JWT") + .claim("realm_access", Map.of("roles", Map.of("notlist", SecurityConstants.ROLE_USER))) + .issuer("http://localhost:8080/fta-fmea") + .subject("termit") + .expiresAt(Instant.now().truncatedTo(ChronoUnit.SECONDS).plusSeconds(300)) + .build(); + + final OidcGrantedAuthoritiesExtractor sut = new OidcGrantedAuthoritiesExtractor(config); + assertThrows(IllegalArgumentException.class, () -> sut.convert(token)); + } +} \ No newline at end of file From 9302e8d2c4553bc7cbd064a05c842652c104a393 Mon Sep 17 00:00:00 2001 From: Bogdan Kostov Date: Tue, 14 May 2024 14:18:37 +0200 Subject: [PATCH 11/11] [Fix fta-fmea-ui/issues#126] Set default authorization to internal --- src/main/resources/application.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 25c4008d..bcf4eb58 100755 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -10,7 +10,7 @@ persistence: language: cs security: - provider: oidc + provider: internal appContext: http://localhost:5173 oidc.RoleClaim: realm_access.roles