How to properly wrap server response to client - java

Task:
Make a single wrapper for server responses to the client.
My decision:
Create an ApiResponse object and use it in the controller and services
Am I doing the right thing? Sorry I don't know much about this...
ApiResponce
#Data
public class ApiResponse {
private Date timestamp;
private int status;
private String message;
private Object data;
public ApiResponse(int status, String message, Object data) {
this.timestamp = new Date();
this.status = status;
this.message = message;
this.data = data;
}
Controller
#RestController
#RequestMapping("/api/v1/admin")
#RequiredArgsConstructor
#PreAuthorize("hasAuthority('ADMIN')")
public class Admin {
private final UserService userService;
#PostMapping(value = "/users/add", produces = APPLICATION_JSON_VALUE)
public ResponseEntity<ApiResponse> addUser(#RequestBody User user) {
return ResponseEntity.ok(userService.addUser(user));
}
}
Service
public interface UserService {
ApiResponse addUser(User user);
ApiResponse updateUser(User user);
ApiResponse getUserByEmail(String email);
ApiResponse getUserById(Long id);
}
Implementation method example
#Override
public ApiResponse addUser(User user) {
log.info("Saving new user to the database. Email: {}", user.getEmail());
// Check if the user is already in the database
User useDB = userRepo.findByEmail(user.getEmail());
if (useDB != null) {
return new ApiResponse(200, "A user with this e-mail already exists in the system!", user);
}
try {
user.setPassword(passwordEncoder.encode(user.getPassword()));
userRepo.save(user);
return new ApiResponse(200, "User added successfully", null);
} catch (Exception ex) {
log.error("New user added error! " + ex.getMessage());
return new ApiResponse(403, "New user registration error! " + ex.getMessage(), null);
}
}
Am I moving in the right direction? Or is it a sign of bad code?
Actually what I tried above)

As per my knowledge, you're doing good. Make sure to use BCryptPasswordEncoder in Configuration file in order to make password encrypted.

Related

code 403 no authorization even after putting the correct jwt token

