Commit f781d1e6 authored by Heber Cordova's avatar Heber Cordova

added authentication with jwt

parent 60732c3c
......@@ -56,6 +56,24 @@
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
<!-- JSON WEB TOKENS -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.12.3</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.12.3</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId> <!-- or jjwt-gson if Gson is preferred -->
<version>0.12.3</version>
<scope>runtime</scope>
</dependency>
</dependencies>
<build>
......
......@@ -5,15 +5,15 @@ import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import com.hcordova.flightagencyapi.shared.dto.ErrorMessage;
import com.hcordova.flightagencyapi.shared.dto.ErrorMessageDTO;
import com.hcordova.flightagencyapi.shared.exceptions.NotFoundException;
@ControllerAdvice
public class ExceptionController {
@ExceptionHandler(NotFoundException.class)
public ResponseEntity<ErrorMessage> notFoundException(NotFoundException ex) {
ErrorMessage errorMessage = new ErrorMessage(ex.getMessage(), HttpStatus.NOT_FOUND, ex.getPath());
public ResponseEntity<ErrorMessageDTO> notFoundException(NotFoundException ex) {
ErrorMessageDTO errorMessage = new ErrorMessageDTO(ex.getMessage(), HttpStatus.NOT_FOUND, ex.getPath());
return new ResponseEntity<>(errorMessage, HttpStatus.NOT_FOUND);
}
}
package com.hcordova.flightagencyapi.shared.config;
import java.io.IOException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import com.hcordova.flightagencyapi.shared.domain.services.JwtService;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtService jwtService;
private final UserDetailsService userService;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
final String authHeader = request.getHeader("Authorization");
final String jwt;
final String userEmail;
if (!StringUtils.hasText(authHeader) || !authHeader.startsWith("Bearer ")) {
filterChain.doFilter(request, response);
return;
}
jwt = authHeader.substring(7);
userEmail = jwtService.extractUserName(jwt);
if (StringUtils.hasText(userEmail) && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = userService.loadUserByUsername(userEmail);
if (jwtService.isTokenValid(jwt, userDetails)) {
SecurityContext context = SecurityContextHolder.createEmptyContext();
UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
context.setAuthentication(authToken);
SecurityContextHolder.setContext(context);
}
}
filterChain.doFilter(request, response);
}
}
package com.hcordova.flightagencyapi.shared.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import lombok.RequiredArgsConstructor;
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
private final JwtAuthenticationFilter jwtAuthenticationFilter;
private final UserDetailsService userService;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(request -> request.requestMatchers("/api/v1/auth/**")
.permitAll().anyRequest().authenticated())
.sessionManagement(manager -> manager.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authenticationProvider(authenticationProvider()).addFilterBefore(
jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public AuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
authProvider.setUserDetailsService(userService);
authProvider.setPasswordEncoder(passwordEncoder());
return authProvider;
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration config)
throws Exception {
return config.getAuthenticationManager();
}
}
package com.hcordova.flightagencyapi.shared.controllers;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.hcordova.flightagencyapi.shared.domain.services.AuthenticationService;
import com.hcordova.flightagencyapi.shared.dto.SignInRequestDTO;
import com.hcordova.flightagencyapi.shared.dto.SignInResponseDTO;
import com.hcordova.flightagencyapi.shared.dto.SignUpRequestDTO;
import com.hcordova.flightagencyapi.shared.dto.SignUpResponseDTO;
import lombok.RequiredArgsConstructor;
@RestController
@RequestMapping("/api/v1/auth")
@RequiredArgsConstructor
public class AuthenticationController {
private final AuthenticationService authenticationService;
@PostMapping("sign-in")
public ResponseEntity<SignInResponseDTO> signIn(@RequestBody SignInRequestDTO requestDTO) {
return authenticationService.signIn(requestDTO);
}
@PostMapping("sign-up")
public ResponseEntity<SignUpResponseDTO> signUp(@RequestBody SignUpRequestDTO requestDTO) {
return authenticationService.signUp(requestDTO);
}
}
package com.hcordova.flightagencyapi.shared.domain.models;
import java.util.List;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.OneToMany;
import jakarta.persistence.Table;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Entity
@Table(name = "roles")
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class Role {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, length = 30)
private String name;
@OneToMany(mappedBy = "role", cascade = CascadeType.ALL)
private List<User> users;
}
package com.hcordova.flightagencyapi.shared.domain.models;
import java.util.Collection;
import java.util.List;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import jakarta.persistence.CascadeType;
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.ManyToOne;
import jakarta.persistence.Table;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Entity
@Table(name = "users")
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class User implements UserDetails {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, length = 30)
private String name;
@Column(nullable = false, length = 50)
private String lastname;
@Column(nullable = false, unique = true, length = 100)
private String email;
@Column(nullable = false)
private String password;
@ManyToOne(cascade = CascadeType.ALL)
@JoinColumn(name = "role_id")
private Role role;
public User(String name, String lastname, String email, String password, Role role) {
this.name = name;
this.lastname = lastname;
this.email = email;
this.password = password;
this.role = role;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return List.of(new SimpleGrantedAuthority(role.getName()));
}
@Override
public String getUsername() {
return this.email;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
package com.hcordova.flightagencyapi.shared.domain.repositories;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import com.hcordova.flightagencyapi.shared.domain.models.Role;
@Repository
public interface RoleRepository extends JpaRepository<Role, Long>{
Optional<Role> findByName(String name);
}
package com.hcordova.flightagencyapi.shared.domain.repositories;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import com.hcordova.flightagencyapi.shared.domain.models.User;
@Repository
public interface UserRepository extends JpaRepository<User, Long>{
Optional<User> findByEmail(String email);
}
package com.hcordova.flightagencyapi.shared.domain.services;
import org.springframework.http.ResponseEntity;
import com.hcordova.flightagencyapi.shared.dto.SignInRequestDTO;
import com.hcordova.flightagencyapi.shared.dto.SignInResponseDTO;
import com.hcordova.flightagencyapi.shared.dto.SignUpRequestDTO;
import com.hcordova.flightagencyapi.shared.dto.SignUpResponseDTO;
public interface AuthenticationService {
ResponseEntity<SignInResponseDTO> signIn(SignInRequestDTO requestDTO);
ResponseEntity<SignUpResponseDTO> signUp(SignUpRequestDTO requestDTO);
}
package com.hcordova.flightagencyapi.shared.domain.services;
import org.springframework.security.core.userdetails.UserDetails;
public interface JwtService {
String extractUserName(String token);
String generateToken(UserDetails userDetails);
boolean isTokenValid(String token, UserDetails userDetails);
}
package com.hcordova.flightagencyapi.shared.domain.services;
import org.springframework.security.core.userdetails.UserDetailsService;
public interface UserService {
UserDetailsService userDetailsService();
}
......@@ -9,7 +9,7 @@ import lombok.Setter;
@AllArgsConstructor
@Getter
@Setter
public class ErrorMessage {
public class ErrorMessageDTO {
private String message;
private HttpStatus code;
private String path;
......
package com.hcordova.flightagencyapi.shared.dto;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class SignInRequestDTO {
private String email;
private String password;
}
package com.hcordova.flightagencyapi.shared.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
@Builder
@AllArgsConstructor
public class SignInResponseDTO {
// Id: Passenger, Supervisor, Agent
private Long id;
private String name;
private String lastname;
private String email;
private String role;
private String token;
}
package com.hcordova.flightagencyapi.shared.dto;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class SignUpRequestDTO {
// Compartido
private String name;
private String lastname;
private String email;
private String password;
private String role;
// Supervisor and Agent
private String details;
// Pasajero
private String secondName;
private String country;
private String city;
private String phone;
private String address;
}
package com.hcordova.flightagencyapi.shared.dto;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
@AllArgsConstructor
public class SignUpResponseDTO {
private Long id;
private String name;
private String lastname;
private String email;
private String role;
}
package com.hcordova.flightagencyapi.shared.services;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import com.hcordova.flightagencyapi.shared.domain.models.User;
import com.hcordova.flightagencyapi.shared.domain.repositories.RoleRepository;
import com.hcordova.flightagencyapi.shared.domain.repositories.UserRepository;
import com.hcordova.flightagencyapi.shared.domain.services.AuthenticationService;
import com.hcordova.flightagencyapi.shared.domain.services.JwtService;
import com.hcordova.flightagencyapi.shared.dto.SignInRequestDTO;
import com.hcordova.flightagencyapi.shared.dto.SignInResponseDTO;
import com.hcordova.flightagencyapi.shared.dto.SignUpRequestDTO;
import com.hcordova.flightagencyapi.shared.dto.SignUpResponseDTO;
import jakarta.transaction.Transactional;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
@Service
public class AuthenticationServiceImpl implements AuthenticationService {
private final UserRepository userRepository;
private final RoleRepository roleRepository;
private final JwtService jService;
private final AuthenticationManager authenticationManager;
private final PasswordEncoder passwordEncoder;
@Override
public ResponseEntity<SignInResponseDTO> signIn(SignInRequestDTO requestDTO) {
authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(requestDTO.getEmail(), requestDTO.getPassword()));
var user = userRepository.findByEmail(requestDTO.getEmail())
.orElseThrow(() -> new UsernameNotFoundException("Invalid email or password"));
var jwt = jService.generateToken(user);
var response = new SignInResponseDTO(user.getId(), user.getName(), user.getLastname(), user.getEmail(), user.getRole().getName(), jwt);
return ResponseEntity.status(HttpStatus.OK).body(response);
}
@Override
@Transactional
public ResponseEntity<SignUpResponseDTO> signUp(SignUpRequestDTO requestDTO) {
var userFound = userRepository.findByEmail(requestDTO.getEmail());
if (userFound.isPresent()) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(null);
}
var role = roleRepository.findByName(requestDTO.getRole());
if (role.isEmpty()) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(null);
}
var user = new User(
requestDTO.getName(),
requestDTO.getLastname(),
requestDTO.getEmail(),
passwordEncoder.encode(requestDTO.getPassword()),
role.get());
var userCreated = userRepository.save(user);
var response = new SignUpResponseDTO(userCreated.getId(), userCreated.getName(), userCreated.getLastname(), userCreated.getEmail(), userCreated.getRole().getName());
return ResponseEntity.status(HttpStatus.CREATED).body(response);
}
// private boolean registerPassenger(String secondName, String country, String city, String phone, String address) {
// }
// private boolean registerAgent(String details) {
// }
// private boolean registerSupervisor(String details) {
// }
}
package com.hcordova.flightagencyapi.shared.services;
import java.security.Key;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Service;
import com.hcordova.flightagencyapi.shared.domain.services.JwtService;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
@Service
public class JwtServiceImpl implements JwtService {
@Value("${jwt.signin.key}")
private String jwtSigninKey;
@Override
public String extractUserName(String token) {
return extractClaim(token, Claims::getSubject);
}
@Override
public String generateToken(UserDetails userDetails) {
return generateToken(new HashMap<>(), userDetails);
}
@Override
public boolean isTokenValid(String token, UserDetails userDetails) {
final String userName = extractUserName(token);
return (userName.equals(userDetails.getUsername())) && !isTokenExpired(token);
}
private <T> T extractClaim(String token, Function<Claims, T> claimsResolvers) {
final Claims claims = extractAllClaims(token);
return claimsResolvers.apply(claims);
}
@SuppressWarnings("deprecation")
private String generateToken(Map<String, Object> extraClaims, UserDetails userDetails) {
return Jwts.builder().setClaims(extraClaims).setSubject(userDetails.getUsername())
.setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + 1000 * 60 * 24))
.signWith(getSigningKey(), SignatureAlgorithm.HS256).compact();
}
private boolean isTokenExpired(String token) {
return extractExpiration(token).before(new Date());
}
private Date extractExpiration(String token) {
return extractClaim(token, Claims::getExpiration);
}
@SuppressWarnings("deprecation")
private Claims extractAllClaims(String token) {
return Jwts.parser().setSigningKey(getSigningKey()).build().parseClaimsJws(token)
.getBody();
}
private Key getSigningKey() {
byte[] keyBytes = Decoders.BASE64.decode(this.jwtSigninKey);
return Keys.hmacShaKeyFor(keyBytes);
}
}
package com.hcordova.flightagencyapi.shared.services;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import com.hcordova.flightagencyapi.shared.domain.repositories.UserRepository;
@Service
public class UserServiceImpl implements UserDetailsService{
@Autowired
private UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
return this.userRepository.findByEmail(email)
.orElseThrow(() -> new UsernameNotFoundException("User not found"));
}
}
......@@ -8,5 +8,10 @@
"name": "app.port",
"type": "java.lang.String",
"description": "A description for 'app.port'"
},
{
"name": "jwt.signin.key",
"type": "java.lang.String",
"description": "A description for 'jwt.signin.key'"
}
]}
\ No newline at end of file
# METADATA
db.host=my-flight-app-network
app.port=8080
jwt.signin.key=aJncb8DoBw4eNK4syWvjpbs8xQkVJDeLE2cthyQYfnc4p2XpsMuamKDe4HxUCXQT
# app.port=8080
# SERVER
server.port=${app.port}:8085}
server.port=8085
# MYSQL
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://${db.host}:localhost}:3306/flightdatabase?useSSL=false&createDatabaseIfNotExists=true
# spring.datasource.url=jdbc:mysql://${db.host}:localhost}:3306/flightdatabase?useSSL=false&createDatabaseIfNotExists=true
spring.datasource.url=jdbc:mysql://localhost:3306/flightdatabase?useSSL=false&createDatabaseIfNotExists=true
spring.datasource.username=root
spring.datasource.password=admin
spring.datasource.password=hbcordova
# JPA
spring.jpa.hibernate.ddl-auto=update
spring.jpa.database-platform=org.hibernate.dialect.MySQL8Dialect
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL8Dialect
# spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL8Dialect
spring.jpa.show-sql=true
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment