Skip to content

Commit

Permalink
Refactor security implementation and other minors (#58)
Browse files Browse the repository at this point in the history
  • Loading branch information
haiphucnguyen authored Jan 17, 2025
1 parent 523e865 commit aa99fc3
Show file tree
Hide file tree
Showing 16 changed files with 92 additions and 81 deletions.
3 changes: 2 additions & 1 deletion commons/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,12 @@ dependencies {
api("com.fasterxml.jackson.datatype:jackson-datatype-hibernate6")
api("com.fasterxml.jackson.datatype:jackson-datatype-jsr310")
api("com.zaxxer:HikariCP")
api("io.micrometer:micrometer-registry-prometheus")
api("jakarta.annotation:jakarta.annotation-api")
api("org.apache.commons:commons-lang3")
api("org.hibernate.orm:hibernate-core")
api("org.hibernate.validator:hibernate-validator")

api("org.springframework.boot:spring-boot-starter-actuator")
api("org.springframework.boot:spring-boot-starter-aop")
api("org.springframework.boot:spring-boot-starter-data-jpa")
api("org.springframework.boot:spring-boot-loader-tools")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import static org.springframework.security.config.Customizer.withDefaults;

import io.flowinquiry.modules.usermanagement.AuthoritiesConstants;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
Expand All @@ -27,6 +28,10 @@ public PasswordEncoder passwordEncoder() {
}

@Bean
@ConditionalOnProperty(
name = "flowinquiry.edition",
havingValue = "community",
matchIfMissing = true)
public SecurityFilterChain filterChain(HttpSecurity http, MvcRequestMatcher.Builder mvc)
throws Exception {
http.cors(withDefaults())
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package io.flowinquiry.modules.fss.service;

import io.flowinquiry.modules.fss.ResourceRemoveEvent;
import io.flowinquiry.modules.fss.service.event.ResourceRemoveEvent;
import org.springframework.context.event.EventListener;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package io.flowinquiry.modules.fss;
package io.flowinquiry.modules.fss.service.event;

import lombok.Getter;
import org.springframework.context.ApplicationEvent;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package io.flowinquiry.modules.shared;

/** Application constants. */
public final class Constants {

public static final String DEFAULT_LANGUAGE = "en";

private Constants() {}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package io.flowinquiry.web.rest;
package io.flowinquiry.modules.shared.controller;

import java.util.ArrayList;
import java.util.Collections;
Expand All @@ -11,7 +11,7 @@

@RestController
@RequestMapping("/api")
public class SharedController {
public class TimezoneController {

@GetMapping("/timezones")
public List<ZoneInfo> getTimezones() {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package io.flowinquiry.modules.teams.controller;

import io.flowinquiry.modules.fss.ResourceRemoveEvent;
import io.flowinquiry.modules.fss.service.StorageService;
import io.flowinquiry.modules.fss.service.event.ResourceRemoveEvent;
import io.flowinquiry.modules.teams.service.TeamService;
import io.flowinquiry.modules.teams.service.dto.TeamDTO;
import io.flowinquiry.modules.usermanagement.service.dto.UserDTO;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,35 +1,20 @@
package io.flowinquiry.modules.usermanagement.controller;

import static io.flowinquiry.security.SecurityUtils.AUTHORITIES_KEY;
import static io.flowinquiry.security.SecurityUtils.JWT_ALGORITHM;
import static io.flowinquiry.security.SecurityUtils.USER_ID;

import io.flowinquiry.modules.usermanagement.InvalidLoginException;
import io.flowinquiry.modules.usermanagement.repository.UserRepository;
import io.flowinquiry.modules.usermanagement.service.UserService;
import io.flowinquiry.modules.usermanagement.service.dto.FwUserDetails;
import io.flowinquiry.modules.usermanagement.service.dto.UserDTO;
import io.flowinquiry.security.service.JwtService;
import jakarta.validation.Valid;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.time.temporal.ChronoUnit;
import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.jwt.JwsHeader;
import org.springframework.security.oauth2.jwt.JwtClaimsSet;
import org.springframework.security.oauth2.jwt.JwtEncoder;
import org.springframework.security.oauth2.jwt.JwtEncoderParameters;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
Expand All @@ -38,15 +23,8 @@
@RestController
@RequestMapping("/api")
public class LoginController {
private static final Logger LOG = LoggerFactory.getLogger(LoginController.class);

private final JwtEncoder jwtEncoder;

@Value("${flowinquiry.security.authentication.jwt.token-validity-in-seconds:0}")
private long tokenValidityInSeconds;

@Value("${flowinquiry.security.authentication.jwt.token-validity-in-seconds-for-remember-me:0}")
private long tokenValidityInSecondsForRememberMe;
private final JwtService jwtService;

private final AuthenticationManagerBuilder authenticationManagerBuilder;

Expand All @@ -55,11 +33,11 @@ public class LoginController {
private final UserRepository userRepository;

public LoginController(
JwtEncoder jwtEncoder,
JwtService jwtService,
AuthenticationManagerBuilder authenticationManagerBuilder,
UserService userService,
UserRepository userRepository) {
this.jwtEncoder = jwtEncoder;
this.jwtService = jwtService;
this.authenticationManagerBuilder = authenticationManagerBuilder;
this.userService = userService;
this.userRepository = userRepository;
Expand All @@ -77,39 +55,12 @@ public ResponseEntity<UserDTO> authorize(@Valid @RequestBody LoginVM loginVM) {
UserDTO adminUserDTO =
userService.getUserWithAuthorities().orElseThrow(InvalidLoginException::new);

String jwt = this.createToken(authentication, loginVM.isRememberMe());
String jwt = jwtService.generateToken(authentication);
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.setBearerAuth(jwt);

// Update last login time for user
userRepository.updateLastLoginTime(loginVM.getEmail(), LocalDateTime.now(ZoneOffset.UTC));
return new ResponseEntity<>(adminUserDTO, httpHeaders, HttpStatus.OK);
}

private String createToken(Authentication authentication, boolean rememberMe) {
String authorities =
authentication.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.joining(" "));

Instant now = Instant.now();
Instant validity;
if (rememberMe) {
validity = now.plus(this.tokenValidityInSecondsForRememberMe, ChronoUnit.SECONDS);
} else {
validity = now.plus(this.tokenValidityInSeconds, ChronoUnit.SECONDS);
}

JwtClaimsSet claims =
JwtClaimsSet.builder()
.issuedAt(now)
.expiresAt(validity)
.subject(authentication.getName())
.claim(AUTHORITIES_KEY, authorities)
.claim(USER_ID, ((FwUserDetails) authentication.getPrincipal()).getUserId())
.build();

JwsHeader jwsHeader = JwsHeader.with(JWT_ALGORITHM).build();
return this.jwtEncoder.encode(JwtEncoderParameters.from(jwsHeader, claims)).getTokenValue();
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package io.flowinquiry.modules.usermanagement.controller;

import io.flowinquiry.exceptions.ResourceNotFoundException;
import io.flowinquiry.modules.fss.ResourceRemoveEvent;
import io.flowinquiry.modules.fss.service.StorageService;
import io.flowinquiry.modules.fss.service.event.ResourceRemoveEvent;
import io.flowinquiry.modules.usermanagement.AuthoritiesConstants;
import io.flowinquiry.modules.usermanagement.EmailAlreadyUsedException;
import io.flowinquiry.modules.usermanagement.domain.User;
Expand Down
14 changes: 0 additions & 14 deletions commons/src/main/java/io/flowinquiry/security/Constants.java

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package io.flowinquiry.security.service;

import static io.flowinquiry.security.SecurityUtils.AUTHORITIES_KEY;
import static io.flowinquiry.security.SecurityUtils.JWT_ALGORITHM;
import static io.flowinquiry.security.SecurityUtils.USER_ID;

import io.flowinquiry.modules.usermanagement.service.dto.FwUserDetails;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Collection;
import java.util.stream.Collectors;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.oauth2.jwt.JwsHeader;
import org.springframework.security.oauth2.jwt.JwtClaimsSet;
import org.springframework.security.oauth2.jwt.JwtEncoder;
import org.springframework.security.oauth2.jwt.JwtEncoderParameters;
import org.springframework.stereotype.Service;

@Service
public class JwtService {
private final JwtEncoder jwtEncoder;

@Value("${flowinquiry.security.authentication.jwt.token-validity-in-seconds:0}")
private long tokenValidityInSeconds;

public JwtService(JwtEncoder jwtEncoder) {
this.jwtEncoder = jwtEncoder;
}

public String generateToken(Authentication authentication) {
return generateToken(
((FwUserDetails) authentication.getPrincipal()).getUserId(),
authentication.getName(),
authentication.getAuthorities());
}

public String generateToken(
Long userId, String email, Collection<? extends GrantedAuthority> grantedAuthorities) {
String authorities =
grantedAuthorities.stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.joining(" "));

Instant now = Instant.now();
Instant validity = now.plus(this.tokenValidityInSeconds, ChronoUnit.SECONDS);
JwtClaimsSet claims =
JwtClaimsSet.builder()
.issuedAt(now)
.expiresAt(validity)
.subject(email)
.claim(AUTHORITIES_KEY, authorities)
.claim(USER_ID, userId)
.build();

JwsHeader jwsHeader = JwsHeader.with(JWT_ALGORITHM).build();
return this.jwtEncoder.encode(JwtEncoderParameters.from(jwsHeader, claims)).getTokenValue();
}
}
5 changes: 1 addition & 4 deletions server/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -57,17 +57,14 @@ configurations {

dependencies {
implementation( project(":commons"))
implementation("org.springframework.boot:spring-boot-starter-actuator")
implementation("io.micrometer:micrometer-registry-prometheus")


testImplementation("org.testcontainers:jdbc")
testImplementation("org.testcontainers:junit-jupiter")
testImplementation("org.testcontainers:testcontainers")
testImplementation("org.springframework.boot:spring-boot-starter-test")
testImplementation("org.springframework.boot:spring-boot-test")
testImplementation("org.springframework.security:spring-security-test")

testImplementation("org.jetbrains.kotlin:kotlin-test")
testImplementation(libs.assertJ)
}

Expand Down
2 changes: 2 additions & 0 deletions server/src/main/resources/config/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -103,3 +103,5 @@ server:
# Add your own application properties here, see the ApplicationProperties class
# to have type-safe configuration
# ===================================================================
flowinquiry:
edition: community
Binary file removed server/src/main/resources/config/tls/keystore.p12
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@
import io.flowinquiry.IntegrationTest;
import io.flowinquiry.config.FlowInquiryProperties;
import io.flowinquiry.modules.collab.service.MailService;
import io.flowinquiry.modules.shared.Constants;
import io.flowinquiry.modules.usermanagement.service.dto.UserDTO;
import io.flowinquiry.security.Constants;
import jakarta.mail.Multipart;
import jakarta.mail.Session;
import jakarta.mail.internet.MimeBodyPart;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

import com.fasterxml.jackson.databind.ObjectMapper;
import io.flowinquiry.IntegrationTest;
import io.flowinquiry.modules.shared.Constants;
import io.flowinquiry.modules.usermanagement.AuthoritiesConstants;
import io.flowinquiry.modules.usermanagement.controller.KeyAndPasswordVM;
import io.flowinquiry.modules.usermanagement.controller.ManagedUserVM;
Expand All @@ -23,7 +24,6 @@
import io.flowinquiry.modules.usermanagement.service.dto.PasswordChangeDTO;
import io.flowinquiry.modules.usermanagement.service.dto.UserDTO;
import io.flowinquiry.modules.usermanagement.service.mapper.UserMapper;
import io.flowinquiry.security.Constants;
import java.time.Instant;
import java.util.HashSet;
import java.util.Optional;
Expand Down

0 comments on commit aa99fc3

Please sign in to comment.