I'm creating an api for use in a pharmacy. When implementing the security, jwt token and filter for using security, an error occurs when validating requests and the token is simply not considered valid. It has an expiration time of 2 hours, the user is allowed to login, I do this via postman, I take the token that appears, I copy and paste it in the authorization validation, I make a protected request in postman and I simply take a forbidden403 . I'm using the post method to make this request, and the baerer Token to send it. In the code, as you can see, it has all the correct configuration for my filter to come before spring's filter and also has the configuration to demonstrate how far the code is running, with a sistem.out with the message("calling filter") What indicates that the code runs until the validation of the token and after the problem. Note: I'm using mysql, springboot 3.0 and java 17. The entire structure of the tables is already created and working, but I can't make any request besides the login after facing this validation error. Follow the code below:
User Entity: #Table(name = "users") #Entity(name = "User") #Getter #NoArgsConstructor #AllArgsConstructor
#EqualsAndHashCode(of ="id") public class User implements UserDetails{
private static final long serialVersionUID = 1L;
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String login;
private String senha;
#Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return List.of(new SimpleGrantedAuthority("ROLE_USER"));
}
#Override
public String getPassword() {
return senha;
}
#Override
public String getUsername() {
return login;
}
#Override
public boolean isAccountNonExpired() {
return true;
}
#Override
public boolean isAccountNonLocked() {
return true;
}
#Override
public boolean isCredentialsNonExpired() {
return true;
}
#Override
public boolean isEnabled() {
return true;
}
}
Authentication Controller:
#RestController #RequestMapping("/login") public class AutenticacaoController {
#Autowired
private AuthenticationManager manager;
#Autowired
private TokenService tokenService;
#PostMapping
public ResponseEntity<DadosTokenJWT> efetuarLogin(#RequestBody #Valid DadosAutenticacao dados) {
var authenticationToken = new UsernamePasswordAuthenticationToken(dados.login(), dados.senha());
var authentication = manager.authenticate(authenticationToken);
var tokenJWT = tokenService.gerarToken((Usuario) authentication.getPrincipal());
return ResponseEntity.ok(new DadosTokenJWT(tokenJWT));
}
}
SecutiryConfigurations:
#Configuration #EnableWebSecurity public class SecutiryConfigurations {
#Autowired
private SecutiryFilter securityFilter;
#Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception{
return http.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and().authorizeHttpRequests().requestMatchers(HttpMethod.POST, "/login").permitAll()
.anyRequest().authenticated()
.and().addFilterBefore(securityFilter, UsernamePasswordAuthenticationFilter.class)
.build();
}
#Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception{
return configuration.getAuthenticationManager();
}
#Bean
public PasswordEncoder passwordEncoder () {
return new BCryptPasswordEncoder();
}
}
Token Service:
#Service public class TokenService {
#Value("${api.security.token.secret}")
private String secret;
public String gerarToken(Usuario usuario) {
try {
var algoritmo = Algorithm.HMAC256(secret);
return JWT.create().withIssuer("API remedios_api").withSubject(usuario.getLogin())
.withExpiresAt(dataExpiracao()).sign(algoritmo);
} catch (JWTCreationException exception) {
throw new RuntimeException("Erro ao gerar Token JWT", exception);
}
}
public String getSubject(String tokenJWT) {
try {
var algoritmo = Algorithm.HMAC256(secret);
return JWT.require(algoritmo).withIssuer("API remedios_api").build().verify(tokenJWT).getSubject();
} catch (JWTVerificationException exception) {
throw new RuntimeException("Token inválido ou expirado");
}
}
private Instant dataExpiracao() {
return LocalDateTime.now().plusHours(2).toInstant(ZoneOffset.of("-03:00"));
}
}
SecurityFilter:
#Component public class SecutiryFilter extends OncePerRequestFilter{
#Autowired
private TokenService tokenService;
#Autowired
private UsuarioRepository repository;
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
var tokenJWT = recuperarToken(request);
System.out.println("Chamando Filter");
if(tokenJWT != null) {
var subject = tokenService.getSubject(tokenJWT);
var usuario = repository.findByLogin(subject);
var authentication = new UsernamePasswordAuthenticationToken(usuario, null, usuario.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authentication);
System.out.println("Logado na requisição");
}
filterChain.doFilter(request, response);
}
private String recuperarToken(HttpServletRequest request) {
var authorizationHeader = request.getHeader("Authorization");
if( authorizationHeader != null) {
return authorizationHeader.replace("Bearer", "");
}
return null;
}
I login via postman, get the token code and paste it in the authorization to then use that token in some other request that is also protected via spring security.
I hope the request is accepted and released after validating the token in the request header.

Extend Micronaut CustomJWTClaimsSetGenerator to provide all attributes

