RHSRV-4 | RHSRV-5 - Implement Authentication with JWT
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
Bea 2024-08-07 21:14:19 +02:00
parent 9fa1a4bfba
commit a0fafcc2dc
17 changed files with 592 additions and 72 deletions

33
pom.xml
View File

@ -8,6 +8,7 @@
<artifactId>release-hive</artifactId> <artifactId>release-hive</artifactId>
<version>0.0.1-SNAPSHOT</version> <version>0.0.1-SNAPSHOT</version>
<dependencies> <dependencies>
<!-- Logging Dependencies --> <!-- Logging Dependencies -->
<dependency> <dependency>
@ -32,13 +33,35 @@
<artifactId>spring-boot-starter-json</artifactId> <artifactId>spring-boot-starter-json</artifactId>
<version>3.3.2</version> <version>3.3.2</version>
</dependency> </dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
<version>3.3.2</version>
</dependency>
<!-- Security and Auth -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<version>3.3.2</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.5</version>
</dependency>
<!-- Database Dependencies --> <!-- Database Dependencies -->
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-core</artifactId>
<version>6.6.0.CR1</version>
</dependency>
<dependency> <dependency>
<groupId>org.postgresql</groupId> <groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId> <artifactId>postgresql</artifactId>

View File

@ -0,0 +1,51 @@
package wtf.beatrice.releasehive.config;
import org.springframework.beans.factory.annotation.Autowired;
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.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import wtf.beatrice.releasehive.repository.UserRepository;
@Configuration
public class ApplicationConfiguration
{
private final UserRepository userRepository;
public ApplicationConfiguration(@Autowired UserRepository userRepository) {
this.userRepository = userRepository;
}
@Bean
UserDetailsService userDetailsService() {
return username -> userRepository.findByEmail(username)
.orElseThrow(() -> new UsernameNotFoundException("User not found"));
}
@Bean
BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
return config.getAuthenticationManager();
}
@Bean
AuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
authProvider.setUserDetailsService(userDetailsService());
authProvider.setPasswordEncoder(passwordEncoder());
return authProvider;
}
}

View File

@ -0,0 +1,79 @@
package wtf.beatrice.releasehive.config;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.lang.NonNull;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
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.web.filter.OncePerRequestFilter;
import org.springframework.web.servlet.HandlerExceptionResolver;
import wtf.beatrice.releasehive.service.JWTService;
import java.io.IOException;
@Component
public class JWTAuthenticationFilter extends OncePerRequestFilter
{
private final HandlerExceptionResolver handlerExceptionResolver;
private final JWTService jwtService;
private final UserDetailsService userDetailsService;
public JWTAuthenticationFilter(
@Autowired JWTService jwtService,
@Autowired UserDetailsService userDetailsService,
@Autowired HandlerExceptionResolver handlerExceptionResolver) {
this.jwtService = jwtService;
this.userDetailsService = userDetailsService;
this.handlerExceptionResolver = handlerExceptionResolver;
}
@Override
protected void doFilterInternal(
@NonNull HttpServletRequest request,
@NonNull HttpServletResponse response,
@NonNull FilterChain filterChain
) throws ServletException, IOException {
final String authHeader = request.getHeader("Authorization");
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
filterChain.doFilter(request, response);
return;
}
try {
final String jwt = authHeader.substring(7);
final String userEmail = jwtService.extractUsername(jwt);
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (userEmail != null && authentication == null) {
UserDetails userDetails = this.userDetailsService.loadUserByUsername(userEmail);
if (jwtService.isTokenValid(jwt, userDetails)) {
UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(
userDetails,
null,
userDetails.getAuthorities()
);
authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authToken);
}
}
filterChain.doFilter(request, response);
} catch (Exception exception) {
handlerExceptionResolver.resolveException(request, response, null, exception);
}
}
}

View File

