diff --git a/pom.xml b/pom.xml index 9e8f609..2405f8c 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ 4.0.0 xyz.aimcup security-config - 0.0.7-TEST + 0.1.0 17 17 @@ -16,6 +16,16 @@ spring-boot-starter-web 3.1.1 + + org.springframework.boot + spring-boot-starter-oauth2-client + 3.1.1 + + + org.springframework.boot + spring-boot-starter-data-jpa + 3.1.1 + org.springframework.boot spring-boot-starter-security @@ -63,6 +73,31 @@ + + maven-resources-plugin + 3.1.0 + + + copy-resources + process-classes + + copy-resources + + + ${project.basedir}/target/classes/static/ + + + ${project.basedir}/src/main/resources/shared + + **/*.* + + + + + + + + org.springframework.boot spring-boot-maven-plugin @@ -87,35 +122,35 @@ - - org.openapitools - openapi-generator-maven-plugin - 6.6.0 - - - - generate - - - - ${project.basedir}/src/main/resources/shared/openapi/schema.yaml - - spring - xyz.aimcup.generated - xyz.aimcup.generated.model - false - - true - true - true - false - true - false - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/java/xyz/aimcup/security/configuration/LocalSecurityConfiguration.java b/src/main/java/xyz/aimcup/security/configuration/LocalSecurityConfiguration.java new file mode 100644 index 0000000..e99ba98 --- /dev/null +++ b/src/main/java/xyz/aimcup/security/configuration/LocalSecurityConfiguration.java @@ -0,0 +1,48 @@ +package xyz.aimcup.security.configuration; + +import jakarta.annotation.PostConstruct; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.cloud.openfeign.EnableFeignClients; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; +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.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import xyz.aimcup.security.filter.LocalTokenAuthenticationFilter; + +@Configuration +@EnableWebSecurity +@EnableFeignClients(basePackages = "xyz.aimcup.security.feign") +@RequiredArgsConstructor +@ComponentScan(basePackages = "xyz.aimcup.security") +@EnableMethodSecurity( + securedEnabled = true, + jsr250Enabled = true +) +@Slf4j +public class LocalSecurityConfiguration { + private final LocalTokenAuthenticationFilter tokenAuthenticationFilter; + + @Bean(name = "globalSecurityFilterChain") + SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception { + return httpSecurity + .cors(AbstractHttpConfigurer::disable) + .csrf(AbstractHttpConfigurer::disable) + .sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .addFilterBefore(tokenAuthenticationFilter, UsernamePasswordAuthenticationFilter.class) + .httpBasic(AbstractHttpConfigurer::disable) + .formLogin(AbstractHttpConfigurer::disable) + .build(); + } + + @PostConstruct + public void postConstruct() { + log.info("SecurityConfiguration loaded. Securing with DEVELOPMENT settings."); + } +} diff --git a/src/main/java/xyz/aimcup/security/configuration/SecurityConfiguration.java b/src/main/java/xyz/aimcup/security/configuration/SecurityConfiguration.java index ba261b8..47027e7 100644 --- a/src/main/java/xyz/aimcup/security/configuration/SecurityConfiguration.java +++ b/src/main/java/xyz/aimcup/security/configuration/SecurityConfiguration.java @@ -1,10 +1,13 @@ package xyz.aimcup.security.configuration; +import jakarta.annotation.PostConstruct; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.cloud.openfeign.EnableFeignClients; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; 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; @@ -23,6 +26,8 @@ securedEnabled = true, jsr250Enabled = true ) +@Profile("!dev") +@Slf4j public class SecurityConfiguration { private final TokenAuthenticationFilter tokenAuthenticationFilter; @@ -37,4 +42,9 @@ SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Except .formLogin(AbstractHttpConfigurer::disable) .build(); } + + @PostConstruct + public void postConstruct() { + log.info("SecurityConfiguration loaded. Securing with PRODUCTION settings."); + } } diff --git a/src/main/java/xyz/aimcup/security/domain/Role.java b/src/main/java/xyz/aimcup/security/domain/Role.java new file mode 100644 index 0000000..a0e461e --- /dev/null +++ b/src/main/java/xyz/aimcup/security/domain/Role.java @@ -0,0 +1,42 @@ +package xyz.aimcup.security.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import java.util.UUID; +import lombok.Getter; +import lombok.Setter; +import org.hibernate.annotations.NaturalId; +import org.springframework.security.core.GrantedAuthority; + + +@Entity +@Getter +@Setter +public class Role implements GrantedAuthority { + @Id + @GeneratedValue(strategy = GenerationType.UUID) + private UUID id; + + @Enumerated(EnumType.STRING) + @NaturalId + @Column(length = 60, name = "name") + private RoleName name; + + public Role() { + + } + + public Role(RoleName name) { + this.name = name; + } + + @Override + public String getAuthority() { + return this.name.toString(); + } +} diff --git a/src/main/java/xyz/aimcup/security/domain/RoleBase.java b/src/main/java/xyz/aimcup/security/domain/RoleBase.java deleted file mode 100644 index 226adc4..0000000 --- a/src/main/java/xyz/aimcup/security/domain/RoleBase.java +++ /dev/null @@ -1,28 +0,0 @@ -package xyz.aimcup.security.domain; - -import java.util.UUID; -import lombok.Getter; -import lombok.Setter; -import org.springframework.security.core.GrantedAuthority; - - -@Getter -@Setter -public class RoleBase implements GrantedAuthority { - private UUID id; - private RoleName name; - - public RoleBase() { - - } - - public RoleBase(UUID id, RoleName name) { - this.id = id; - this.name = name; - } - - @Override - public String getAuthority() { - return this.name.toString(); - } -} diff --git a/src/main/java/xyz/aimcup/security/domain/User.java b/src/main/java/xyz/aimcup/security/domain/User.java new file mode 100644 index 0000000..8f8835c --- /dev/null +++ b/src/main/java/xyz/aimcup/security/domain/User.java @@ -0,0 +1,41 @@ +package xyz.aimcup.security.domain; + +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.JoinTable; +import jakarta.persistence.ManyToMany; +import jakarta.persistence.Table; +import java.util.HashSet; +import java.util.Set; +import java.util.UUID; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Entity +@Table(name="\"user\"") +@Getter +@Setter +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class User { + @Id + @GeneratedValue(strategy = GenerationType.UUID) + private UUID id; + private String username; + private Long osuId; + private Boolean isRestricted; + + @ManyToMany(fetch = FetchType.EAGER) + @JoinTable(name = "user_roles", + joinColumns = @JoinColumn(name = "user_id"), + inverseJoinColumns = @JoinColumn(name = "role_id")) + private Set roles = new HashSet<>(); +} diff --git a/src/main/java/xyz/aimcup/security/domain/UserBase.java b/src/main/java/xyz/aimcup/security/domain/UserBase.java deleted file mode 100644 index d1d5e72..0000000 --- a/src/main/java/xyz/aimcup/security/domain/UserBase.java +++ /dev/null @@ -1,21 +0,0 @@ -package xyz.aimcup.security.domain; - -import java.util.UUID; -import lombok.*; - -import java.util.HashSet; -import java.util.Set; - -@Getter -@Setter -@Builder -@AllArgsConstructor -@NoArgsConstructor -public class UserBase { - @Setter(AccessLevel.NONE) - private UUID id; - private String username; - private Long osuId; - private Boolean isRestricted; - private Set roles = new HashSet<>(); -} diff --git a/src/main/java/xyz/aimcup/security/dto/RoleResponseDto.java b/src/main/java/xyz/aimcup/security/dto/RoleResponseDto.java new file mode 100644 index 0000000..f58f73d --- /dev/null +++ b/src/main/java/xyz/aimcup/security/dto/RoleResponseDto.java @@ -0,0 +1,13 @@ +package xyz.aimcup.security.dto; + +import lombok.Data; + +import java.util.UUID; + +@Data +public class RoleResponseDto { + private UUID id; + + private String name; + +} diff --git a/src/main/java/xyz/aimcup/security/dto/UserResponseDto.java b/src/main/java/xyz/aimcup/security/dto/UserResponseDto.java new file mode 100644 index 0000000..f59c4d5 --- /dev/null +++ b/src/main/java/xyz/aimcup/security/dto/UserResponseDto.java @@ -0,0 +1,21 @@ +package xyz.aimcup.security.dto; + +import jakarta.validation.Valid; +import lombok.Data; + +import java.util.List; +import java.util.UUID; + +@Data +public class UserResponseDto { + private UUID id; + + private String username; + + private Integer osuId; + + private Boolean isRestricted; + + @Valid + private List roles; +} diff --git a/src/main/java/xyz/aimcup/security/feign/AuthServiceClient.java b/src/main/java/xyz/aimcup/security/feign/AuthServiceClient.java index e7acaf3..8fc6268 100644 --- a/src/main/java/xyz/aimcup/security/feign/AuthServiceClient.java +++ b/src/main/java/xyz/aimcup/security/feign/AuthServiceClient.java @@ -5,13 +5,12 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestHeader; -import xyz.aimcup.generated.model.UserResponseDto; -import xyz.aimcup.security.domain.UserBase; +import xyz.aimcup.security.dto.UserResponseDto; -@FeignClient(name = "user-microservice", path = "/user/auth-service") +@FeignClient(name = "user-microservice", path = "/user") @Headers("Authorization: Bearer {token}") public interface AuthServiceClient { @GetMapping("/me") - ResponseEntity user(@RequestHeader("Authorization") String token); + ResponseEntity me(@RequestHeader("Authorization") String token); } diff --git a/src/main/java/xyz/aimcup/security/filter/LocalTokenAuthenticationFilter.java b/src/main/java/xyz/aimcup/security/filter/LocalTokenAuthenticationFilter.java new file mode 100644 index 0000000..a82d851 --- /dev/null +++ b/src/main/java/xyz/aimcup/security/filter/LocalTokenAuthenticationFilter.java @@ -0,0 +1,60 @@ +package xyz.aimcup.security.filter; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.annotation.Profile; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; +import org.springframework.web.filter.OncePerRequestFilter; +import xyz.aimcup.security.domain.Role; +import xyz.aimcup.security.domain.RoleName; +import xyz.aimcup.security.domain.User; +import xyz.aimcup.security.principal.UserPrincipal; + +import java.io.IOException; +import java.util.Set; +import java.util.UUID; + +@Component +@RequiredArgsConstructor +public class LocalTokenAuthenticationFilter extends OncePerRequestFilter { + + private static final Logger logger = LoggerFactory.getLogger(LocalTokenAuthenticationFilter.class); + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { + try { + var user = this.createDevUser(); + UserDetails userDetails = UserPrincipal.create(user); + UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); + authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); + SecurityContextHolder.getContext().setAuthentication(authentication); + } catch (Exception ex) { + logger.error("Could not set user authentication in security context", ex); + } + + filterChain.doFilter(request, response); + } + + private User createDevUser() { + Role userRole = new Role(RoleName.ROLE_USER); + Role adminRole = new Role(RoleName.ROLE_ADMIN); + return User.builder() + .id(UUID.randomUUID()) + .osuId(-1L) + .username("Test user") + .isRestricted(false) + .roles(Set.of(userRole, adminRole)) + .build(); + } +} + diff --git a/src/main/java/xyz/aimcup/security/filter/TokenAuthenticationFilter.java b/src/main/java/xyz/aimcup/security/filter/TokenAuthenticationFilter.java index 4398082..14c8563 100644 --- a/src/main/java/xyz/aimcup/security/filter/TokenAuthenticationFilter.java +++ b/src/main/java/xyz/aimcup/security/filter/TokenAuthenticationFilter.java @@ -4,7 +4,6 @@ import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; -import java.util.List; import java.util.Set; import lombok.RequiredArgsConstructor; import org.slf4j.Logger; @@ -16,11 +15,11 @@ import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; import org.springframework.web.filter.OncePerRequestFilter; -import xyz.aimcup.generated.model.UserResponseDto; -import xyz.aimcup.security.domain.RoleBase; -import xyz.aimcup.security.domain.UserBase; +import xyz.aimcup.security.domain.Role; +import xyz.aimcup.security.domain.User; +import xyz.aimcup.security.dto.UserResponseDto; import xyz.aimcup.security.feign.AuthServiceClient; -import xyz.aimcup.security.mapper.UserMapper; +import xyz.aimcup.security.mapper.ResponseUserMapper; import xyz.aimcup.security.principal.UserPrincipal; import java.io.IOException; @@ -29,7 +28,7 @@ @RequiredArgsConstructor public class TokenAuthenticationFilter extends OncePerRequestFilter { private final AuthServiceClient authServiceClient; - private final UserMapper userMapper; + private final ResponseUserMapper userMapper; private static final Logger logger = LoggerFactory.getLogger(TokenAuthenticationFilter.class); @@ -43,13 +42,13 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse return; } - UserResponseDto userResponseDto = authServiceClient.user("Bearer " + jwt).getBody(); - UserBase userBase = userMapper.mapUserResponseDtoToUser(userResponseDto); + UserResponseDto userResponseDto = authServiceClient.me("Bearer " + jwt).getBody(); + User userBase = userMapper.mapUserResponseDtoToUser(userResponseDto); if (userBase == null) { filterChain.doFilter(request, response); return; } - Set roles = userMapper.mapRoles(userResponseDto.getRoles()); + Set roles = userMapper.mapRoles(userResponseDto.getRoles()); userBase.setRoles(roles); UserDetails userDetails = UserPrincipal.create(userBase); if (userDetails == null) { diff --git a/src/main/java/xyz/aimcup/security/mapper/ResponseUserMapper.java b/src/main/java/xyz/aimcup/security/mapper/ResponseUserMapper.java new file mode 100644 index 0000000..a5aac4e --- /dev/null +++ b/src/main/java/xyz/aimcup/security/mapper/ResponseUserMapper.java @@ -0,0 +1,19 @@ +package xyz.aimcup.security.mapper; + +import java.util.List; +import java.util.Set; +import org.mapstruct.Mapper; +import xyz.aimcup.security.domain.Role; +import xyz.aimcup.security.domain.User; +import xyz.aimcup.security.dto.RoleResponseDto; +import xyz.aimcup.security.dto.UserResponseDto; + +@Mapper(componentModel = "spring") +public interface ResponseUserMapper { + User mapUserResponseDtoToUser(UserResponseDto userResponseDto); + + Role mapRoleResponseToRole(RoleResponseDto userBase); + Role mapRoleResponseToRole(Object userBase); + Set mapRoles(List objectList); + +} diff --git a/src/main/java/xyz/aimcup/security/mapper/UserMapper.java b/src/main/java/xyz/aimcup/security/mapper/UserMapper.java deleted file mode 100644 index 2e67f1f..0000000 --- a/src/main/java/xyz/aimcup/security/mapper/UserMapper.java +++ /dev/null @@ -1,19 +0,0 @@ -package xyz.aimcup.security.mapper; - -import java.util.List; -import java.util.Set; -import org.mapstruct.Mapper; -import xyz.aimcup.generated.model.RoleResponseDto; -import xyz.aimcup.generated.model.UserResponseDto; -import xyz.aimcup.security.domain.RoleBase; -import xyz.aimcup.security.domain.UserBase; - -@Mapper(componentModel = "spring") -public interface UserMapper { - UserBase mapUserResponseDtoToUser(UserResponseDto userResponseDto); - - RoleBase mapRoleResponseToRole(RoleResponseDto userBase); - RoleBase mapRoleResponseToRole(Object userBase); - Set mapRoles(List objectList); - -} diff --git a/src/main/java/xyz/aimcup/security/principal/UserPrincipal.java b/src/main/java/xyz/aimcup/security/principal/UserPrincipal.java index ad76217..7b5ad26 100644 --- a/src/main/java/xyz/aimcup/security/principal/UserPrincipal.java +++ b/src/main/java/xyz/aimcup/security/principal/UserPrincipal.java @@ -1,11 +1,13 @@ package xyz.aimcup.security.principal; +import java.util.UUID; import lombok.Builder; import lombok.Getter; import lombok.Setter; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; -import xyz.aimcup.security.domain.UserBase; +import org.springframework.security.oauth2.core.user.OAuth2User; +import xyz.aimcup.security.domain.User; import java.util.Collection; import java.util.Map; @@ -13,8 +15,9 @@ @Builder @Getter @Setter -public class UserPrincipal implements UserDetails { +public class UserPrincipal implements UserDetails, OAuth2User { + private UUID id; private String username; private Long osuId; @@ -22,13 +25,20 @@ public class UserPrincipal implements UserDetails { private Collection authorities; private Map attributes; - public static UserPrincipal create(UserBase userBase) { + public static UserPrincipal create(User userBase) { return UserPrincipal.builder() - .username(userBase.getUsername()) - .osuId(userBase.getOsuId()) - .active(!userBase.getIsRestricted()) - .authorities(userBase.getRoles()) - .build(); + .id(userBase.getId()) + .username(userBase.getUsername()) + .osuId(userBase.getOsuId()) + .active(!userBase.getIsRestricted()) + .authorities(userBase.getRoles()) + .build(); + } + + public static UserPrincipal create(User user, Map attributes) { + UserPrincipal userPrincipal = UserPrincipal.create(user); + userPrincipal.setAttributes(attributes); + return userPrincipal; } @Override @@ -65,4 +75,9 @@ public boolean isCredentialsNonExpired() { public boolean isEnabled() { return true; } + + @Override + public String getName() { + return this.username; + } } diff --git a/src/main/resources/shared/openapi/models/role-response.yaml b/src/main/resources/shared/openapi/models/role-response.yaml deleted file mode 100644 index 5987693..0000000 --- a/src/main/resources/shared/openapi/models/role-response.yaml +++ /dev/null @@ -1,8 +0,0 @@ -RoleResponseDTO: - type: object - properties: - id: - type: string - format: uuid - name: - type: string diff --git a/src/main/resources/shared/openapi/models/user-response.yaml b/src/main/resources/shared/openapi/models/user-response.yaml deleted file mode 100644 index 088f3e9..0000000 --- a/src/main/resources/shared/openapi/models/user-response.yaml +++ /dev/null @@ -1,16 +0,0 @@ -UserResponseDTO: - type: object - properties: - id: - type: string - format: uuid - username: - type: string - osuId: - type: integer - isRestricted: - type: boolean - roles: - type: array - items: - $ref: '/role-response.yaml#/RoleResponseDTO' \ No newline at end of file diff --git a/src/main/resources/shared/openapi/schema.yaml b/src/main/resources/shared/openapi/schema.yaml deleted file mode 100644 index c8afc8a..0000000 --- a/src/main/resources/shared/openapi/schema.yaml +++ /dev/null @@ -1,13 +0,0 @@ -openapi: 3.0.1 -info: - title: Auth microservice - version: 1.0.0 - -paths: {} - -components: - schemas: - RoleResponseDto: - $ref: 'models/role-response.yaml#/RoleResponseDTO' - UserResponseDto: - $ref: 'models/user-response.yaml#/UserResponseDTO'