I have the following two classes which provide the JWT authentication mechanisem.
CustomDelegatingAuthenticationProvider
#Singleton
#Replaces(value = DelegatingAuthenticationProvider.class)
public class CustomDelegatingAuthenticationProvider extends DelegatingAuthenticationProvider {
/**
* #param userFetcher Fetches users from persistence
* #param passwordEncoder Collaborator which checks if a raw password matches an encoded password
* #param authoritiesFetcher Fetches authorities for a particular user
*/
public CustomDelegatingAuthenticationProvider(UserFetcher userFetcher, PasswordEncoder passwordEncoder, AuthoritiesFetcher authoritiesFetcher) {
super(userFetcher, passwordEncoder, authoritiesFetcher);
}
#Override
protected Publisher<AuthenticationResponse> createSuccessfulAuthenticationResponse(AuthenticationRequest authenticationRequest, UserState userState) {
if (userState instanceof UserMember) {
UserMember user = (UserMember) userState;
return Flowable
.fromPublisher(authoritiesFetcher.findAuthoritiesByUsername(user.getUsername()))
.map(authorities -> new HDSUser(user.getUsername(), authorities, user.getId()));
}
return super.createSuccessfulAuthenticationResponse(authenticationRequest, userState);
}
}
CustomJWTClaimsSetGenerator
#Singleton
#Replaces(value = JWTClaimsSetGenerator.class)
public class CustomJWTClaimsSetGenerator extends JWTClaimsSetGenerator {
CustomJWTClaimsSetGenerator(TokenConfiguration tokenConfiguration, #Nullable JwtIdGenerator jwtIdGenerator, #Nullable ClaimsAudienceProvider claimsAudienceProvider) {
super(tokenConfiguration, jwtIdGenerator, claimsAudienceProvider);
}
protected void populateWithUserDetails(JWTClaimsSet.Builder builder, UserDetails userDetails) {
super.populateWithUserDetails(builder, userDetails);
if (userDetails instanceof HDSUser) {
builder.claim("userId", ((HDSUser) userDetails).getId());
}
}
}
The default response to the client looks like this:
My question. How can I extend the class to return all user attributes? Besides username I want to have the user id.
UPDATE
HDS user class which gathers the DB id
#CompileStatic
public class HDSUser extends UserDetails {
private long id;
public HDSUser(String username, Collection<String> roles, long id) {
super(username, roles);
this.id = id;
}
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
}
To extend the returned data you need to extend (implement custom) TokenRenderer as well as a custom version of the AccessRefreshToken.
Just as a simple example see the following code snipped which will extend the default access token payload with userId field.
First, create a custom AccessRefreshToken class with additional fields which are required.
#Introspected
#Getter
#Setter
public class CustomAccessRefreshToken extends BearerAccessRefreshToken {
// the new field which will be in the response
private String userId;
public CustomAccessRefreshToken(String username,
Collection<String> roles,
Integer expiresIn,
String accessToken,
String refreshToken,
String tokenType
) {
super(username, roles, expiresIn, accessToken, refreshToken, tokenType);
}
}
Next, we will need a TokenRenderer which will be used by the underlying subsystem to generate our custom token.
#Singleton
#Replaces(value = BearerTokenRenderer.class)
public class CustomTokenRenderer implements TokenRenderer {
private static final String BEARER_TOKEN_TYPE = HttpHeaderValues.AUTHORIZATION_PREFIX_BEARER;
#Override
public AccessRefreshToken render(Integer expiresIn, String accessToken, #Nullable String refreshToken) {
return new AccessRefreshToken(accessToken, refreshToken, BEARER_TOKEN_TYPE, expiresIn);
}
#Override
public AccessRefreshToken render(Authentication authentication, Integer expiresIn, String accessToken, #Nullable String refreshToken) {
CustomAccessRefreshToken token = new CustomAccessRefreshToken(authentication.getName(), authentication.getRoles(), expiresIn, accessToken, refreshToken, BEARER_TOKEN_TYPE);
// here just take the user data from Authentication object or access any other service
token.setUserId("Some user id");
return token;
}
}
That's it )) Just implement render() method the way you want and add as many custom fields as needed.
The response from the given example will look like
{
"username": "sherlock",
"userId": "Some user id",
"access_token": "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJzaGVybG9jayIsIm5iZiI6MTYzNjk5MTgzMSwicm9sZXMiOltdLCJpc3MiOiJtaWNyb25hdXRndWlkZSIsImV4cCI6MTYzNjk5NTQzMSwiaWF0IjoxNjM2OTkxODMxfQ.Cat1CTsUZkCj-OHGafiefNm1snPsALoaNw9y2xwF5Pw",
"token_type": "Bearer",
"expires_in": 3600
}
If you are on the older version of the Micronaut v1.x the TokenRenderer will look like this.
#Singleton
#Replaces(value = BearerTokenRenderer.class)
public class CustomTokenRenderer implements TokenRenderer {
private static final String BEARER_TOKEN_TYPE = HttpHeaderValues.AUTHORIZATION_PREFIX_BEARER;
public AccessRefreshToken render(Integer expiresIn, String accessToken, String refreshToken) {
return new AccessRefreshToken(accessToken, refreshToken, BEARER_TOKEN_TYPE, expiresIn);
}
public AccessRefreshToken render(UserDetails userDetails, Integer expiresIn, String accessToken, String refreshToken) {
CustomAccessRefreshToken token = new CustomAccessRefreshToken(userDetails.getUsername(), userDetails.getRoles(), expiresIn, accessToken, refreshToken, BEARER_TOKEN_TYPE);
token.setUserId("Some user id, Some user id");
return token;
}
}

Optional and Java Spring Testing