@ -0,0 +1,71 @@
package wtf.beatrice.releasehive.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import java.util.List;
@Configuration
@EnableWebSecurity
public class SecurityConfiguration
{
private final AuthenticationProvider authenticationProvider;
private final JWTAuthenticationFilter jwtAuthenticationFilter;
public SecurityConfiguration(
@Autowired JWTAuthenticationFilter jwtAuthenticationFilter,
@Autowired AuthenticationProvider authenticationProvider)
{
this.authenticationProvider = authenticationProvider;
this.jwtAuthenticationFilter = jwtAuthenticationFilter;
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.csrf()
.disable()
.authorizeHttpRequests()
.requestMatchers("/api/v1/auth/**")
.permitAll()
.anyRequest()
.authenticated()
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authenticationProvider(authenticationProvider)
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
@Bean
CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(List.of("http://localhost:8080"));
configuration.setAllowedMethods(List.of("GET","POST"));
configuration.setAllowedHeaders(List.of("Authorization","Content-Type"));
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**",configuration);
return source;
}
}

View File

@ -0,0 +1,24 @@
package wtf.beatrice.releasehive.dto;
public class LoginUserDto
{
private String email;
private String password;
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}

View File

@ -0,0 +1,34 @@
package wtf.beatrice.releasehive.dto;
public class RegisterUserDto
{
private String email;
private String password;
private String username;
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
}

View File

