2
\$\begingroup\$

Here's a simple authentication server. It's pretty basic but has the core functionality of sign-up and log-in handling. Your best bet is to simply run the docker-compose file

Main

package com.example.tokenservice;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class TokenServiceApplication {
 public static void main(String[] args) {
 SpringApplication.run(TokenServiceApplication.class, args);
 }
}
package com.example.tokenservice.config;
import com.example.tokenservice.handler.TokenHandler;
import com.example.tokenservice.util.WebFilterFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.AuthenticationException;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.RouterFunctions;
import org.springframework.web.reactive.function.server.ServerResponse;
import org.springframework.web.server.WebFilter;
@Configuration
public class RouterConfig {
 private final TokenHandler tokenHandler;
 public RouterConfig(TokenHandler tokenHandler) {
 this.tokenHandler = tokenHandler;
 }
 @Bean
 public RouterFunction<ServerResponse> signUpRoute() {
 return RouterFunctions.route()
 .POST("/signup", tokenHandler::signUp)
 .build();
 }
 @Bean
 public RouterFunction<ServerResponse> logInRoute() {
 return RouterFunctions.route()
 .POST("/login", tokenHandler::logIn)
 .build();
 }
 @Bean
 public WebFilter authenticationExceptionToUnauthorizedFilter() {
 return WebFilterFactory.exceptionHandlingWebFilter(AuthenticationException.class, HttpStatus.UNAUTHORIZED);
 }
 @Bean
 public WebFilter duplicateKeyExceptionToConflictFilter() {
 return WebFilterFactory.exceptionHandlingWebFilter(DuplicateKeyException.class, HttpStatus.CONFLICT);
 }
}
package com.example.tokenservice.config;
import com.example.tokenservice.repository.UserRepository;
import com.example.tokenservice.util.WebFilterFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.ReactiveAuthenticationManager;
import org.springframework.security.authentication.UserDetailsRepositoryReactiveAuthenticationManager;
import org.springframework.security.core.userdetails.ReactiveUserDetailsService;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.server.WebFilterChainProxy;
import reactor.core.publisher.Mono;
@Configuration
public class SecurityConfig {
 @Bean
 public WebFilterChainProxy noSecurityWebFilterChainProxy() {
 return WebFilterFactory.noOpWebFilterChainProxy();
 }
 @Bean
 public ReactiveUserDetailsService userDetailsService(UserRepository userRepository) {
 return username -> Mono.<UserDetails>justOrEmpty(userRepository.findByUsername(username))
 .switchIfEmpty(Mono.error(() -> new UsernameNotFoundException("No such user: " + username)));
 }
 @Bean
 public ReactiveAuthenticationManager authenticationManager(ReactiveUserDetailsService userDetailsService,
 PasswordEncoder passwordEncoder) {
 UserDetailsRepositoryReactiveAuthenticationManager authenticationManager =
 new UserDetailsRepositoryReactiveAuthenticationManager(userDetailsService);
 authenticationManager.setPasswordEncoder(passwordEncoder);
 return authenticationManager;
 }
 @Bean
 public PasswordEncoder passwordEncoder() {
 return new BCryptPasswordEncoder();
 }
}
package com.example.tokenservice.data.constant;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class JWT {
 public static String ROLES = "roles";
 public static String KEY = "jxgEQeXHuPq8VdbyYFNkANdudQ53YUn4";
}
package com.example.tokenservice.data.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@NoArgsConstructor
@AllArgsConstructor
@Setter
@Getter
@Builder
public class UserDto {
 private String username;
 private String password;
}
package com.example.tokenservice.data.entity;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.ManyToMany;
import jakarta.persistence.Table;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.springframework.security.core.GrantedAuthority;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
@Entity
@Getter
@Setter
@NoArgsConstructor
@Table(name = "roles")
public class Role implements GrantedAuthority {
 public static final String USER = "user";
 @Id
 @GeneratedValue(strategy = GenerationType.UUID)
 private UUID id;
 @Column(nullable = false, unique = true)
 private String authority;
 @ManyToMany(mappedBy = "authorities")
 private Set<User> users = ConcurrentHashMap.newKeySet();
 public Role(String authority) {
 this.authority = authority;
 }
 public void addUser(User user) {
 users.add(user);
 }
 @Override
 public boolean equals(Object o) {
 if (this == o) return true;
 if (o == null || getClass() != o.getClass()) return false;
 Role role = (Role) o;
 return authority.equals(role.authority);
 }
 @Override
 public int hashCode() {
 return Objects.hash(authority);
 }
 @Override
 public String toString() {
 return authority;
 }
}
package com.example.tokenservice.data.entity;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
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 lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.springframework.security.core.userdetails.UserDetails;
import java.text.MessageFormat;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
@Entity
@Getter
@Setter
@NoArgsConstructor
@Table(name = "users")
public class User implements UserDetails {
 @Id
 @GeneratedValue(strategy = GenerationType.UUID)
 private UUID id;
 @Column(nullable = false, unique = true)
 private String username;
 @Column(nullable = false)
 private String password;
 @Column(nullable = false)
 private Boolean enabled = true;
 @ManyToMany
 @JoinTable(name = "users_roles",
 joinColumns = @JoinColumn(name = "user_id"),
 inverseJoinColumns = @JoinColumn(name = "role_id"))
 private Set<Role> authorities = ConcurrentHashMap.newKeySet();
 public void addRole(Role role) {
 authorities.add(role);
 }
 @Override
 public boolean isEnabled() {
 return enabled;
 }
 @Override
 public boolean isAccountNonExpired() {
 return true;
 }
 @Override
 public boolean isAccountNonLocked() {
 return true;
 }
 @Override
 public boolean isCredentialsNonExpired() {
 return true;
 }
 @Override
 public boolean equals(Object o) {
 if (this == o) return true;
 if (o == null || getClass() != o.getClass()) return false;
 User user = (User) o;
 return username.equals(user.username);
 }
 @Override
 public int hashCode() {
 return Objects.hash(username);
 }
 @Override
 public String toString() {
 return MessageFormat.format("User[username={0}]", username);
 }
}
package com.example.tokenservice.handler;
import com.example.tokenservice.data.dto.UserDto;
import com.example.tokenservice.data.entity.User;
import com.example.tokenservice.mapper.UserMapper;
import com.example.tokenservice.service.token.TokenService;
import com.example.tokenservice.service.user.UserService;
import org.springframework.http.HttpStatus;
import org.springframework.security.authentication.ReactiveAuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import reactor.core.publisher.Mono;
@Component
public class BasicTokenHandler implements TokenHandler {
 private final UserService userService;
 private final TokenService tokenService;
 private final ReactiveAuthenticationManager authenticationManager;
 private final UserMapper userMapper;
 public BasicTokenHandler(UserService userService,
 TokenService tokenService,
 ReactiveAuthenticationManager authenticationManager,
 UserMapper userMapper) {
 this.userService = userService;
 this.tokenService = tokenService;
 this.authenticationManager = authenticationManager;
 this.userMapper = userMapper;
 }
 @Override
 public Mono<ServerResponse> signUp(ServerRequest request) {
 return request.bodyToMono(UserDto.class)
 .map(userMapper::toUser)
 .map(userService::encodePassword)
 .map(userService::addDefaultRoles)
 .map(userService::save)
 .map(this::toAuthenticatedUpat)
 .map(tokenService::generateTokenFor)
 .transform(jwt -> ServerResponse.status(HttpStatus.CREATED).body(jwt, String.class));
 }
 private UsernamePasswordAuthenticationToken toAuthenticatedUpat(User user) {
 return UsernamePasswordAuthenticationToken.authenticated(
 user.getUsername(), user.getPassword(), user.getAuthorities());
 }
 @Override
 public Mono<ServerResponse> logIn(ServerRequest request) {
 return request.bodyToMono(UserDto.class)
 .map(this::toUnauthenticatedUpat)
 .flatMap(authenticationManager::authenticate)
 .map(tokenService::generateTokenFor)
 .transform(jwt -> ServerResponse.status(HttpStatus.OK).body(jwt, String.class));
 }
 private UsernamePasswordAuthenticationToken toUnauthenticatedUpat(UserDto userDto) {
 return UsernamePasswordAuthenticationToken.unauthenticated(
 userDto.getUsername(), userDto.getPassword());
 }
}
package com.example.tokenservice.handler;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import reactor.core.publisher.Mono;
public interface TokenHandler {
 Mono<ServerResponse> signUp(ServerRequest request);
 Mono<ServerResponse> logIn(ServerRequest request);
}
package com.example.tokenservice.mapper;
import com.example.tokenservice.data.dto.UserDto;
import com.example.tokenservice.data.entity.User;
import org.mapstruct.Mapper;
@Mapper(componentModel = "spring")
public interface UserMapper {
 User toUser(UserDto userDto);
}
package com.example.tokenservice.repository;
import com.example.tokenservice.data.entity.Role;
import org.springframework.data.jpa.repository.EntityGraph;
import org.springframework.data.repository.Repository;
import java.util.Optional;
import java.util.UUID;
public interface RoleRepository extends Repository<Role, UUID> {
 @EntityGraph(attributePaths = "users")
 Optional<Role> findByAuthority(String authority);
 Role save(Role role);
}
package com.example.tokenservice.repository;
import com.example.tokenservice.data.entity.User;
import org.springframework.data.jpa.repository.EntityGraph;
import org.springframework.data.repository.Repository;
import java.util.Optional;
import java.util.UUID;
public interface UserRepository extends Repository<User, UUID> {
 @EntityGraph(attributePaths = "authorities")
 User save(User user);
 @EntityGraph(attributePaths = "authorities")
 Optional<User> findByUsername(String username);
 Boolean existsByUsername(String username);
}
package com.example.tokenservice.service.role;
import com.example.tokenservice.data.entity.Role;
import com.example.tokenservice.repository.RoleRepository;
import org.springframework.stereotype.Service;
import java.util.Optional;
@Service
public class BasicRoleService implements RoleService {
 private final RoleRepository roleRepository;
 public BasicRoleService(RoleRepository roleRepository) {
 this.roleRepository = roleRepository;
 }
 @Override
 public Optional<Role> findByAuthority(String authority) {
 return roleRepository.findByAuthority(authority);
 }
 @Override
 public Role save(Role role) {
 return roleRepository.save(role);
 }
}
package com.example.tokenservice.service.role;
import com.example.tokenservice.data.entity.Role;
import java.util.Optional;
public interface RoleService {
 Optional<Role> findByAuthority(String authority);
 Role save(Role authority);
}
package com.example.tokenservice.service.token;
import com.example.tokenservice.data.constant.JWT;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.stereotype.Service;
import java.sql.Date;
import java.time.DayOfWeek;
import java.time.LocalDate;
@Service
public class JwtTokenService implements TokenService {
 @Override
 public String generateTokenFor(Authentication authentication) {
 return Jwts.builder()
 .setSubject(authentication.getName())
 .claim(JWT.ROLES, authentication.getAuthorities()
 .stream()
 .map(GrantedAuthority::getAuthority)
 .toList())
 .setExpiration(Date.valueOf(LocalDate.now()
 .plusDays(DayOfWeek.values().length)))
 .signWith(Keys.hmacShaKeyFor(JWT.KEY.getBytes()))
 .compact();
 }
}
package com.example.tokenservice.service.token;
import org.springframework.security.core.Authentication;
public interface TokenService {
 String generateTokenFor(Authentication authentication);
}
package com.example.tokenservice.service.user;
import com.example.tokenservice.data.entity.Role;
import com.example.tokenservice.data.entity.User;
import com.example.tokenservice.repository.UserRepository;
import com.example.tokenservice.service.role.RoleService;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.Optional;
@Service
public class BasicUserService implements UserService {
 private final UserRepository userRepository;
 private final RoleService roleService;
 private final PasswordEncoder encoder;
 public BasicUserService(UserRepository userRepository,
 RoleService roleService,
 PasswordEncoder encoder) {
 this.userRepository = userRepository;
 this.roleService = roleService;
 this.encoder = encoder;
 }
 @Override
 public User save(User user) {
 if (userRepository.existsByUsername(user.getUsername()))
 throw new DuplicateKeyException("Username already taken: " + user.getUsername());
 return userRepository.save(user);
 }
 @Override
 public User encodePassword(User user) {
 String encodedPassword = encoder.encode(user.getPassword());
 user.setPassword(encodedPassword);
 return user;
 }
 @Override
 @Transactional
 public User addDefaultRoles(User user) {
 Optional<Role> userRoleOptional = roleService.findByAuthority(Role.USER);
 Role userRole = userRoleOptional.orElseGet(() -> roleService.save(new Role(Role.USER)));
 userRole.addUser(user);
 user.addRole(userRole);
 return user;
 }
}
package com.example.tokenservice.service.user;
import com.example.tokenservice.data.entity.User;
public interface UserService {
 User save(User user);
 User encodePassword(User user);
 User addDefaultRoles(User user);
}
package com.example.tokenservice.util;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DefaultDataBufferFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.security.web.server.MatcherSecurityWebFilterChain;
import org.springframework.security.web.server.WebFilterChainProxy;
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter;
import reactor.core.publisher.Mono;
import java.util.List;
import java.util.function.Function;
public class WebFilterFactory {
 private WebFilterFactory() {
 }
 public static WebFilter noOpWebFilter() {
 return (exchange, chain) -> chain.filter(exchange);
 }
 public static WebFilterChainProxy noOpWebFilterChainProxy() {
 return new WebFilterChainProxy(
 new MatcherSecurityWebFilterChain(
 exchange -> ServerWebExchangeMatcher.MatchResult.match(),
 List.of(noOpWebFilter())
 ));
 }
 public static WebFilter exceptionHandlingWebFilter(Class<? extends Throwable> throwableClass,
 HttpStatus status) {
 return exceptionHandlingWebFilter(throwableClass, status, Throwable::getMessage);
 }
 public static <T extends Throwable> WebFilter exceptionHandlingWebFilter(Class<T> throwableClass,
 HttpStatus status,
 Function<T, String> responseBodyValueFunction) {
 return (exchange, chain) -> chain.filter(exchange)
 .onErrorResume(throwableClass,
 t -> setResponse(exchange, status, responseBodyValueFunction.apply(t)));
 }
 private static Mono<Void> setResponse(ServerWebExchange exchange,
 HttpStatus status,
 String responseBody) {
 ServerHttpResponse response = exchange.getResponse();
 response.setStatusCode(status);
 DataBuffer dataBuffer = DefaultDataBufferFactory.sharedInstance
 .wrap(responseBody.getBytes());
 return response.writeWith(Mono.just(dataBuffer));
 }
}
# application.yml
server:
 port: 8100