I have a problem with my ControllerTest. I'm not sure how to test for the Optional - does someone know how? The other test gives me a NullPointerException for the stubbing : when(couponService.getCouponById(id)).thenReturn(expectedCoupon);
Would be awesome if someone could help me.
public class CouponControllerTest {
#MockBean
private CouponService couponService;
#MockBean
private UserService userService;
#Autowired
MockMvc mockMvc;
#Test
public void checkAndUpdateCoupon() throws Exception {
int id = 1;
int userId = 1;
Coupon expectedCoupon = new Coupon(1, 1);
when(couponService.getCouponById(id)).thenReturn(expectedCoupon);
List<User> userList = new ArrayList<User>();
when(userService.getAllUser()).thenReturn(userList);
List<Coupon> couponList = new ArrayList<Coupon>();
when(couponService.getAllCoupons()).thenReturn(couponList);
mockMvc.perform(get("/checkCoupon")
.param("id", "1")
.param("userId", "1"))
.andExpect(status().isOk())
.andExpect(view().name("couponPage"))
.andExpect(model().attribute("error", "Not correct user id or coupon id."))
.andExpect(model().attribute("users", userList))
.andExpect(model().attribute("coupons", couponList));
verify(couponService).updateCoupons(id, userId);
}
}
#Controller
public class CouponController {
#Autowired
CouponService couponService;
#Autowired
UserService userService;
#GetMapping("/checkCoupon")
public String checkCoupon(ModelMap model, #RequestParam Integer id, #RequestParam Integer userId, Coupon coupon) {
Optional<Coupon> couponFromDatabase = couponService.byUserIdAndId(coupon.getUserId(), coupon.getId());
if(couponFromDatabase.isEmpty()) {
String error = "Not correct user id or coupon id.";
model.addAttribute("error", error);
} else {
String message = couponService.updateCoupons(id, userId);
model.addAttribute("message", message);
}
List<User> userList = userService.getAllUser();
model.addAttribute("users", userList);
List<Coupon> couponList = couponService.getAllCoupons();
model.addAttribute("coupons", couponList);
return "couponPage";
}
}
I think you need to do some changes in mocking the first service.
when( couponService.byUserIdAndId(anyLong(), anyLong()) ).thenReturn( Optional.of(expectedCoupon) );
Here the anyLong() refer to any Incoming long data type number.
Override your existing code with this above line.

Transaction doesn't works, saves users with the same email

I have to save users with unique emails. I added Transactional on my saveUser method. But when I tested I saw that I can add users with the same email. Here is my code:
#Transactional(isolation = Isolation.SERIALIZABLE)
public User saveUser(String email) {
if (userRepository.findByEmailIgnoreCase(email) != null) {
throw new UserAlreadyExistException(format("User with email %s already exists.", email.toLowerCase()));
}
var user = new User();
user.setEmail(email.toLowerCase());
return userRepository.save(user);
}
UserRepository it is a JpaRepository:
import org.springframework.data.jpa.repository.JpaRepository;
public interface UserRepository extends JpaRepository<User, String> {
User findByEmailIgnoreCase(String email);
}
Here is my test (SprinBootTest):
#Test
public void shouldNotCreateNewUserWithSameEmail() throws Exception {
String requestPayload = objectMapper.writeValueAsString(UserCreateRequest.builder()
.email(EMAIL1)
.rolesName(Set.of(role.getName())).build());
for (int i=0; i<15; i++) {
mockMvc.perform(MockMvcRequestBuilders.post(MAIN_REQUEST_URL)
.contentType(MediaType.APPLICATION_JSON)
.content(requestPayload))
.andExpect(MockMvcResultMatchers.status().isCreated())
.andExpect(jsonPath("$.id").exists())
.andExpect(jsonPath("$.name.firstName").doesNotExist())
.andExpect(jsonPath("$.name.lastName").doesNotExist())
.andReturn();
var r = new Runnable() {
#Override
public void run() {
try {
mockMvc.perform(MockMvcRequestBuilders.post(MAIN_REQUEST_URL)
.contentType(MediaType.APPLICATION_JSON)
.content(requestPayload))
.andExpect(MockMvcResultMatchers.status().isBadRequest());
} catch (Exception e) {
e.printStackTrace();
}
}
};
new Thread(r).start();
userRepository.deleteAll();
}
}
As you can see I run second user creation in other thread. Test passed when I do 3-5 iterations. But with 10-15 iteration it fails with error:
Exception in thread "Thread-10" java.lang.AssertionError: Status
expected:<400> but was:<201>
I.e. user was created. Also I tried to add synchronized to saveUser method, it doesn't helps. What is wrong? And how I can properly prevent not unique emails?
UPD: In User class:
#Column(unique = true, updatable = false, nullable = false)
private String email;
If you don't mind to use hibernate dependency.
#Table(uniqueConstraints = {#UniqueConstraint(columnNames = "email")})
public class user {
String email;
String displayName;
}