@ -1,33 +0,0 @@
package wtf.beatrice.releasehive.model;
import jakarta.persistence.Entity;
import java.util.UUID;
@Entity
public class ApiError
{
UUID exceptionId;
String message;
public ApiError() {
exceptionId = UUID.randomUUID();
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public UUID getExceptionId() {
return exceptionId;
}
public void setExceptionId(UUID exceptionId) {
this.exceptionId = exceptionId;
}
}

View File

@ -0,0 +1,34 @@
package wtf.beatrice.releasehive.model;
public class LoginResponse
{
private String token;
private long expiresIn;
public String getToken() {
return token;
}
public LoginResponse setToken(String token) {
this.token = token;
return this;
}
public long getExpiresIn() {
return expiresIn;
}
public LoginResponse setExpiresIn(long expiresIn) {
this.expiresIn = expiresIn;
return this;
}
@Override
public String toString() {
return "LoginResponse{" +
"token='" + token + '\'' +
", expiresIn=" + expiresIn +
'}';
}
}

View File

@ -2,36 +2,81 @@ package wtf.beatrice.releasehive.model;
import jakarta.persistence.*; import jakarta.persistence.*;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import java.util.UUID; import java.util.UUID;
@Entity @Entity
@Table(name="users") @Table(name="users")
public class User public class User implements UserDetails
{ {
@Id @Id
@GeneratedValue(strategy = GenerationType.UUID) @GeneratedValue(strategy = GenerationType.UUID)
private UUID uuid; private UUID uuid;
@Column @Column(nullable = false)
private String username; private String username;
@Column
@Column(unique = true, length = 64, nullable = false)
private String email;
@Column(nullable = false)
private String password; private String password;
@CreationTimestamp
@Column(updatable = false, name = "created_at")
private Date createdAt;
@UpdateTimestamp
@Column(name = "updated_at")
private Date updatedAt;
public UUID getUuid() { public UUID getUuid() {
return uuid; return uuid;
} }
@Override
public String getUsername() { public String getUsername() {
return username; return username;
} }
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
public void setUsername(String username) { public void setUsername(String username) {
this.username = username; this.username = username;
} }
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return List.of();
}
public String getPassword() { public String getPassword() {
return password; return password;
} }
@ -43,4 +88,28 @@ public class User
public void setUuid(UUID uuid) { public void setUuid(UUID uuid) {
this.uuid = uuid; this.uuid = uuid;
} }
public Date getCreatedAt() {
return createdAt;
}
public void setCreatedAt(Date createdAt) {
this.createdAt = createdAt;
}
public Date getUpdatedAt() {
return updatedAt;
}
public void setUpdatedAt(Date updatedAt) {
this.updatedAt = updatedAt;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
} }

View File

@ -0,0 +1,14 @@
package wtf.beatrice.releasehive.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import wtf.beatrice.releasehive.model.User;
import java.util.Optional;
import java.util.UUID;
@Repository
public interface UserRepository extends JpaRepository<User, UUID> {
Optional<User> findByEmail(String email);
}

View File

@ -1,29 +1,53 @@
package wtf.beatrice.releasehive.resource; package wtf.beatrice.releasehive.resource;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import wtf.beatrice.releasehive.dto.LoginUserDto;
import wtf.beatrice.releasehive.dto.RegisterUserDto;
import wtf.beatrice.releasehive.model.LoginResponse;
import wtf.beatrice.releasehive.model.User; import wtf.beatrice.releasehive.model.User;
import wtf.beatrice.releasehive.service.AccountService; import wtf.beatrice.releasehive.service.AccountService;
import wtf.beatrice.releasehive.service.JWTService;
@RestController @RestController
@RequestMapping("/api/v1/users") @RequestMapping("/api/v1/auth")
public class AccountResource { public class AccountResource {
private final AccountService accountService; private final AccountService accountService;
private final JWTService jwtService;
public AccountResource(@Autowired AccountService accountService) { public AccountResource(
@Autowired AccountService accountService,
@Autowired JWTService jwtService) {
this.accountService = accountService; this.accountService = accountService;
this.jwtService = jwtService;
} }
@PostMapping( @PostMapping(
value="/register", value="/register",
produces="application/json") produces="application/json")
public String register(@RequestBody User user) public ResponseEntity<User> register(@RequestBody RegisterUserDto userDto)
{ {
return accountService.registerUser(user); User user = accountService.register(userDto);
return ResponseEntity.ok(user);
} }
@PostMapping(
value="/login",
produces="application/json")
public ResponseEntity<LoginResponse> login(@RequestBody LoginUserDto userDto)
{
User authenticatedUser = accountService.login(userDto);
String jwtToken = jwtService.generateToken(authenticatedUser);
LoginResponse loginResponse = new LoginResponse().setToken(jwtToken).setExpiresIn(jwtService.getExpirationTime());
return ResponseEntity.ok(loginResponse);
}
} }

View File

@ -1,9 +1,13 @@
package wtf.beatrice.releasehive.service; package wtf.beatrice.releasehive.service;
import wtf.beatrice.releasehive.dto.LoginUserDto;
import wtf.beatrice.releasehive.dto.RegisterUserDto;
import wtf.beatrice.releasehive.model.User; import wtf.beatrice.releasehive.model.User;
public interface AccountService public interface AccountService
{ {
String registerUser(User user); User register(RegisterUserDto user);
User login(LoginUserDto user);
} }

View File

@ -2,30 +2,58 @@ package wtf.beatrice.releasehive.service;
import org.hibernate.Session; import org.hibernate.Session;
import org.hibernate.Transaction; import org.hibernate.Transaction;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import wtf.beatrice.releasehive.db.HibernateManager; import wtf.beatrice.releasehive.db.HibernateManager;
import wtf.beatrice.releasehive.dto.LoginUserDto;
import wtf.beatrice.releasehive.dto.RegisterUserDto;
import wtf.beatrice.releasehive.model.User; import wtf.beatrice.releasehive.model.User;
import wtf.beatrice.releasehive.repository.UserRepository;
import wtf.beatrice.releasehive.util.JsonUtil; import wtf.beatrice.releasehive.util.JsonUtil;
@Service @Service
public class AccountServiceImpl implements AccountService { public class AccountServiceImpl implements AccountService {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
private final AuthenticationManager authenticationManager;
public AccountServiceImpl(
@Autowired UserRepository userRepository,
@Autowired AuthenticationManager authenticationManager,
@Autowired PasswordEncoder passwordEncoder
) {
this.authenticationManager = authenticationManager;
this.userRepository = userRepository;
this.passwordEncoder = passwordEncoder;
}
@Override @Override
public String registerUser(User user) { public User register(RegisterUserDto userDto) {
if(null == user.getUsername() || user.getUsername().isEmpty()) { User user = new User();
return JsonUtil.spawnJsonError("Cannot register user without username"); user.setUsername(userDto.getUsername());
user.setEmail(userDto.getEmail());
user.setPassword(passwordEncoder.encode(userDto.getPassword()));
return userRepository.save(user);
} }
if(null == user.getPassword() || user.getPassword().isEmpty()) { @Override
return JsonUtil.spawnJsonError("Cannot register user without password"); public User login(LoginUserDto user) {
authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
user.getEmail(),
user.getPassword()
)
);
return userRepository.findByEmail(user.getEmail())
.orElseThrow();
} }
Session session = HibernateManager.getSession();
Transaction transaction = session.beginTransaction();
session.persist(user);
transaction.commit();
return JsonUtil.convertToJson(user);
}
} }