logging:
 level:
 root: ${ROOT_LOGGING_LEVEL:info}
spring:
 datasource:
 username: ${POSTGRES_USERNAME:postgres}
 password: ${POSTGRES_PASSWORD:postgres}
 url: ${POSTGRES_URL:jdbc:postgresql://localhost:5432/postgres}
 jpa:
 hibernate:
 ddl-auto: create-drop
 show-sql: true
 application:
 name: token-service
eureka:
 client:
 service-url:
 defaultZone: ${EUREKA_URL:http://localhost:8761/eureka}
<?xml version="1.0" encoding="UTF-8"?>
<!--suppress VulnerableLibrariesLocal -->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
 <modelVersion>4.0.0</modelVersion>
 <parent>
 <groupId>org.springframework.boot</groupId>
 <artifactId>spring-boot-starter-parent</artifactId>
 <version>3.2.2</version>
 <relativePath/> <!-- lookup parent from repository -->
 </parent>
 <groupId>com.example</groupId>
 <artifactId>token-service</artifactId>
 <version>0.0.1-SNAPSHOT</version>
 <name>token-service</name>
 <description>token-service</description>
 <properties>
 <java.version>17</java.version>
 <spring-cloud.version>202300</spring-cloud.version>
 <jsonwebtoken.version>0.11.5</jsonwebtoken.version>
 <mapstruct.version>1.5.5.Final</mapstruct.version>
 </properties>
 <dependencies>
 <dependency>
 <groupId>org.springframework.boot</groupId>
 <artifactId>spring-boot-starter-data-jpa</artifactId>
 </dependency>
 <dependency>
 <groupId>org.springframework.boot</groupId>
 <artifactId>spring-boot-starter-security</artifactId>
 </dependency>
 <dependency>
 <groupId>org.springframework.boot</groupId>
 <artifactId>spring-boot-starter-webflux</artifactId>
 </dependency>
 <dependency>
 <groupId>org.springframework.cloud</groupId>
 <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
 </dependency>
 <dependency>
 <groupId>org.postgresql</groupId>
 <artifactId>postgresql</artifactId>
 </dependency>
 <dependency>
 <groupId>org.projectlombok</groupId>
 <artifactId>lombok</artifactId>
 <optional>true</optional>
 </dependency>
 <dependency>
 <groupId>org.mapstruct</groupId>
 <artifactId>mapstruct</artifactId>
 <version>${mapstruct.version}</version>
 </dependency>
 <dependency>
 <groupId>io.jsonwebtoken</groupId>
 <artifactId>jjwt-api</artifactId>
 <version>${jsonwebtoken.version}</version>
 </dependency>
 <dependency>
 <groupId>io.jsonwebtoken</groupId>
 <artifactId>jjwt-impl</artifactId>
 <version>${jsonwebtoken.version}</version>
 <scope>runtime</scope>
 </dependency>
 <dependency>
 <groupId>io.jsonwebtoken</groupId>
 <artifactId>jjwt-jackson</artifactId>
 <version>${jsonwebtoken.version}</version>
 <scope>runtime</scope>
 </dependency>
 <dependency>
 <groupId>org.springframework.boot</groupId>
 <artifactId>spring-boot-starter-test</artifactId>
 <scope>test</scope>
 </dependency>
 <dependency>
 <groupId>io.projectreactor</groupId>
 <artifactId>reactor-test</artifactId>
 <scope>test</scope>
 </dependency>
 <dependency>
 <groupId>org.springframework.security</groupId>
 <artifactId>spring-security-test</artifactId>
 <scope>test</scope>
 </dependency>
 </dependencies>
 <dependencyManagement>
 <dependencies>
 <dependency>
 <groupId>org.springframework.cloud</groupId>
 <artifactId>spring-cloud-dependencies</artifactId>
 <version>${spring-cloud.version}</version>
 <type>pom</type>
 <scope>import</scope>
 </dependency>
 </dependencies>
 </dependencyManagement>
 <build>
 <plugins>
 <plugin>
 <groupId>org.springframework.boot</groupId>
 <artifactId>spring-boot-maven-plugin</artifactId>
 <configuration>
 <excludes>
 <exclude>
 <groupId>org.projectlombok</groupId>
 <artifactId>lombok</artifactId>
 </exclude>
 </excludes>
 </configuration>
 </plugin>
 <plugin>
 <groupId>org.apache.maven.plugins</groupId>
 <artifactId>maven-compiler-plugin</artifactId>
 <version>3.8.1</version>
 <configuration>
 <source>${java.version}</source>
 <target>${java.version}</target>
 <annotationProcessorPaths>
 <path>
 <groupId>org.mapstruct</groupId>
 <artifactId>mapstruct-processor</artifactId>
 <version>${mapstruct.version}</version>
 </path>
 <path>
 <groupId>org.projectlombok</groupId>
 <artifactId>lombok-mapstruct-binding</artifactId>
 <version>0.2.0</version>
 </path>
 <path>
 <groupId>org.projectlombok</groupId>
 <artifactId>lombok</artifactId>
 <version>${lombok.version}</version>
 </path>
<!-- <path>-->
<!-- <groupId>org.springframework.boot</groupId>-->
<!-- <artifactId>spring-boot-configuration-processor</artifactId>-->
<!-- <version>2.6.6</version>-->
<!-- </path>-->
 </annotationProcessorPaths>
 <compilerArgs>
 <arg>
 -Amapstruct.defaultComponentModel=spring
 </arg>
 </compilerArgs>
 </configuration>
 </plugin>
 </plugins>
 </build>
</project>

Tests

package com.example.tokenservice.config;
import com.example.tokenservice.data.entity.Role;
import com.example.tokenservice.data.entity.User;
import com.example.tokenservice.testUtil.UserUtil;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.security.authentication.AccountStatusException;
import org.springframework.security.authentication.ReactiveAuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.ReactiveUserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import java.util.List;
import java.util.function.Consumer;
import java.util.stream.Stream;
import static org.mockito.BDDMockito.given;
@ExtendWith(MockitoExtension.class)
public class AuthenticationManagerTest {
 SecurityConfig securityConfig = new SecurityConfig();
 @Mock
 ReactiveUserDetailsService userDetailsService;
 @Mock
 PasswordEncoder passwordEncoder;
 ReactiveAuthenticationManager authenticationManager;
 @BeforeEach
 void setUp() {
 authenticationManager = securityConfig.authenticationManager(userDetailsService, passwordEncoder);
 }
 @Test
 void testAuthenticate_withNonExistentUser_throws() {
 String nonExistentUsername = "mystery-man";
 given(userDetailsService.findByUsername(nonExistentUsername))
 .willReturn(Mono.error(new UsernameNotFoundException("No such user")));
 Authentication authentication = UsernamePasswordAuthenticationToken.unauthenticated(nonExistentUsername, null);
 StepVerifier.create(authenticationManager.authenticate(authentication))
 .expectError(UsernameNotFoundException.class)
 .verify();
 }
 @Test
 void testAuthenticate_withExistingUser_butInvalidCredentials_stillThrows() {
 String realPassword = "real_password";
 User user = new User();
 user.setUsername("minnie_m");
 user.setPassword(realPassword);
 given(userDetailsService.findByUsername(user.getUsername()))
 .willReturn(Mono.just(user));
 String fakePassword = "fake_password";
 given(passwordEncoder.matches(fakePassword, realPassword)).willReturn(false);
 Authentication authentication = UsernamePasswordAuthenticationToken.unauthenticated(user.getUsername(), fakePassword);
 StepVerifier.create(authenticationManager.authenticate(authentication))
 .expectError(AuthenticationException.class)
 .verify();
 }
 @ParameterizedTest
 @MethodSource("accountInvalidators")
 void testAuthenticate_withExistingUser_butIllegalAccountState_throws(Consumer<User> accountInvalidator) {
 String realPassword = "real_password";
 User user = new User();
 user.setUsername("minnie_m");
 user.setPassword(realPassword);
 String encodedPassword = "#nc0ded_pa$$word";
 List<Role> someRoles = Stream.of("role", "another_role")
 .map(Role::new).toList();
 given(userDetailsService.findByUsername(user.getUsername())).willReturn(Mono.just(
 UserUtil.cloneAndMutate(user, u -> {
 u.setPassword(encodedPassword);
 someRoles.forEach(u::addRole);
 accountInvalidator.accept(u);
 }))
 );
 Authentication authentication =
 UsernamePasswordAuthenticationToken.unauthenticated(user.getUsername(), user.getPassword());
 StepVerifier.create(authenticationManager.authenticate(authentication))
 .expectError(AccountStatusException.class)
 .verify();
 }
 static List<Consumer<User>> accountInvalidators() {
 return List.of(
 u -> u.setEnabled(false)
 );
 }
 @Test
 void testAuthenticate_withExistingUser_withValidCredentials_authenticates() {
 String realPassword = "real_password";
 User user = new User();
 user.setUsername("minnie_m");
 user.setPassword(realPassword);
 String encodedPassword = "#nc0ded_pa$$word";
 List<Role> someRoles = Stream.of("role", "another_role")
 .map(Role::new).toList();
 given(userDetailsService.findByUsername(user.getUsername())).willReturn(Mono.just(
 UserUtil.cloneAndMutate(user, u -> {
 u.setPassword(encodedPassword);
 someRoles.forEach(u::addRole);
 }))
 );
 given(passwordEncoder.matches(realPassword, encodedPassword)).willReturn(true);
 Authentication expectedAuthentication = UsernamePasswordAuthenticationToken.authenticated(
 user, encodedPassword, someRoles
 );
 Authentication passedAuthentication =
 UsernamePasswordAuthenticationToken.unauthenticated(user.getUsername(), user.getPassword());
 StepVerifier.create(authenticationManager.authenticate(passedAuthentication))
 .expectNext(expectedAuthentication)
 .verifyComplete();
 }
}
package com.example.tokenservice.config;
import com.example.tokenservice.handler.TokenHandler;
import com.example.tokenservice.testUtil.ServerResponseUtil;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
import org.springframework.mock.web.reactive.function.server.MockServerRequest;
import org.springframework.mock.web.server.MockServerWebExchange;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import java.net.URI;
import java.util.List;
import static org.mockito.BDDMockito.given;
@ExtendWith(MockitoExtension.class)
class RouterFunctionTest {
 @Mock
 TokenHandler tokenHandler;
 @InjectMocks
 RouterConfig routerConfig;
 @ParameterizedTest
 @MethodSource("nonMatchingSignupRequests")
 void signUpRoute_withWithNonMatchingRequests(ServerRequest nonMatchingRequest) {
 RouterFunction<ServerResponse> signUpRoute = routerConfig.signUpRoute();
 StepVerifier.create(signUpRoute.route(nonMatchingRequest))
 .verifyComplete();
 }
 static List<ServerRequest> nonMatchingSignupRequests() {
 return List.of(
 MockServerRequest.builder()
 .method(HttpMethod.DELETE)
 .uri(URI.create("/signup"))
 .exchange(MockServerWebExchange.from(MockServerHttpRequest.delete("/signup")))
 .build(),
 MockServerRequest.builder()
 .method(HttpMethod.POST)
 .uri(URI.create("/wrong-signup"))
 .exchange(MockServerWebExchange.from(MockServerHttpRequest.post("/wrong-signup")))
 .build()
 );
 }
 @Test
 void signUpRoute_withMatchingRequest() {
 ServerRequest matchingRequest = MockServerRequest.builder()
 .method(HttpMethod.POST)
 .uri(URI.create("/signup"))
 .exchange(MockServerWebExchange.from(MockServerHttpRequest.post("/signup")))
 .build();
 HttpStatus status = HttpStatus.CREATED;
 String bodyValue = "j.w.token";
 Mono<ServerResponse> response = ServerResponse.status(status)
 .body(Mono.just(bodyValue), String.class);
 given(tokenHandler.signUp(matchingRequest)).willReturn(response);
 RouterFunction<ServerResponse> signUpRoute = routerConfig.signUpRoute();
 StepVerifier.create(signUpRoute.route(matchingRequest))
 .assertNext(handlerFunction -> StepVerifier.create(handlerFunction.handle(matchingRequest))
 .assertNext(r -> ServerResponseUtil.responseChecksOut(r, status, bodyValue))
 .verifyComplete())
 .verifyComplete();
 }
 @ParameterizedTest
 @MethodSource("nonMatchingLoginRequests")
 void testLogInRoute_withWithNonMatchingRequests(ServerRequest nonMatchingRequest) {
 RouterFunction<ServerResponse> signUpRoute = routerConfig.logInRoute();
 StepVerifier.create(signUpRoute.route(nonMatchingRequest))
 .verifyComplete();
 }
 static List<ServerRequest> nonMatchingLoginRequests() {
 return List.of(
 MockServerRequest.builder()
 .method(HttpMethod.DELETE)
 .uri(URI.create("/login"))
 .exchange(MockServerWebExchange.from(MockServerHttpRequest.delete("/login")))
 .build(),
 MockServerRequest.builder()
 .method(HttpMethod.POST)
 .uri(URI.create("/wrong-login"))
 .exchange(MockServerWebExchange.from(MockServerHttpRequest.post("/wrong-login")))
 .build()
 );
 }
 @Test
 void testLogInRoute_withMatchingRequest() {
 ServerRequest matchingRequest = MockServerRequest.builder()
 .method(HttpMethod.POST)
 .uri(URI.create("/login"))
 .exchange(MockServerWebExchange.from(MockServerHttpRequest.post("/login")))
 .build();
 HttpStatus status = HttpStatus.OK;
 String bodyValue = "j.w.token";
 Mono<ServerResponse> response = ServerResponse.status(status)
 .body(Mono.just(bodyValue), String.class);
 given(tokenHandler.logIn(matchingRequest)).willReturn(response);
 RouterFunction<ServerResponse> signUpRoute = routerConfig.logInRoute();
 StepVerifier.create(signUpRoute.route(matchingRequest))
 .assertNext(handlerFunction -> StepVerifier.create(handlerFunction.handle(matchingRequest))
 .assertNext(r -> ServerResponseUtil.responseChecksOut(r, status, bodyValue))
 .verifyComplete())
 .verifyComplete();
 }
}
package com.example.tokenservice.config;
import com.example.tokenservice.data.entity.User;
import com.example.tokenservice.repository.UserRepository;
import com.example.tokenservice.testUtil.UserUtil;
import org.junit.jupiter.api.BeforeEach;
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.userdetails.ReactiveUserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import reactor.test.StepVerifier;
import java.util.Optional;
import static org.mockito.BDDMockito.given;
@ExtendWith(MockitoExtension.class)
class UserDetailsServiceTest {
 SecurityConfig securityConfig = new SecurityConfig();
 @Mock
 UserRepository userRepository;
 ReactiveUserDetailsService userDetailsService;
 @BeforeEach
 void setUp() {
 userDetailsService = securityConfig.userDetailsService(userRepository);
 }
 @Test
 void testUserDetailsService_ifUserFetched_returnsItBack() {
 User user = new User();
 user.setUsername("scrooge_m");
 user.setPassword("password");
 given(userRepository.findByUsername(user.getUsername())).willReturn(Optional.of(user));
 StepVerifier.create(userDetailsService.findByUsername(user.getUsername()))
 .expectNextMatches(u -> UserUtil.haveEqualFields(u, user))
 .verifyComplete();
 }
 @Test
 void testUserDetailsService_ifUserNotFetched_throwsUserNotFoundException() {
 String unknownUsername = "Unknown_user";
 given(userRepository.findByUsername(unknownUsername)).willReturn(Optional.empty());
 StepVerifier.create(userDetailsService.findByUsername(unknownUsername))
 .expectError(UsernameNotFoundException.class)
 .verify();
 }
}
package com.example.tokenservice.handler;
import com.example.tokenservice.data.dto.UserDto;
import com.example.tokenservice.data.entity.Role;
import com.example.tokenservice.data.entity.User;
import com.example.tokenservice.mapper.UserMapper;
import com.example.tokenservice.service.token.TokenService;
import com.example.tokenservice.service.user.UserService;
import com.example.tokenservice.testUtil.ServerResponseUtil;
import com.example.tokenservice.testUtil.UserUtil;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.mock.web.reactive.function.server.MockServerRequest;
import org.springframework.security.authentication.ReactiveAuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import java.net.URI;
import java.util.List;
import java.util.UUID;
import static org.mockito.BDDMockito.given;
@ExtendWith(MockitoExtension.class)
class BasicTokenHandlerTest {
 @Mock
 UserService userService;
 @Mock
 TokenService tokenService;
 @Mock
 ReactiveAuthenticationManager authenticationManager;
 @Mock
 UserMapper userMapper;
 @InjectMocks
 BasicTokenHandler tokenHandler;
 @Test
 void testSignUp() {
 UserDto userDto = new UserDto();
 userDto.setUsername("goofus_d");
 userDto.setPassword("12345");
 User user = new User();
 user.setUsername(userDto.getUsername());
 user.setPassword(userDto.getPassword());
 given(userMapper.toUser(userDto)).willReturn(user);
 User userAfterPasswordEncoding = UserUtil.cloneAndMutate(user, u -> u.setPassword("encoded_pass"));
 given(userService.encodePassword(user)).willReturn(userAfterPasswordEncoding);
 User userWithDefaultRoles = UserUtil.cloneAndMutate(userAfterPasswordEncoding, u -> u.addRole(new Role(Role.USER)));
 given(userService.addDefaultRoles(userAfterPasswordEncoding)).willReturn(userWithDefaultRoles);
 UUID assignedId = UUID.randomUUID();
 User persistedUser = UserUtil.cloneAndMutate(userWithDefaultRoles, u -> u.setId(assignedId));
 given(userService.save(userWithDefaultRoles)).willReturn(persistedUser);
 String jwt = "just.imagine.its.a.JWT";
 given(tokenService.generateTokenFor(toAuthenticatedUpat(persistedUser))).willReturn(jwt);
 MockServerRequest request = MockServerRequest.builder()
 .method(HttpMethod.POST)
 .uri(URI.create("/signup"))
 .body(Mono.just(userDto));
 StepVerifier.create(tokenHandler.signUp(request))
 .assertNext(response -> ServerResponseUtil.responseChecksOut(response, HttpStatus.CREATED, jwt))
 .verifyComplete();
 }
 private UsernamePasswordAuthenticationToken toAuthenticatedUpat(User user) {
 return UsernamePasswordAuthenticationToken.authenticated(
 user.getUsername(), user.getPassword(), user.getAuthorities());
 }
 @Test
 void testLogIn() {
 UserDto userDto = new UserDto();
 userDto.setUsername("goofus_d");
 userDto.setPassword("12345");
 Authentication passedAuthentication = toUnauthenticatedUpat(userDto);
 Authentication returnedAuthentication = UsernamePasswordAuthenticationToken.authenticated(
 userDto, userDto.getUsername(), List.of(new Role("some_default_role")
 ));
 given(authenticationManager.authenticate(passedAuthentication)).willReturn(Mono.just(returnedAuthentication));
 String jwt = "json.web.token";
 given(tokenService.generateTokenFor(returnedAuthentication)).willReturn(jwt);
 MockServerRequest request = MockServerRequest.builder()
 .method(HttpMethod.POST)
 .uri(URI.create("/login"))
 .body(Mono.just(userDto));
 StepVerifier.create(tokenHandler.logIn(request))
 .assertNext(response -> ServerResponseUtil.responseChecksOut(response, HttpStatus.OK, jwt))
 .verifyComplete();
 }
 private UsernamePasswordAuthenticationToken toUnauthenticatedUpat(UserDto userDto) {
 return UsernamePasswordAuthenticationToken.unauthenticated(userDto.getUsername(), userDto.getPassword());
 }
}
package com.example.tokenservice.mapper;
import com.example.tokenservice.data.dto.UserDto;
import com.example.tokenservice.data.entity.User;
import org.junit.jupiter.api.Test;
import org.mapstruct.factory.Mappers;
import static org.assertj.core.api.SoftAssertions.assertSoftly;
class UserMapperTest {
 UserMapper mapper = Mappers.getMapper(UserMapper.class);
 @Test
 void testToUser() {
 String username = "mickey_m", password = "pass";
 UserDto userDto = UserDto.builder().username(username).password(password).build();
 User user = mapper.toUser(userDto);
 assertSoftly(soft -> {
 soft.assertThat(user.getUsername()).isEqualTo(username);
 soft.assertThat(user.getPassword()).isEqualTo(password);
 });
 }
}
package com.example.tokenservice.service.role;
import com.example.tokenservice.data.entity.Role;
import com.example.tokenservice.repository.RoleRepository;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.util.Optional;
import java.util.UUID;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.SoftAssertions.assertSoftly;
import static org.mockito.BDDMockito.given;
@ExtendWith(MockitoExtension.class)
class BasicRoleServiceTest {
 @Mock
 RoleRepository roleRepository;
 @InjectMocks
 BasicRoleService roleService;
 @Test
 void testFindByAuthority() {
 Role role = new Role(Role.USER);
 role.setId(UUID.randomUUID());
 given(roleRepository.findByAuthority(Role.USER)).willReturn(Optional.of(role));
 Optional<Role> roleOptional = roleService.findByAuthority(Role.USER);
 assertThat(roleOptional).isPresent();
 assertSoftly(soft -> {
 soft.assertThat(roleOptional.get())
 .extracting(Role::getId)
 .isEqualTo(role.getId());
 soft.assertThat(roleOptional.get())
 .extracting(Role::getAuthority)
 .isEqualTo(role.getAuthority());
 });
 }
 @Test
 void testSave() {
 Role role = new Role(Role.USER);
 Role persistedRole = new Role();
 persistedRole.setAuthority(role.getAuthority());
 persistedRole.setId(UUID.randomUUID());
 given(roleRepository.save(role)).willReturn(persistedRole);
 Role returnedRole = roleService.save(role);
 assertSoftly(soft -> {
 soft.assertThat(returnedRole)
 .extracting(Role::getId)
 .isEqualTo(persistedRole.getId());
 soft.assertThat(returnedRole)
 .extracting(Role::getAuthority)
 .isEqualTo(persistedRole.getAuthority());
 });
 }
}
package com.example.tokenservice.service.token;
import com.example.tokenservice.data.constant.JWT;
import com.example.tokenservice.data.entity.Role;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import org.junit.jupiter.api.Test;
import org.springframework.security.authentication.TestingAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
class JwtTokenServiceTest {
 TokenService tokenService = new JwtTokenService();
 @Test
 void testGenerateTokenFor() {
 String principal = "daisy_d";
 List<GrantedAuthority> roles = List.of(new SimpleGrantedAuthority(Role.USER));
 TestingAuthenticationToken authentication = new TestingAuthenticationToken(principal, null, roles);
 String jwt = tokenService.generateTokenFor(authentication);
 Claims jwtClaims = Jwts.parserBuilder()
 .setSigningKey(Keys.hmacShaKeyFor(JWT.KEY.getBytes()))
 .build()
 .parseClaimsJws(jwt)
 .getBody();
 assertThat(jwtClaims.getSubject()).isEqualTo(principal);
 assertThat(jwtClaims.get(JWT.ROLES))
 .extracting(List.class::cast)
 .asList()
 .containsExactlyInAnyOrderElementsOf(roles.stream()
 .map(GrantedAuthority::getAuthority)
 .toList());
 }
}
package com.example.tokenservice.service.user;
import com.example.tokenservice.data.entity.Role;
import com.example.tokenservice.data.entity.User;
import com.example.tokenservice.repository.UserRepository;
import com.example.tokenservice.service.role.RoleService;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.security.crypto.password.PasswordEncoder;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.assertj.core.api.Assumptions.assumeThat;
import static org.assertj.core.api.SoftAssertions.assertSoftly;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.then;
import static org.mockito.Mockito.never;
@ExtendWith(MockitoExtension.class)
class BasicUserServiceTest {
 @Mock
 UserRepository userRepository;
 @Mock
 RoleService roleService;
 @Mock
 PasswordEncoder passwordEncoder;
 @InjectMocks
 BasicUserService userService;
 @Test
 void testSave_withOccupiedUsername() {
 String occupiedUsername = "some_occupied_username";
 given(userRepository.existsByUsername(occupiedUsername)).willReturn(true);
 User user = new User();
 user.setUsername(occupiedUsername);
 assertThatThrownBy(() -> userService.save(user)).isInstanceOf(DuplicateKeyException.class);
 then(userRepository).should(never()).save(any());
 }
 @Test
 void testSave_withVacantUsername() {
 String someVacantUsername = "some_vacant_username";
 given(userRepository.existsByUsername(someVacantUsername)).willReturn(false);
 User userToSave = new User();
 userToSave.setUsername(someVacantUsername);
 User persistedUser = new User();
 persistedUser.setUsername(userToSave.getUsername());
 persistedUser.setId(UUID.randomUUID());
 given(userRepository.save(userToSave)).willReturn(persistedUser);
 User returnedUser = userService.save(userToSave);
 then(userRepository).should().save(userToSave);
 assertThat(returnedUser).isEqualTo(persistedUser);
 }
 @Test
 void testEncodePassword() {
 String password = "password", username = "donald_d";
 User user = new User();
 user.setPassword(password);
 user.setUsername(username);
 String encodedPassword = "drowssap";
 given(passwordEncoder.encode(password)).willReturn(encodedPassword);
 User userWithEncodedPassword = userService.encodePassword(user);
 assertThat(userWithEncodedPassword).extracting(User::getPassword).isEqualTo(encodedPassword);
 assertThat(userWithEncodedPassword).extracting(User::getUsername).isEqualTo(username);
 }
 @Test
 void testAddDefaultRoles_ifDefaultRoleAbsent_persistsOne_thenAddsToUser() {
 User user = new User();
 assumeThat(user.getAuthorities()).isNullOrEmpty();
 given(roleService.findByAuthority(Role.USER)).willReturn(Optional.empty());
 Role defaultRole = new Role(Role.USER);
 given(roleService.save(defaultRole)).willAnswer(i -> {
 Role persistedRole = new Role(Role.USER);
 persistedRole.setId(UUID.randomUUID());
 return persistedRole;
 });
 User userWithDefaultRoles = userService.addDefaultRoles(user);
 then(roleService).should().save(defaultRole);
 Set<Role> userAuthorities = userWithDefaultRoles.getAuthorities();
 assertThat(userAuthorities).hasSize(1);
 Role roleInReturnedUser = userAuthorities.iterator().next();
 assertSoftly(soft -> {
 soft.assertThat(roleInReturnedUser.getAuthority()).isEqualTo(defaultRole.getAuthority());
 soft.assertThat(roleInReturnedUser.getId()).isNotNull();
 soft.assertThat(roleInReturnedUser.getUsers()).contains(user);
 });
 }
 @Test
 void testAddDefaultRoles_ifDefaultRoleAlreadyPersisted_addsFetchedRoleToUser() {
 User user = new User();
 assumeThat(user.getAuthorities()).isNullOrEmpty();
 Role role = new Role(Role.USER);
 role.setId(UUID.randomUUID());
 given(roleService.findByAuthority(Role.USER)).willReturn(Optional.of(role));
 User userWithDefaultRoles = userService.addDefaultRoles(user);
 then(roleService).should(never()).save(any());
 Set<Role> userAuthorities = userWithDefaultRoles.getAuthorities();
 assertThat(userAuthorities).hasSize(1);
 Role roleInReturnedUser = userAuthorities.iterator().next();
 assertSoftly(soft -> {
 soft.assertThat(roleInReturnedUser.getAuthority()).isEqualTo(role.getAuthority());
 soft.assertThat(roleInReturnedUser.getId()).isEqualTo(role.getId());
 soft.assertThat(roleInReturnedUser.getUsers()).contains(user);
 });
 }
}
package com.example.tokenservice.testUtil;
import org.springframework.http.HttpStatus;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.web.reactive.function.server.ServerResponse;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import static org.assertj.core.api.SoftAssertions.assertSoftly;
public class ServerResponseUtil {
 @SuppressWarnings({"unchecked", "DataFlowIssue"})
 public static <T> void responseChecksOut(ServerResponse response, HttpStatus expectedStatus, T expectedBody) {
 assertSoftly(soft -> {
 soft.assertThat(response.statusCode()).isEqualTo(expectedStatus);
 Mono<T> body = (Mono<T>) ReflectionTestUtils.getField(response, "entity");
 StepVerifier.create(body)
 .expectNext(expectedBody)
 .verifyComplete();
 });
 }
}
package com.example.tokenservice.testUtil;
import com.example.tokenservice.data.entity.User;
import lombok.SneakyThrows;
import org.springframework.security.core.userdetails.UserDetails;
import java.lang.reflect.Field;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Consumer;
import java.util.stream.Collectors;
public class UserUtil {
 private UserUtil() {
 }
 public static User cloneAndMutate(User user, Consumer<User> mutator) {
 User userCopy = clone(user);
 mutator.accept(userCopy);
 return userCopy;
 }
 public static User clone(User user) {
 User userCopy = new User();
 userCopy.setUsername(user.getUsername());
 userCopy.setPassword(user.getPassword());
 userCopy.setId(user.getId());
 userCopy.setEnabled(user.getEnabled());
 userCopy.setAuthorities(user.getAuthorities()
 .stream()
 .collect(Collectors.toCollection(ConcurrentHashMap::newKeySet)));
 return userCopy;
 }
 @SneakyThrows
 public static <T extends UserDetails> boolean haveEqualFields(T oneUser, T anotherUser) {
 Field[] oneUserFields = oneUser.getClass().getDeclaredFields();
 Field[] anotherUserFields = anotherUser.getClass().getDeclaredFields();
 for (int i = 0; i < oneUserFields.length; i++) {
 Field oneUserField = oneUserFields[i];
 oneUserField.setAccessible(true);
 Field anotherUserField = anotherUserFields[i];
 anotherUserField.setAccessible(true);
 boolean areFieldsEqual = Objects.equals(oneUserField.get(oneUser), anotherUserField.get(anotherUser));
 if (!areFieldsEqual) return false;
 }
 return true;
 }
}
package com.example.tokenservice.util;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.mock.http.server.reactive.MockServerHttpResponse;
import org.springframework.security.web.server.WebFilterChainProxy;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import java.text.MessageFormat;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.then;
@ExtendWith(MockitoExtension.class)
class WebFilterFactoryTest {
 @Mock
 private ServerWebExchange exchange;
 @Mock
 private WebFilterChain chain;
 @Test
 void noOpWebFilter() {
 given(chain.filter(any())).willReturn(Mono.empty());
 WebFilter noOpWebFilter = WebFilterFactory.noOpWebFilter();
 StepVerifier.create(noOpWebFilter.filter(exchange, chain))
 .verifyComplete();
 then(chain).should().filter(exchange);
 then(chain).shouldHaveNoMoreInteractions();
 then(exchange).shouldHaveNoInteractions();
 }
 @Test
 void noOpWebFilterChainProxy() {
 given(chain.filter(any())).willReturn(Mono.empty());
 WebFilterChainProxy filterChainProxy = WebFilterFactory.noOpWebFilterChainProxy();
 StepVerifier.create(filterChainProxy.filter(exchange, chain))
 .verifyComplete();
 then(chain).should().filter(exchange);
 then(chain).shouldHaveNoMoreInteractions();
 then(exchange).shouldHaveNoInteractions();
 }
 @Test
 void testExceptionHandlingWebFilter_withDefaultBodyMapping() {
 String exceptionMessage = "Something happened!";
 given(chain.filter(exchange)).willReturn(Mono.error(new SomeException(exceptionMessage)));
 given(exchange.getResponse()).willReturn(new MockServerHttpResponse());
 HttpStatus status = HttpStatus.I_AM_A_TEAPOT;
 WebFilter webFilter =
 WebFilterFactory.exceptionHandlingWebFilter(SomeException.class, status);
 StepVerifier.create(webFilter.filter(exchange, chain))
 .verifyComplete();
 ArgumentCaptor<ServerWebExchange> captor = ArgumentCaptor.forClass(ServerWebExchange.class);
 then(chain).should().filter(captor.capture());
 ServerWebExchange newExchange = captor.getValue();
 assertThat(newExchange).extracting(ServerWebExchange::getResponse)
 .extracting(ServerHttpResponse::getStatusCode)
 .isEqualTo(status);
 StepVerifier.create(((MockServerHttpResponse) exchange.getResponse()).getBodyAsString())
 .expectNext(exceptionMessage)
 .verifyComplete();
 }
 @Test
 void testExceptionHandlingWebFilter_withCustomBodyMapping() {
 Throwable exceptionCause = new UnknownError("Nobody knows what happened");
 Throwable exception = new SomeException("Something happened!", exceptionCause);
 given(chain.filter(exchange)).willReturn(Mono.error(exception));
 given(exchange.getResponse()).willReturn(new MockServerHttpResponse());
 HttpStatus status = HttpStatus.I_AM_A_TEAPOT;
 WebFilter webFilter = WebFilterFactory.exceptionHandlingWebFilter(SomeException.class,
 status, t -> MessageFormat.format(
 "This happened: {0}[{1}]. It was caused by: {2}[{3}]",
 t.getClass().getSimpleName(), t.getMessage(),
 t.getCause().getClass().getSimpleName(), t.getCause().getMessage()
 ));
 StepVerifier.create(webFilter.filter(exchange, chain))
 .verifyComplete();
 ArgumentCaptor<ServerWebExchange> captor = ArgumentCaptor.forClass(ServerWebExchange.class);
 then(chain).should().filter(captor.capture());
 ServerWebExchange newExchange = captor.getValue();
 assertThat(newExchange).extracting(ServerWebExchange::getResponse)
 .extracting(ServerHttpResponse::getStatusCode)
 .isEqualTo(status);
 StepVerifier.create(((MockServerHttpResponse) exchange.getResponse()).getBodyAsString())
 .expectNext(MessageFormat.format(
 "This happened: {0}[{1}]. It was caused by: {2}[{3}]",
 exception.getClass().getSimpleName(), exception.getMessage(),
 exceptionCause.getClass().getSimpleName(), exceptionCause.getMessage()
 ))
 .verifyComplete();
 }
 static class SomeException extends RuntimeException {
 public SomeException(String exceptionMessage) {
 super(exceptionMessage);
 }
 public SomeException(String exceptionMessage, Throwable cause) {
 super(exceptionMessage, cause);
 }
 }
}

Docker

# docker-compose.yml
version: '2.1'
services:
 token-service:
 build:
 context: .
 dockerfile: Dockerfile
 ports:
 - "8100:8100"
 depends_on:
 db:
 condition: service_healthy
 eureka:
 condition: service_healthy
 environment:
 POSTGRES_URL: jdbc:postgresql://db:5432/postgres
 POSTGRES_USERNAME: postgres
 POSTGRES_PASSWORD: postgres
 EUREKA_URL: http://eureka:8761/eureka
 db:
 image: postgres
 ports:
 - "5432:5432"
 environment:
 POSTGRES_USER: postgres
 POSTGRES_PASSWORD: postgres
 healthcheck:
 test: ["CMD", "pg_isready"]
 interval: 10s
 timeout: 10s
 retries: 10
 eureka:
 image: nadchel/eureka-server:1.0
 ports:
 - "8761:8761"
 healthcheck:
 test: ["CMD-SHELL", "curl -f http://eureka:8761"]
 interval: 10s
 timeout: 10s
 retries: 10
# Dockerfile
FROM amazoncorretto:17-alpine-jdk AS builder
WORKDIR /app
COPY . .
RUN apk add --no-cache maven && \
 mvn package -Dmaven.test.skip=true
FROM amazoncorretto:17-alpine-jdk
WORKDIR /app
COPY --from=builder /app/target/token-service-0.0.1-SNAPSHOT.jar .
EXPOSE 8100
CMD ["java", "-jar", "token-service-0.0.1-SNAPSHOT.jar"]
asked Feb 27, 2024 at 0:32
\$\endgroup\$
14
  • \$\begingroup\$ My considerations: \$\endgroup\$ Commented Feb 27, 2024 at 0:33
  • \$\begingroup\$ 1. Should my server prepend the bearer scheme prefix to JWT tokens? It doesn't. In truth, I just winged it, I don't know for sure how it's supposed to be done \$\endgroup\$ Commented Feb 27, 2024 at 0:33
  • 3
    \$\begingroup\$ I know the character limit has been reached, however when adding additional information you should edit your question instead of adding a comment. Learn more about comments including when to comment and when not to in the Help Center page about Comments. Perhaps it would be wise to remove any code that is not critical for review. \$\endgroup\$ Commented Feb 27, 2024 at 0:41
  • 1
    \$\begingroup\$ In class JWT, a 192-bit private KEY does not belong checked in to source control. Better to grab it from an env var, a vault, a file, something like that. // Also, nice tests! \$\endgroup\$ Commented Feb 27, 2024 at 2:46
  • 1
    \$\begingroup\$ If you’re using the RCS revision control system to store source code, do not "ci" checkin edits containing secret keying material. If you’re using CVS, ClearCase, P4 perforce, SVN subversion, or git on GitHub, don’t make such secrets part of a commit. \$\endgroup\$ Commented Feb 27, 2024 at 12:59

2 Answers 2

3
\$\begingroup\$

My considerations:

  1. You hash passwords (but a hacker could use a rainbow-table), so store only salted and hashed passwords having a (wild guess) 64-char-salt and a modern hash algorithm please.

  2. You could reduce code by using RestRepository but Repository. This will bring eTag features and give correct http-status-codes.

  3. You should mvn spotbugs:check pmd:check findbugs:check.

  4. You can use @Table("users") instead of @Table(name = "users").

  5. Logging is your friend.

  6. Have you heard about brute-force-attack prevention? The app may not Pentest-save.

answered Feb 27, 2024 at 10:25
\$\endgroup\$
8
  • 2
    \$\begingroup\$ OP is using bcrypt, although I also would recommend a more modern algorithm, such as Argon2id. \$\endgroup\$ Commented Feb 27, 2024 at 12:31
  • \$\begingroup\$ Yes, Argon2id + salt would be great! \$\endgroup\$ Commented Feb 27, 2024 at 13:17
  • 1
    \$\begingroup\$ What is RestRepository? Can you share a link to the javadoc? I haven't found it \$\endgroup\$ Commented Feb 27, 2024 at 15:35
  • 1
    \$\begingroup\$ What do you mean by it may not be Pentest-safe? Why isn't it anyway? \$\endgroup\$ Commented Feb 27, 2024 at 15:40
  • 1
    \$\begingroup\$ @Sergey For RestRepository see here: spring.io/guides/gs/accessing-data-rest \$\endgroup\$ Commented Feb 28, 2024 at 12:44
3
\$\begingroup\$

It might not be a concern when the live run environment is safe, if that's the case just skip following.

Store sensitive data in a vault, there are advantages, vaults are safe, just authorised actors have access to them, and decouples the developers from the burden of dealing with sensitive data.

There is a mix of two concerns, data access and security, in User entity that adds tight coupling between presentation layer and data access layer since security is a web concern. UserDto should implement UserDetails interface instead.

The global variables in test classes would be nice to have private access modifier.

One of the files listed by Docker section seems different than a Docker file. Additional details about following could be helpful.

version: '2.1' 
 
services: 
 token-service: 
 build: 
 context: . 
 dockerfile: Dockerfile 
 ports: 
 - "8100:8100" 
 depends_on: 
 db: 
 condition: service_healthy 
 eureka: 
 condition: service_healthy 
 environment: 
 POSTGRES_URL: jdbc:postgresql://db:5432/postgres 
 POSTGRES_USERNAME: postgres 
 POSTGRES_PASSWORD: postgres 
 EUREKA_URL: http://eureka:8761/eureka 
 
 db: 
 image: postgres 
 ports: 
 - "5432:5432" 
 environment: 
 POSTGRES_USER: postgres 
 POSTGRES_PASSWORD: postgres 
 healthcheck: 
 test: ["CMD", "pg_isready"] 
 interval: 10s 
 timeout: 10s 
 retries: 10 
 
 eureka: 
 image: nadchel/eureka-server:1.0 
 ports: 
 - "8761:8761" 
 healthcheck: 
 test: ["CMD-SHELL", "curl -f http://eureka:8761"] 
 interval: 10s 
 timeout: 10s 
 retries: 10 
Grim
5963 silver badges14 bronze badges
answered Feb 27, 2024 at 14:21
\$\endgroup\$
2
  • \$\begingroup\$ Thank you! Can you please expand on the User issue? UserDetails is (actual) user information fetched from a DB, including their authorities, from which an authenticated Authentication is built. UserDto, on the other hand, is sign-up/log-in data provided by a user which, I believe, should not include authorities (and requires authentication) \$\endgroup\$ Commented Feb 27, 2024 at 15:56
  • \$\begingroup\$ That file you mentioned is a docker-compose.yml file. I added the caption \$\endgroup\$ Commented Feb 27, 2024 at 15:57

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.