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>2023.0.0</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"]
bearerscheme 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\$class JWT, a 192-bit privateKEYdoes 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\$