Architecture pattern for "microservice" with hard logic (Spring boot)

i've got a microservice which implements some optimization function by calling many times another microservice (the second one calculates so called target function value and the first micriservice changes paramters of this tagrget function)
It leads to necessity of writing some logic in Rest Controller layer. To be clear some simplified code will be represented below
#RestController
public class OptimizerController {
private OptimizationService service;
private RestTemplate restTemplate;
#GetMapping("/run_opt")
public DailyOptResponse doOpt(){
Data iniData = service.prepareData(null);
Result r = restTemplate.postForObject(http://calc-service/plain_calc", iniData, Result.class);
double dt = service.assessResult(r);
while(dt > 0.1){
Data newData = service.preapreData(r);
r = restTemplate.postForObject(http://calc-service/plain_calc", newData , Result.class);
dt = service.assessResult(r);
}
return service.prepareResponce(r);
}
As i saw in examples all people are striving to keep rest controller as simple as possible and move all logic to service layer. But what if i have to call some other microservices from service layer? Should i keep logic of data formin in service layer and return it to controller layer, use RestTemplate object in service layer or something else?
Thank you for your help
It is straightforward.
The whole logic is in the service layer (including other services).
Simple example:
Controller:
#RestController
#RequestMapping("/api/users")
public class UserController {
private final UserManager userManager;
#Autowired
public UserController(UserManager userManager) {
super();
this.userManager = userManager;
}
#GetMapping()
public List<UserResource> getUsers() {
return userManager.getUsers();
}
#GetMapping("/{userId}")
public UserResource getUser(#PathVariable Integer userId) {
return userManager.getUser(userId);
}
#PutMapping
public void updateUser(#RequestBody UserResource resource) {
userManager.updateUser(resource);
}
}
Service:
#Service
public class UserManager {
private static final Logger log = LoggerFactory.getLogger(UserManager.class);
private final UserRepository userRepository;
private final UserResourceAssembler userResourceAssembler;
private final PictureManager pictureManager;
#Autowired
public UserManager(
UserRepository userRepository,
UserResourceAssembler userResourceAssembler,
PictureManager pictureManager
) {
super();
this.userRepository = userRepository;
this.userResourceAssembler = userResourceAssembler;
this.pictureManager= pictureManager;
}
public UserResource getUser(Integer userId) {
User user = userRepository.findById(userId).orElseThrow(() -> new NotFoundException("User with ID " + userId + " not found!"));
return userResourceAssembler.toResource(user);
}
public List<UserResource> getUsers() {
return userResourceAssembler.toResources(userRepository.findAll());
}
public void updateUser(UserResource resource) {
User user = userRepository.findById(resource.getId()).orElseThrow(() -> new NotFoundException("User with ID " + resource.getId() + " not found!"));
PictureResource pictureResource = pictureManager.savePicture(user);
user = userResourceAssembler.fromResource(user, resource);
user = userRepository.save(user);
log.debug("User {} updated.", user);
}
}
Service 2:
#Service
public class PictureManager {
private static final Logger log = LoggerFactory.getLogger(PictureManager.class);
private final RestTemplate restTemplate;
#Autowired
public PictureManager(RestTemplate restTemplate) {
super();
this.restTemplate = restTemplate;
}
public PictureResource savePicture(User user) {
//do some logic with user
ResponseEntity<PictureResource> response = restTemplate.exchange(
"url",
HttpMethod.POST,
requestEntity,
PictureResource.class);
return response.getBody();
}
}
Repository:
public interface UserRepository extends JpaRepository<User, Integer> {
User findByUsername(String username);
}

Categories