View File

@ -0,0 +1,89 @@
package wtf.beatrice.releasehive.service;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Service;
import java.security.Key;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;
@Service
public class JWTService
{
@Value("${security.jwt.secret-key}")
private String secretKey;
@Value("${security.jwt.expiration-time}")
private long jwtExpiration;
public String extractUsername(String token) {
return extractClaim(token, Claims::getSubject);
}
public <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
final Claims claims = extractAllClaims(token);
return claimsResolver.apply(claims);
}
public String generateToken(UserDetails userDetails) {
return generateToken(new HashMap<>(), userDetails);
}
public String generateToken(Map<String, Object> extraClaims, UserDetails userDetails) {
return buildToken(extraClaims, userDetails, jwtExpiration);
}
public long getExpirationTime() {
return jwtExpiration;
}
private String buildToken(
Map<String, Object> extraClaims,
UserDetails userDetails,
long expiration
) {
return Jwts
.builder()
.setClaims(extraClaims)
.setSubject(userDetails.getUsername())
.setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + expiration))
.signWith(getSignInKey(), SignatureAlgorithm.HS256)
.compact();
}
public boolean isTokenValid(String token, UserDetails userDetails) {
final String username = extractUsername(token);
return (username.equals(userDetails.getUsername())) && !isTokenExpired(token);
}
private boolean isTokenExpired(String token) {
return extractExpiration(token).before(new Date());
}
private Date extractExpiration(String token) {
return extractClaim(token, Claims::getExpiration);
}
private Claims extractAllClaims(String token) {
return Jwts
.parserBuilder()
.setSigningKey(getSignInKey())
.build()
.parseClaimsJws(token)
.getBody();
}
private Key getSignInKey() {
byte[] keyBytes = Decoders.BASE64.decode(secretKey);
return Keys.hmacShaKeyFor(keyBytes);
}
}

View File

@ -4,7 +4,6 @@ import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
import wtf.beatrice.releasehive.model.ApiError;
public class JsonUtil public class JsonUtil
{ {
@ -24,12 +23,4 @@ public class JsonUtil
return e.getMessage(); return e.getMessage();
} }
} }
public static String spawnJsonError(String errorMessage) {
ApiError apiError = new ApiError();
apiError.setMessage(errorMessage);
String error = convertToJson(apiError);
LOGGER.error(error);
return error;
}
} }

View File

@ -0,0 +1,13 @@
server.port=8080
spring.datasource.url=jdbc:postgresql://localhost:5432/releasehive
spring.datasource.username=relhive
spring.datasource.password=beelover
## Hibernate properties
spring.jpa.hibernate.ddl-auto=update
spring.jpa.open-in-view=false
security.jwt.secret-key=ed725256582a23e94f81ba36d7df498ea330c7ba978e2d8876fe135b4bb34068
# 1h in millisecond
security.jwt.expiration-time=3600000

View File

@ -0,0 +1,5 @@
spring:
autoconfigure:
exclude:
- org.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfiguration
- org.springframework.boot.autoconfigure.integration.IntegrationAutoConfiguration