diff --git a/pom.xml b/pom.xml index d028657f5f7bb9e97df7e026ffc8f057ec5fb5ed..f056a3def032442e353770477484d031cfdb792b 100644 --- a/pom.xml +++ b/pom.xml @@ -29,11 +29,25 @@ <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</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-security</artifactId> + </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> <dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> diff --git a/src/main/java/hdm/mi/growbros/auth/AuthenticationRequest.java b/src/main/java/hdm/mi/growbros/auth/AuthenticationRequest.java new file mode 100644 index 0000000000000000000000000000000000000000..0dfec3d2e88505610806d1a4bd7a972d222fc41e --- /dev/null +++ b/src/main/java/hdm/mi/growbros/auth/AuthenticationRequest.java @@ -0,0 +1,15 @@ +package hdm.mi.growbros.auth; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class AuthenticationRequest { + private String email; + String password; +} diff --git a/src/main/java/hdm/mi/growbros/auth/AuthenticationResponse.java b/src/main/java/hdm/mi/growbros/auth/AuthenticationResponse.java new file mode 100644 index 0000000000000000000000000000000000000000..a16fe46d690544679546018623b87b33ebf1b837 --- /dev/null +++ b/src/main/java/hdm/mi/growbros/auth/AuthenticationResponse.java @@ -0,0 +1,14 @@ +package hdm.mi.growbros.auth; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class AuthenticationResponse { + private String token; +} diff --git a/src/main/java/hdm/mi/growbros/auth/AuthenticationService.java b/src/main/java/hdm/mi/growbros/auth/AuthenticationService.java new file mode 100644 index 0000000000000000000000000000000000000000..acecf64d1eda1e83d56ab87035757732a12c1b44 --- /dev/null +++ b/src/main/java/hdm/mi/growbros/auth/AuthenticationService.java @@ -0,0 +1,55 @@ +package hdm.mi.growbros.auth; + +import hdm.mi.growbros.models.user.Role; +import hdm.mi.growbros.models.user.User; +import hdm.mi.growbros.models.user.UserRepository; +import hdm.mi.growbros.security.JwtService; +import lombok.RequiredArgsConstructor; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class AuthenticationService { + private final UserRepository repository; + private final PasswordEncoder passwordEncoder; + private final JwtService jwtService; + private final AuthenticationManager authenticationManager; + /** + * Ermöglicht es einen User zu erstellen und diesen in der Datenbank zu registrieren + * und daraus den Token zu erstellen. + * @param request + * @return + */ + public AuthenticationResponse register(RegisterRequest request) { + var user = User.builder() + .firstname(request.getFirstname()) + .lastname(request.getLastname()) + .email(request.getEmail()) + .password(passwordEncoder.encode(request.getPassword())) + .role(Role.USER) + .build(); + repository.save(user); + var jwtToken = jwtService.generateToken(user); + return AuthenticationResponse.builder() + .token(jwtToken) + .build(); + } + + public AuthenticationResponse authenticate(AuthenticationRequest request) { + authenticationManager.authenticate( + new UsernamePasswordAuthenticationToken( + request.getEmail(), + request.getPassword() + ) + ); + var user = repository.findByEmail(request.getEmail()) //wenn der Nutzername und Passwort korrekt sind wird ein Token generiert + .orElseThrow(); + var jwtToken = jwtService.generateToken(user); + return AuthenticationResponse.builder() + .token(jwtToken) + .build(); + } +} diff --git a/src/main/java/hdm/mi/growbros/auth/RegisterRequest.java b/src/main/java/hdm/mi/growbros/auth/RegisterRequest.java new file mode 100644 index 0000000000000000000000000000000000000000..574edf0b297ab671621d1dc5a56923e291218de6 --- /dev/null +++ b/src/main/java/hdm/mi/growbros/auth/RegisterRequest.java @@ -0,0 +1,17 @@ +package hdm.mi.growbros.auth; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class RegisterRequest { + private String firstname; + private String lastname; + private String email; + private String password; +} diff --git a/src/main/java/hdm/mi/growbros/controllers/AuthenticationController.java b/src/main/java/hdm/mi/growbros/controllers/AuthenticationController.java new file mode 100644 index 0000000000000000000000000000000000000000..01e4c0f0c8a5d0dc29baa1486b297657630d30e1 --- /dev/null +++ b/src/main/java/hdm/mi/growbros/controllers/AuthenticationController.java @@ -0,0 +1,29 @@ +package hdm.mi.growbros.controllers; + +import hdm.mi.growbros.auth.AuthenticationRequest; +import hdm.mi.growbros.auth.AuthenticationResponse; +import hdm.mi.growbros.auth.AuthenticationService; +import hdm.mi.growbros.auth.RegisterRequest; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/api/v1/auth") +@RequiredArgsConstructor +public class AuthenticationController { + private final AuthenticationService service; + @PostMapping + public ResponseEntity<AuthenticationResponse> register( + @RequestBody RegisterRequest request + ) { + return ResponseEntity.ok(service.register(request)); + } + + @PostMapping("/authenticate") + public ResponseEntity<AuthenticationResponse> authenticate( + @RequestBody AuthenticationRequest request + ) { + return ResponseEntity.ok(service.authenticate(request)); + } +} diff --git a/src/main/java/hdm/mi/growbros/controllers/DemoController.java b/src/main/java/hdm/mi/growbros/controllers/DemoController.java new file mode 100644 index 0000000000000000000000000000000000000000..cbee92257c468ef33188574a6548d28ce75e26ea --- /dev/null +++ b/src/main/java/hdm/mi/growbros/controllers/DemoController.java @@ -0,0 +1,15 @@ +package hdm.mi.growbros.controllers; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/v1/demo-controller") +public class DemoController { + @GetMapping + public ResponseEntity<String> sayHello() { + return ResponseEntity.ok("Hello from secured endpoint."); + } +} diff --git a/src/main/java/hdm/mi/growbros/models/user/Role.java b/src/main/java/hdm/mi/growbros/models/user/Role.java new file mode 100644 index 0000000000000000000000000000000000000000..515b3ac4b547620929f60390f0777bc4309e8bf9 --- /dev/null +++ b/src/main/java/hdm/mi/growbros/models/user/Role.java @@ -0,0 +1,6 @@ +package hdm.mi.growbros.models.user; + +public enum Role { + USER, + ADMIN +} diff --git a/src/main/java/hdm/mi/growbros/models/user/User.java b/src/main/java/hdm/mi/growbros/models/user/User.java index a92fa91dd2a95c218e58ce3494b502404f2aa894..5d5f328f96d3a2846160482cd582514d9813e38c 100644 --- a/src/main/java/hdm/mi/growbros/models/user/User.java +++ b/src/main/java/hdm/mi/growbros/models/user/User.java @@ -1,18 +1,71 @@ package hdm.mi.growbros.models.user; import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Data; +import lombok.NoArgsConstructor; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + +import java.util.Collection; +import java.util.List; -@Entity @Data -public class User { +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Entity +@Table(name = "user") +public class User implements UserDetails { @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) + @GeneratedValue private long id; + private String firstname; + private String lastname; + @Column(unique = true) private String email; private String password; + @Enumerated(EnumType.STRING) + private Role role; + + @Override + public Collection<? extends GrantedAuthority> getAuthorities() { + return List.of(new SimpleGrantedAuthority(role.name())); + } + + @Override + public String getPassword() { + return password; + } + + @Override + public String getUsername() { + return 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; + } } diff --git a/src/main/java/hdm/mi/growbros/repositories/UserRepository.java b/src/main/java/hdm/mi/growbros/models/user/UserRepository.java similarity index 53% rename from src/main/java/hdm/mi/growbros/repositories/UserRepository.java rename to src/main/java/hdm/mi/growbros/models/user/UserRepository.java index a0c375045c03b0e02bdb0696088309822d41f656..87b067d9ff512b4de55b60251b173f5c91428324 100644 --- a/src/main/java/hdm/mi/growbros/repositories/UserRepository.java +++ b/src/main/java/hdm/mi/growbros/models/user/UserRepository.java @@ -1,13 +1,9 @@ -package hdm.mi.growbros.repositories; +package hdm.mi.growbros.models.user; -import hdm.mi.growbros.models.user.User; import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; import java.util.Optional; -@Repository public interface UserRepository extends JpaRepository<User, Long> { - //User anhand seiner Mail finden Optional<User> findByEmail(String email); } diff --git a/src/main/java/hdm/mi/growbros/security/ApplicationConfig.java b/src/main/java/hdm/mi/growbros/security/ApplicationConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..5c2d13d458f73fb73036cc4e6dda1831e4b7dc0d --- /dev/null +++ b/src/main/java/hdm/mi/growbros/security/ApplicationConfig.java @@ -0,0 +1,45 @@ +package hdm.mi.growbros.security; + +import hdm.mi.growbros.models.user.UserRepository; +import lombok.RequiredArgsConstructor; +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 org.springframework.security.crypto.password.PasswordEncoder; + +@Configuration +@RequiredArgsConstructor +public class ApplicationConfig { + private final UserRepository repository; + @Bean + public UserDetailsService userDetailsService() { + return username -> repository.findByEmail(username) + .orElseThrow(() -> new UsernameNotFoundException("User not found")); + } + + /** + * Fetcht die Benutzer Daten und decodiert das Passwort. + * @return + */ + @Bean + public AuthenticationProvider authenticationProvider() { + DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider(); + authProvider.setUserDetailsService(userDetailsService()); + authProvider.setPasswordEncoder(passwordEncoder()); + return authProvider; + } + @Bean + public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception { + return config.getAuthenticationManager(); + } + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } +} diff --git a/src/main/java/hdm/mi/growbros/security/CustomUserDetailsService.java b/src/main/java/hdm/mi/growbros/security/CustomUserDetailsService.java deleted file mode 100644 index 6992eec7ea2ee4cfe6724c64907fb6624125e3e2..0000000000000000000000000000000000000000 --- a/src/main/java/hdm/mi/growbros/security/CustomUserDetailsService.java +++ /dev/null @@ -1,24 +0,0 @@ -package hdm.mi.growbros.security; - -import hdm.mi.growbros.models.user.User; -import hdm.mi.growbros.repositories.UserRepository; -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 java.util.Collections; - -//versorgt Spring mit dem User der überprüft werden soll -@Service -public class CustomUserDetailsService implements UserDetailsService { - - private UserRepository userRepository; - //user anhand der email (stellt Username dar) aus der DB holen - @Override - public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { - User user = userRepository.findByEmail(username).orElseThrow(() -> new UsernameNotFoundException("Dieser Nutzer wurde nicht gefunden")); - - return new org.springframework.security.core.userdetails.User(user.getEmail(),user.getPassword(), Collections.emptyList()); //empty list, weils noch keine Privilegien gibt - } -} diff --git a/src/main/java/hdm/mi/growbros/security/JwTokenProvider.java b/src/main/java/hdm/mi/growbros/security/JwTokenProvider.java deleted file mode 100644 index 554d3019f4f8e34bac54c90da9389e0f9909f5d1..0000000000000000000000000000000000000000 --- a/src/main/java/hdm/mi/growbros/security/JwTokenProvider.java +++ /dev/null @@ -1,19 +0,0 @@ -package hdm.mi.growbros.security; - -import java.time.Instant; - -//Json Web Token wird zur Autorisierung verwendet -public class JwTokenProvider { - //Jw Token werden mit einem Schlüssel/ Passwort signiert -> so können unr wir überprüfen ob der Token gültig ist oder nicht - private String jwTSecret; - //generiert Token anhand der Email - public String generateToken(String userEmail) { - Instant now = Instant.now(); - String returnvalue = "hi"; - return returnvalue; - } - public boolean validateToken(String token) { - return true; - - } -} diff --git a/src/main/java/hdm/mi/growbros/security/JwtAuthenticationFilter.java b/src/main/java/hdm/mi/growbros/security/JwtAuthenticationFilter.java new file mode 100644 index 0000000000000000000000000000000000000000..95afb44ac212e804570cb385b64396f24a7b42c6 --- /dev/null +++ b/src/main/java/hdm/mi/growbros/security/JwtAuthenticationFilter.java @@ -0,0 +1,51 @@ +package hdm.mi.growbros.security; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +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 java.io.IOException; + +@Component +@RequiredArgsConstructor //nutzt jedes final Feld +public class JwtAuthenticationFilter extends OncePerRequestFilter { + + private final JwtService jwtService; + private final UserDetailsService userDetailsService; + + @Override //überprüft bei jedem request ob ein Jwt dabei ist, wenn ja ob er gültig ist -> melde Nutzer an + protected void doFilterInternal(@NonNull HttpServletRequest request,@NonNull HttpServletResponse response, @NonNull FilterChain filterChain) throws ServletException, IOException { + final String authHeader = request.getHeader("Authorization"); + final String jwt; + final String userEmail; + if(authHeader == null ||!authHeader.startsWith("Bearer ")) { + filterChain.doFilter(request, response); + return; + } + jwt = authHeader.substring(7); //extract jwt + userEmail = jwtService.extractUsername(jwt);// extract userEmail from jwt token with the help of a class that can manipulate the jwt -> jwt Service + if (userEmail != null && SecurityContextHolder.getContext().getAuthentication() == null) { //case user is not yet authenticated + UserDetails userDetails = this.userDetailsService.loadUserByUsername(userEmail); //get UserDetails from DataBase + if(jwtService.isTokenValid(jwt,userDetails)) { //case User is valid + UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken( + userDetails, + null, + userDetails.getAuthorities() + ); + authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); //set request details on authToken + SecurityContextHolder.getContext().setAuthentication(authToken);//update SecurityContextHolder + } + } + filterChain.doFilter(request,response); + } +} diff --git a/src/main/java/hdm/mi/growbros/security/JwtService.java b/src/main/java/hdm/mi/growbros/security/JwtService.java new file mode 100644 index 0000000000000000000000000000000000000000..13f8cb5f15722a0890c73e06401d789e9aa4f4cc --- /dev/null +++ b/src/main/java/hdm/mi/growbros/security/JwtService.java @@ -0,0 +1,84 @@ +package hdm.mi.growbros.security; + +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.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 { + private static final String SECRET_KEY = "222972303468637b5e6a2c467e5f6c4d4542632e4257797b6d6d7e28564d534a"; + public String extractUsername(String token) { + return extractClaim(token,Claims::getSubject); //Subject ist der Username(für Spring) bzw. bei uns email + } + + /** + * Extrahiert einen einzelnen Claim aus dem Jwt. + * @param token + * @param claimsResolver generische Funktion welche angibt welcher Claim aus dem Jwt extrahiert werden soll + * @return Ein einzelner Claim aud dem Jwt. + * @param <T> + */ + 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 Jwts + .builder() + .setClaims(extraClaims) + .setSubject(userDetails.getUsername()) + .setIssuedAt(new Date(System.currentTimeMillis())) + .setExpiration(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 24)) //token ist 24h gültig + .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); + } + + /** + * extrahiert die "Claims" = Ansprüche die im Token selbst enthalten sind. + * Ein JWT besteht aus drei Teilen: dem Header, den Claims und der Signatur. + * Die Claims sind der Hauptteil, der die nützlichen Informationen enthält (z.B. iss = Aussteller des Tokens...) + * @param token + * @return alle Claims des Jwt + */ + private Claims extractAllClaims(String token) { + return Jwts + .parserBuilder() + .setSigningKey(getSignInKey()) + .build() + .parseClaimsJws(token) + .getBody(); + + } + + private Key getSignInKey() { + byte[] keyBytes = Decoders.BASE64.decode(SECRET_KEY); + return Keys.hmacShaKeyFor(keyBytes); //hmacShaKeyFor = Algorithmus + } +} diff --git a/src/main/java/hdm/mi/growbros/security/SecurityConfiguration.java b/src/main/java/hdm/mi/growbros/security/SecurityConfiguration.java new file mode 100644 index 0000000000000000000000000000000000000000..de4e72ffb1e5f0a028baf27e708348c041496c03 --- /dev/null +++ b/src/main/java/hdm/mi/growbros/security/SecurityConfiguration.java @@ -0,0 +1,43 @@ +package hdm.mi.growbros.security; + +import lombok.RequiredArgsConstructor; +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; + +@Configuration +@EnableWebSecurity +@RequiredArgsConstructor +public class SecurityConfiguration { + private final JwtAuthenticationFilter jwtAuthFilter; + private final AuthenticationProvider authenticationProvider; + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception{ + + http + .csrf((csrf) -> csrf.disable()) + .authorizeHttpRequests((authorizeHttpRequests) -> //white List + authorizeHttpRequests + .requestMatchers("/api/v1/auth/**") + .permitAll() + .anyRequest() + .authenticated() + ) + .sessionManagement((sessionManagement) -> + sessionManagement + .sessionCreationPolicy(SessionCreationPolicy.STATELESS) //stellt sicher dass man sich in jeder Session authentifizieren muss + ) + .authenticationProvider(authenticationProvider) + .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class); + ; + + + return http.build(); + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 443fc3744bfd09b45424b28773ae4ebb0c19888d..4acb70f3a591130de61b384472ff92b7c81e07f8 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -4,3 +4,5 @@ spring.datasource.username=admin spring.datasource.password=admin spring.jpa.database-platform=org.hibernate.dialect.H2Dialect spring.h2.console.enabled=true + +app.jwtSecret = ffjsdafhjkfkafSecretjkdlfhw \ No newline at end of file